Re-run autocorrect until all correctable issues have been corrected

This commit is contained in:
fn ⌃ ⌥ 2021-10-25 15:09:39 -07:00
parent 573881cb8a
commit f39a7a4cd4
12 changed files with 78 additions and 60 deletions

View file

@ -69,24 +69,24 @@ module Ameba::Formatter
describe "#affected_code" do describe "#affected_code" do
it "returns nil if there is no such a line number" do it "returns nil if there is no such a line number" do
source = Source.new %( code = <<-EOF
a = 1 a = 1
) EOF
location = Crystal::Location.new("filename", 2, 1) location = Crystal::Location.new("filename", 2, 1)
subject.affected_code(source, location).should be_nil subject.affected_code(code, location).should be_nil
end end
it "returns correct line if it is found" do it "returns correct line if it is found" do
source = Source.new %( code = <<-EOF
a = 1 a = 1
) EOF
location = Crystal::Location.new("filename", 1, 1) location = Crystal::Location.new("filename", 1, 1)
subject.deansify(subject.affected_code(source, location)) subject.deansify(subject.affected_code(code, location))
.should eq "> a = 1\n ^\n" .should eq "> a = 1\n ^\n"
end end
it "returns correct line if it is found" do it "returns correct line if it is found" do
source = Source.new <<-EOF code = <<-EOF
# pre:1 # pre:1
# pre:2 # pre:2
# pre:3 # pre:3
@ -101,7 +101,7 @@ module Ameba::Formatter
EOF EOF
location = Crystal::Location.new("filename", 6, 1) location = Crystal::Location.new("filename", 6, 1)
subject.deansify(subject.affected_code(source, location, context_lines: 3)) subject.deansify(subject.affected_code(code, location, context_lines: 3))
.should eq <<-STR .should eq <<-STR
> # pre:3 > # pre:3
> # pre:4 > # pre:4

View file

@ -3,7 +3,8 @@ require "../spec_helper"
module Ameba module Ameba
describe Issue do describe Issue do
it "accepts rule and message" do it "accepts rule and message" do
issue = Issue.new rule: DummyRule.new, issue = Issue.new code: "",
rule: DummyRule.new,
location: nil, location: nil,
end_location: nil, end_location: nil,
message: "Blah", message: "Blah",
@ -15,7 +16,8 @@ module Ameba
it "accepts location" do it "accepts location" do
location = Crystal::Location.new("path", 3, 2) location = Crystal::Location.new("path", 3, 2)
issue = Issue.new rule: DummyRule.new, issue = Issue.new code: "",
rule: DummyRule.new,
location: location, location: location,
end_location: nil, end_location: nil,
message: "Blah", message: "Blah",
@ -27,7 +29,8 @@ module Ameba
it "accepts end_location" do it "accepts end_location" do
location = Crystal::Location.new("path", 3, 2) location = Crystal::Location.new("path", 3, 2)
issue = Issue.new rule: DummyRule.new, issue = Issue.new code: "",
rule: DummyRule.new,
location: nil, location: nil,
end_location: location, end_location: location,
message: "Blah", message: "Blah",
@ -38,7 +41,8 @@ module Ameba
end end
it "accepts status" do it "accepts status" do
issue = Issue.new rule: DummyRule.new, issue = Issue.new code: "",
rule: DummyRule.new,
location: nil, location: nil,
end_location: nil, end_location: nil,
message: "", message: "",
@ -50,7 +54,8 @@ module Ameba
end end
it "sets status to :enabled by default" do it "sets status to :enabled by default" do
issue = Issue.new rule: DummyRule.new, issue = Issue.new code: "",
rule: DummyRule.new,
location: nil, location: nil,
end_location: nil, end_location: nil,
message: "" message: ""

View file

