mirror of
https://gitea.invidious.io/iv-org/shard-ameba.git
synced 2024-08-15 00:53:29 +00:00
Raise error if infinite correction loop
This commit is contained in:
parent
16608965f5
commit
437584f9db
3 changed files with 277 additions and 7 deletions
|
@ -8,6 +8,13 @@ module Ameba
|
||||||
|
|
||||||
config.update_rule ErrorRule.rule_name, enabled: false
|
config.update_rule ErrorRule.rule_name, enabled: false
|
||||||
config.update_rule PerfRule.rule_name, enabled: false
|
config.update_rule PerfRule.rule_name, enabled: false
|
||||||
|
config.update_rule AtoAA.rule_name, enabled: false
|
||||||
|
config.update_rule AtoB.rule_name, enabled: false
|
||||||
|
config.update_rule BtoA.rule_name, enabled: false
|
||||||
|
config.update_rule BtoC.rule_name, enabled: false
|
||||||
|
config.update_rule CtoA.rule_name, enabled: false
|
||||||
|
config.update_rule ClassToModule.rule_name, enabled: false
|
||||||
|
config.update_rule ModuleToClass.rule_name, enabled: false
|
||||||
|
|
||||||
Runner.new(config)
|
Runner.new(config)
|
||||||
end
|
end
|
||||||
|
@ -54,6 +61,15 @@ module Ameba
|
||||||
Runner.new(all_rules, [source], formatter, default_severity).run.success?.should be_true
|
Runner.new(all_rules, [source], formatter, default_severity).run.success?.should be_true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "aborts because of an infinite loop" do
|
||||||
|
rules = [AtoAA.new] of Rule::Base
|
||||||
|
source = Source.new "class A; end", "source.cr"
|
||||||
|
message = "Infinite loop in source.cr caused by Ameba/AtoAA"
|
||||||
|
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
|
||||||
|
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "exception in rule" do
|
context "exception in rule" do
|
||||||
it "raises an exception raised in fiber while running a rule" do
|
it "raises an exception raised in fiber while running a rule" do
|
||||||
rule = RaiseRule.new
|
rule = RaiseRule.new
|
||||||
|
@ -180,5 +196,56 @@ module Ameba
|
||||||
.run.success?.should be_true
|
.run.success?.should be_true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#run with rules autocorrecting each other" do
|
||||||
|
context "with two conflicting rules" do
|
||||||
|
context "if there is an offense in an inspected file" do
|
||||||
|
it "aborts because of an infinite loop" do
|
||||||
|
rules = [AtoB.new, BtoA.new]
|
||||||
|
source = Source.new "class A; end", "source.cr"
|
||||||
|
message = "Infinite loop in source.cr caused by Ameba/AtoB -> Ameba/BtoA"
|
||||||
|
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
|
||||||
|
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "if there are multiple offenses in an inspected file" do
|
||||||
|
it "aborts because of an infinite loop" do
|
||||||
|
rules = [AtoB.new, BtoA.new]
|
||||||
|
source = Source.new %(
|
||||||
|
class A; end
|
||||||
|
class A_A; end
|
||||||
|
), "source.cr"
|
||||||
|
message = "Infinite loop in source.cr caused by Ameba/AtoB -> Ameba/BtoA"
|
||||||
|
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
|
||||||
|
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with two pairs of conflicting rules" do
|
||||||
|
it "aborts because of an infinite loop" do
|
||||||
|
rules = [ClassToModule.new, ModuleToClass.new, AtoB.new, BtoA.new]
|
||||||
|
source = Source.new "class A_A; end", "source.cr"
|
||||||
|
message = "Infinite loop in source.cr caused by Ameba/ClassToModule, Ameba/AtoB -> Ameba/ModuleToClass, Ameba/BtoA"
|
||||||
|
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
|
||||||
|
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with three rule cycle" do
|
||||||
|
it "aborts because of an infinite loop" do
|
||||||
|
rules = [AtoB.new, BtoC.new, CtoA.new]
|
||||||
|
source = Source.new "class A; end", "source.cr"
|
||||||
|
message = "Infinite loop in source.cr caused by Ameba/AtoB -> Ameba/BtoC -> Ameba/CtoA"
|
||||||
|
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
|
||||||
|
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -95,6 +95,133 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class AtoAA < Rule::Base
|
||||||
|
include AST::Util
|
||||||
|
|
||||||
|
properties do
|
||||||
|
description "This rule is only used to test infinite loop detection"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
|
||||||
|
return unless (name = node_source(node.name, source.lines))
|
||||||
|
return unless name.includes?("A")
|
||||||
|
|
||||||
|
issue_for(node.name, message: "A to AA") do |corrector|
|
||||||
|
corrector.replace(node.name, name.sub("A", "AA"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class AtoB < Rule::Base
|
||||||
|
include AST::Util
|
||||||
|
|
||||||
|
properties do
|
||||||
|
description "This rule is only used to test infinite loop detection"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
|
||||||
|
return unless (name = node_source(node.name, source.lines))
|
||||||
|
return unless name.includes?("A")
|
||||||
|
|
||||||
|
issue_for(node.name, message: "A to B") do |corrector|
|
||||||
|
corrector.replace(node.name, name.tr("A", "B"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class BtoA < Rule::Base
|
||||||
|
include AST::Util
|
||||||
|
|
||||||
|
properties do
|
||||||
|
description "This rule is only used to test infinite loop detection"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
|
||||||
|
return unless (name = node_source(node.name, source.lines))
|
||||||
|
return unless name.includes?("B")
|
||||||
|
|
||||||
|
issue_for(node.name, message: "B to A") do |corrector|
|
||||||
|
corrector.replace(node.name, name.tr("B", "A"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class BtoC < Rule::Base
|
||||||
|
include AST::Util
|
||||||
|
|
||||||
|
properties do
|
||||||
|
description "This rule is only used to test infinite loop detection"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
|
||||||
|
return unless (name = node_source(node.name, source.lines))
|
||||||
|
return unless name.includes?("B")
|
||||||
|
|
||||||
|
issue_for(node.name, message: "B to C") do |corrector|
|
||||||
|
corrector.replace(node.name, name.tr("B", "C"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class CtoA < Rule::Base
|
||||||
|
include AST::Util
|
||||||
|
|
||||||
|
properties do
|
||||||
|
description "This rule is only used to test infinite loop detection"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
|
||||||
|
return unless (name = node_source(node.name, source.lines))
|
||||||
|
return unless name.includes?("C")
|
||||||
|
|
||||||
|
issue_for(node.name, message: "C to A") do |corrector|
|
||||||
|
corrector.replace(node.name, name.tr("C", "A"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ClassToModule < Ameba::Rule::Base
|
||||||
|
include Ameba::AST::Util
|
||||||
|
|
||||||
|
properties do
|
||||||
|
description "This rule is only used to test infinite loop detection"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::ClassDef)
|
||||||
|
return unless (location = node.location)
|
||||||
|
|
||||||
|
end_location = Crystal::Location.new(
|
||||||
|
location.filename,
|
||||||
|
location.line_number,
|
||||||
|
location.column_number + "class".size - 1
|
||||||
|
)
|
||||||
|
issue_for(location, end_location, message: "class to module") do |corrector|
|
||||||
|
corrector.replace(location, end_location, "module")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ModuleToClass < Ameba::Rule::Base
|
||||||
|
include Ameba::AST::Util
|
||||||
|
|
||||||
|
properties do
|
||||||
|
description "This rule is only used to test infinite loop detection"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::ModuleDef)
|
||||||
|
return unless (location = node.location)
|
||||||
|
|
||||||
|
end_location = Crystal::Location.new(
|
||||||
|
location.filename,
|
||||||
|
location.line_number,
|
||||||
|
location.column_number + "module".size - 1
|
||||||
|
)
|
||||||
|
issue_for(location, end_location, message: "module to class") do |corrector|
|
||||||
|
corrector.replace(location, end_location, "class")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class DummyFormatter < Formatter::BaseFormatter
|
class DummyFormatter < Formatter::BaseFormatter
|
||||||
property started_sources : Array(Source)?
|
property started_sources : Array(Source)?
|
||||||
property finished_sources : Array(Source)?
|
property finished_sources : Array(Source)?
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "digest"
|
||||||
|
|
||||||
module Ameba
|
module Ameba
|
||||||
# Represents a runner for inspecting sources files.
|
# Represents a runner for inspecting sources files.
|
||||||
# Holds a list of rules to do inspection based on,
|
# Holds a list of rules to do inspection based on,
|
||||||
|
@ -11,6 +13,25 @@ module Ameba
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
class Runner
|
class Runner
|
||||||
|
# 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]
|
||||||
|
.map(&.map(&.rule.name).uniq!.join(", "))
|
||||||
|
.join(" -> ")
|
||||||
|
|
||||||
|
message = String.build do |io|
|
||||||
|
io << "Infinite loop"
|
||||||
|
io << " in " << path unless path.empty?
|
||||||
|
io << " caused by " << root_cause
|
||||||
|
end
|
||||||
|
|
||||||
|
super message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# A list of rules to do inspection based on.
|
# A list of rules to do inspection based on.
|
||||||
@rules : Array(Rule::Base)
|
@rules : Array(Rule::Base)
|
||||||
|
|
||||||
|
@ -52,8 +73,7 @@ module Ameba
|
||||||
.find &.name.==(Rule::Lint::UnneededDisableDirective.rule_name)
|
.find &.name.==(Rule::Lint::UnneededDisableDirective.rule_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
protected def initialize(@rules, @sources, @formatter, @severity)
|
protected def initialize(@rules, @sources, @formatter, @severity, @autocorrect = false)
|
||||||
@autocorrect = false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Performs the inspection. Iterates through all sources and test it using
|
# Performs the inspection. Iterates through all sources and test it using
|
||||||
|
@ -93,8 +113,18 @@ module Ameba
|
||||||
private def run_source(source)
|
private def run_source(source)
|
||||||
@formatter.source_started source
|
@formatter.source_started source
|
||||||
|
|
||||||
corrected_issues = [] of Issue
|
# This variable is a 2D array used to track corrected issues after each
|
||||||
loop do
|
# 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.
|
||||||
@syntax_rule.test(source)
|
@syntax_rule.test(source)
|
||||||
break unless source.valid?
|
break unless source.valid?
|
||||||
|
|
||||||
|
@ -105,12 +135,18 @@ module Ameba
|
||||||
check_unneeded_directives(source)
|
check_unneeded_directives(source)
|
||||||
break unless autocorrect? && source.correct
|
break unless autocorrect? && source.correct
|
||||||
|
|
||||||
corrected_issues += source.issues.select(&.correctable?)
|
# 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?)
|
||||||
source.issues.clear
|
source.issues.clear
|
||||||
end
|
end
|
||||||
corrected_issues.reverse_each { |issue| source.issues.unshift(issue) }
|
|
||||||
File.write(source.path, source.code) unless corrected_issues.empty?
|
|
||||||
|
|
||||||
|
corrected_issues.flatten.reverse_each do |issue|
|
||||||
|
source.issues.unshift(issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
File.write(source.path, source.code) unless corrected_issues.empty?
|
||||||
|
ensure
|
||||||
@formatter.source_finished source
|
@formatter.source_finished source
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -144,6 +180,46 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private MAX_ITERATIONS = 200
|
||||||
|
|
||||||
|
private def loop_unless_infinite(source, corrected_issues)
|
||||||
|
# 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.
|
||||||
|
processed_sources = [] of String
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
checksum = Digest::SHA1.hexdigest(source.code)
|
||||||
|
|
||||||
|
if (loop_start = processed_sources.index(checksum))
|
||||||
|
raise InfiniteCorrectionLoopError.new(
|
||||||
|
source.path,
|
||||||
|
corrected_issues,
|
||||||
|
loop_start: loop_start
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
processed_sources << checksum
|
||||||
|
end
|
||||||
|
|
||||||
private def check_unneeded_directives(source)
|
private def check_unneeded_directives(source)
|
||||||
if (rule = @unneeded_disable_directive_rule) && rule.enabled
|
if (rule = @unneeded_disable_directive_rule) && rule.enabled
|
||||||
rule.test(source)
|
rule.test(source)
|
||||||
|
|
Loading…
Reference in a new issue