erb_lint 0.1.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84ee5b7d05405468648ea7ea174c2f8f57ec1e56490873e1e89e1e9f618df585
4
- data.tar.gz: 3f7db38955673a6218517c8a031c97a9aed5a03fe8a1a9ed11c631cc8aa920f7
3
+ metadata.gz: 98ba7a87f348e584502a9e4810f9311329c04aeddf34af0b05a37702648b495e
4
+ data.tar.gz: 8728892a4c09fcbdbddb60316b911bf73ce099cc04e9ac10534b76f8aa6708a5
5
5
  SHA512:
6
- metadata.gz: e466a9178358d9400fa7a38cda19d1263efecaa1729391b1eea5c9dae2cf928686f97c5d84a0df85ba6774465f415ef94ec628cda8cc38834f2983fe525f41e7
7
- data.tar.gz: 1f7cc9b5a8994d9378f70085d69e0012baa78c0da7896b366dfd32f894f05b4c3978c831eab31f6b209c9c4a42bfe36dcf0bb413a872a91a2bb22bfc79effc16
6
+ metadata.gz: 8d01f3c07ddebb9482ee744c69df6e799b4c0cb06bd24ffd7939f3d86e612a4f1bb2d5b4040c8505d7c8f8e9cceaecaabcca7dc03cab6ea5f23e73f10e098345
7
+ data.tar.gz: '080398f6522352b669bf21ea1fbdd8850b7b6b8dcef60493d8349fbfeb98eceeefc77c15442cad339612e83c5056d5498a286046c3a3749a73428661c962b695'
data/lib/erb_lint/all.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require "rubocop"
4
4
 
5
5
  require "erb_lint"
6
+ require "erb_lint/cache"
7
+ require "erb_lint/cached_offense"
6
8
  require "erb_lint/corrector"
7
9
  require "erb_lint/file_loader"
8
10
  require "erb_lint/linter_config"
@@ -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
- failure!("no files found...\n")
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 = run_with_corrections(runner, filename)
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 run_with_corrections(runner, filename)
103
- file_content = read_content(filename)
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.merge!(runner_config_override)
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
@@ -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
 
@@ -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
- TEXT_NOT_ALLOWED = Set.new([
20
+ NO_TRANSLATION_NEEDED = Set.new([
21
21
  "&nbsp;",
22
22
  "&amp;",
23
23
  "&lt;",
@@ -40,6 +40,7 @@ module ERBLint
40
40
  "&ensp;",
41
41
  "&emsp;",
42
42
  "&thinsp;",
43
+ "&times;",
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 && !TEXT_NOT_ALLOWED.include?(string)
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 = config_from_hash(@config.rubocop_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
- ::RuboCop::ConfigLoader.load_file(tempfile.path)
170
+ config_from_path(tempfile.path)
166
171
  end
167
172
  end
168
173
 
169
- def resolve_inheritance(hash, inherit_from)
170
- base_configs(inherit_from)
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)
@@ -10,6 +10,7 @@ module ERBLint
10
10
  class ConfigSchema < LinterConfig
11
11
  property :only, accepts: array_of?(String)
12
12
  property :rubocop_config, accepts: Hash
13
+ property :config_file_path, accepts: String
13
14
  end
14
15
 
15
16
  self.config_schema = ConfigSchema
@@ -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.linter.class.simple_name,
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.source_range.last_line,
65
- last_column: offense.source_range.last_column,
66
- length: offense.source_range.length,
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
@@ -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 { |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
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.run(processed_source)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ERBLint
4
- VERSION = "0.1.3"
4
+ VERSION = "0.5.0"
5
5
  end
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.1.3
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: 2022-06-16 00:00:00.000000000 Z
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: '0'
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: '0'
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
- - justin.the.c@gmail.com
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.5.0
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.3.3
218
+ rubygems_version: 3.4.18
226
219
  signing_key:
227
220
  specification_version: 4
228
221
  summary: ERB lint tool