# frozen_string_literal: true module RuboCop module Formatter # This formatter displays a YAML configuration file where all cops that # detected any offenses are configured to not detect the offense. class DisabledConfigFormatter < BaseFormatter HEADING = <<~COMMENTS # This configuration was generated by # `%s` # %susing RuboCop version #{Version.version}. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. COMMENTS @config_to_allow_offenses = {} @detected_styles = {} class << self attr_accessor :config_to_allow_offenses, :detected_styles end def initialize(output, options = {}) super @cops_with_offenses ||= Hash.new(0) @files_with_offenses ||= {} end def file_started(_file, _file_info) @exclude_limit_option = @options[:exclude_limit] @exclude_limit = Integer(@exclude_limit_option || RuboCop::Options::DEFAULT_MAXIMUM_EXCLUSION_ITEMS) @show_offense_counts = !@options[:no_offense_counts] end def file_finished(file, offenses) offenses.each do |o| @cops_with_offenses[o.cop_name] += 1 @files_with_offenses[o.cop_name] ||= Set.new @files_with_offenses[o.cop_name] << file end end def finished(_inspected_files) output.puts format(HEADING, command: command, timestamp: timestamp) # Syntax isn't a real cop and it can't be disabled. @cops_with_offenses.delete('Lint/Syntax') output_offenses puts "Created #{output.path}." end private def command command = 'rubocop --auto-gen-config' if @options[:auto_gen_only_exclude] command += ' --auto-gen-only-exclude' end if @exclude_limit_option command += format(' --exclude-limit %d', limit: Integer(@exclude_limit_option)) end command += ' --no-offense-counts' if @options[:no_offense_counts] if @options[:no_auto_gen_timestamp] command += ' --no-auto-gen-timestamp' end command end def timestamp @options[:no_auto_gen_timestamp] ? '' : "on #{Time.now} " end def output_offenses @cops_with_offenses.sort.each do |cop_name, offense_count| output_cop(cop_name, offense_count) end end def output_cop(cop_name, offense_count) output.puts cfg = self.class.config_to_allow_offenses[cop_name] || {} set_max(cfg, cop_name) # To avoid malformed YAML when potentially reading the config in # #excludes, we use an output buffer and append it to the actual output # only when it results in valid YAML. output_buffer = StringIO.new output_cop_comments(output_buffer, cfg, cop_name, offense_count) output_cop_config(output_buffer, cfg, cop_name) output.puts(output_buffer.string) end def set_max(cfg, cop_name) return unless cfg[:exclude_limit] # In case auto_gen_only_exclude is set, only modify the maximum if the # files are not excluded one by one. if !@options[:auto_gen_only_exclude] || @files_with_offenses[cop_name].size > @exclude_limit cfg.merge!(cfg[:exclude_limit]) end # Remove already used exclude_limit. cfg.reject! { |key| key == :exclude_limit } end def output_cop_comments(output_buffer, cfg, cop_name, offense_count) if @show_offense_counts output_buffer.puts "# Offense count: #{offense_count}" end cop_class = Cop::Cop.registry.find_by_cop_name(cop_name) if cop_class&.new&.support_autocorrect? output_buffer.puts '# Cop supports --auto-correct.' end default_cfg = default_config(cop_name) return unless default_cfg params = cop_config_params(default_cfg, cfg) return if params.empty? output_cop_param_comments(output_buffer, params, default_cfg) end def cop_config_params(default_cfg, cfg) default_cfg.keys - %w[Description StyleGuide Reference Enabled Exclude Safe SafeAutoCorrect VersionAdded VersionChanged VersionRemoved] - cfg.keys end def output_cop_param_comments(output_buffer, params, default_cfg) config_params = params.reject { |p| p.start_with?('Supported') } output_buffer.puts( "# Configuration parameters: #{config_params.join(', ')}." ) params.each do |param| value = default_cfg[param] next unless value.is_a?(Array) next if value.empty? output_buffer.puts "# #{param}: #{value.join(', ')}" end end def default_config(cop_name) RuboCop::ConfigLoader.default_configuration[cop_name] end def output_cop_config(output_buffer, cfg, cop_name) # 'Enabled' option will be put into file only if exclude # limit is exceeded. cfg_without_enabled = cfg.reject { |key| key == 'Enabled' } output_buffer.puts "#{cop_name}:" cfg_without_enabled.each do |key, value| value = value[0] if value.is_a?(Array) output_buffer.puts " #{key}: #{value}" end output_offending_files(output_buffer, cfg_without_enabled, cop_name) end def output_offending_files(output_buffer, cfg, cop_name) return unless cfg.empty? offending_files = @files_with_offenses[cop_name].sort if offending_files.count > @exclude_limit output_buffer.puts ' Enabled: false' else output_exclude_list(output_buffer, offending_files, cop_name) end end def output_exclude_list(output_buffer, offending_files, cop_name) require 'pathname' parent = Pathname.new(Dir.pwd) output_buffer.puts ' Exclude:' excludes(offending_files, cop_name, parent).each do |exclude_path| output_exclude_path(output_buffer, exclude_path, parent) end end def excludes(offending_files, cop_name, parent) # Exclude properties in .rubocop_todo.yml override default ones, as well # as any custom excludes in .rubocop.yml, so in order to retain those # excludes we must copy them. # There can be multiple .rubocop.yml files in subdirectories, but we # just look at the current working directory config = ConfigStore.new.for(parent) cfg = config[cop_name] || {} ((cfg['Exclude'] || []) + offending_files).uniq end def output_exclude_path(output_buffer, exclude_path, parent) # exclude_path is either relative path, an absolute path, or a regexp. file_path = Pathname.new(exclude_path) relative = file_path.relative_path_from(parent) output_buffer.puts " - '#{relative}'" rescue ArgumentError file = exclude_path output_buffer.puts " - '#{file}'" rescue TypeError regexp = exclude_path output_buffer.puts " - !ruby/regexp /#{regexp.source}/" end end end end