Add inline directives parsing and disabling

This commit is contained in:
Vitalii Elenhaupt 2018-01-30 00:25:36 +02:00 committed by V. Elenhaupt
parent 55b66e7975
commit 9f85b16e09
9 changed files with 212 additions and 7 deletions

View File

@ -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

View File

@ -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

View 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

View File

@ -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"

View File

@ -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

View File

@ -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",

View File

@ -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

View 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

View File

@ -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.