Class: RuboCop::Runner
| Relationships & Source Files | |
| Namespace Children | |
|
Exceptions:
| |
| Extension / Inclusion / Inheritance Descendants | |
|
Subclasses:
|
|
| Inherits: | Object |
| Defined in: | lib/rubocop/runner.rb |
Overview
This class handles the processing of files, which includes dealing with formatters and letting cops inspect the files.
Constant Summary
-
MAX_ITERATIONS =
# File 'lib/rubocop/runner.rb', line 49200 -
REDUNDANT_COP_DISABLE_DIRECTIVE_RULES =
# File 'lib/rubocop/runner.rb', line 52%w[ Lint/RedundantCopDisableDirective RedundantCopDisableDirective Lint ].freeze
Class Method Summary
Instance Attribute Summary
- #aborting=(value) rw
- #aborting? ⇒ Boolean rw
- #errors readonly
- #warnings readonly
- #cached_run? ⇒ Boolean readonly private
- #except_redundant_cop_disable_directive? ⇒ Boolean readonly private
-
#in_memory_corrections_possible? ⇒ Boolean
readonly
private
Custom ruby extractors may derive their fragments from the file on disk rather than from the passed processed source, so corrections can only be kept in memory when the default extractor is used.
- #project_index_disables_parallel? ⇒ Boolean readonly private
- #project_index_enabled? ⇒ Boolean readonly private
Instance Method Summary
- #run(paths)
- #add_redundant_disables(file, offenses, source) private
- #cached_result(file, team) private
-
#check_for_infinite_loop(processed_source, offenses_by_iteration)
private
Check whether a run created source identical to a previous run, which means that we definitely have an infinite loop.
- #check_for_redundant_disables?(source) ⇒ Boolean private
- #considered_failure?(offense) ⇒ Boolean private
- #default_config(cop_name) private
- #do_inspection_loop(file) private
-
#emulate_write_read_cycle(source)
private
When corrections were written to disk and read back between iterations, the text-mode write converted LF to CRLF on Windows, and cops like
Layout/EndOfLinerely on seeing the source as it would be on disk. - #extract_ruby_sources(processed_source) private
- #file_finished(file, offenses) private
- #file_iterator(files, &block) private
- #file_offense_cache(file) private
- #file_offenses(file) private
- #file_started(file) private
- #filter_cop_classes(cop_classes, config) private
- #find_target_files(paths) private
- #finished_report(file, index, offenses) private
- #formatter_set private
- #get_processed_source(file, prism_result, source: nil) private
- #inspect_file(processed_source, team = mobilize_team(processed_source)) private
- #inspect_files(files) private
- #inspect_iteration(processed_source) private
- #iterate_until_no_changes(source, offenses_by_iteration) private
- #list_files(paths) private
- #mark_as_safe_by_config?(config) ⇒ Boolean private
- #minimum_severity_to_fail private
- #mobilize_team(processed_source) private
- #mobilized_cop_classes(config) private
- #offense_displayed?(offense) ⇒ Boolean private
- #offenses_to_report(offenses) private
- #parallel_file_iterator(files, on_start, on_finish, &block) private
- #process_file(file) private
- #process_remaining_report_queue private
- #process_report_queue_entry(index) private
- #qualify_option_cop_names private
- #redundant_cop_disable_directive(file) {|cop| ... } private
- #run_in_parallel?(files) ⇒ Boolean private
- #save_in_cache(cache, offenses) private
- #serial_file_iterator(files, on_start, on_finish, &block) private
-
#standby_team(config)
private
A Cop::Team instance is stateful and may change when inspecting.
- #style_guide_cops_only?(config) ⇒ Boolean private
- #supports_safe_autocorrect?(offense) ⇒ Boolean private
- #team_for_redundant_disables(file, offenses, source) {|team| ... } private
Constructor Details
.new(options, config_store) ⇒ Runner
# File 'lib/rubocop/runner.rb', line 59
def initialize(, config_store) @options = @config_store = config_store @errors = [] @warnings = [] @aborting = false @inspected_files = [] @report_queue = {} end
Class Method Details
.default_ruby_extractor ⇒ #call (private)
# File 'lib/rubocop/runner.rb', line 36
def default_ruby_extractor lambda do |processed_source| [ { offset: 0, processed_source: processed_source } ] end end
.ruby_extractors ⇒ Array<#call>
# File 'lib/rubocop/runner.rb', line 29
def ruby_extractors @ruby_extractors ||= [default_ruby_extractor] end
Instance Attribute Details
#aborting=(value) (rw)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 57
attr_writer :aborting
#aborting? ⇒ Boolean (rw)
[ GitHub ]
# File 'lib/rubocop/runner.rb', line 90
def aborting? @aborting end
#cached_run? ⇒ Boolean (readonly, private)
[ GitHub ]
# File 'lib/rubocop/runner.rb', line 324
def cached_run? @cached_run ||= (@options[:cache] == 'true' || (@options[:cache] != 'false' && @config_store.for_pwd.for_all_cops['UseCache'])) && # We can't cache results from code which is piped in to stdin !@options[:stdin] end
#errors (readonly)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 56
attr_reader :errors, :warnings
#except_redundant_cop_disable_directive? ⇒ Boolean (readonly, private)
[ GitHub ]
# File 'lib/rubocop/runner.rb', line 309
def except_redundant_cop_disable_directive? @options[:except] && (@options[:except] & REDUNDANT_COP_DISABLE_DIRECTIVE_RULES).any? end
#in_memory_corrections_possible? ⇒ Boolean (readonly, private)
Custom ruby extractors may derive their fragments from the file on disk rather than from the passed processed source, so corrections can only be kept in memory when the default extractor is used.
# File 'lib/rubocop/runner.rb', line 404
def in_memory_corrections_possible? self.class.ruby_extractors.one? end
#project_index_disables_parallel? ⇒ Boolean (readonly, private)
[ GitHub ]
# File 'lib/rubocop/runner.rb', line 193
def project_index_disables_parallel? return false if @project_index.nil? || !Gem.win_platform? if @options[:debug] puts 'Skipping parallel inspection: the project index is enabled and parallel ' \ 'inspection is not yet supported on Windows.' end true end
#project_index_enabled? ⇒ Boolean (readonly, private)
[ GitHub ]
# File 'lib/rubocop/runner.rb', line 107
def project_index_enabled? return false unless @config_store.for_pwd.for_all_cops['UseProjectIndex'] unless ProjectIndexLoader.available? ProjectIndexLoader.warn_unavailable return false end true end
#warnings (readonly)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 56
attr_reader :errors, :warnings
Instance Method Details
#add_redundant_disables(file, offenses, source) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 269
def add_redundant_disables(file, offenses, source) team_for_redundant_disables(file, offenses, source) do |team| new_offenses, redundant_updated = inspect_file(source, team) offenses += new_offenses if redundant_updated # Do one extra inspection loop if any redundant disables were # removed. This is done in order to find rubocop:enable directives that # have now become useless. _source, new_offenses = do_inspection_loop(file) offenses |= new_offenses end end offenses end
#cached_result(file, team) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 243
def cached_result(file, team) ResultCache.new(file, team, @options, @config_store) end
#check_for_infinite_loop(processed_source, offenses_by_iteration) (private)
Check whether a run created source identical to a previous run, which means that we definitely have an infinite loop.
# File 'lib/rubocop/runner.rb', line 433
def check_for_infinite_loop(processed_source, offenses_by_iteration) checksum = processed_source.checksum if (loop_start_index = @processed_sources.index(checksum)) raise InfiniteCorrectionLoop.new( processed_source.path, offenses_by_iteration, loop_start: loop_start_index ) end @processed_sources << checksum end
#check_for_redundant_disables?(source) ⇒ Boolean (private)
# File 'lib/rubocop/runner.rb', line 295
def check_for_redundant_disables?(source) return false if source.disabled_line_ranges.empty? || except_redundant_cop_disable_directive? !@options[:only] end
#considered_failure?(offense) ⇒ Boolean (private)
# File 'lib/rubocop/runner.rb', line 536
def considered_failure?(offense) return false if offense.disabled? # For :autocorrect level, any correctable offense is a failure, regardless of severity return true if @options[:fail_level] == :autocorrect && offense.correctable? !offense.corrected? && offense.severity >= minimum_severity_to_fail end
#default_config(cop_name) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 573
def default_config(cop_name) RuboCop::ConfigLoader.default_configuration[cop_name] end
#do_inspection_loop(file) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 342
def do_inspection_loop(file) # We can reuse the prism result since the source did not change yet. processed_source = get_processed_source(file, @prism_result) # This variable is 2d array used to track corrected offenses after each # inspection iteration. This is used to output meaningful infinite loop # error message. offenses_by_iteration = [] corrected_source = nil # When running with --autocorrect, we need to inspect the file until no # more corrections are made. This is because automatic corrections can # introduce new offenses. In the normal case the loop is only executed # once. The corrections are kept in memory while iterating and written # back to the file when the loop is done. iterate_until_no_changes(processed_source, offenses_by_iteration) do # The offenses that couldn't be corrected will be found again so we # only keep the corrected ones in order to avoid duplicate reporting. !offenses_by_iteration.empty? && offenses_by_iteration.last.select!(&:corrected?) team, new_offenses, updated_source_file = inspect_iteration(processed_source) offenses_by_iteration.push(new_offenses) # We have to reprocess the source to pickup the changes. Since the # change could (theoretically) introduce parsing errors, we break the # loop if we find any. break unless updated_source_file # Autocorrect has happened, don't use the prism result since it is stale. # With --stdin the corrected source is kept in @options[:stdin] instead. corrected_source = team.updated_source processed_source = get_processed_source(file, nil, source: corrected_source) end # Return summary of corrected offenses after all iterations [processed_source, offenses_by_iteration.flatten.uniq] ensure # Write the file once, even when the loop was left through an exception # (e.g. an infinite correction loop), like the per-iteration writes # used to be. File.write(file, corrected_source) if corrected_source end
#emulate_write_read_cycle(source) (private)
When corrections were written to disk and read back between iterations,
the text-mode write converted LF to CRLF on Windows, and cops like
Layout/EndOfLine rely on seeing the source as it would be on disk.
Apply the same conversion to the in-memory source. The final File.write
still performs it for the file itself.
#extract_ruby_sources(processed_source) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 455
def extract_ruby_sources(processed_source) self.class.ruby_extractors.find do |ruby_extractor| result = ruby_extractor.call(processed_source) break result if result rescue StandardError location = if ruby_extractor.is_a?(Proc) ruby_extractor.source_location else ruby_extractor.method(:call).source_location end raise Error, "Ruby extractor #{location[0]} failed to process #{processed_source.path}." end end
#file_finished(file, offenses) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 318
def file_finished(file, offenses) @inspected_files << file offenses = offenses_to_report(offenses) formatter_set.file_finished(file, offenses) end
#file_iterator(files, &block) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 137
def file_iterator(files, &block) all_passed = true on_start = ->(file, _index) { file_started(file) } on_finish = lambda do |file, index, (offenses, passed)| all_passed &&= passed finished_report(file, index, offenses) end if run_in_parallel?(files) parallel_file_iterator(files, on_start, on_finish, &block) else serial_file_iterator(files, on_start, on_finish, &block) end process_remaining_report_queue all_passed end
#file_offense_cache(file) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 247
def file_offense_cache(file) config = @config_store.for_file(file) cache = cached_result(file, standby_team(config)) if cached_run? if cache&.valid? offenses = cache.load # If we're running --autocorrect and the cache says there are # offenses, we need to actually inspect the file. If the cache shows no # offenses, we're good. real_run_needed = @options[:autocorrect] && offenses.any? else real_run_needed = true end if real_run_needed offenses = yield save_in_cache(cache, offenses) unless Cop::Registry.global.warnings?(file) end offenses end
#file_offenses(file) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 235
def file_offenses(file) file_offense_cache(file) do source, offenses = do_inspection_loop(file) offenses = add_redundant_disables(file, offenses.compact.sort, source) offenses.sort.reject(&:disabled?).freeze end end
#file_started(file) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 313
def file_started(file) puts "Scanning #{file}" if @options[:debug] formatter_set.file_started(file, cli_options: @options, config_store: @config_store) end
#filter_cop_classes(cop_classes, config) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 516
def filter_cop_classes(cop_classes, config) # use only cops that link to a style guide if requested return unless style_guide_cops_only?(config) cop_classes.select! { |cop| config.for_cop(cop)['StyleGuide'] } end
#find_target_files(paths) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 96
def find_target_files(paths) target_finder = TargetFinder.new(@config_store, @options) mode = if @options[:only_recognized_file_types] :only_recognized_file_types else :all_file_types end target_files = target_finder.find(paths, mode) target_files.each(&:freeze).freeze end
#finished_report(file, index, offenses) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 157
def finished_report(file, index, offenses) @report_queue[index] = [file, offenses] @next_index_to_report ||= 0 while @report_queue.key?(@next_index_to_report) process_report_queue_entry(@next_index_to_report) @next_index_to_report += 1 end end
#formatter_set (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 527
def formatter_set @formatter_set ||= begin set = Formatter::FormatterSet.new(@options) pairs = @options[:formatters] || [['progress']] pairs.each { |formatter_key, output_path| set.add_formatter(formatter_key, output_path) } set end end
#get_processed_source(file, prism_result, source: nil) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 588
def get_processed_source(file, prism_result, source: nil) config = @config_store.for_file(file) ruby_version = config.target_ruby_version parser_engine = config.parser_engine processed_source = if source ProcessedSource.new( emulate_write_read_cycle(source), ruby_version, file, parser_engine: parser_engine ) elsif @options[:stdin] ProcessedSource.new( @options[:stdin], ruby_version, file, parser_engine: parser_engine, prism_result: prism_result ) else begin ProcessedSource.from_file( file, ruby_version, parser_engine: parser_engine ) rescue Errno::ENOENT raise RuboCop::Error, "No such file or directory: #{file}" end end processed_source.config = config processed_source.registry = mobilized_cop_classes(config) processed_source end
#inspect_file(processed_source, team = mobilize_team(processed_source)) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 447
def inspect_file(processed_source, team = mobilize_team(processed_source)) extracted_ruby_sources = extract_ruby_sources(processed_source) offenses = team.investigate_fragments(extracted_ruby_sources, original: processed_source) @errors.concat(team.errors) @warnings.concat(team.warnings) [offenses, team.updated_source_file?] end
#inspect_files(files) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 118
def inspect_files(files) # rubocop:disable Metrics/AbcSize formatter_set.started(files) file_iterator(files) do |file| offenses = process_file(file) succeeded = offenses.none? { |o| considered_failure?(o) && offense_displayed?(o) } [offenses, succeeded] end ensure # OPTIMIZE: Calling `ResultCache.cleanup` takes time. This optimization # mainly targets editors that integrates RuboCop. When RuboCop is run # by an editor, it should be inspecting only one file. if files.size > 1 && cached_run? ResultCache.cleanup(@config_store, @options[:debug], @options[:cache_root]) end formatter_set.finished(@inspected_files.freeze) formatter_set.close_output_files end
#inspect_iteration(processed_source) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 383
def inspect_iteration(processed_source) team = mobilize_team(processed_source) team.defer_corrections = in_memory_corrections_possible? offenses, updated_source_file = inspect_file(processed_source, team) [team, offenses, updated_source_file] end
#iterate_until_no_changes(source, offenses_by_iteration) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 408
def iterate_until_no_changes(source, offenses_by_iteration) # Keep track of the state of the source. If a cop modifies the source # and another cop undoes it producing identical source we have an # infinite loop. @processed_sources = [] # It is also possible for a cop to keep adding indefinitely to a file, # making it bigger and bigger. If the inspection loop runs for an # excessively high number of iterations, this is likely happening. iterations = 0 loop do check_for_infinite_loop(source, offenses_by_iteration) if (iterations += 1) > MAX_ITERATIONS raise InfiniteCorrectionLoop.new(source.path, offenses_by_iteration) end source = yield break unless source end end
#list_files(paths) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 221
def list_files(paths) paths.each { |path| puts PathUtil.relative_path(path) } end
#mark_as_safe_by_config?(config) ⇒ Boolean (private)
#minimum_severity_to_fail (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 577
def minimum_severity_to_fail @minimum_severity_to_fail ||= begin # Unless given explicitly as `fail_level`, `:info` severity offenses do not fail name = @options[:fail_level] || :refactor # autocorrect is a fake level - use the default RuboCop::Cop::Severity.new(name == :autocorrect ? :refactor : name) end end
#mobilize_team(processed_source) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 469
def mobilize_team(processed_source) config = @config_store.for_file(processed_source.path) team = Cop::Team.mobilize(mobilized_cop_classes(config), config, @options) if @project_index team.cops.each do |cop| cop.project_index = @project_index end end team end
#mobilized_cop_classes(config) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 482
def mobilized_cop_classes(config) # rubocop:disable Metrics/AbcSize @mobilized_cop_classes ||= {}.compare_by_identity @mobilized_cop_classes[config] ||= begin cop_classes = Cop::Registry.all # `@options[:only]` and `@options[:except]` are not qualified until # needed so that the Registry can be fully loaded, including any # cops added by `require`s. qualify_option_cop_names OptionsValidator.new(@options). if @options[:only] cop_classes.select! { |c| c.match?(@options[:only]) } else filter_cop_classes(cop_classes, config) end cop_classes.reject! { |c| c.match?(@options[:except]) } Cop::Registry.new(cop_classes, @options) end end
#offense_displayed?(offense) ⇒ Boolean (private)
# File 'lib/rubocop/runner.rb', line 545
def offense_displayed?(offense) if @options[:display_only_fail_level_offenses] considered_failure?(offense) elsif @options[:display_only_safe_correctable] supports_safe_autocorrect?(offense) elsif @options[:display_only_correctable] offense.correctable? else true end end
#offenses_to_report(offenses) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 557
def offenses_to_report(offenses) offenses.select { |o| offense_displayed?(o) } end
#parallel_file_iterator(files, on_start, on_finish, &block) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 204
def parallel_file_iterator(files, on_start, on_finish, &block) Parallel.each(files, start: on_start, finish: on_finish, &block) end
#process_file(file) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 225
def process_file(file) file_offenses(file) rescue InfiniteCorrectionLoop => e raise e if @options[:raise_cop_error] errors << e warn Rainbow(e.).red e.offenses.compact.sort.freeze end
#process_remaining_report_queue (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 171
def process_remaining_report_queue @report_queue.keys.sort.each do |index| process_report_queue_entry(index) end end
#process_report_queue_entry(index) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 166
def process_report_queue_entry(index) file, offenses = @report_queue.delete(index) file_finished(file, offenses) end
#qualify_option_cop_names (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 506
def qualify_option_cop_names %i[only except].each do |option| next unless @options[option] @options[option].map! do |cop_name| Cop::Registry.qualified_cop_name(cop_name, "--#{option} option") end end end
#redundant_cop_disable_directive(file) {|cop| ... } (private)
# File 'lib/rubocop/runner.rb', line 301
def redundant_cop_disable_directive(file) config = @config_store.for_file(file) return unless config.for_cop(Cop::Lint::RedundantCopDisableDirective).fetch('Enabled') cop = Cop::Lint::RedundantCopDisableDirective.new(config, @options) yield cop if cop.relevant_file?(file) end
#run(paths)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 69
def run(paths) # Compute the cache source checksum once to avoid potential # inconsistencies between workers. ResultCache.source_checksum target_files = find_target_files(paths) @project_index = ProjectIndexLoader.build_index(target_files) if project_index_enabled? if @options[:list_target_files] list_files(target_files) else inspect_files(target_files) end rescue Interrupt self.aborting = true warn '' warn 'Exiting...' false end
#run_in_parallel?(files) ⇒ Boolean (private)
# File 'lib/rubocop/runner.rb', line 177
def run_in_parallel?(files) return false if @options[:auto_gen_config] return false unless @options[:parallel] if files.size <= 1 puts 'Skipping parallel inspection: only a single file needs inspection' if @options[:debug] return false end return false if project_index_disables_parallel? puts 'Running parallel inspection' if @options[:debug] true end
#save_in_cache(cache, offenses) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 332
def save_in_cache(cache, offenses) return unless cache # Caching results when a cop has crashed would prevent the crash in the # next run, since the cop would not be called then. We want crashes to # show up the same in each run. return if errors.any? || warnings.any? cache.save(offenses) end
#serial_file_iterator(files, on_start, on_finish, &block) (private)
[ GitHub ]# File 'lib/rubocop/runner.rb', line 208
def serial_file_iterator(files, on_start, on_finish, &block) files.each_with_index do |file, index| on_start.call(file, index) result = yield file on_finish.call(file, index, result) # Report and count the offending file before stopping so `--fail-fast` # still shows its offenses and exits with a failing status. _offenses, succeeded = result break if @options[:fail_fast] && !succeeded end end
#standby_team(config) (private)
A Cop::Team instance is stateful and may change when inspecting. The "standby" team for a given config is an initialized but otherwise dormant team that can be used for config- and option- level caching in ResultCache.
# File 'lib/rubocop/runner.rb', line 627
def standby_team(config) @team_by_config ||= {}.compare_by_identity @team_by_config[config] ||= begin team = Cop::Team.mobilize(mobilized_cop_classes(config), config, @options) if @project_index team.cops.each do |cop| cop.project_index = @project_index end end team end end
#style_guide_cops_only?(config) ⇒ Boolean (private)
#supports_safe_autocorrect?(offense) ⇒ Boolean (private)
# File 'lib/rubocop/runner.rb', line 561
def supports_safe_autocorrect?(offense) cop_class = Cop::Registry.global.find_by_cop_name(offense.cop_name) default_cfg = default_config(offense.cop_name) offense.correctable? && cop_class&.support_autocorrect? && mark_as_safe_by_config?(default_cfg) end
#team_for_redundant_disables(file, offenses, source) {|team| ... } (private)
# File 'lib/rubocop/runner.rb', line 284
def team_for_redundant_disables(file, offenses, source) return unless check_for_redundant_disables?(source) config = @config_store.for_file(file) team = Cop::Team.mobilize([Cop::Lint::RedundantCopDisableDirective], config, @options) return if team.cops.empty? team.cops.first.offenses_to_check = offenses yield team end