lib/reek/smell_detectors/base_detector.rb
# frozen_string_literal: true
require 'set'
require_relative '../smell_warning'
require_relative '../smell_configuration'
module Reek
module SmellDetectors
#
# Shared responsibilities of all smell detectors.
#
# See
# - {file:docs/Basic-Smell-Options.md}
# - {file:docs/Code-Smells.md}
# - {file:README.md}
# for details.
#
# @quality :reek:UnusedPrivateMethod { exclude: [ smell_warning ] }
# @quality :reek:TooManyMethods { max_methods: 18 }
class BaseDetector
attr_reader :config
# The name of the config field that lists the names of code contexts
# that should not be checked. Add this field to the config for each
# smell that should ignore this code element.
EXCLUDE_KEY = 'exclude'
# The default value for the +EXCLUDE_KEY+ if it isn't specified
# in any configuration file.
DEFAULT_EXCLUDE_SET = [].freeze
def initialize(configuration: {}, context: nil)
@config = SmellConfiguration.new self.class.default_config.merge(configuration)
@context = context
end
def smell_type
self.class.smell_type
end
def run
return [] unless enabled?
return [] if exception?
sniff
end
def self.todo_configuration_for(smells)
default_exclusions = default_config.fetch 'exclude'
exclusions = default_exclusions + smells.map(&:context)
{ smell_type => { 'exclude' => exclusions.uniq } }
end
private
attr_reader :context
def expression
@expression ||= context.exp
end
def source_line
@source_line ||= expression.line
end
def exception?
context.matches?(value(EXCLUDE_KEY, context))
end
def enabled?
config.enabled? && config_for(context)[SmellConfiguration::ENABLED_KEY] != false
end
def value(key, ctx)
config_for(ctx)[key] || config.value(key, ctx)
end
def config_for(ctx)
ctx.config_for(self.class)
end
def smell_warning(**options)
SmellWarning.new(smell_type,
source: expression.source,
context: context.full_name,
lines: options.fetch(:lines),
message: options.fetch(:message),
parameters: options.fetch(:parameters, {}))
end
class << self
def smell_type
@smell_type ||= name.split('::').last
end
def contexts
[:def, :defs]
end
# @quality :reek:UtilityFunction
def default_config
{
SmellConfiguration::ENABLED_KEY => true,
EXCLUDE_KEY => DEFAULT_EXCLUDE_SET.dup
}
end
def inherited(subclass)
descendants << subclass
end
#
# Returns all descendants of BaseDetector
#
# @return [Array<Constant>], e.g.:
# [Reek::SmellDetectors::Attribute,
# Reek::SmellDetectors::BooleanParameter,
# Reek::SmellDetectors::ClassVariable,
# ...]
#
def descendants
@descendants ||= []
end
#
# Transform a detector name to the corresponding constant.
# Note that we assume a valid name - exceptions are not handled here.
#
# @param detector_name [String] the detector in question, e.g. 'DuplicateMethodCall'
# @return [SmellDetector] this will return the class, not an instance
#
def to_detector(detector_name)
SmellDetectors.const_get detector_name
end
#
# @return [Set<Symbol>] all configuration keys that are available for this detector
#
def configuration_keys
Set.new(default_config.keys.map(&:to_sym))
end
end
end
end
end