rubocop-hq/rubocop

View on GitHub
lib/rubocop/cop/team.rb

Summary

Maintainability
B
6 hrs
Test Coverage
A
93%
# frozen_string_literal: true

module RuboCop
  module Cop
    # A group of cops, ready to be called on duty to inspect files.
    # Team is responsible for selecting only relevant cops to be sent on duty,
    # as well as insuring that the needed forces are sent along with them.
    #
    # For performance reasons, Team will first dispatch cops & forces in two groups,
    # first the ones needed for autocorrection (if any), then the rest
    # (unless autocorrections happened).
    class Team
      # @return [Team]
      def self.new(cop_or_classes, config, options = {})
        # Support v0 api:
        return mobilize(cop_or_classes, config, options) if cop_or_classes.first.is_a?(Class)

        super
      end

      # @return [Team] with cops assembled from the given `cop_classes`
      def self.mobilize(cop_classes, config, options = {})
        cops = mobilize_cops(cop_classes, config, options)
        new(cops, config, options)
      end

      # @return [Array<Cop::Base>]
      def self.mobilize_cops(cop_classes, config, options = {})
        cop_classes = Registry.new(cop_classes.to_a, options) unless cop_classes.is_a?(Registry)

        cop_classes.map do |cop_class|
          cop_class.new(config, options)
        end
      end

      # @return [Array<Force>] needed for the given cops
      def self.forces_for(cops)
        needed = Hash.new { |h, k| h[k] = [] }
        cops.each do |cop|
          forces = cop.class.joining_forces
          if forces.is_a?(Array)
            forces.each { |force| needed[force] << cop }
          elsif forces
            needed[forces] << cop
          end
        end

        needed.map { |force_class, joining_cops| force_class.new(joining_cops) }
      end

      attr_reader :errors, :warnings, :updated_source_file, :cops

      alias updated_source_file? updated_source_file

      def initialize(cops, config = nil, options = {})
        @cops = cops
        @config = config
        @options = options
        reset
        @ready = true
        @registry = Registry.new(cops, options.dup)

        validate_config
      end

      def autocorrect?
        @options[:autocorrect]
      end

      def debug?
        @options[:debug]
      end

      # @deprecated. Use investigate
      # @return Array<offenses>
      def inspect_file(processed_source)
        investigate(processed_source).offenses
      end

      # @return [Commissioner::InvestigationReport]
      def investigate(processed_source, offset: 0, original: processed_source)
        be_ready

        # The autocorrection process may have to be repeated multiple times
        # until there are no corrections left to perform
        # To speed things up, run autocorrecting cops by themselves, and only
        # run the other cops when no corrections are left
        on_duty = roundup_relevant_cops(processed_source)

        autocorrect_cops, other_cops = on_duty.partition(&:autocorrect?)
        report = investigate_partial(autocorrect_cops, processed_source,
                                     offset: offset, original: original)

        unless autocorrect(processed_source, report, offset: offset, original: original)
          # If we corrected some errors, another round of inspection will be
          # done, and any other offenses will be caught then, so only need
          # to check other_cops if no correction was done
          report = report.merge(investigate_partial(other_cops, processed_source,
                                                    offset: offset, original: original))
        end

        process_errors(processed_source.path, report.errors)

        report
      ensure
        @ready = false
      end

      # @deprecated
      def forces
        @forces ||= self.class.forces_for(cops)
      end

      def external_dependency_checksum
        keys = cops.filter_map(&:external_dependency_checksum)
        Digest::SHA1.hexdigest(keys.join)
      end

      private

      def autocorrect(processed_source, report, original:, offset:)
        @updated_source_file = false
        return unless autocorrect?
        return if report.processed_source.parser_error

        new_source = autocorrect_report(report, original: original, offset: offset)

        return unless new_source

        if @options[:stdin]
          # holds source read in from stdin, when --stdin option is used
          @options[:stdin] = new_source
        else
          filename = processed_source.file_path
          File.write(filename, new_source)
        end
        @updated_source_file = true
      end

      def be_ready
        return if @ready

        reset
        @cops.map!(&:ready)
        @ready = true
      end

      def reset
        @errors = []
        @warnings = []
      end

      # @return [Commissioner::InvestigationReport]
      def investigate_partial(cops, processed_source, offset:, original:)
        commissioner = Commissioner.new(cops, self.class.forces_for(cops), @options)
        commissioner.investigate(processed_source, offset: offset, original: original)
      end

      # @return [Array<cop>]
      def roundup_relevant_cops(processed_source)
        cops.select do |cop|
          next true if processed_source.comment_config.cop_opted_in?(cop)
          next false if cop.excluded_file?(processed_source.file_path)
          next false unless @registry.enabled?(cop, @config)

          support_target_ruby_version?(cop) && support_target_rails_version?(cop)
        end
      end

      def support_target_ruby_version?(cop)
        return true unless cop.class.respond_to?(:support_target_ruby_version?)

        cop.class.support_target_ruby_version?(cop.target_ruby_version)
      end

      def support_target_rails_version?(cop)
        # In this case, the rails version was already checked by `#excluded_file?`
        return true if defined?(RuboCop::Rails::TargetRailsVersion::USES_REQUIRES_GEM_API)

        return true unless cop.class.respond_to?(:support_target_rails_version?)

        cop.class.support_target_rails_version?(cop.target_rails_version)
      end

      def autocorrect_report(report, offset:, original:)
        corrector = collate_corrections(report, offset: offset, original: original)

        corrector.rewrite unless corrector.empty?
      end

      def collate_corrections(report, offset:, original:)
        corrector = Corrector.new(original)

        each_corrector(report) do |to_merge|
          suppress_clobbering do
            if offset.positive?
              corrector.import!(to_merge, offset: offset)
            else
              corrector.merge!(to_merge)
            end
          end
        end

        corrector
      end

      def each_corrector(report)
        skips = Set.new
        report.cop_reports.each do |cop_report|
          cop = cop_report.cop
          corrector = cop_report.corrector

          next if corrector.nil? || corrector.empty?
          next if skips.include?(cop.class)

          yield corrector

          skips.merge(cop.class.autocorrect_incompatible_with)
        end
      end

      def suppress_clobbering
        yield
      rescue ::Parser::ClobberingError
        # ignore Clobbering errors
      end

      def validate_config
        cops.each do |cop|
          cop.validate_config if cop.respond_to?(:validate_config)
        end
      end

      def process_errors(file, errors)
        errors.each do |error|
          line = ":#{error.line}" if error.line
          column = ":#{error.column}" if error.column
          location = "#{file}#{line}#{column}"
          cause = error.cause

          if cause.is_a?(Warning)
            handle_warning(cause, location)
          else
            handle_error(cause, location, error.cop)
          end
        end
      end

      def handle_warning(error, location)
        message = Rainbow("#{error.message} (from file: #{location})").yellow

        @warnings << message
        warn message
        puts error.backtrace if debug?
      end

      def handle_error(error, location, cop)
        message = Rainbow("An error occurred while #{cop.name} cop was inspecting #{location}.").red
        @errors << message
        warn message
        if debug?
          puts error.message, error.backtrace
        else
          warn 'To see the complete backtrace run rubocop -d.'
        end
      end
    end
  end
end