lib/cc/engine/analyzers/analyzer_base.rb
# 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