2017-11-13 21:20:22 +00:00
|
|
|
module Ameba
|
2017-11-15 18:49:09 +00:00
|
|
|
# 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
|
|
|
|
# ```
|
2017-11-13 21:20:22 +00:00
|
|
|
class Runner
|
2021-10-26 20:05:22 +00:00
|
|
|
# An error indicating that the inspection loop got stuck correcting
|
|
|
|
# issues back and forth.
|
|
|
|
class InfiniteCorrectionLoopError < RuntimeError
|
|
|
|
def initialize(path, issues_by_iteration, loop_start = -1)
|
|
|
|
root_cause =
|
|
|
|
issues_by_iteration[loop_start..-1]
|
2021-10-27 17:08:36 +00:00
|
|
|
.join(" -> ", &.map(&.rule.name).uniq!.join(", "))
|
2021-10-26 20:05:22 +00:00
|
|
|
|
|
|
|
message = String.build do |io|
|
|
|
|
io << "Infinite loop"
|
|
|
|
io << " in " << path unless path.empty?
|
|
|
|
io << " caused by " << root_cause
|
|
|
|
end
|
|
|
|
|
|
|
|
super message
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# A list of rules to do inspection based on.
|
2017-11-13 21:20:22 +00:00
|
|
|
@rules : Array(Rule::Base)
|
2017-11-15 18:49:09 +00:00
|
|
|
|
|
|
|
# A list of sources to run inspection on.
|
2017-11-23 21:59:59 +00:00
|
|
|
getter sources : Array(Source)
|
2017-11-15 18:49:09 +00:00
|
|
|
|
2019-04-14 12:57:48 +00:00
|
|
|
# A level of severity to be reported.
|
|
|
|
@severity : Severity
|
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# A formatter to prepare report.
|
2017-11-13 21:20:22 +00:00
|
|
|
@formatter : Formatter::BaseFormatter
|
|
|
|
|
2018-01-25 09:50:11 +00:00
|
|
|
# A syntax rule which always inspects a source first
|
2018-06-16 11:50:59 +00:00
|
|
|
@syntax_rule = Rule::Lint::Syntax.new
|
2018-01-25 09:50:11 +00:00
|
|
|
|
2018-02-02 20:11:18 +00:00
|
|
|
# Checks for unneeded disable directives. Always inspects a source last
|
|
|
|
@unneeded_disable_directive_rule : Rule::Base?
|
|
|
|
|
2022-12-09 23:20:20 +00:00
|
|
|
# Returns `true` if correctable issues should be autocorrected.
|
2021-10-24 18:58:32 +00:00
|
|
|
private getter? autocorrect : Bool
|
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# Instantiates a runner using a `config`.
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# config = Ameba::Config.load
|
|
|
|
# config.files = files
|
|
|
|
# config.formatter = formatter
|
|
|
|
#
|
|
|
|
# Ameba::Runner.new config
|
|
|
|
# ```
|
2017-11-13 21:20:22 +00:00
|
|
|
def initialize(config : Config)
|
2019-01-12 21:19:00 +00:00
|
|
|
@sources = config.sources
|
2017-11-13 21:20:22 +00:00
|
|
|
@formatter = config.formatter
|
2019-04-14 12:57:48 +00:00
|
|
|
@severity = config.severity
|
2022-11-22 18:46:38 +00:00
|
|
|
@rules = config.rules.select(&.enabled?).reject!(&.special?)
|
2021-10-24 18:58:32 +00:00
|
|
|
@autocorrect = config.autocorrect?
|
2018-02-02 20:11:18 +00:00
|
|
|
|
|
|
|
@unneeded_disable_directive_rule =
|
|
|
|
config.rules
|
2018-09-02 21:17:56 +00:00
|
|
|
.find &.name.==(Rule::Lint::UnneededDisableDirective.rule_name)
|
2017-11-13 21:20:22 +00:00
|
|
|
end
|
|
|
|
|
2021-10-26 20:05:22 +00:00
|
|
|
protected def initialize(@rules, @sources, @formatter, @severity, @autocorrect = false)
|
2017-11-14 07:24:36 +00:00
|
|
|
end
|
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# 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
|
2018-06-10 21:15:12 +00:00
|
|
|
# an issue to that source.
|
2017-11-15 18:49:09 +00:00
|
|
|
#
|
|
|
|
# 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
|
|
|
|
# ```
|
2017-11-13 21:20:22 +00:00
|
|
|
def run
|
|
|
|
@formatter.started @sources
|
2022-11-14 00:24:29 +00:00
|
|
|
|
2019-11-09 17:31:41 +00:00
|
|
|
channels = @sources.map { Channel(Exception?).new }
|
2022-11-23 15:16:34 +00:00
|
|
|
@sources.zip(channels).each do |source, channel|
|
2019-11-09 17:31:41 +00:00
|
|
|
spawn do
|
|
|
|
run_source(source)
|
|
|
|
rescue e
|
|
|
|
channel.send(e)
|
|
|
|
else
|
|
|
|
channel.send(nil)
|
2017-11-30 21:50:07 +00:00
|
|
|
end
|
2019-11-09 17:31:41 +00:00
|
|
|
end
|
2017-11-13 21:20:22 +00:00
|
|
|
|
2021-01-11 18:13:58 +00:00
|
|
|
channels.each do |chan|
|
|
|
|
chan.receive.try { |e| raise e }
|
2017-11-13 21:20:22 +00:00
|
|
|
end
|
2019-11-09 17:31:41 +00:00
|
|
|
|
2017-11-13 21:20:22 +00:00
|
|
|
self
|
|
|
|
ensure
|
|
|
|
@formatter.finished @sources
|
|
|
|
end
|
|
|
|
|
2019-11-09 17:31:41 +00:00
|
|
|
private def run_source(source)
|
|
|
|
@formatter.source_started source
|
|
|
|
|
2021-10-26 20:05:22 +00:00
|
|
|
# This variable is a 2D array used to track corrected issues after each
|
|
|
|
# inspection iteration. This is used to output meaningful infinite loop
|
|
|
|
# error message.
|
|
|
|
corrected_issues = [] of Array(Issue)
|
|
|
|
|
|
|
|
# When running with --fix, we need to inspect the source until no more
|
|
|
|
# corrections are made (because automatic corrections can introduce new
|
|
|
|
# issues). In the normal case the loop is only executed once.
|
|
|
|
loop_unless_infinite(source, corrected_issues) do
|
|
|
|
# We have to reprocess the source to pick up any changes. Since a
|
|
|
|
# change could (theoretically) introduce syntax errors, we break the
|
|
|
|
# loop if we find any.
|
2021-10-25 22:09:39 +00:00
|
|
|
@syntax_rule.test(source)
|
|
|
|
break unless source.valid?
|
|
|
|
|
2019-11-09 17:31:41 +00:00
|
|
|
@rules.each do |rule|
|
|
|
|
next if rule.excluded?(source)
|
|
|
|
rule.test(source)
|
|
|
|
end
|
|
|
|
check_unneeded_directives(source)
|
2022-12-20 13:53:23 +00:00
|
|
|
break unless autocorrect? && source.correct?
|
2021-10-25 22:09:39 +00:00
|
|
|
|
2021-10-26 20:05:22 +00:00
|
|
|
# The issues that couldn't be corrected will be found again so we
|
|
|
|
# only keep the corrected ones in order to avoid duplicate reporting.
|
|
|
|
corrected_issues << source.issues.select(&.correctable?)
|
2021-10-25 22:09:39 +00:00
|
|
|
source.issues.clear
|
2019-11-09 17:31:41 +00:00
|
|
|
end
|
|
|
|
|
2021-10-26 20:05:22 +00:00
|
|
|
corrected_issues.flatten.reverse_each do |issue|
|
|
|
|
source.issues.unshift(issue)
|
|
|
|
end
|
|
|
|
|
|
|
|
File.write(source.path, source.code) unless corrected_issues.empty?
|
|
|
|
ensure
|
2019-11-09 17:31:41 +00:00
|
|
|
@formatter.source_finished source
|
|
|
|
end
|
|
|
|
|
2018-12-27 21:34:10 +00:00
|
|
|
# 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})
|
|
|
|
# ```
|
2021-11-01 05:12:21 +00:00
|
|
|
def explain(location, output = STDOUT)
|
|
|
|
Formatter::ExplainFormatter.new(output, location).finished @sources
|
2018-12-27 21:34:10 +00:00
|
|
|
end
|
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# Indicates whether the last inspection successful or not.
|
2022-12-09 23:20:20 +00:00
|
|
|
# It returns `true` if no issues matching severity in sources found, `false` otherwise.
|
2017-11-15 18:49:09 +00:00
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# runner = Ameba::Runner.new config
|
|
|
|
# runner.run
|
|
|
|
# runner.success? # => true or false
|
|
|
|
# ```
|
2017-11-13 21:20:22 +00:00
|
|
|
def success?
|
2019-04-14 12:57:48 +00:00
|
|
|
@sources.all? do |source|
|
2019-04-25 05:47:37 +00:00
|
|
|
source.issues
|
|
|
|
.reject(&.disabled?)
|
2021-01-25 01:34:33 +00:00
|
|
|
.none?(&.rule.severity.<=(@severity))
|
2019-04-14 12:57:48 +00:00
|
|
|
end
|
2017-11-13 21:20:22 +00:00
|
|
|
end
|
|
|
|
|
2021-10-26 20:05:22 +00:00
|
|
|
private MAX_ITERATIONS = 200
|
|
|
|
|
2022-12-17 22:47:22 +00:00
|
|
|
private def loop_unless_infinite(source, corrected_issues, &)
|
2021-10-26 20:05:22 +00:00
|
|
|
# Keep track of the state of the source. If a rule modifies the source
|
|
|
|
# and another rule undoes it producing identical source we have an
|
|
|
|
# infinite loop.
|
2022-10-29 19:44:06 +00:00
|
|
|
processed_sources = [] of UInt64
|
2021-10-26 20:05:22 +00:00
|
|
|
|
|
|
|
# It is possible for a rule to keep adding indefinitely to a file,
|
|
|
|
# making it bigger and bigger. If the inspection loop runs for an
|
|
|
|
# excessively high number of iterations, this is likely happening.
|
|
|
|
iterations = 0
|
|
|
|
|
|
|
|
loop do
|
|
|
|
check_for_infinite_loop(source, corrected_issues, processed_sources)
|
|
|
|
|
|
|
|
if (iterations += 1) > MAX_ITERATIONS
|
|
|
|
raise InfiniteCorrectionLoopError.new(source.path, corrected_issues)
|
|
|
|
end
|
|
|
|
|
|
|
|
yield
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Check whether a run created source identical to a previous run, which
|
|
|
|
# means that we definitely have an infinite loop.
|
|
|
|
private def check_for_infinite_loop(source, corrected_issues, processed_sources)
|
2022-10-29 19:44:06 +00:00
|
|
|
checksum = source.code.hash
|
2021-10-26 20:05:22 +00:00
|
|
|
|
2021-10-27 17:08:36 +00:00
|
|
|
if loop_start = processed_sources.index(checksum)
|
2021-10-26 20:05:22 +00:00
|
|
|
raise InfiniteCorrectionLoopError.new(
|
|
|
|
source.path,
|
|
|
|
corrected_issues,
|
|
|
|
loop_start: loop_start
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
processed_sources << checksum
|
|
|
|
end
|
|
|
|
|
2018-02-02 20:11:18 +00:00
|
|
|
private def check_unneeded_directives(source)
|
2022-11-14 00:24:29 +00:00
|
|
|
return unless rule = @unneeded_disable_directive_rule
|
2022-11-22 18:46:38 +00:00
|
|
|
return unless rule.enabled?
|
2022-11-14 00:24:29 +00:00
|
|
|
|
2021-12-09 20:33:47 +00:00
|
|
|
rule.test(source)
|
2018-02-02 20:11:18 +00:00
|
|
|
end
|
2017-11-13 21:20:22 +00:00
|
|
|
end
|
|
|
|
end
|