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