Skip to content

Commit fe0faef

Browse files
authored
Merge pull request #249 from khiga8/kh-allow_linter_disable_by_comment
[Feature] Allow rule to be disabled by comment
2 parents 5ffd068 + 0afcd51 commit fe0faef

File tree

12 files changed

+417
-4
lines changed

12 files changed

+417
-4
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ Make sure to add `**/` to exclude patterns; it matches the target files' absolut
7474
## Enable or disable default linters
7575
`EnableDefaultLinters`: enables or disables default linters. [Default linters](#Linters) are enabled by default.
7676

77+
## Disable rule at offense-level
78+
You can disable a rule by placing a disable comment in the following format:
79+
80+
Comment on offending lines
81+
```.erb
82+
<hr /> <%# erblint:disable SelfClosingTag %>
83+
```
84+
85+
To raise an error when there is a useless disable comment, enable `NoUnusedDisable`.
86+
87+
To disable inline comments and report all offenses, set `--disable-inline-configs` option.
88+
7789
## Exclude
7890

7991
You can specify the exclude patterns both of global and lint-local.

lib/erb_lint/cli.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,15 @@ def run(args = ARGV)
6969

7070
@options[:format] ||= :multiline
7171
@options[:fail_level] ||= severity_level_for_name(:refactor)
72+
@options[:disable_inline_configs] ||= false
7273
@stats.files = lint_files.size
7374
@stats.linters = enabled_linter_classes.size
7475
@stats.autocorrectable_linters = enabled_linter_classes.count(&:support_autocorrect?)
7576

7677
reporter = Reporter.create_reporter(@options[:format], @stats, autocorrect?)
7778
reporter.preview
7879

79-
runner = ERBLint::Runner.new(file_loader, @config)
80+
runner = ERBLint::Runner.new(file_loader, @config, @options[:disable_inline_configs])
8081
file_content = nil
8182

8283
lint_files.each do |filename|
@@ -374,6 +375,10 @@ def option_parser
374375
@options[:allow_no_files] = config
375376
end
376377

378+
opts.on("--disable-inline-configs", "Report all offenses while ignoring inline disable comments") do
379+
@options[:disable_inline_configs] = true
380+
end
381+
377382
opts.on(
378383
"-sFILE",
379384
"--stdin FILE",

lib/erb_lint/linter.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require "erb_lint/utils/inline_configs"
4+
35
module ERBLint
46
# Defines common functionality available to all linters.
57
class Linter
@@ -53,12 +55,36 @@ def run(_processed_source)
5355
raise NotImplementedError, "must implement ##{__method__}"
5456
end
5557

58+
def run_and_update_offense_status(processed_source, enable_inline_configs = true)
59+
run(processed_source)
60+
if @offenses.any? && enable_inline_configs
61+
update_offense_status(processed_source)
62+
end
63+
end
64+
5665
def add_offense(source_range, message, context = nil, severity = nil)
5766
@offenses << Offense.new(self, source_range, message, context, severity)
5867
end
5968

6069
def clear_offenses
6170
@offenses = []
6271
end
72+
73+
private
74+
75+
def update_offense_status(processed_source)
76+
@offenses.each do |offense|
77+
offense_line_range = offense.source_range.line_range
78+
offense_lines = source_for_line_range(processed_source, offense_line_range)
79+
80+
if Utils::InlineConfigs.rule_disable_comment_for_lines?(self.class.simple_name, offense_lines)
81+
offense.disabled = true
82+
end
83+
end
84+
end
85+
86+
def source_for_line_range(processed_source, line_range)
87+
processed_source.source_buffer.source_lines[line_range.first - 1..line_range.last - 1].join
88+
end
6389
end
6490
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
require "erb_lint/utils/inline_configs"
4+
5+
module ERBLint
6+
module Linters
7+
# Checks for unused disable comments.
8+
class NoUnusedDisable < Linter
9+
include LinterRegistry
10+
11+
def run(processed_source, offenses)
12+
disabled_rules_and_line_number = {}
13+
14+
processed_source.source_buffer.source_lines.each_with_index do |line, index|
15+
rule_disables = Utils::InlineConfigs.disabled_rules(line)
16+
next unless rule_disables
17+
18+
rule_disables.split(",").each do |rule|
19+
disabled_rules_and_line_number[rule.strip] =
20+
(disabled_rules_and_line_number[rule.strip] ||= []).push(index + 1)
21+
end
22+
end
23+
24+
offenses.each do |offense|
25+
rule_name = offense.linter.class.simple_name
26+
line_numbers = disabled_rules_and_line_number[rule_name]
27+
next unless line_numbers
28+
29+
line_numbers.reject do |line_number|
30+
if (offense.source_range.line_span.first..offense.source_range.line_span.last).include?(line_number)
31+
disabled_rules_and_line_number[rule_name].delete(line_number)
32+
end
33+
end
34+
end
35+
36+
disabled_rules_and_line_number.each do |rule, line_numbers|
37+
line_numbers.each do |line_number|
38+
add_offense(processed_source.source_buffer.line_range(line_number),
39+
"Unused erblint:disable comment for #{rule}")
40+
end
41+
end
42+
end
43+
end
44+
end
45+
end

lib/erb_lint/offense.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def initialize(linter, source_range, message, context = nil, severity = nil)
1515
@message = message
1616
@context = context
1717
@severity = severity
18+
@disabled = false
1819
end
1920

2021
def to_cached_offense_hash
@@ -44,6 +45,12 @@ def line_number
4445
line_range.begin
4546
end
4647

48+
attr_writer :disabled
49+
50+
def disabled?
51+
@disabled
52+
end
53+
4754
def column
4855
source_range.column
4956
end

lib/erb_lint/runner.rb

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,63 @@ module ERBLint
55
class Runner
66
attr_reader :offenses
77

8-
def initialize(file_loader, config)
8+
def initialize(file_loader, config, disable_inline_configs = false)
99
@file_loader = file_loader
1010
@config = config || RunnerConfig.default
1111
raise ArgumentError, "expect `config` to be a RunnerConfig instance" unless @config.is_a?(RunnerConfig)
1212

13-
linter_classes = LinterRegistry.linters.select { |klass| @config.for_linter(klass).enabled? }
13+
linter_classes = LinterRegistry.linters.select do |klass|
14+
@config.for_linter(klass).enabled? && klass != ERBLint::Linters::NoUnusedDisable
15+
end
1416
@linters = linter_classes.map do |linter_class|
1517
linter_class.new(@file_loader, @config.for_linter(linter_class))
1618
end
19+
@no_unused_disable = nil
20+
@disable_inline_configs = disable_inline_configs
1721
@offenses = []
1822
end
1923

2024
def run(processed_source)
2125
@linters
2226
.reject { |linter| linter.excludes_file?(processed_source.filename) }
2327
.each do |linter|
24-
linter.run(processed_source)
28+
linter.run_and_update_offense_status(processed_source, enable_inline_configs?)
2529
@offenses.concat(linter.offenses)
2630
end
31+
report_unused_disable(processed_source)
32+
@offenses = @offenses.reject(&:disabled?)
2733
end
2834

2935
def clear_offenses
3036
@offenses = []
3137
@linters.each(&:clear_offenses)
38+
@no_unused_disable&.clear_offenses
3239
end
3340

3441
def restore_offenses(offenses)
3542
@offenses.concat(offenses)
3643
end
44+
45+
private
46+
47+
def enable_inline_configs?
48+
!@disable_inline_configs
49+
end
50+
51+
def no_unused_disable_enabled?
52+
LinterRegistry.linters.include?(ERBLint::Linters::NoUnusedDisable) &&
53+
@config.for_linter(ERBLint::Linters::NoUnusedDisable).enabled?
54+
end
55+
56+
def report_unused_disable(processed_source)
57+
if no_unused_disable_enabled? && enable_inline_configs?
58+
@no_unused_disable = ERBLint::Linters::NoUnusedDisable.new(
59+
@file_loader,
60+
@config.for_linter(ERBLint::Linters::NoUnusedDisable)
61+
)
62+
@no_unused_disable.run(processed_source, @offenses)
63+
@offenses.concat(@no_unused_disable.offenses)
64+
end
65+
end
3766
end
3867
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
module ERBLint
4+
module Utils
5+
class InlineConfigs
6+
def self.rule_disable_comment_for_lines?(rule, lines)
7+
lines.match?(/# erblint:disable (?<rules>.*#{rule}).*/)
8+
end
9+
10+
def self.disabled_rules(line)
11+
line.match(/# erblint:disable (?<rules>.*) %>/)&.named_captures&.fetch("rules")
12+
end
13+
end
14+
end
15+
end

spec/erb_lint/cli_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "spec_helper"
4+
require "spec_utils"
45
require "erb_lint/cli"
56
require "erb_lint/cache"
67
require "pp"
@@ -98,6 +99,33 @@ def run(processed_source)
9899
end
99100
end
100101

102+
context "with --disable-inline-configs" do
103+
module ERBLint
104+
module Linters
105+
class FakeLinter < Linter
106+
def run(processed_source)
107+
add_offense(SpecUtils.source_range_for_code(processed_source, "<violation></violation>"),
108+
"#{self.class.name} error")
109+
end
110+
end
111+
end
112+
end
113+
let(:linted_file) { "app/views/template.html.erb" }
114+
let(:args) { ["--disable-inline-configs", "--enable-linter", "fake_linter", linted_file] }
115+
let(:file_content) { "<violation></violation> <%# erblint:disable FakeLinter %>" }
116+
117+
before do
118+
allow(ERBLint::LinterRegistry).to(receive(:linters)
119+
.and_return([ERBLint::Linters::FakeLinter]))
120+
FileUtils.mkdir_p(File.dirname(linted_file))
121+
File.write(linted_file, file_content)
122+
end
123+
124+
it "shows all errors regardless of inline disables " do
125+
expect { subject }.to(output(/ERBLint::Linters::FakeLinter error/).to_stdout)
126+
end
127+
end
128+
101129
context "with --clear-cache" do
102130
let(:args) { ["--clear-cache"] }
103131
context "without a cache folder" do
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
require "spec_utils"
5+
6+
describe ERBLint::Linters::NoUnusedDisable do
7+
let(:linter_config) { described_class.config_schema.new }
8+
9+
let(:file_loader) { ERBLint::FileLoader.new(".") }
10+
let(:linter) { described_class.new(file_loader, linter_config) }
11+
let(:processed_source) { ERBLint::ProcessedSource.new("file.rb", file) }
12+
let(:offenses) { linter.offenses }
13+
14+
module ERBLint
15+
module Linters
16+
class Fake < ERBLint::Linter
17+
attr_accessor :offenses
18+
end
19+
end
20+
end
21+
22+
describe "offenses" do
23+
subject { offenses }
24+
context "when file has unused disable comment" do
25+
let(:file) { "<span></span><%# erblint:disable Fake %>" }
26+
before { linter.run(processed_source, []) }
27+
it do
28+
expect(subject.size).to(eq(1))
29+
expect(subject.first.message).to(eq("Unused erblint:disable comment for Fake"))
30+
end
31+
end
32+
33+
context "when file has a disable comment and a corresponding offense" do
34+
let(:file) { "<span></span><%# erblint:disable Fake %>" }
35+
before do
36+
offense = ERBLint::Offense.new(ERBLint::Linters::Fake.new(file_loader, linter_config),
37+
SpecUtils.source_range_for_code(processed_source, "<span></span>"),
38+
"some fake linter message")
39+
offense.disabled = true
40+
linter.run(processed_source, [offense])
41+
end
42+
43+
it "does not report anything" do
44+
expect(subject.size).to(eq(0))
45+
end
46+
end
47+
48+
context "when file has a disable comment in wrong place and a corresponding offense" do
49+
let(:file) { <<~FILE }
50+
<%# erblint:disable Fake %>
51+
<span>bad content</span>
52+
FILE
53+
before do
54+
offense = ERBLint::Offense.new(
55+
ERBLint::Linters::Fake.new(file_loader, linter_config),
56+
SpecUtils.source_range_for_code(processed_source, "<span>bad content</span>"),
57+
"some fake linter message"
58+
)
59+
offense.disabled = true
60+
linter.run(processed_source, [offense])
61+
end
62+
63+
it "reports the unused inline comment" do
64+
expect(subject.size).to(eq(1))
65+
expect(subject.first.message).to(eq("Unused erblint:disable comment for Fake"))
66+
end
67+
end
68+
69+
context "when file has disable comment for multiple rules" do
70+
let(:file) { "<span></span><%# erblint:disable Fake, Fake2 %>" }
71+
before do
72+
offense = ERBLint::Offense.new(
73+
ERBLint::Linters::Fake.new(file_loader, linter_config),
74+
SpecUtils.source_range_for_code(processed_source, "<span></span>"),
75+
"some fake linter message"
76+
)
77+
offense.disabled = true
78+
linter.run(processed_source, [offense])
79+
end
80+
81+
it "reports the unused inline comment" do
82+
expect(subject.size).to(eq(1))
83+
expect(subject.first.message).to(eq("Unused erblint:disable comment for Fake2"))
84+
end
85+
end
86+
87+
context "when file has multiple disable comments for one offense" do
88+
let(:file) { <<~ERB }
89+
<%# erblint:disable Fake %>
90+
<span></span><%# erblint:disable Fake %>
91+
ERB
92+
before do
93+
offense = ERBLint::Offense.new(
94+
ERBLint::Linters::Fake.new(file_loader, linter_config),
95+
SpecUtils.source_range_for_code(processed_source, "<span></span>"),
96+
"some fake linter message"
97+
)
98+
offense.disabled = true
99+
linter.run(processed_source, [offense])
100+
end
101+
102+
it "reports the unused inline comment" do
103+
expect(subject.size).to(eq(1))
104+
expect(subject.first.message).to(eq("Unused erblint:disable comment for Fake"))
105+
end
106+
end
107+
end
108+
end

0 commit comments

Comments
 (0)