erb_lint 0.1.3 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/erb_lint/all.rb +2 -0
- data/lib/erb_lint/cache.rb +88 -0
- data/lib/erb_lint/cached_offense.rb +58 -0
- data/lib/erb_lint/cli.rb +90 -9
- data/lib/erb_lint/corrector.rb +1 -1
- data/lib/erb_lint/linter.rb +27 -1
- data/lib/erb_lint/linters/comment_syntax.rb +52 -0
- data/lib/erb_lint/linters/hard_coded_string.rb +3 -2
- data/lib/erb_lint/linters/no_unused_disable.rb +45 -0
- data/lib/erb_lint/linters/rubocop.rb +13 -26
- data/lib/erb_lint/linters/rubocop_text.rb +1 -0
- data/lib/erb_lint/offense.rb +27 -0
- data/lib/erb_lint/reporters/json_reporter.rb +4 -4
- data/lib/erb_lint/reporters/junit_reporter.rb +112 -0
- data/lib/erb_lint/runner.rb +36 -3
- data/lib/erb_lint/runner_config.rb +2 -1
- data/lib/erb_lint/utils/inline_configs.rb +15 -0
- data/lib/erb_lint/version.rb +1 -1
- metadata +14 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98ba7a87f348e584502a9e4810f9311329c04aeddf34af0b05a37702648b495e
|
4
|
+
data.tar.gz: 8728892a4c09fcbdbddb60316b911bf73ce099cc04e9ac10534b76f8aa6708a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d01f3c07ddebb9482ee744c69df6e799b4c0cb06bd24ffd7939f3d86e612a4f1bb2d5b4040c8505d7c8f8e9cceaecaabcca7dc03cab6ea5f23e73f10e098345
|
7
|
+
data.tar.gz: '080398f6522352b669bf21ea1fbdd8850b7b6b8dcef60493d8349fbfeb98eceeefc77c15442cad339612e83c5056d5498a286046c3a3749a73428661c962b695'
|
data/lib/erb_lint/all.rb
CHANGED
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ERBLint
|
4
|
+
class Cache
|
5
|
+
CACHE_DIRECTORY = ".erb-lint-cache"
|
6
|
+
|
7
|
+
def initialize(config, cache_dir = nil)
|
8
|
+
@config = config
|
9
|
+
@cache_dir = cache_dir || CACHE_DIRECTORY
|
10
|
+
@hits = []
|
11
|
+
@new_results = []
|
12
|
+
puts "Cache mode is on"
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(filename, file_content)
|
16
|
+
file_checksum = checksum(filename, file_content)
|
17
|
+
begin
|
18
|
+
cache_file_contents_as_offenses = JSON.parse(
|
19
|
+
File.read(File.join(@cache_dir, file_checksum))
|
20
|
+
).map do |offense_hash|
|
21
|
+
ERBLint::CachedOffense.new(offense_hash)
|
22
|
+
end
|
23
|
+
rescue Errno::ENOENT
|
24
|
+
return false
|
25
|
+
end
|
26
|
+
@hits.push(file_checksum)
|
27
|
+
cache_file_contents_as_offenses
|
28
|
+
end
|
29
|
+
|
30
|
+
def set(filename, file_content, offenses_as_json)
|
31
|
+
file_checksum = checksum(filename, file_content)
|
32
|
+
@new_results.push(file_checksum)
|
33
|
+
|
34
|
+
FileUtils.mkdir_p(@cache_dir)
|
35
|
+
|
36
|
+
File.open(File.join(@cache_dir, file_checksum), "wb") do |f|
|
37
|
+
f.write(offenses_as_json)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def close
|
42
|
+
prune_cache
|
43
|
+
end
|
44
|
+
|
45
|
+
def prune_cache
|
46
|
+
if hits.empty?
|
47
|
+
puts "Cache being created for the first time, skipping prune"
|
48
|
+
return
|
49
|
+
end
|
50
|
+
|
51
|
+
cache_files = Dir.new(@cache_dir).children
|
52
|
+
cache_files.each do |cache_file|
|
53
|
+
next if hits.include?(cache_file) || new_results.include?(cache_file)
|
54
|
+
|
55
|
+
File.delete(File.join(@cache_dir, cache_file))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def cache_dir_exists?
|
60
|
+
File.directory?(@cache_dir)
|
61
|
+
end
|
62
|
+
|
63
|
+
def clear
|
64
|
+
return unless cache_dir_exists?
|
65
|
+
|
66
|
+
puts "Clearing cache by deleting cache directory"
|
67
|
+
FileUtils.rm_r(@cache_dir)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
attr_reader :config, :hits, :new_results
|
73
|
+
|
74
|
+
def checksum(filename, file_content)
|
75
|
+
digester = Digest::SHA1.new
|
76
|
+
mode = File.stat(filename).mode
|
77
|
+
|
78
|
+
digester.update(
|
79
|
+
"#{mode}#{config.to_hash}#{ERBLint::VERSION}#{file_content}"
|
80
|
+
)
|
81
|
+
digester.hexdigest
|
82
|
+
rescue Errno::ENOENT
|
83
|
+
# Spurious files that come and go should not cause a crash, at least not
|
84
|
+
# here.
|
85
|
+
"_"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ERBLint
|
4
|
+
# A Cached version of an Offense with only essential information represented as strings
|
5
|
+
class CachedOffense
|
6
|
+
attr_reader(
|
7
|
+
:message,
|
8
|
+
:line_number,
|
9
|
+
:severity,
|
10
|
+
:column,
|
11
|
+
:simple_name,
|
12
|
+
:last_line,
|
13
|
+
:last_column,
|
14
|
+
:length,
|
15
|
+
)
|
16
|
+
|
17
|
+
def initialize(params)
|
18
|
+
params = params.transform_keys(&:to_sym)
|
19
|
+
|
20
|
+
@message = params[:message]
|
21
|
+
@line_number = params[:line_number]
|
22
|
+
@severity = params[:severity]&.to_sym
|
23
|
+
@column = params[:column]
|
24
|
+
@simple_name = params[:simple_name]
|
25
|
+
@last_line = params[:last_line]
|
26
|
+
@last_column = params[:last_column]
|
27
|
+
@length = params[:length]
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.new_from_offense(offense)
|
31
|
+
new(
|
32
|
+
{
|
33
|
+
message: offense.message,
|
34
|
+
line_number: offense.line_number,
|
35
|
+
severity: offense.severity,
|
36
|
+
column: offense.column,
|
37
|
+
simple_name: offense.simple_name,
|
38
|
+
last_line: offense.last_line,
|
39
|
+
last_column: offense.last_column,
|
40
|
+
length: offense.length,
|
41
|
+
}
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_h
|
46
|
+
{
|
47
|
+
message: message,
|
48
|
+
line_number: line_number,
|
49
|
+
severity: severity,
|
50
|
+
column: column,
|
51
|
+
simple_name: simple_name,
|
52
|
+
last_line: last_line,
|
53
|
+
last_column: last_column,
|
54
|
+
length: length,
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/erb_lint/cli.rb
CHANGED
@@ -30,12 +30,33 @@ module ERBLint
|
|
30
30
|
def run(args = ARGV)
|
31
31
|
dupped_args = args.dup
|
32
32
|
load_options(dupped_args)
|
33
|
+
|
34
|
+
if cache? && autocorrect?
|
35
|
+
failure!("cannot run autocorrect mode with cache")
|
36
|
+
end
|
37
|
+
|
33
38
|
@files = @options[:stdin] || dupped_args
|
34
39
|
|
35
40
|
load_config
|
36
41
|
|
42
|
+
cache_dir = @options[:cache_dir]
|
43
|
+
@cache = Cache.new(@config, cache_dir) if cache? || clear_cache?
|
44
|
+
|
45
|
+
if clear_cache?
|
46
|
+
if cache.cache_dir_exists?
|
47
|
+
cache.clear
|
48
|
+
success!("cache directory cleared")
|
49
|
+
else
|
50
|
+
failure!("cache directory doesn't exist, skipping deletion.")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
37
54
|
if !@files.empty? && lint_files.empty?
|
38
|
-
|
55
|
+
if allow_no_files?
|
56
|
+
success!("no files found...\n")
|
57
|
+
else
|
58
|
+
failure!("no files found...\n")
|
59
|
+
end
|
39
60
|
elsif lint_files.empty?
|
40
61
|
failure!("no files found or given, specify files or config...\n#{option_parser}")
|
41
62
|
end
|
@@ -48,6 +69,7 @@ module ERBLint
|
|
48
69
|
|
49
70
|
@options[:format] ||= :multiline
|
50
71
|
@options[:fail_level] ||= severity_level_for_name(:refactor)
|
72
|
+
@options[:disable_inline_configs] ||= false
|
51
73
|
@stats.files = lint_files.size
|
52
74
|
@stats.linters = enabled_linter_classes.size
|
53
75
|
@stats.autocorrectable_linters = enabled_linter_classes.count(&:support_autocorrect?)
|
@@ -55,13 +77,13 @@ module ERBLint
|
|
55
77
|
reporter = Reporter.create_reporter(@options[:format], @stats, autocorrect?)
|
56
78
|
reporter.preview
|
57
79
|
|
58
|
-
runner = ERBLint::Runner.new(file_loader, @config)
|
80
|
+
runner = ERBLint::Runner.new(file_loader, @config, @options[:disable_inline_configs])
|
59
81
|
file_content = nil
|
60
82
|
|
61
83
|
lint_files.each do |filename|
|
62
84
|
runner.clear_offenses
|
63
85
|
begin
|
64
|
-
file_content =
|
86
|
+
file_content = run_on_file(runner, filename)
|
65
87
|
rescue => e
|
66
88
|
@stats.exceptions += 1
|
67
89
|
puts "Exception occurred when processing: #{relative_filename(filename)}"
|
@@ -73,6 +95,8 @@ module ERBLint
|
|
73
95
|
end
|
74
96
|
end
|
75
97
|
|
98
|
+
cache&.close
|
99
|
+
|
76
100
|
reporter.show
|
77
101
|
|
78
102
|
if stdin? && autocorrect?
|
@@ -95,13 +119,43 @@ module ERBLint
|
|
95
119
|
|
96
120
|
private
|
97
121
|
|
122
|
+
attr_reader :cache, :config
|
123
|
+
|
124
|
+
def run_on_file(runner, filename)
|
125
|
+
file_content = read_content(filename)
|
126
|
+
|
127
|
+
if cache? && !autocorrect?
|
128
|
+
run_using_cache(runner, filename, file_content)
|
129
|
+
else
|
130
|
+
file_content = run_with_corrections(runner, filename, file_content)
|
131
|
+
end
|
132
|
+
|
133
|
+
log_offense_stats(runner, filename)
|
134
|
+
file_content
|
135
|
+
end
|
136
|
+
|
137
|
+
def run_using_cache(runner, filename, file_content)
|
138
|
+
if (cache_result_offenses = cache.get(filename, file_content))
|
139
|
+
runner.restore_offenses(cache_result_offenses)
|
140
|
+
else
|
141
|
+
run_with_corrections(runner, filename, file_content)
|
142
|
+
cache.set(filename, file_content, runner.offenses.map(&:to_cached_offense_hash).to_json)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
98
146
|
def autocorrect?
|
99
147
|
@options[:autocorrect]
|
100
148
|
end
|
101
149
|
|
102
|
-
def
|
103
|
-
|
150
|
+
def cache?
|
151
|
+
@options[:cache]
|
152
|
+
end
|
104
153
|
|
154
|
+
def clear_cache?
|
155
|
+
@options[:clear_cache]
|
156
|
+
end
|
157
|
+
|
158
|
+
def run_with_corrections(runner, filename, file_content)
|
105
159
|
7.times do
|
106
160
|
processed_source = ERBLint::ProcessedSource.new(filename, file_content)
|
107
161
|
runner.run(processed_source)
|
@@ -123,6 +177,11 @@ module ERBLint
|
|
123
177
|
file_content = corrector.corrected_content
|
124
178
|
runner.clear_offenses
|
125
179
|
end
|
180
|
+
|
181
|
+
file_content
|
182
|
+
end
|
183
|
+
|
184
|
+
def log_offense_stats(runner, filename)
|
126
185
|
offenses_filename = relative_filename(filename)
|
127
186
|
offenses = runner.offenses || []
|
128
187
|
|
@@ -134,8 +193,6 @@ module ERBLint
|
|
134
193
|
|
135
194
|
@stats.processed_files[offenses_filename] ||= []
|
136
195
|
@stats.processed_files[offenses_filename] |= offenses
|
137
|
-
|
138
|
-
file_content
|
139
196
|
end
|
140
197
|
|
141
198
|
def read_content(filename)
|
@@ -165,7 +222,7 @@ module ERBLint
|
|
165
222
|
rescue Psych::SyntaxError => e
|
166
223
|
failure!("error parsing config: #{e.message}")
|
167
224
|
ensure
|
168
|
-
@config
|
225
|
+
@config&.merge!(runner_config_override)
|
169
226
|
end
|
170
227
|
|
171
228
|
def file_loader
|
@@ -262,7 +319,7 @@ module ERBLint
|
|
262
319
|
end
|
263
320
|
end
|
264
321
|
|
265
|
-
opts.on("--format FORMAT", format_options_help) do |format|
|
322
|
+
opts.on("-f", "--format FORMAT", format_options_help) do |format|
|
266
323
|
unless Reporter.available_format?(format)
|
267
324
|
error_message = invalid_format_error_message(format)
|
268
325
|
failure!(error_message)
|
@@ -279,6 +336,18 @@ module ERBLint
|
|
279
336
|
@options[:enabled_linters] = known_linter_names
|
280
337
|
end
|
281
338
|
|
339
|
+
opts.on("--cache", "Enable caching") do |config|
|
340
|
+
@options[:cache] = config
|
341
|
+
end
|
342
|
+
|
343
|
+
opts.on("--cache-dir DIR", "Set the cache directory") do |dir|
|
344
|
+
@options[:cache_dir] = dir
|
345
|
+
end
|
346
|
+
|
347
|
+
opts.on("--clear-cache", "Clear cache") do |config|
|
348
|
+
@options[:clear_cache] = config
|
349
|
+
end
|
350
|
+
|
282
351
|
opts.on("--enable-linters LINTER[,LINTER,...]", Array,
|
283
352
|
"Only use specified linter", "Known linters are: #{known_linter_names.join(", ")}") do |linters|
|
284
353
|
linters.each do |linter|
|
@@ -302,6 +371,14 @@ module ERBLint
|
|
302
371
|
@options[:autocorrect] = config
|
303
372
|
end
|
304
373
|
|
374
|
+
opts.on("--allow-no-files", "When no matching files found, exit successfully (default: false)") do |config|
|
375
|
+
@options[:allow_no_files] = config
|
376
|
+
end
|
377
|
+
|
378
|
+
opts.on("--disable-inline-configs", "Report all offenses while ignoring inline disable comments") do
|
379
|
+
@options[:disable_inline_configs] = true
|
380
|
+
end
|
381
|
+
|
305
382
|
opts.on(
|
306
383
|
"-sFILE",
|
307
384
|
"--stdin FILE",
|
@@ -333,5 +410,9 @@ module ERBLint
|
|
333
410
|
def stdin?
|
334
411
|
@options[:stdin].present?
|
335
412
|
end
|
413
|
+
|
414
|
+
def allow_no_files?
|
415
|
+
@options[:allow_no_files]
|
416
|
+
end
|
336
417
|
end
|
337
418
|
end
|
data/lib/erb_lint/corrector.rb
CHANGED
@@ -12,7 +12,7 @@ module ERBLint
|
|
12
12
|
|
13
13
|
def corrections
|
14
14
|
@corrections ||= @offenses.map do |offense|
|
15
|
-
offense.linter.autocorrect(@processed_source, offense)
|
15
|
+
offense.linter.autocorrect(@processed_source, offense) if offense.linter.class.support_autocorrect?
|
16
16
|
end.compact
|
17
17
|
end
|
18
18
|
|
data/lib/erb_lint/linter.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "erb_lint/utils/inline_configs"
|
4
|
+
|
3
5
|
module ERBLint
|
4
6
|
# Defines common functionality available to all linters.
|
5
7
|
class Linter
|
@@ -30,7 +32,7 @@ module ERBLint
|
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
33
|
-
attr_reader :offenses
|
35
|
+
attr_reader :offenses, :config
|
34
36
|
|
35
37
|
# Must be implemented by the concrete inheriting class.
|
36
38
|
def initialize(file_loader, config)
|
@@ -53,6 +55,13 @@ module ERBLint
|
|
53
55
|
raise NotImplementedError, "must implement ##{__method__}"
|
54
56
|
end
|
55
57
|
|
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
|
+
|
56
65
|
def add_offense(source_range, message, context = nil, severity = nil)
|
57
66
|
@offenses << Offense.new(self, source_range, message, context, severity)
|
58
67
|
end
|
@@ -60,5 +69,22 @@ module ERBLint
|
|
60
69
|
def clear_offenses
|
61
70
|
@offenses = []
|
62
71
|
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
|
63
89
|
end
|
64
90
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ERBLint
|
4
|
+
module Linters
|
5
|
+
# Detects comment syntax that isn't valid ERB.
|
6
|
+
class CommentSyntax < Linter
|
7
|
+
include LinterRegistry
|
8
|
+
|
9
|
+
def initialize(file_loader, config)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(processed_source)
|
14
|
+
file_content = processed_source.file_content
|
15
|
+
return if file_content.empty?
|
16
|
+
|
17
|
+
processed_source.ast.descendants(:erb).each do |erb_node|
|
18
|
+
indicator_node, _, code_node, _ = *erb_node
|
19
|
+
next if code_node.nil?
|
20
|
+
|
21
|
+
indicator_node_str = indicator_node&.deconstruct&.last
|
22
|
+
next if indicator_node_str == "#"
|
23
|
+
|
24
|
+
code_node_str = code_node.deconstruct.last
|
25
|
+
next unless code_node_str.start_with?(" #")
|
26
|
+
|
27
|
+
range = find_range(erb_node, code_node_str)
|
28
|
+
source_range = processed_source.to_source_range(range)
|
29
|
+
|
30
|
+
correct_erb_tag = indicator_node_str == "=" ? "<%#=" : "<%#"
|
31
|
+
|
32
|
+
add_offense(
|
33
|
+
source_range,
|
34
|
+
<<~EOF.chomp
|
35
|
+
Bad ERB comment syntax. Should be #{correct_erb_tag} without a space between.
|
36
|
+
Leaving a space between ERB tags and the Ruby comment character can cause parser errors.
|
37
|
+
EOF
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_range(node, str)
|
43
|
+
match = node.loc.source.match(Regexp.new(Regexp.quote(str.strip)))
|
44
|
+
return unless match
|
45
|
+
|
46
|
+
range_begin = match.begin(0) + node.loc.begin_pos
|
47
|
+
range_end = match.end(0) + node.loc.begin_pos
|
48
|
+
(range_begin...range_end)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -17,7 +17,7 @@ module ERBLint
|
|
17
17
|
ALLOWED_CORRECTORS = ["I18nCorrector", "RuboCop::Corrector::I18n::HardCodedString"]
|
18
18
|
|
19
19
|
NON_TEXT_TAGS = Set.new(["script", "style", "xmp", "iframe", "noembed", "noframes", "listing"])
|
20
|
-
|
20
|
+
NO_TRANSLATION_NEEDED = Set.new([
|
21
21
|
" ",
|
22
22
|
"&",
|
23
23
|
"<",
|
@@ -40,6 +40,7 @@ module ERBLint
|
|
40
40
|
" ",
|
41
41
|
" ",
|
42
42
|
" ",
|
43
|
+
"×",
|
43
44
|
])
|
44
45
|
|
45
46
|
class ConfigSchema < LinterConfig
|
@@ -96,7 +97,7 @@ module ERBLint
|
|
96
97
|
|
97
98
|
def check_string?(str)
|
98
99
|
string = str.gsub(/\s*/, "")
|
99
|
-
string.length > 1 && !
|
100
|
+
string.length > 1 && !NO_TRANSLATION_NEEDED.include?(string)
|
100
101
|
end
|
101
102
|
|
102
103
|
def load_corrector
|
@@ -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
|
@@ -13,6 +13,7 @@ module ERBLint
|
|
13
13
|
class ConfigSchema < LinterConfig
|
14
14
|
property :only, accepts: array_of?(String)
|
15
15
|
property :rubocop_config, accepts: Hash, default: -> { {} }
|
16
|
+
property :config_file_path, accepts: String
|
16
17
|
end
|
17
18
|
|
18
19
|
self.config_schema = ConfigSchema
|
@@ -24,7 +25,8 @@ module ERBLint
|
|
24
25
|
def initialize(file_loader, config)
|
25
26
|
super
|
26
27
|
@only_cops = @config.only
|
27
|
-
custom_config =
|
28
|
+
custom_config = config_from_path(@config.config_file_path) if @config.config_file_path
|
29
|
+
custom_config ||= config_from_hash(@config.rubocop_config)
|
28
30
|
@rubocop_config = ::RuboCop::ConfigLoader.merge_with_default(custom_config, "")
|
29
31
|
end
|
30
32
|
|
@@ -129,11 +131,17 @@ module ERBLint
|
|
129
131
|
end
|
130
132
|
|
131
133
|
def rubocop_processed_source(content, filename)
|
132
|
-
::RuboCop::ProcessedSource.new(
|
134
|
+
source = ::RuboCop::ProcessedSource.new(
|
133
135
|
content,
|
134
136
|
@rubocop_config.target_ruby_version,
|
135
137
|
filename
|
136
138
|
)
|
139
|
+
if ::RuboCop::Version::STRING.to_f >= 1.38
|
140
|
+
registry = RuboCop::Cop::Registry.global
|
141
|
+
source.registry = registry
|
142
|
+
source.config = @rubocop_config
|
143
|
+
end
|
144
|
+
source
|
137
145
|
end
|
138
146
|
|
139
147
|
def cop_classes
|
@@ -158,34 +166,13 @@ module ERBLint
|
|
158
166
|
end
|
159
167
|
|
160
168
|
def config_from_hash(hash)
|
161
|
-
inherit_from = hash&.delete("inherit_from")
|
162
|
-
resolve_inheritance(hash, inherit_from)
|
163
|
-
|
164
169
|
tempfile_from(".erblint-rubocop", hash.to_yaml) do |tempfile|
|
165
|
-
|
170
|
+
config_from_path(tempfile.path)
|
166
171
|
end
|
167
172
|
end
|
168
173
|
|
169
|
-
def
|
170
|
-
|
171
|
-
.reverse_each do |base_config|
|
172
|
-
base_config.each do |k, v|
|
173
|
-
hash[k] = hash.key?(k) ? ::RuboCop::ConfigLoader.merge(v, hash[k]) : v if v.is_a?(Hash)
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def base_configs(inherit_from)
|
179
|
-
regex = URI::DEFAULT_PARSER.make_regexp(["http", "https"])
|
180
|
-
configs = Array(inherit_from).compact.map do |base_name|
|
181
|
-
if base_name =~ /\A#{regex}\z/
|
182
|
-
::RuboCop::ConfigLoader.load_file(::RuboCop::RemoteConfig.new(base_name, Dir.pwd))
|
183
|
-
else
|
184
|
-
config_from_hash(@file_loader.yaml(base_name))
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
configs.compact
|
174
|
+
def config_from_path(path)
|
175
|
+
::RuboCop::ConfigLoader.load_file(path)
|
189
176
|
end
|
190
177
|
|
191
178
|
def add_offense(rubocop_offense, offense_range, correction, offset, bound_range)
|
data/lib/erb_lint/offense.rb
CHANGED
@@ -15,6 +15,11 @@ module ERBLint
|
|
15
15
|
@message = message
|
16
16
|
@context = context
|
17
17
|
@severity = severity
|
18
|
+
@disabled = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_cached_offense_hash
|
22
|
+
ERBLint::CachedOffense.new_from_offense(self).to_h
|
18
23
|
end
|
19
24
|
|
20
25
|
def inspect
|
@@ -40,8 +45,30 @@ module ERBLint
|
|
40
45
|
line_range.begin
|
41
46
|
end
|
42
47
|
|
48
|
+
attr_writer :disabled
|
49
|
+
|
50
|
+
def disabled?
|
51
|
+
@disabled
|
52
|
+
end
|
53
|
+
|
43
54
|
def column
|
44
55
|
source_range.column
|
45
56
|
end
|
57
|
+
|
58
|
+
def simple_name
|
59
|
+
linter.class.simple_name
|
60
|
+
end
|
61
|
+
|
62
|
+
def last_line
|
63
|
+
source_range.last_line
|
64
|
+
end
|
65
|
+
|
66
|
+
def last_column
|
67
|
+
source_range.last_column
|
68
|
+
end
|
69
|
+
|
70
|
+
def length
|
71
|
+
source_range.length
|
72
|
+
end
|
46
73
|
end
|
47
74
|
end
|
@@ -56,14 +56,14 @@ module ERBLint
|
|
56
56
|
|
57
57
|
def format_offense(offense)
|
58
58
|
{
|
59
|
-
linter: offense.
|
59
|
+
linter: offense.simple_name,
|
60
60
|
message: offense.message.to_s,
|
61
61
|
location: {
|
62
62
|
start_line: offense.line_number,
|
63
63
|
start_column: offense.column,
|
64
|
-
last_line: offense.
|
65
|
-
last_column: offense.
|
66
|
-
length: offense.
|
64
|
+
last_line: offense.last_line,
|
65
|
+
last_column: offense.last_column,
|
66
|
+
length: offense.length,
|
67
67
|
},
|
68
68
|
}
|
69
69
|
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rexml/document"
|
4
|
+
require "rexml/formatters/pretty"
|
5
|
+
|
6
|
+
module ERBLint
|
7
|
+
module Reporters
|
8
|
+
class JunitReporter < Reporter
|
9
|
+
def preview; end
|
10
|
+
|
11
|
+
def show
|
12
|
+
xml = create_junit_xml
|
13
|
+
formatted_xml_string = StringIO.new
|
14
|
+
REXML::Formatters::Pretty.new.write(xml, formatted_xml_string)
|
15
|
+
puts formatted_xml_string.string
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
CONTEXT = {
|
21
|
+
prologue_quote: :quote,
|
22
|
+
attribute_quote: :quote,
|
23
|
+
}
|
24
|
+
|
25
|
+
def create_junit_xml
|
26
|
+
# create prologue
|
27
|
+
xml = REXML::Document.new(nil, CONTEXT)
|
28
|
+
xml << REXML::XMLDecl.new("1.0", "UTF-8")
|
29
|
+
|
30
|
+
xml.add_element(create_testsuite_element)
|
31
|
+
|
32
|
+
xml
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_testsuite_element
|
36
|
+
tests = stats.processed_files.size
|
37
|
+
failures = stats.found
|
38
|
+
testsuite_element = REXML::Element.new("testsuite", nil, CONTEXT)
|
39
|
+
testsuite_element.add_attribute("name", "erblint")
|
40
|
+
testsuite_element.add_attribute("tests", tests.to_s)
|
41
|
+
testsuite_element.add_attribute("failures", failures.to_s)
|
42
|
+
|
43
|
+
testsuite_element.add_element(create_properties)
|
44
|
+
|
45
|
+
processed_files.each do |filename, offenses|
|
46
|
+
if offenses.empty?
|
47
|
+
testcase_element = REXML::Element.new("testcase", nil, CONTEXT)
|
48
|
+
testcase_element.add_attribute("name", filename.to_s)
|
49
|
+
testcase_element.add_attribute("file", filename.to_s)
|
50
|
+
|
51
|
+
testsuite_element.add_element(testcase_element)
|
52
|
+
end
|
53
|
+
|
54
|
+
offenses.each do |offense|
|
55
|
+
testsuite_element.add_element(create_testcase(filename, offense))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
testsuite_element
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_properties
|
63
|
+
properties_element = REXML::Element.new("properties", nil, CONTEXT)
|
64
|
+
|
65
|
+
[
|
66
|
+
["erb_lint_version", ERBLint::VERSION],
|
67
|
+
["ruby_engine", RUBY_ENGINE],
|
68
|
+
["ruby_version", RUBY_VERSION],
|
69
|
+
["ruby_patchlevel", RUBY_PATCHLEVEL.to_s],
|
70
|
+
["ruby_platform", RUBY_PLATFORM],
|
71
|
+
].each do |property_attribute|
|
72
|
+
properties_element.add_element(create_property(*property_attribute))
|
73
|
+
end
|
74
|
+
|
75
|
+
properties_element
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_property(name, value)
|
79
|
+
property_element = REXML::Element.new("property")
|
80
|
+
property_element.add_attribute("name", name)
|
81
|
+
property_element.add_attribute("value", value)
|
82
|
+
|
83
|
+
property_element
|
84
|
+
end
|
85
|
+
|
86
|
+
def create_testcase(filename, offense)
|
87
|
+
testcase_element = REXML::Element.new("testcase", nil, CONTEXT)
|
88
|
+
testcase_element.add_attribute("name", filename.to_s)
|
89
|
+
testcase_element.add_attribute("file", filename.to_s)
|
90
|
+
testcase_element.add_attribute("lineno", offense.line_number.to_s)
|
91
|
+
|
92
|
+
testcase_element.add_element(create_failure(filename, offense))
|
93
|
+
|
94
|
+
testcase_element
|
95
|
+
end
|
96
|
+
|
97
|
+
def create_failure(filename, offense)
|
98
|
+
message = offense.message
|
99
|
+
type = offense.simple_name
|
100
|
+
|
101
|
+
failure_element = REXML::Element.new("failure", nil, CONTEXT)
|
102
|
+
failure_element.add_attribute("message", "#{type}: #{message}")
|
103
|
+
failure_element.add_attribute("type", type.to_s)
|
104
|
+
|
105
|
+
cdata_element = REXML::CData.new("#{type}: #{message} at #{filename}:#{offense.line_number}:#{offense.column}")
|
106
|
+
failure_element.add_text(cdata_element)
|
107
|
+
|
108
|
+
failure_element
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/erb_lint/runner.rb
CHANGED
@@ -5,15 +5,19 @@ module ERBLint
|
|
5
5
|
class Runner
|
6
6
|
attr_reader :offenses
|
7
7
|
|
8
|
-
def initialize(file_loader, config)
|
8
|
+
def initialize(file_loader, config, disable_inline_configs = false)
|
9
9
|
@file_loader = file_loader
|
10
10
|
@config = config || RunnerConfig.default
|
11
11
|
raise ArgumentError, "expect `config` to be a RunnerConfig instance" unless @config.is_a?(RunnerConfig)
|
12
12
|
|
13
|
-
linter_classes = LinterRegistry.linters.select
|
13
|
+
linter_classes = LinterRegistry.linters.select do |klass|
|
14
|
+
@config.for_linter(klass).enabled? && klass != ERBLint::Linters::NoUnusedDisable
|
15
|
+
end
|
14
16
|
@linters = linter_classes.map do |linter_class|
|
15
17
|
linter_class.new(@file_loader, @config.for_linter(linter_class))
|
16
18
|
end
|
19
|
+
@no_unused_disable = nil
|
20
|
+
@disable_inline_configs = disable_inline_configs
|
17
21
|
@offenses = []
|
18
22
|
end
|
19
23
|
|
@@ -21,14 +25,43 @@ module ERBLint
|
|
21
25
|
@linters
|
22
26
|
.reject { |linter| linter.excludes_file?(processed_source.filename) }
|
23
27
|
.each do |linter|
|
24
|
-
linter.
|
28
|
+
linter.run_and_update_offense_status(processed_source, enable_inline_configs?)
|
25
29
|
@offenses.concat(linter.offenses)
|
26
30
|
end
|
31
|
+
report_unused_disable(processed_source)
|
32
|
+
@offenses = @offenses.reject(&:disabled?)
|
27
33
|
end
|
28
34
|
|
29
35
|
def clear_offenses
|
30
36
|
@offenses = []
|
31
37
|
@linters.each(&:clear_offenses)
|
38
|
+
@no_unused_disable&.clear_offenses
|
39
|
+
end
|
40
|
+
|
41
|
+
def restore_offenses(offenses)
|
42
|
+
@offenses.concat(offenses)
|
43
|
+
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
|
32
65
|
end
|
33
66
|
end
|
34
67
|
end
|
@@ -63,6 +63,7 @@ module ERBLint
|
|
63
63
|
SpaceInHtmlTag: { enabled: default_enabled },
|
64
64
|
TrailingWhitespace: { enabled: default_enabled },
|
65
65
|
RequireInputAutocomplete: { enabled: default_enabled },
|
66
|
+
CommentSyntax: { enabled: default_enabled },
|
66
67
|
},
|
67
68
|
)
|
68
69
|
end
|
@@ -82,7 +83,7 @@ module ERBLint
|
|
82
83
|
def config_hash_for_linter(klass_name)
|
83
84
|
config_hash = linters_config[klass_name] || {}
|
84
85
|
config_hash["exclude"] ||= []
|
85
|
-
config_hash["exclude"].concat(global_exclude) if config_hash["exclude"].is_a?(Array)
|
86
|
+
config_hash["exclude"].concat(global_exclude).uniq! if config_hash["exclude"].is_a?(Array)
|
86
87
|
config_hash
|
87
88
|
end
|
88
89
|
|
@@ -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
|
data/lib/erb_lint/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: erb_lint
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Chan
|
8
|
+
- Shopify Developers
|
8
9
|
autorequire:
|
9
10
|
bindir: exe
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2023-08-25 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: activesupport
|
@@ -26,32 +27,18 @@ dependencies:
|
|
26
27
|
version: '0'
|
27
28
|
- !ruby/object:Gem::Dependency
|
28
29
|
name: better_html
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: 1.0.7
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: 1.0.7
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: html_tokenizer
|
43
30
|
requirement: !ruby/object:Gem::Requirement
|
44
31
|
requirements:
|
45
32
|
- - ">="
|
46
33
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
34
|
+
version: 2.0.1
|
48
35
|
type: :runtime
|
49
36
|
prerelease: false
|
50
37
|
version_requirements: !ruby/object:Gem::Requirement
|
51
38
|
requirements:
|
52
39
|
- - ">="
|
53
40
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
41
|
+
version: 2.0.1
|
55
42
|
- !ruby/object:Gem::Dependency
|
56
43
|
name: parser
|
57
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -152,7 +139,7 @@ dependencies:
|
|
152
139
|
version: '0'
|
153
140
|
description: ERB Linter tool.
|
154
141
|
email:
|
155
|
-
-
|
142
|
+
- ruby@shopify.com
|
156
143
|
executables:
|
157
144
|
- erblint
|
158
145
|
extensions: []
|
@@ -161,6 +148,8 @@ files:
|
|
161
148
|
- exe/erblint
|
162
149
|
- lib/erb_lint.rb
|
163
150
|
- lib/erb_lint/all.rb
|
151
|
+
- lib/erb_lint/cache.rb
|
152
|
+
- lib/erb_lint/cached_offense.rb
|
164
153
|
- lib/erb_lint/cli.rb
|
165
154
|
- lib/erb_lint/corrector.rb
|
166
155
|
- lib/erb_lint/file_loader.rb
|
@@ -169,12 +158,14 @@ files:
|
|
169
158
|
- lib/erb_lint/linter_registry.rb
|
170
159
|
- lib/erb_lint/linters/allowed_script_type.rb
|
171
160
|
- lib/erb_lint/linters/closing_erb_tag_indent.rb
|
161
|
+
- lib/erb_lint/linters/comment_syntax.rb
|
172
162
|
- lib/erb_lint/linters/deprecated_classes.rb
|
173
163
|
- lib/erb_lint/linters/erb_safety.rb
|
174
164
|
- lib/erb_lint/linters/extra_newline.rb
|
175
165
|
- lib/erb_lint/linters/final_newline.rb
|
176
166
|
- lib/erb_lint/linters/hard_coded_string.rb
|
177
167
|
- lib/erb_lint/linters/no_javascript_tag_helper.rb
|
168
|
+
- lib/erb_lint/linters/no_unused_disable.rb
|
178
169
|
- lib/erb_lint/linters/parser_errors.rb
|
179
170
|
- lib/erb_lint/linters/partial_instance_variable.rb
|
180
171
|
- lib/erb_lint/linters/require_input_autocomplete.rb
|
@@ -192,12 +183,14 @@ files:
|
|
192
183
|
- lib/erb_lint/reporter.rb
|
193
184
|
- lib/erb_lint/reporters/compact_reporter.rb
|
194
185
|
- lib/erb_lint/reporters/json_reporter.rb
|
186
|
+
- lib/erb_lint/reporters/junit_reporter.rb
|
195
187
|
- lib/erb_lint/reporters/multiline_reporter.rb
|
196
188
|
- lib/erb_lint/runner.rb
|
197
189
|
- lib/erb_lint/runner_config.rb
|
198
190
|
- lib/erb_lint/runner_config_resolver.rb
|
199
191
|
- lib/erb_lint/stats.rb
|
200
192
|
- lib/erb_lint/utils/block_map.rb
|
193
|
+
- lib/erb_lint/utils/inline_configs.rb
|
201
194
|
- lib/erb_lint/utils/offset_corrector.rb
|
202
195
|
- lib/erb_lint/utils/ruby_to_erb.rb
|
203
196
|
- lib/erb_lint/utils/severity_levels.rb
|
@@ -215,14 +208,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
215
208
|
requirements:
|
216
209
|
- - ">="
|
217
210
|
- !ruby/object:Gem::Version
|
218
|
-
version: 2.
|
211
|
+
version: 2.7.0
|
219
212
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
220
213
|
requirements:
|
221
214
|
- - ">="
|
222
215
|
- !ruby/object:Gem::Version
|
223
216
|
version: '0'
|
224
217
|
requirements: []
|
225
|
-
rubygems_version: 3.
|
218
|
+
rubygems_version: 3.4.18
|
226
219
|
signing_key:
|
227
220
|
specification_version: 4
|
228
221
|
summary: ERB lint tool
|