rubocop-hq/rubocop

View on GitHub
lib/rubocop/cli/command/auto_generate_config.rb

Summary

Maintainability
A
25 mins
Test Coverage
A
100%
# frozen_string_literal: true

module RuboCop
  class CLI
    module Command
      # Generate a configuration file acting as a TODO list.
      # @api private
      class AutoGenerateConfig < Base
        self.command_name = :auto_gen_config

        AUTO_GENERATED_FILE = '.rubocop_todo.yml'
        YAML_OPTIONAL_DOC_START = /\A---(\s+#|\s*\z)/.freeze
        PLACEHOLDER = '###rubocop:inherit_here'

        PHASE_1 = 'Phase 1 of 2: run Layout/LineLength cop'
        PHASE_2 = 'Phase 2 of 2: run all cops'

        PHASE_1_OVERRIDDEN = '(skipped because the default Layout/LineLength:Max is overridden)'
        PHASE_1_DISABLED = '(skipped because Layout/LineLength is disabled)'
        PHASE_1_SKIPPED_ONLY_COPS =
          '(skipped because a list of cops is passed to the `--only` flag)'
        PHASE_1_SKIPPED_ONLY_EXCLUDE =
          '(skipped because only excludes will be generated due to `--auto-gen-only-exclude` flag)'

        def run
          add_formatter
          reset_config_and_auto_gen_file
          line_length_contents = maybe_run_line_length_cop
          run_all_cops(line_length_contents)
        end

        private

        def maybe_run_line_length_cop
          if only_exclude?
            skip_line_length_cop(PHASE_1_SKIPPED_ONLY_EXCLUDE)
          elsif !line_length_enabled?(@config_store.for_pwd)
            skip_line_length_cop(PHASE_1_DISABLED)
          elsif !same_max_line_length?(@config_store.for_pwd, ConfigLoader.default_configuration)
            skip_line_length_cop(PHASE_1_OVERRIDDEN)
          elsif options_has_only_flag?
            skip_line_length_cop(PHASE_1_SKIPPED_ONLY_COPS)
          else
            run_line_length_cop
          end
        end

        def skip_line_length_cop(reason)
          puts Rainbow("#{PHASE_1} #{reason}").yellow
          ''
        end

        def line_length_enabled?(config)
          line_length_cop(config)['Enabled']
        end

        def same_max_line_length?(config1, config2)
          max_line_length(config1) == max_line_length(config2)
        end

        def max_line_length(config)
          line_length_cop(config)['Max']
        end

        def line_length_cop(config)
          config.for_cop('Layout/LineLength')
        end

        def options_has_only_flag?
          @options[:only]
        end

        def only_exclude?
          @options[:auto_gen_only_exclude]
        end

        # Do an initial run with only Layout/LineLength so that cops that
        # depend on Layout/LineLength:Max get the correct value for that
        # parameter.
        def run_line_length_cop
          puts Rainbow(PHASE_1).yellow
          @options[:only] = ['Layout/LineLength']
          execute_runner
          @options.delete(:only)
          @config_store = ConfigStore.new
          @config_store.options_config = @options[:config] if @options[:config]
          # Save the todo configuration of the LineLength cop.
          File.read(AUTO_GENERATED_FILE).lines.drop_while { |line| line.start_with?('#') }.join
        end

        def run_all_cops(line_length_contents)
          puts Rainbow(PHASE_2).yellow
          result = execute_runner
          # This run was made with the current maximum length allowed, so append
          # the saved setting for LineLength.
          File.open(AUTO_GENERATED_FILE, 'a') { |f| f.write(line_length_contents) }
          result
        end

        def reset_config_and_auto_gen_file
          @config_store = ConfigStore.new
          @config_store.options_config = @options[:config] if @options[:config]
          File.open(AUTO_GENERATED_FILE, 'w') {} # create or truncate if exists
          add_inheritance_from_auto_generated_file(@options[:config])
        end

        def add_formatter
          @options[:formatters] << [Formatter::DisabledConfigFormatter, AUTO_GENERATED_FILE]
        end

        def execute_runner
          Environment.new(@options, @config_store, @paths).run(:execute_runner)
        end

        def add_inheritance_from_auto_generated_file(config_file)
          file_string = " #{relative_path_to_todo_from_options_config}"

          config_file ||= ConfigFinder::DOTFILE

          if File.exist?(config_file)
            files = Array(ConfigLoader.load_yaml_configuration(config_file)['inherit_from'])

            return if files.include?(relative_path_to_todo_from_options_config)

            files.unshift(relative_path_to_todo_from_options_config)
            file_string = "\n  - #{files.join("\n  - ")}" if files.size > 1
            rubocop_yml_contents = existing_configuration(config_file)
          end

          write_config_file(config_file, file_string, rubocop_yml_contents)

          puts "Added inheritance from `#{relative_path_to_todo_from_options_config}` " \
               "in `#{ConfigFinder::DOTFILE}`."
        end

        def existing_configuration(config_file)
          File.read(config_file, encoding: Encoding::UTF_8)
              .sub(/^inherit_from: *[^\n]+/, PLACEHOLDER)
              .sub(/^inherit_from: *(\n *- *[^\n]+)+/, PLACEHOLDER)
        end

        def write_config_file(file_name, file_string, rubocop_yml_contents)
          lines = /\S/.match?(rubocop_yml_contents) ? rubocop_yml_contents.split("\n", -1) : []
          unless rubocop_yml_contents&.include?(PLACEHOLDER)
            doc_start_index = lines.index { |line| YAML_OPTIONAL_DOC_START.match?(line) } || -1
            lines.insert(doc_start_index + 1, PLACEHOLDER)
          end
          File.write(file_name, lines.join("\n")
                                     .sub(/#{PLACEHOLDER}\n*/o, "inherit_from:#{file_string}\n\n")
                                     .sub(/\n\n+\Z/, "\n"))
        end

        def relative_path_to_todo_from_options_config
          return AUTO_GENERATED_FILE if !@options[:config] || options_config_in_root?

          base = Pathname.new('.')
          config_dir = Pathname.new(File.dirname(@options[:config]))

          "#{base.relative_path_from(config_dir)}/#{AUTO_GENERATED_FILE}"
        end

        def options_config_in_root?
          File.dirname(@options[:config]) == '.'
        end
      end
    end
  end
end