rubocop-hq/rubocop

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

Summary

Maintainability
B
4 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
      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

        validate_config
      end

      # @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::Cop>]
      def self.mobilize_cops(cop_classes, config, options = {})
        cop_classes = Registry.new(cop_classes.to_a) unless cop_classes.is_a?(Registry)
        only = options.fetch(:only, [])
        safe = options.fetch(:safe, false)
        cop_classes.enabled(config, only, only_safe: safe).map do |cop_class|
          cop_class.new(config, options)
        end
      end

      def autocorrect?
        @options[:auto_correct]
      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)
        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 auto-correcting cops by themselves, and only
        # run the other cops when no corrections are left
        on_duty = roundup_relevant_cops(processed_source.file_path)

        autocorrect_cops, other_cops = on_duty.partition(&:autocorrect?)

        report = investigate_partial(autocorrect_cops, processed_source)

        unless autocorrect(processed_source, report)
          # 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))
        end

        process_errors(processed_source.path, report.errors)

        report
      ensure
        @ready = false
      end

      # @deprecated
      def forces
        @forces ||= self.class.forces_for(cops)
      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 do |force_class, joining_cops|
          force_class.new(joining_cops)
        end
      end

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

      private

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

        new_source = autocorrect_report(report)

        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.buffer.name
          File.open(filename, 'w') { |f| f.write(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)
        commissioner = Commissioner.new(cops, self.class.forces_for(cops), @options)
        commissioner.investigate(processed_source)
      end

      # @return [Array<cop>]
      def roundup_relevant_cops(filename)
        cops.reject do |cop|
          cop.excluded_file?(filename) ||
            !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)
        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)
        corrector = collate_corrections(report)

        corrector.rewrite unless corrector.empty?
      end

      def collate_corrections(report)
        corrector = Corrector.new(report.processed_source)

        each_corrector(report) do |to_merge|
          suppress_clobbering do
            corrector.merge!(to_merge)
          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