@ -1,6 +1,7 @@
require "../../spec_helper" require "../../spec_helper"
private def dummy_issue(message, private def dummy_issue(code,
message,
position : {Int32, Int32}?, position : {Int32, Int32}?,
end_position : {Int32, Int32}?, end_position : {Int32, Int32}?,
path = "") path = "")
@ -9,6 +10,7 @@ private def dummy_issue(message,
end_location = Crystal::Location.new(path, *end_position) if end_position end_location = Crystal::Location.new(path, *end_position) if end_position
Ameba::Issue.new( Ameba::Issue.new(
code: code,
rule: Ameba::DummyRule.new, rule: Ameba::DummyRule.new,
location: location, location: location,
end_location: end_location, end_location: end_location,
@ -25,7 +27,7 @@ private def expect_invalid_location(code,
expect_raises Exception, exception_message, file, line do expect_raises Exception, exception_message, file, line do
Ameba::Spec::AnnotatedSource.new( Ameba::Spec::AnnotatedSource.new(
lines: code.lines, lines: code.lines,
issues: [dummy_issue("Message", position, end_position, "path")] issues: [dummy_issue(code, "Message", position, end_position, "path")]
) )
end end
end end

View file

@ -50,7 +50,7 @@ module Ameba::Formatter
"#{issue.rule.name}: " \ "#{issue.rule.name}: " \
"#{issue.message}".colorize(:red) "#{issue.message}".colorize(:red)
if show_affected_code && (code = affected_code(source, location, issue.end_location)) if show_affected_code && (code = affected_code(issue))
output << code.colorize(:default) output << code.colorize(:default)
end end

View file

@ -38,10 +38,7 @@ module Ameba::Formatter
private def explain(source, issue) private def explain(source, issue)
rule = issue.rule rule = issue.rule
location, end_location = return unless (location = issue.location)
issue.location, issue.end_location
return unless location
output_title "ISSUE INFO" output_title "ISSUE INFO"
output_paragraph [ output_paragraph [
@ -49,7 +46,7 @@ module Ameba::Formatter
location.to_s.colorize(:cyan).to_s, location.to_s.colorize(:cyan).to_s,
] ]
if affected_code = affected_code(source, location, end_location, context_lines: 3) if affected_code = affected_code(issue, context_lines: 3)
output_title "AFFECTED CODE" output_title "AFFECTED CODE"
output_paragraph affected_code output_paragraph affected_code
end end

View file

@ -40,8 +40,14 @@ module Ameba::Formatter
{pre_context, post_context} {pre_context, post_context}
end end
def affected_code(source, location, end_location = nil, context_lines = 0, max_length = 120, ellipsis = " ...", prompt = "> ") def affected_code(issue : Issue, context_lines = 0, max_length = 120, ellipsis = " ...", prompt = "> ")
lines = source.lines return unless (location = issue.location)
affected_code(issue.code, location, issue.end_location, context_lines, max_length, ellipsis, prompt)
end
def affected_code(code, location, end_location = nil, context_lines = 0, max_length = 120, ellipsis = " ...", prompt = "> ")
lines = code.split('\n') # must preserve trailing newline
lineno, column = lineno, column =
location.line_number, location.column_number location.line_number, location.column_number

View file

@ -6,6 +6,9 @@ module Ameba
Disabled Disabled
end end
# The source code that triggered this issue.
getter code : String
# A rule that triggers this issue. # A rule that triggers this issue.
getter rule : Rule::Base getter rule : Rule::Base
@ -24,7 +27,7 @@ module Ameba
delegate :enabled?, :disabled?, delegate :enabled?, :disabled?,
to: status to: status
def initialize(@rule, @location, @end_location, @message, status : Status? = nil, @block : (Source::Corrector ->)? = nil) def initialize(@code, @rule, @location, @end_location, @message, status : Status? = nil, @block : (Source::Corrector ->)? = nil)
@status = status || Status::Enabled @status = status || Status::Enabled
end end

View file

@ -9,7 +9,7 @@ module Ameba
status ||= status ||=
Issue::Status::Disabled if location_disabled?(location, rule) Issue::Status::Disabled if location_disabled?(location, rule)
Issue.new(rule, location, end_location, message, status, block).tap do |issue| Issue.new(code, rule, location, end_location, message, status, block).tap do |issue|
issues << issue issues << issue
end end
end end

View file

@ -93,17 +93,23 @@ module Ameba
private def run_source(source) private def run_source(source)
@formatter.source_started source @formatter.source_started source
# TODO: run autocorrection recursively. A new `Issue#source` property must corrected_issues = [] of Issue
# be added so that `affected_code` will return the code from the old loop do
# source instead of the autocorrected one. @syntax_rule.test(source)
if @syntax_rule.catch(source).valid? break unless source.valid?
@rules.each do |rule| @rules.each do |rule|
next if rule.excluded?(source) next if rule.excluded?(source)
rule.test(source) rule.test(source)
end end
check_unneeded_directives(source) check_unneeded_directives(source)
autocorrect(source) if autocorrect? break unless autocorrect? && source.correct
corrected_issues += source.issues.select(&.correctable?)
source.issues.clear
end end
corrected_issues.reverse_each { |issue| source.issues.unshift(issue) }
File.write(source.path, source.code) if corrected_issues.any?
@formatter.source_finished source @formatter.source_finished source
end end
@ -143,12 +149,5 @@ module Ameba
rule.test(source) rule.test(source)
end end
end end
private def autocorrect(source)
corrected_code = Source::Corrector.correct(source)
return unless corrected_code
File.write(source.path, corrected_code)
end
end end
end end

View file

@ -11,6 +11,9 @@ module Ameba
# Crystal code (content of a source file). # Crystal code (content of a source file).
getter code : String getter code : String
@lines : Array(String)?
@ast : Crystal::ASTNode?
# Creates a new source by `code` and `path`. # Creates a new source by `code` and `path`.
# #
# For example: # For example:
@ -22,6 +25,18 @@ module Ameba
def initialize(@code, @path = "") def initialize(@code, @path = "")
end end
def correct
corrector = Corrector.new(code)
issues.each(&.correct(corrector))
corrected_code = corrector.process
return false if code == corrected_code
@code = corrected_code
@lines = nil
@ast = nil
true
end
# Returns lines of code splitted by new line character. # Returns lines of code splitted by new line character.
# Since `code` is immutable and can't be changed, this # Since `code` is immutable and can't be changed, this
# method caches lines in an instance variable, so calling # method caches lines in an instance variable, so calling
@ -33,7 +48,9 @@ module Ameba
# source.lines # => ["a = 1", "b = 2"] # source.lines # => ["a = 1", "b = 2"]
# ``` # ```
# #
getter lines : Array(String) { code.split('\n') } def lines : Array(String)
@lines ||= code.split('\n')
end
# Returns AST nodes constructed by `Crystal::Parser`. # Returns AST nodes constructed by `Crystal::Parser`.
# #
@ -42,7 +59,8 @@ module Ameba
# source.ast # source.ast
# ``` # ```
# #
getter ast : Crystal::ASTNode do def ast : Crystal::ASTNode
@ast ||=
Crystal::Parser.new(code) Crystal::Parser.new(code)
.tap(&.wants_doc = true) .tap(&.wants_doc = true)
.tap(&.filename = path) .tap(&.filename = path)

View file

@ -2,16 +2,6 @@ require "./rewriter"
class Ameba::Source class Ameba::Source
class Corrector class Corrector
def self.correct(source : Source)
code = source.code
corrector = new(code)
source.issues.each(&.correct(corrector))
corrected_code = corrector.process
return if code == corrected_code
corrected_code
end
@line_sizes : Array(Int32) @line_sizes : Array(Int32)
def initialize(code : String) def initialize(code : String)

View file

@ -85,9 +85,8 @@ module Ameba::Spec::ExpectIssue
source = ExpectIssue.source source = ExpectIssue.source
raise "`expect_correction` must follow `expect_issue`" unless source raise "`expect_correction` must follow `expect_issue`" unless source
corrected_code = Source::Corrector.correct(source) # TODO: recursive raise "Use `expect_no_corrections` if the code will not change" unless source.correct
raise "Use `expect_no_corrections` if the code will not change" unless corrected_code return if correction == source.code
return if correction == corrected_code
fail <<-MSG, file, line fail <<-MSG, file, line
Expected correction: Expected correction:
@ -96,7 +95,7 @@ module Ameba::Spec::ExpectIssue
Got: Got:
#{corrected_code} #{source.code}
MSG MSG
end end
@ -104,13 +103,12 @@ module Ameba::Spec::ExpectIssue
source = ExpectIssue.source source = ExpectIssue.source
raise "`expect_no_corrections` must follow `expect_offense`" unless source raise "`expect_no_corrections` must follow `expect_offense`" unless source
corrected_code = Source::Corrector.correct(source) return unless source.correct
return unless corrected_code
fail <<-MSG, file, line fail <<-MSG, file, line
Expected no corrections, but got: Expected no corrections, but got:
#{corrected_code} #{source.code}
MSG MSG
end end