lib/rubocop/cop/style/file_name.rb



# encoding: utf-8
# frozen_string_literal: true

require 'pathname'

module RuboCop
  module Cop
    module Style
      # This cop makes sure that Ruby source files have snake_case
      # names. Ruby scripts (i.e. source files with a shebang in the
      # first line) are ignored.
      class FileName < Cop
        MSG_SNAKE_CASE = 'Use snake_case for source file names.'.freeze
        MSG_NO_DEFINITION = '%s should define a class or module ' \
                            'called `%s`.'.freeze
        MSG_REGEX = '`%s` should match `%s`.'.freeze

        SNAKE_CASE = /^[\da-z_.?!]+$/

        def investigate(processed_source)
          file_path = processed_source.buffer.name
          return if config.file_to_include?(file_path)

          basename = File.basename(file_path)
          if filename_good?(basename)
            return unless expect_matching_definition?
            return if find_class_or_module(processed_source.ast,
                                           to_namespace(file_path))
            range = source_range(processed_source.buffer, 1, 0)
            msg   = format(MSG_NO_DEFINITION,
                           basename,
                           to_namespace(file_path).join('::'))
          else
            first_line = processed_source.lines.first
            return if cop_config['IgnoreExecutableScripts'] &&
                      shebang?(first_line)

            range = source_range(processed_source.buffer, 1, 0)
            msg = regex ? format(MSG_REGEX, basename, regex) : MSG_SNAKE_CASE
          end

          add_offense(nil, range, msg)
        end

        private

        def shebang?(line)
          line && line.start_with?('#!')
        end

        def expect_matching_definition?
          cop_config['ExpectMatchingDefinition']
        end

        def regex
          cop_config['Regex']
        end

        def filename_good?(basename)
          basename = basename.sub(/\.[^\.]+$/, '')
          basename =~ (regex || SNAKE_CASE)
        end

        def find_class_or_module(node, namespace)
          return nil if node.nil?
          name = namespace.pop

          on_node([:class, :module, :casgn], node) do |child|
            next unless (const = child.defined_module)

            const_namespace, const_name = *const
            next unless name == const_name

            return node if namespace.empty?
            return node if match_namespace(child, const_namespace, namespace)
          end
          nil
        end

        def match_namespace(node, namespace, expected)
          expected = expected.dup

          match_partial = lambda do |ns|
            next if ns.nil?
            while ns
              return expected.empty? || expected == [:Object] if ns.cbase_type?
              ns, name = *ns
              name == expected.last ? expected.pop : (return false)
            end
          end

          match_partial.call(namespace)

          node.each_ancestor(:class, :module, :sclass, :casgn) do |ancestor|
            return false if ancestor.sclass_type?
            match_partial.call(ancestor.defined_module)
          end

          expected.empty? || expected == [:Object]
        end

        def to_namespace(path)
          components = Pathname(path).each_filename.to_a
          # To convert a pathname to a Ruby namespace, we need a starting point
          # But RC can be run from any working directory, and can check any path
          # We can't assume that the working directory, or any other, is the
          # "starting point" to build a namespace
          start = %w(lib spec test src)
          if components.find { |c| start.include?(c) }
            components = components.drop_while { |c| !start.include?(c) }
            components.drop(1).map { |fn| to_module_name(fn) }
          else
            [to_module_name(components.last)]
          end
        end

        def to_module_name(basename)
          words = basename.sub(/\..*/, '').split('_')
          words.map(&:capitalize).join.to_sym
        end
      end
    end
  end
end