codeclimate/codeclimate-duplication

View on GitHub
lib/cc/engine/analyzers/analyzer_base.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# frozen_string_literal: true

# Monkey patch for Parser class
# used in language analyzers via Sexp::Matcher.parse
# https://github.com/seattlerb/sexp_processor/blob/master/lib/sexp_matcher.rb
class Sexp
  class Matcher < Sexp
    class Parser
      def parse_sexp
        token = next_token

        case token
        when "(" then
          parse_list
        when "[" then
          parse_cmd
        when "nil" then
          nil
        when /^\d+$/ then
          token.to_i
        when "___" then
          Sexp.___
        when "_" then
          Sexp._
        when %r%^/(.*)/$% then
          re = $1
          raise SyntaxError, "Not allowed: /%p/" % [re] unless
            re =~ /\A([\w()|.*+^$]+)\z/
          Regexp.new re
        when /^"(.*)"$/ then
          $1
        when /^([A-Z]\w*)$/ then
          if Object.const_defined?($1)
              Object.const_get $1
            else
              # Handle as a symbol or string
              $1.to_sym  # or return $1 as a string
            end
        when /^:?([\w?!=~-]+)$/ then
          $1.to_sym
        else
          raise SyntaxError, "unhandled token: %p" % [token]
        end
      end
    end
  end
end

require "cc/engine/analyzers/parser_error"
require "cc/engine/analyzers/parser_base"
require "cc/engine/analyzers/file_list"
require "cc/engine/processed_source"
require "cc/engine/sexp_builder"

module CC
  module Engine
    module Analyzers
      class Base
        RESCUABLE_ERRORS = [
          ::CC::Engine::Analyzers::ParserError,
          ::Errno::ENOENT,
          ::Racc::ParseError,
          ::RubyParser::SyntaxError,
          ::RuntimeError,
        ].freeze

        POINTS_PER_MINUTE = 10_000 # Points represent engineering time to resolve issue
        BASE_POINTS = 30 * POINTS_PER_MINUTE

        SEVERITIES = [
          MAJOR = "major".freeze,
          MINOR = "minor".freeze,
        ].freeze

        MAJOR_SEVERITY_THRESHOLD = 120 * POINTS_PER_MINUTE

        def initialize(engine_config:, parse_metrics:)
          @engine_config = engine_config
          @parse_metrics = parse_metrics
        end

        def run(file)
          if (skip_reason = skip?(file))
            CC.logger.info("Skipping file #{file} because #{skip_reason}")
            nil
          else
            process_file(file)
          end
        rescue => ex
          if RESCUABLE_ERRORS.map { |klass| ex.instance_of?(klass) }.include?(true)
            CC.logger.info("Skipping file #{file} due to exception (#{ex.class}): #{ex.message}\n")
            nil
          else
            CC.logger.info("#{ex.class} error occurred processing file #{file}: aborting.")
            raise ex
          end
        end

        def files
          file_list.files
        end

        def filters
          engine_config.filters_for(language) | default_filters
        end

        def post_filters
          engine_config.post_filters_for(language) | default_post_filters
        end

        def language
          self.class::LANGUAGE
        end

        def check_mass_threshold(check)
          engine_config.mass_threshold_for(language, check) || self.class::DEFAULT_MASS_THRESHOLD
        end

        def mass_threshold
          engine_config.minimum_mass_threshold_for(language) || self.class::DEFAULT_MASS_THRESHOLD
        end

        def count_threshold
          engine_config.count_threshold_for(language)
        end

        def calculate_points(violation)
          overage = violation.mass - check_mass_threshold(violation.check_name)
          base_points + (overage * points_per_overage)
        end

        def calculate_severity(points)
          if points >= MAJOR_SEVERITY_THRESHOLD
            MAJOR
          else
            MINOR
          end
        end

        def transform_sexp(sexp)
          sexp
        end

        # Please see: codeclimate/app#6227
        def use_sexp_lines?
          true
        end

        private

        attr_reader :engine_config, :parse_metrics

        def base_points
          self.class::BASE_POINTS
        end

        def default_filters
          []
        end

        def default_post_filters
          []
        end

        def points_per_overage
          self.class::POINTS_PER_OVERAGE
        end

        def process_file(_path)
          raise NoMethodError, "Subclass must implement `process_file`"
        end

        def file_list
          @_file_list ||= ::CC::Engine::Analyzers::FileList.new(
            engine_config: engine_config,
            patterns: engine_config.patterns_for(
              language,
              patterns,
            ),
          )
        end

        def skip?(_path)
          nil
        end

        def parse(file, request_path)
          processed_source = ProcessedSource.new(file, request_path)
          parse_metrics.incr(:succeeded)
          SexpBuilder.new(processed_source.ast, file).build
        rescue => ex
          handle_exception(processed_source, ex)
        end

        def handle_exception(processed_source, ex)
          CC.logger.debug { "Contents:\n#{processed_source.raw_source}" }

          case
          when ex.is_a?(CC::Parser::Client::HTTPError) && ex.response_status.to_s.start_with?("4")
            CC.logger.warn("Skipping #{processed_source.path} due to #{ex.class}")
            CC.logger.warn("Response status: #{ex.response_status}")
            CC.logger.debug { "Response:\n#{ex.response_body}" }
            parse_metrics.incr(ex.code.to_sym)
          when ex.is_a?(CC::Parser::Client::EncodingError)
            CC.logger.warn("Skipping #{processed_source.path} due to #{ex.class}: #{ex.message}")
            parse_metrics.incr(:encoding_error)
          when ex.is_a?(CC::Parser::Client::NestingDepthError)
            CC.logger.warn("Skipping #{processed_source.path} due to #{ex.class}")
            CC.logger.warn(ex.message)
            parse_metrics.incr(:client_nesting_depth_error)
          else
            CC.logger.error("Error processing file: #{processed_source.path}")
            CC.logger.error(ex.message)
            raise ex
          end
          nil
        end

        def patterns
          self.class::PATTERNS
        end
      end
    end
  end
end