mirror of
https://gitea.invidious.io/iv-org/shard-ameba.git
synced 2024-08-15 00:53:29 +00:00
Let ameba explain the issue at the specified location (#86)
This commit is contained in:
parent
4e19571fb3
commit
c91da1aa08
12 changed files with 359 additions and 22 deletions
13
README.md
13
README.md
|
@ -126,6 +126,19 @@ $ ameba --except Lint/Syntax # runs all rules except Lint/Syntax
|
||||||
$ ameba --except Style,Lint # runs all rules except rules in Style and Lint groups
|
$ ameba --except Style,Lint # runs all rules except rules in Style and Lint groups
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Explanation
|
||||||
|
|
||||||
|
Ameba allows you to dig deeper into an issue, by showing you details about the issue
|
||||||
|
and the reasoning by it being reported.
|
||||||
|
|
||||||
|
To be convenient, you can just copy-paste the `PATH:line:column` string from the
|
||||||
|
report and paste behind the `ameba` command to check it out.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ameba crystal/command/format.cr:26:83 # show explanation for the issue
|
||||||
|
$ ameba --explain crystal/command/format.cr:26:83 # same thing
|
||||||
|
```
|
||||||
|
|
||||||
### Inline disabling
|
### Inline disabling
|
||||||
|
|
||||||
One or more rules or one or more group of rules can be disabled using inline directives:
|
One or more rules or one or more group of rules can be disabled using inline directives:
|
||||||
|
|
|
@ -78,11 +78,57 @@ module Ameba::Cli
|
||||||
c.config.should eq ""
|
c.config.should eq ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "-e/--explain" do
|
||||||
|
it "configures file/line/column" do
|
||||||
|
c = Cli.parse_args %w(--explain src/file.cr:3:5)
|
||||||
|
c.location_to_explain.should_not be_nil
|
||||||
|
|
||||||
|
location_to_explain = c.location_to_explain.not_nil!
|
||||||
|
location_to_explain[:file].should eq "src/file.cr"
|
||||||
|
location_to_explain[:line].should eq 3
|
||||||
|
location_to_explain[:column].should eq 5
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error if location is not valid" do
|
||||||
|
expect_raises(Exception, "location should have PATH:line:column") do
|
||||||
|
Cli.parse_args %w(--explain src/file.cr:3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error if line number is not valid" do
|
||||||
|
expect_raises(Exception, "location should have PATH:line:column") do
|
||||||
|
Cli.parse_args %w(--explain src/file.cr:a:3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error if column number is not valid" do
|
||||||
|
expect_raises(Exception, "location should have PATH:line:column") do
|
||||||
|
Cli.parse_args %w(--explain src/file.cr:3:&)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error if line/column are missing" do
|
||||||
|
expect_raises(Exception, "location should have PATH:line:column") do
|
||||||
|
Cli.parse_args %w(--explain src/file.cr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "accepts unknown args as files" do
|
it "accepts unknown args as files" do
|
||||||
c = Cli.parse_args %w(source1.cr source2.cr)
|
c = Cli.parse_args %w(source1.cr source2.cr)
|
||||||
c.files.should eq %w(source1.cr source2.cr)
|
c.files.should eq %w(source1.cr source2.cr)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "accepts one unknown arg as explain location if it has correct format" do
|
||||||
|
c = Cli.parse_args %w(source.cr:3:22)
|
||||||
|
c.location_to_explain.should_not be_nil
|
||||||
|
|
||||||
|
location_to_explain = c.location_to_explain.not_nil!
|
||||||
|
location_to_explain[:file].should eq "source.cr"
|
||||||
|
location_to_explain[:line].should eq 3
|
||||||
|
location_to_explain[:column].should eq 22
|
||||||
|
end
|
||||||
|
|
||||||
it "allows args to be blank" do
|
it "allows args to be blank" do
|
||||||
c = Cli.parse_args [] of String
|
c = Cli.parse_args [] of String
|
||||||
c.formatter.should be_nil
|
c.formatter.should be_nil
|
||||||
|
|
70
spec/ameba/formatter/explain_formatter_spec.cr
Normal file
70
spec/ameba/formatter/explain_formatter_spec.cr
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
require "../../spec_helper"
|
||||||
|
|
||||||
|
module Ameba
|
||||||
|
def explanation(source)
|
||||||
|
output = IO::Memory.new
|
||||||
|
ErrorRule.new.catch(source)
|
||||||
|
location = {file: "source.cr", line: 1, column: 1}
|
||||||
|
Formatter::ExplainFormatter.new(output, location).finished([source])
|
||||||
|
output.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Formatter::ExplainFormatter do
|
||||||
|
describe "#location" do
|
||||||
|
it "returns crystal location" do
|
||||||
|
location = Formatter::ExplainFormatter
|
||||||
|
.new(STDOUT, {file: "compiler.cr", line: 3, column: 8}).location
|
||||||
|
|
||||||
|
location.is_a?(Crystal::Location).should be_true
|
||||||
|
location.filename.should eq "compiler.cr"
|
||||||
|
location.line_number.should eq 3
|
||||||
|
location.column_number.should eq 8
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#output" do
|
||||||
|
it "returns io" do
|
||||||
|
output = Formatter::ExplainFormatter
|
||||||
|
.new(STDOUT, {file: "compiler.cr", line: 3, column: 8}).output
|
||||||
|
output.should eq STDOUT
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#finished" do
|
||||||
|
it "writes issue info" do
|
||||||
|
source = Source.new "a = 42", "source.cr"
|
||||||
|
output = explanation(source)
|
||||||
|
output.should contain "ISSUE INFO"
|
||||||
|
output.should contain "This rule always adds an error"
|
||||||
|
output.should contain "source.cr:1:1"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "writes affected code" do
|
||||||
|
source = Source.new "a = 42", "source.cr"
|
||||||
|
output = explanation(source)
|
||||||
|
output.should contain "AFFECTED CODE"
|
||||||
|
output.should contain "a = 42"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "writes rule info" do
|
||||||
|
source = Source.new "a = 42", "source.cr"
|
||||||
|
output = explanation(source)
|
||||||
|
output.should contain "RULE INFO"
|
||||||
|
output.should contain "Ameba/ErrorRule"
|
||||||
|
output.should contain "Always adds an error at 1:1"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "writes detailed description" do
|
||||||
|
source = Source.new "a = 42", "source.cr"
|
||||||
|
output = explanation(source)
|
||||||
|
output.should contain "DETAILED DESCRIPTION"
|
||||||
|
output.should contain "TO BE DONE..."
|
||||||
|
end
|
||||||
|
|
||||||
|
it "writes nothing if location not found" do
|
||||||
|
source = Source.new "a = 42", "another_source.cr"
|
||||||
|
explanation(source).should be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
spec/ameba/formatter/util_spec.cr
Normal file
29
spec/ameba/formatter/util_spec.cr
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
require "../../spec_helper"
|
||||||
|
|
||||||
|
module Ameba::Formatter
|
||||||
|
class Subject
|
||||||
|
include Util
|
||||||
|
end
|
||||||
|
|
||||||
|
subject = Subject.new
|
||||||
|
|
||||||
|
describe Util do
|
||||||
|
describe "#affected_code" do
|
||||||
|
it "returns nil if there is no such a line number" do
|
||||||
|
source = Source.new %(
|
||||||
|
a = 1
|
||||||
|
)
|
||||||
|
location = Crystal::Location.new("filename", 2, 1)
|
||||||
|
subject.affected_code(source, location).should be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns correct line if it is found" do
|
||||||
|
source = Source.new %(
|
||||||
|
a = 1
|
||||||
|
)
|
||||||
|
location = Crystal::Location.new("filename", 1, 1)
|
||||||
|
subject.affected_code(source, location).should eq "> a = 1\n \e[33m^\e[0m"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -90,6 +90,41 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#explain" do
|
||||||
|
io = IO::Memory.new
|
||||||
|
|
||||||
|
it "writes nothing if sources are valid" do
|
||||||
|
io.clear
|
||||||
|
runner = runner(formatter: formatter).run
|
||||||
|
runner.explain({file: "source.cr", line: 1, column: 2}, io)
|
||||||
|
io.to_s.should be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "writes the explanation if sources are not valid and location found" do
|
||||||
|
io.clear
|
||||||
|
rules = [ErrorRule.new] of Rule::Base
|
||||||
|
source = Source.new %(
|
||||||
|
a = 1
|
||||||
|
), "source.cr"
|
||||||
|
|
||||||
|
runner = Runner.new(rules, [source], formatter).run
|
||||||
|
runner.explain({file: "source.cr", line: 1, column: 1}, io)
|
||||||
|
io.to_s.should_not be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "writes nothing if sources are not valid and location is not found" do
|
||||||
|
io.clear
|
||||||
|
rules = [ErrorRule.new] of Rule::Base
|
||||||
|
source = Source.new %(
|
||||||
|
a = 1
|
||||||
|
), "source.cr"
|
||||||
|
|
||||||
|
runner = Runner.new(rules, [source], formatter).run
|
||||||
|
runner.explain({file: "source.cr", line: 1, column: 2}, io)
|
||||||
|
io.to_s.should be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#success?" do
|
describe "#success?" do
|
||||||
it "returns true if runner has not been run" do
|
it "returns true if runner has not been run" do
|
||||||
runner.success?.should be_true
|
runner.success?.should be_true
|
||||||
|
|
|
@ -50,6 +50,10 @@ module Ameba
|
||||||
end
|
end
|
||||||
|
|
||||||
struct ErrorRule < Rule::Base
|
struct ErrorRule < Rule::Base
|
||||||
|
properties do
|
||||||
|
description : String = "Always adds an error at 1:1"
|
||||||
|
end
|
||||||
|
|
||||||
def test(source)
|
def test(source)
|
||||||
issue_for({1, 1}, "This rule always adds an error")
|
issue_for({1, 1}, "This rule always adds an error")
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,13 @@ module Ameba::Cli
|
||||||
configure_formatter(config, opts)
|
configure_formatter(config, opts)
|
||||||
configure_rules(config, opts)
|
configure_rules(config, opts)
|
||||||
|
|
||||||
exit 1 unless Ameba.run(config).success?
|
runner = Ameba.run(config)
|
||||||
|
|
||||||
|
if location = opts.location_to_explain
|
||||||
|
runner.explain(location)
|
||||||
|
else
|
||||||
|
exit 1 unless runner.success?
|
||||||
|
end
|
||||||
rescue e
|
rescue e
|
||||||
puts "Error: #{e.message}"
|
puts "Error: #{e.message}"
|
||||||
exit 255
|
exit 255
|
||||||
|
@ -25,6 +31,7 @@ module Ameba::Cli
|
||||||
property files : Array(String)?
|
property files : Array(String)?
|
||||||
property only : Array(String)?
|
property only : Array(String)?
|
||||||
property except : Array(String)?
|
property except : Array(String)?
|
||||||
|
property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)?
|
||||||
property? all = false
|
property? all = false
|
||||||
property? colors = true
|
property? colors = true
|
||||||
property? without_affected_code = false
|
property? without_affected_code = false
|
||||||
|
@ -37,7 +44,13 @@ module Ameba::Cli
|
||||||
parser.on("-v", "--version", "Print version") { print_version }
|
parser.on("-v", "--version", "Print version") { print_version }
|
||||||
parser.on("-h", "--help", "Show this help") { show_help parser }
|
parser.on("-h", "--help", "Show this help") { show_help parser }
|
||||||
parser.on("-s", "--silent", "Disable output") { opts.formatter = :silent }
|
parser.on("-s", "--silent", "Disable output") { opts.formatter = :silent }
|
||||||
parser.unknown_args { |f| opts.files = f if f.any? }
|
parser.unknown_args do |f|
|
||||||
|
if f.size == 1 && f.first =~ /.+:\d+:\d+/
|
||||||
|
configure_explain_opts(f.first, opts)
|
||||||
|
else
|
||||||
|
opts.files = f if f.any?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
parser.on("-c", "--config PATH",
|
parser.on("-c", "--config PATH",
|
||||||
"Specify a configuration file") do |path|
|
"Specify a configuration file") do |path|
|
||||||
|
@ -69,6 +82,11 @@ module Ameba::Cli
|
||||||
opts.config = ""
|
opts.config = ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
parser.on("-e", "--explain PATH:line:column",
|
||||||
|
"Explain an issue at a specified location") do |loc|
|
||||||
|
configure_explain_opts(loc, opts)
|
||||||
|
end
|
||||||
|
|
||||||
parser.on("--without-affected-code",
|
parser.on("--without-affected-code",
|
||||||
"Stop showing affected code while using a default formatter") do
|
"Stop showing affected code while using a default formatter") do
|
||||||
opts.without_affected_code = true
|
opts.without_affected_code = true
|
||||||
|
@ -100,6 +118,22 @@ module Ameba::Cli
|
||||||
config.formatter.config[:without_affected_code] = opts.without_affected_code?
|
config.formatter.config[:without_affected_code] = opts.without_affected_code?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private def configure_explain_opts(loc, opts)
|
||||||
|
location_to_explain = parse_explain_location(loc)
|
||||||
|
opts.location_to_explain = location_to_explain
|
||||||
|
opts.files = [location_to_explain[:file]]
|
||||||
|
opts.formatter = :silent
|
||||||
|
end
|
||||||
|
|
||||||
|
private def parse_explain_location(arg)
|
||||||
|
location = arg.split(":", remove_empty: true).map &.strip
|
||||||
|
raise ArgumentError.new unless location.size === 3
|
||||||
|
file, line, column = location
|
||||||
|
{file: file, line: line.to_i, column: column.to_i}
|
||||||
|
rescue
|
||||||
|
raise "location should have PATH:line:column format"
|
||||||
|
end
|
||||||
|
|
||||||
private def print_version
|
private def print_version
|
||||||
puts VERSION
|
puts VERSION
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "./util"
|
||||||
|
|
||||||
# A module that utilizes Ameba's formatters.
|
# A module that utilizes Ameba's formatters.
|
||||||
module Ameba::Formatter
|
module Ameba::Formatter
|
||||||
# A base formatter for all formatters. It uses `output` IO
|
# A base formatter for all formatters. It uses `output` IO
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
require "./util"
|
||||||
|
|
||||||
module Ameba::Formatter
|
module Ameba::Formatter
|
||||||
# A formatter that shows a progress of inspection in a terminal using dots.
|
# A formatter that shows a progress of inspection in a terminal using dots.
|
||||||
# It is similar to Crystal's dot formatter for specs.
|
# It is similar to Crystal's dot formatter for specs.
|
||||||
class DotFormatter < BaseFormatter
|
class DotFormatter < BaseFormatter
|
||||||
|
include Util
|
||||||
|
|
||||||
@started_at : Time?
|
@started_at : Time?
|
||||||
|
|
||||||
# Reports a message when inspection is started.
|
# Reports a message when inspection is started.
|
||||||
|
@ -87,25 +91,5 @@ module Ameba::Formatter
|
||||||
|
|
||||||
"#{total} inspected, #{failures} failure#{s}.\n".colorize color
|
"#{total} inspected, #{failures} failure#{s}.\n".colorize color
|
||||||
end
|
end
|
||||||
|
|
||||||
private def affected_code(source, location, max_length = 100, placeholder = " ...", prompt = "> ")
|
|
||||||
line, column = location.line_number, location.column_number
|
|
||||||
affected_line = source.lines[line - 1]?
|
|
||||||
|
|
||||||
return unless affected_line
|
|
||||||
|
|
||||||
if affected_line.size > max_length && column < max_length
|
|
||||||
affected_line = affected_line[0, max_length - placeholder.size - 1] + placeholder
|
|
||||||
end
|
|
||||||
|
|
||||||
stripped = affected_line.lstrip
|
|
||||||
position = column - (affected_line.size - stripped.size) + prompt.size
|
|
||||||
|
|
||||||
String.build do |str|
|
|
||||||
str << prompt << stripped << "\n"
|
|
||||||
str << " " * (position - 1)
|
|
||||||
str << "^".colorize(:yellow)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
82
src/ameba/formatter/explain_formatter.cr
Normal file
82
src/ameba/formatter/explain_formatter.cr
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
require "./util"
|
||||||
|
|
||||||
|
module Ameba::Formatter
|
||||||
|
# A formatter that shows the detailed explanation of the issue at
|
||||||
|
# a specific location.
|
||||||
|
class ExplainFormatter
|
||||||
|
LINE_BREAK = "\n"
|
||||||
|
PREFIX = "| ".colorize(:yellow)
|
||||||
|
|
||||||
|
include Util
|
||||||
|
|
||||||
|
getter output : IO::FileDescriptor | IO::Memory
|
||||||
|
getter location : Crystal::Location
|
||||||
|
|
||||||
|
# Creates a new instance of ExplainFormatter.
|
||||||
|
# Accepts *output* which indicates the io where the explaination will be wrtitten to.
|
||||||
|
# Second argument is *location* which indicates the location to explain.
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# ExplainFormatter.new output,
|
||||||
|
# {file: path, line: line_number, column: column_number}
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
def initialize(@output, loc)
|
||||||
|
@location = Crystal::Location.new(loc[:file], loc[:line], loc[:column])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reports the explainations at the *@location*.
|
||||||
|
def finished(sources)
|
||||||
|
source = sources.find { |s| s.path == @location.filename }
|
||||||
|
|
||||||
|
return unless source
|
||||||
|
|
||||||
|
source.issues.each do |issue|
|
||||||
|
if (location = issue.location) &&
|
||||||
|
location.line_number == @location.line_number &&
|
||||||
|
location.column_number == @location.column_number
|
||||||
|
explain(source, issue)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def explain(source, issue)
|
||||||
|
rule = issue.rule
|
||||||
|
|
||||||
|
output_title "ISSUE INFO"
|
||||||
|
output_paragraph [
|
||||||
|
issue.message.colorize(:red).to_s,
|
||||||
|
@location.to_s.colorize(:cyan).to_s,
|
||||||
|
]
|
||||||
|
|
||||||
|
if affected_code = affected_code(source, @location)
|
||||||
|
output_title "AFFECTED CODE"
|
||||||
|
output_paragraph affected_code
|
||||||
|
end
|
||||||
|
|
||||||
|
if rule.responds_to?(:description)
|
||||||
|
output_title "RULE INFO"
|
||||||
|
output_paragraph [rule.name, rule.description]
|
||||||
|
end
|
||||||
|
|
||||||
|
output_title "DETAILED DESCRIPTION"
|
||||||
|
output_paragraph(rule.class.parsed_doc || "TO BE DONE...")
|
||||||
|
end
|
||||||
|
|
||||||
|
private def output_title(title)
|
||||||
|
output << PREFIX << title.colorize(:yellow) << LINE_BREAK
|
||||||
|
output << PREFIX << LINE_BREAK
|
||||||
|
end
|
||||||
|
|
||||||
|
private def output_paragraph(paragraph : String)
|
||||||
|
output_paragraph(paragraph.split(LINE_BREAK))
|
||||||
|
end
|
||||||
|
|
||||||
|
private def output_paragraph(paragraph : Array(String))
|
||||||
|
paragraph.each do |line|
|
||||||
|
output << PREFIX << PREFIX << line << LINE_BREAK
|
||||||
|
end
|
||||||
|
output << PREFIX << LINE_BREAK
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
src/ameba/formatter/util.cr
Normal file
23
src/ameba/formatter/util.cr
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
module Ameba::Formatter
|
||||||
|
module Util
|
||||||
|
def affected_code(source, location, max_length = 100, placeholder = " ...", prompt = "> ")
|
||||||
|
line, column = location.line_number, location.column_number
|
||||||
|
affected_line = source.lines[line - 1]?
|
||||||
|
|
||||||
|
return if affected_line.nil? || affected_line.strip.empty?
|
||||||
|
|
||||||
|
if affected_line.size > max_length && column < max_length
|
||||||
|
affected_line = affected_line[0, max_length - placeholder.size - 1] + placeholder
|
||||||
|
end
|
||||||
|
|
||||||
|
stripped = affected_line.lstrip
|
||||||
|
position = column - (affected_line.size - stripped.size) + prompt.size
|
||||||
|
|
||||||
|
String.build do |str|
|
||||||
|
str << prompt << stripped << "\n"
|
||||||
|
str << " " * (position - 1)
|
||||||
|
str << "^".colorize(:yellow)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -82,6 +82,21 @@ module Ameba
|
||||||
@formatter.finished @sources
|
@formatter.finished @sources
|
||||||
end
|
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.
|
# Indicates whether the last inspection successful or not.
|
||||||
# It returns true if no issues in sources found, false otherwise.
|
# It returns true if no issues in sources found, false otherwise.
|
||||||
#
|
#
|
||||||
|
|
Loading…
Reference in a new issue