shard-ameba/src/ameba/runner.cr
2021-02-04 21:59:22 +01:00

139 lines
3.8 KiB
Crystal

module Ameba
# Represents a runner for inspecting sources files.
# Holds a list of rules to do inspection based on,
# list of sources to run inspection on and a formatter
# to prepare a report.
#
# ```
# config = Ameba::Config.load
# runner = Ameba::Runner.new config
# runner.run.success? # => true or false
# ```
#
class Runner
# A list of rules to do inspection based on.
@rules : Array(Rule::Base)
# A list of sources to run inspection on.
getter sources : Array(Source)
# A level of severity to be reported.
@severity : Severity
# A formatter to prepare report.
@formatter : Formatter::BaseFormatter
# A syntax rule which always inspects a source first
@syntax_rule = Rule::Lint::Syntax.new
# Checks for unneeded disable directives. Always inspects a source last
@unneeded_disable_directive_rule : Rule::Base?
# Instantiates a runner using a `config`.
#
# ```
# config = Ameba::Config.load
# config.files = files
# config.formatter = formatter
#
# Ameba::Runner.new config
# ```
def initialize(config : Config)
@sources = config.sources
@formatter = config.formatter
@severity = config.severity
@rules = config.rules.select(&.enabled).reject!(&.special?)
@unneeded_disable_directive_rule =
config.rules
.find &.name.==(Rule::Lint::UnneededDisableDirective.rule_name)
end
protected def initialize(@rules, @sources, @formatter, @severity)
end
# Performs the inspection. Iterates through all sources and test it using
# list of rules. If a specific rule fails on a specific source, it adds
# an issue to that source.
#
# This action also notifies formatter when inspection is started/finished,
# and when a specific source started/finished to be inspected.
#
# ```
# runner = Ameba::Runner.new config
# runner.run # => returns runner again
# ```
def run
@formatter.started @sources
channels = @sources.map { Channel(Exception?).new }
@sources.each_with_index do |source, idx|
channel = channels[idx]
spawn do
run_source(source)
rescue e
channel.send(e)
else
channel.send(nil)
end
end
channels.each do |chan|
chan.receive.try { |e| raise e }
end
self
ensure
@formatter.finished @sources
end
private def run_source(source)
@formatter.source_started source
if @syntax_rule.catch(source).valid?
@rules.each do |rule|
next if rule.excluded?(source)
rule.test(source)
end
check_unneeded_directives(source)
end
@formatter.source_finished source
end
# Explains an issue at a specified *location*.
#
# Runner should perform inspection before doing the explain.
# This is necessary to be able to find the issue at a specified location.
#
# ```
# runner = Ameba::Runner.new config
# runner.run
# runner.explain({file: file, line: l, column: c})
# ```
def explain(location, output = STDOUT)
Formatter::ExplainFormatter.new(output, location).finished @sources
end
# Indicates whether the last inspection successful or not.
# It returns true if no issues matching severity in sources found, false otherwise.
#
# ```
# runner = Ameba::Runner.new config
# runner.run
# runner.success? # => true or false
# ```
def success?
@sources.all? do |source|
source.issues
.reject(&.disabled?)
.none?(&.rule.severity.<=(@severity))
end
end
private def check_unneeded_directives(source)
if (rule = @unneeded_disable_directive_rule) && rule.enabled
rule.test(source)
end
end
end
end