mirror of
https://gitea.invidious.io/iv-org/shard-ameba.git
synced 2024-08-15 00:53:29 +00:00
Add inline directives parsing and disabling
This commit is contained in:
parent
55b66e7975
commit
9f85b16e09
9 changed files with 212 additions and 7 deletions
11
README.md
11
README.md
|
@ -106,6 +106,17 @@ It allows to configure rule properties, disable specific rules and exclude sourc
|
|||
|
||||
Generate new file by running `ameba --gen-config`.
|
||||
|
||||
### Inline disabling
|
||||
|
||||
One or more rules can't be disabled using inline directives:
|
||||
|
||||
```crystal
|
||||
# ameba:disable LargeNumbers
|
||||
time = Time.epoch(1483859302)
|
||||
|
||||
time = Time.epoch(1483859302) # ameba:disable LargeNumbers
|
||||
```
|
||||
|
||||
## Writing a new Rule
|
||||
|
||||
Adding a new rule is as simple as inheriting from `Ameba::Rule::Base` struct and implementing
|
||||
|
|
|
@ -36,6 +36,27 @@ module Ameba::Formatter
|
|||
subject.finished [Source.new ""]
|
||||
output.to_s.should contain "Finished in"
|
||||
end
|
||||
|
||||
context "when errors found" do
|
||||
it "writes each error" do
|
||||
s = Source.new("").tap do |s|
|
||||
s.error(DummyRule.new, 1, 1, "DummyRuleError")
|
||||
s.error(NamedRule.new, 1, 2, "NamedRuleError")
|
||||
end
|
||||
subject.finished [s]
|
||||
log = output.to_s
|
||||
log.should contain "1 inspected, 2 failures."
|
||||
log.should contain "DummyRuleError"
|
||||
log.should contain "NamedRuleError"
|
||||
end
|
||||
|
||||
it "does not write disabled errors" do
|
||||
s = Source.new ""
|
||||
s.error(DummyRule.new, 1, 1, "DummyRuleError", :disabled)
|
||||
subject.finished [s]
|
||||
output.to_s.should contain "1 inspected, 0 failures."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
87
spec/ameba/inline_comments_spec.cr
Normal file
87
spec/ameba/inline_comments_spec.cr
Normal file
|
@ -0,0 +1,87 @@
|
|||
require "../spec_helper"
|
||||
|
||||
module Ameba
|
||||
describe InlineComments do
|
||||
it "disables a rule with a comment directive" do
|
||||
s = Source.new %Q(
|
||||
# ameba:disable #{NamedRule.name}
|
||||
Time.epoch(1483859302)
|
||||
)
|
||||
s.error(NamedRule.new, 3, 12, "Error!")
|
||||
s.should be_valid
|
||||
end
|
||||
|
||||
it "disables a rule with a line that ends with a comment directive" do
|
||||
s = Source.new %Q(
|
||||
Time.epoch(1483859302) # ameba:disable #{NamedRule.name}
|
||||
)
|
||||
s.error(NamedRule.new, 2, 12, "Error!")
|
||||
s.should be_valid
|
||||
end
|
||||
|
||||
it "does not disable a rule of a different name" do
|
||||
s = Source.new %Q(
|
||||
# ameba:disable WrongName
|
||||
Time.epoch(1483859302)
|
||||
)
|
||||
s.error(NamedRule.new, 3, 12, "Error!")
|
||||
s.should_not be_valid
|
||||
end
|
||||
|
||||
it "disables a rule if multiple rule names provided" do
|
||||
s = Source.new %Q(
|
||||
# ameba:disable SomeRule LargeNumbers #{NamedRule.name} SomeOtherRule
|
||||
Time.epoch(1483859302)
|
||||
)
|
||||
s.error(NamedRule.new, 3, 12, "")
|
||||
s.should be_valid
|
||||
end
|
||||
|
||||
it "disables a rule if multiple rule names are separated by comma" do
|
||||
s = Source.new %Q(
|
||||
# ameba:disable SomeRule, LargeNumbers, #{NamedRule.name}, SomeOtherRule
|
||||
Time.epoch(1483859302)
|
||||
)
|
||||
s.error(NamedRule.new, 3, 12, "")
|
||||
s.should be_valid
|
||||
end
|
||||
|
||||
it "does not disable if multiple rule names used without required one" do
|
||||
s = Source.new %(
|
||||
# ameba:disable SomeRule, SomeOtherRule LargeNumbers
|
||||
Time.epoch(1483859302)
|
||||
)
|
||||
s.error(NamedRule.new, 3, 12, "")
|
||||
s.should_not be_valid
|
||||
end
|
||||
|
||||
it "does not disable if comment directive has wrong place" do
|
||||
s = Source.new %Q(
|
||||
# ameba:disable #{NamedRule.name}
|
||||
#
|
||||
Time.epoch(1483859302)
|
||||
)
|
||||
s.error(NamedRule.new, 4, 12, "")
|
||||
s.should_not be_valid
|
||||
end
|
||||
|
||||
it "does not disable if comment directive added to the wrong line" do
|
||||
s = Source.new %Q(
|
||||
if use_epoch? # ameba:disable #{NamedRule.name}
|
||||
Time.epoch(1483859302)
|
||||
end
|
||||
)
|
||||
s.error(NamedRule.new, 3, 12, "")
|
||||
s.should_not be_valid
|
||||
end
|
||||
|
||||
it "does not disable if that is not a comment directive" do
|
||||
s = Source.new %Q(
|
||||
"ameba:disable #{NamedRule.name}"
|
||||
Time.epoch(1483859302)
|
||||
)
|
||||
s.error(NamedRule.new, 3, 12, "")
|
||||
s.should_not be_valid
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,6 +11,19 @@ module Ameba
|
|||
end
|
||||
end
|
||||
|
||||
struct NamedRule < Rule::Base
|
||||
properties do
|
||||
description : String = "A rule with a custom name."
|
||||
end
|
||||
|
||||
def test(source)
|
||||
end
|
||||
|
||||
def self.name
|
||||
"BreakingRule"
|
||||
end
|
||||
end
|
||||
|
||||
struct ErrorRule < Rule::Base
|
||||
def test(source)
|
||||
source.error self, 1, 1, "This rule always adds an error"
|
||||
|
|
|
@ -25,6 +25,7 @@ module Ameba::Formatter
|
|||
|
||||
failed_sources.each do |source|
|
||||
source.errors.each do |error|
|
||||
next if error.disabled?
|
||||
output << "#{error.location}\n".colorize(:cyan)
|
||||
output << "#{error.rule.name}: #{error.message}\n\n".colorize(:red)
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ module Ameba::Formatter
|
|||
class FlycheckFormatter < BaseFormatter
|
||||
def source_finished(source : Source)
|
||||
source.errors.each do |e|
|
||||
next if e.disabled?
|
||||
if loc = e.location
|
||||
output.printf "%s:%d:%d: %s: %s\n",
|
||||
source.path, loc.line_number, loc.column_number, "E",
|
||||
|
|
|
@ -31,7 +31,7 @@ module Ameba::Formatter
|
|||
private def rule_errors_map(errors)
|
||||
Hash(Rule::Base, Array(Source::Error)).new.tap do |h|
|
||||
errors.each do |error|
|
||||
next if error.rule.is_a? Rule::Syntax
|
||||
next if error.disabled? || error.rule.is_a? Rule::Syntax
|
||||
h[error.rule] ||= Array(Source::Error).new
|
||||
h[error.rule] << error
|
||||
end
|
||||
|
|
63
src/ameba/inline_comments.cr
Normal file
63
src/ameba/inline_comments.cr
Normal file
|
@ -0,0 +1,63 @@
|
|||
module Ameba
|
||||
# A module that represents inline comments parsing and processing logic.
|
||||
module InlineComments
|
||||
COMMENT_DIRECTIVE_REGEX = Regex.new "# ameba : (\\w+) ([\\w, ]+)".gsub(" ", "\\s*")
|
||||
|
||||
# Returns true if current location is disabled for a particular rule,
|
||||
# false otherwise.
|
||||
#
|
||||
# Location is disabled in two cases:
|
||||
# 1. The line of the location ends with a comment directive.
|
||||
# 2. The line above the location is a comment directive.
|
||||
#
|
||||
# For example, here is two examples of disabled location:
|
||||
#
|
||||
# ```
|
||||
# # ameba:disable LargeNumbers
|
||||
# Time.epoch(1483859302)
|
||||
#
|
||||
# Time.epoch(1483859302) # ameba:disable LargeNumbers
|
||||
# ```
|
||||
#
|
||||
# But here are examples which are not considered as disabled location:
|
||||
#
|
||||
# ```
|
||||
# # ameba:disable LargeNumbers
|
||||
# #
|
||||
# Time.epoch(1483859302)
|
||||
#
|
||||
# if use_epoch? # ameba:disable LargeNumbers
|
||||
# Time.epoch(1483859302)
|
||||
# end
|
||||
# ```
|
||||
#
|
||||
def location_disabled?(location, rule)
|
||||
return false unless line_number = location.try &.line_number.try &.- 1
|
||||
return false unless line = lines[line_number]?
|
||||
|
||||
line_disabled?(line, rule) ||
|
||||
(line_number > 0 &&
|
||||
(prev_line = lines[line_number - 1]) &&
|
||||
comment?(prev_line) &&
|
||||
line_disabled?(prev_line, rule))
|
||||
end
|
||||
|
||||
private def comment?(line)
|
||||
line.lstrip.starts_with? '#'
|
||||
end
|
||||
|
||||
private def line_disabled?(line, rule)
|
||||
return false unless inline_comment = parse_inline_comment(line)
|
||||
inline_comment[:action] == "disable" && inline_comment[:rules].includes?(rule)
|
||||
end
|
||||
|
||||
private def parse_inline_comment(line)
|
||||
if comment = COMMENT_DIRECTIVE_REGEX.match(line)
|
||||
{
|
||||
action: comment[1],
|
||||
rules: comment[2].split(/[\s,]/, remove_empty: true),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,6 +2,8 @@ module Ameba
|
|||
# An entity that represents a Crystal source file.
|
||||
# Has path, lines of code and errors reported by rules.
|
||||
class Source
|
||||
include InlineComments
|
||||
|
||||
# Represents an error caught by Ameba.
|
||||
#
|
||||
# Each error has the rule that created this error,
|
||||
|
@ -9,7 +11,12 @@ module Ameba
|
|||
record Error,
|
||||
rule : Rule::Base,
|
||||
location : Crystal::Location?,
|
||||
message : String
|
||||
message : String,
|
||||
status : Symbol? do
|
||||
def disabled?
|
||||
status == :disabled
|
||||
end
|
||||
end
|
||||
|
||||
# Path to the source file.
|
||||
getter path : String
|
||||
|
@ -42,8 +49,9 @@ module Ameba
|
|||
# source.error rule, location, "Line too long"
|
||||
# ```
|
||||
#
|
||||
def error(rule : Rule::Base, location, message : String)
|
||||
errors << Error.new rule, location, message
|
||||
def error(rule : Rule::Base, location, message : String, status = nil)
|
||||
status ||= :disabled if location_disabled?(location, rule.name)
|
||||
errors << Error.new rule, location, message, status
|
||||
end
|
||||
|
||||
# Adds new error to the list of errors using line and column number.
|
||||
|
@ -52,9 +60,9 @@ module Ameba
|
|||
# source.error rule, line_number, column_number, "Bad code"
|
||||
# ```
|
||||
#
|
||||
def error(rule : Rule::Base, l : Int32, c : Int32, message : String)
|
||||
def error(rule : Rule::Base, l, c, message : String, status = nil)
|
||||
location = Crystal::Location.new path, l, c
|
||||
error rule, location, message
|
||||
error rule, location, message, status
|
||||
end
|
||||
|
||||
# Indicates whether source is valid or not.
|
||||
|
@ -68,7 +76,7 @@ module Ameba
|
|||
# ```
|
||||
#
|
||||
def valid?
|
||||
errors.empty?
|
||||
errors.reject(&.disabled?).empty?
|
||||
end
|
||||
|
||||
# Returns lines of code splitted by new line character.
|
||||
|
|
Loading…
Reference in a new issue