haml_lint 0.45.0 → 0.48.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/bin/haml-lint +1 -1
  3. data/config/default.yml +6 -28
  4. data/config/forced_rubocop_config.yml +171 -0
  5. data/lib/haml_lint/adapter/haml_4.rb +18 -0
  6. data/lib/haml_lint/adapter/haml_5.rb +11 -0
  7. data/lib/haml_lint/adapter/haml_6.rb +11 -0
  8. data/lib/haml_lint/cli.rb +8 -3
  9. data/lib/haml_lint/configuration_loader.rb +9 -10
  10. data/lib/haml_lint/document.rb +89 -8
  11. data/lib/haml_lint/exceptions.rb +6 -0
  12. data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +35 -0
  13. data/lib/haml_lint/file_finder.rb +2 -2
  14. data/lib/haml_lint/lint.rb +10 -1
  15. data/lib/haml_lint/linter/final_newline.rb +4 -3
  16. data/lib/haml_lint/linter/implicit_div.rb +1 -1
  17. data/lib/haml_lint/linter/indentation.rb +3 -3
  18. data/lib/haml_lint/linter/no_placeholders.rb +18 -0
  19. data/lib/haml_lint/linter/repeated_id.rb +2 -1
  20. data/lib/haml_lint/linter/rubocop.rb +353 -60
  21. data/lib/haml_lint/linter/space_before_script.rb +8 -10
  22. data/lib/haml_lint/linter/unnecessary_string_output.rb +1 -1
  23. data/lib/haml_lint/linter/view_length.rb +1 -1
  24. data/lib/haml_lint/linter.rb +60 -10
  25. data/lib/haml_lint/linter_registry.rb +3 -5
  26. data/lib/haml_lint/logger.rb +2 -2
  27. data/lib/haml_lint/options.rb +26 -2
  28. data/lib/haml_lint/rake_task.rb +2 -2
  29. data/lib/haml_lint/reporter/hash_reporter.rb +1 -3
  30. data/lib/haml_lint/reporter/offense_count_reporter.rb +1 -1
  31. data/lib/haml_lint/reporter/utils.rb +33 -4
  32. data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +24 -0
  33. data/lib/haml_lint/ruby_extraction/base_chunk.rb +114 -0
  34. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +630 -0
  35. data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
  36. data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +34 -0
  37. data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +17 -0
  38. data/lib/haml_lint/ruby_extraction/interpolation_chunk.rb +26 -0
  39. data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +32 -0
  40. data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +40 -0
  41. data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +33 -0
  42. data/lib/haml_lint/ruby_extraction/ruby_source.rb +5 -0
  43. data/lib/haml_lint/ruby_extraction/script_chunk.rb +244 -0
  44. data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +63 -0
  45. data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
  46. data/lib/haml_lint/runner.rb +35 -3
  47. data/lib/haml_lint/spec/matchers/report_lint.rb +22 -7
  48. data/lib/haml_lint/spec/normalize_indent.rb +2 -2
  49. data/lib/haml_lint/spec/shared_linter_context.rb +9 -1
  50. data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +158 -0
  51. data/lib/haml_lint/spec.rb +1 -0
  52. data/lib/haml_lint/tree/filter_node.rb +10 -0
  53. data/lib/haml_lint/tree/node.rb +13 -4
  54. data/lib/haml_lint/tree/tag_node.rb +5 -9
  55. data/lib/haml_lint/utils.rb +135 -5
  56. data/lib/haml_lint/version.rb +1 -1
  57. data/lib/haml_lint/version_comparer.rb +25 -0
  58. data/lib/haml_lint.rb +12 -0
  59. metadata +24 -6
  60. data/lib/haml_lint/ruby_extractor.rb +0 -223
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint
4
+ # Checks that placeholder attributes are not used.
5
+ class Linter::NoPlaceholders < Linter
6
+ include LinterRegistry
7
+
8
+ MSG = 'Placeholders attributes should not be used.'
9
+ HASH_REGEXP = /:?['"]?placeholder['"]?(?::| *=>)/.freeze
10
+ HTML_REGEXP = /placeholder=/.freeze
11
+
12
+ def visit_tag(node)
13
+ return unless node.hash_attributes_source =~ HASH_REGEXP || node.html_attributes_source =~ HTML_REGEXP
14
+
15
+ record_lint(node, MSG)
16
+ end
17
+ end
18
+ end
@@ -8,13 +8,14 @@ module HamlLint
8
8
  MESSAGE_FORMAT = %{Do not repeat id "#%s" on the page}
9
9
 
10
10
  def visit_root(_node)
11
- @id_map = Hash.new { |hash, key| hash[key] = [] }
11
+ @id_map = {}
12
12
  end
13
13
 
14
14
  def visit_tag(node)
15
15
  id = node.tag_id
16
16
  return unless id && !id.empty?
17
17
 
18
+ id_map[id] ||= []
18
19
  nodes = (id_map[id] << node)
19
20
  case nodes.size
20
21
  when 1 then nil
@@ -1,74 +1,253 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'haml_lint/ruby_extractor'
4
3
  require 'rubocop'
4
+ require 'tempfile'
5
5
 
6
6
  module HamlLint
7
- # Runs RuboCop on Ruby code contained within HAML templates.
7
+ # Runs RuboCop on the Ruby code contained within HAML templates.
8
+ #
9
+ # The processing is done by extracting a Ruby file that matches the content, including
10
+ # the indentation, of the HAML file. This way, we can run RuboCop with autocorrect
11
+ # and get new Ruby code which should be HAML compatible.
12
+ #
13
+ # The ruby extraction makes "Chunks" which wrap each HAML constructs. The Chunks can then
14
+ # use the corrected Ruby code to apply the corrections back in the HAML using logic specific
15
+ # to each type of Chunk.
16
+ #
17
+ # The work is spread across the classes in the HamlLint::RubyExtraction module.
8
18
  class Linter::RuboCop < Linter
9
19
  include LinterRegistry
10
20
 
21
+ supports_autocorrect(true)
22
+
11
23
  # Maps the ::RuboCop::Cop::Severity levels to our own levels.
12
24
  SEVERITY_MAP = {
13
25
  error: :error,
14
26
  fatal: :error,
15
-
16
27
  convention: :warning,
17
28
  refactor: :warning,
18
29
  warning: :warning,
30
+ info: :info,
19
31
  }.freeze
20
32
 
21
- def visit_root(_node)
22
- extractor = HamlLint::RubyExtractor.new
23
- extracted_source = extractor.extract(document)
33
+ # Debug fields, also used in tests
34
+ attr_accessor :last_extracted_source
35
+ attr_accessor :last_new_ruby_source
36
+
37
+ def visit_root(_node) # rubocop:disable Metrics
38
+ # Need to call the received block to avoid Linter automatically visiting children
39
+ # Only important thing is that the argument is not ":children"
40
+ yield :skip_children
41
+
42
+ if document.indentation && document.indentation != ' '
43
+ @lints <<
44
+ HamlLint::Lint.new(
45
+ self,
46
+ document.file,
47
+ nil,
48
+ "Only supported indentation is 2 spaces, got: #{document.indentation.dump}",
49
+ :error
50
+ )
51
+ return
52
+ end
53
+
54
+ @last_extracted_source = nil
55
+ @last_new_ruby_source = nil
56
+
57
+ coordinator = HamlLint::RubyExtraction::Coordinator.new(document)
58
+
59
+ extracted_source = coordinator.extract_ruby_source
60
+ if ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
61
+ puts "------ Extracted ruby from #{@document.file}:"
62
+ puts extracted_source.source
63
+ puts '------'
64
+ end
65
+
66
+ @last_extracted_source = extracted_source
67
+
68
+ if extracted_source.source.empty?
69
+ @last_new_ruby_source = ''
70
+ return
71
+ end
72
+
73
+ new_ruby_code = process_ruby_source(extracted_source.source, extracted_source.source_map)
74
+
75
+ if @autocorrect && ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
76
+ puts "------ Autocorrected extracted ruby from #{@document.file}:"
77
+ puts new_ruby_code
78
+ puts '------'
79
+ end
80
+
81
+ if @autocorrect && transfer_corrections?(extracted_source.source, new_ruby_code)
82
+ @last_new_ruby_source = new_ruby_code
83
+ transfer_corrections(coordinator, new_ruby_code)
84
+ end
85
+ end
86
+
87
+ def self.cops_names_not_supporting_autocorrect
88
+ return @cops_names_not_supporting_autocorrect if @cops_names_not_supporting_autocorrect
89
+ return [] unless ::RuboCop::Cop::Registry.respond_to?(:all)
90
+
91
+ cops_without_autocorrect = ::RuboCop::Cop::Registry.all.reject(&:support_autocorrect?)
92
+ # This cop cannot be disabled
93
+ cops_without_autocorrect.delete(::RuboCop::Cop::Lint::Syntax)
94
+ @cops_names_not_supporting_autocorrect = cops_without_autocorrect.map { |cop| cop.badge.to_s }.freeze
95
+ end
96
+
97
+ private
98
+
99
+ def rubocop_config_for(path)
100
+ user_config_path = ENV['HAML_LINT_RUBOCOP_CONF'] || config['config_file']
101
+ user_config_path ||= self.class.rubocop_config_store.user_rubocop_config_path_for(path)
102
+ user_config_path = File.absolute_path(user_config_path)
103
+ self.class.rubocop_config_store.config_object_pointing_to(user_config_path)
104
+ end
105
+
106
+ # Extracted here so that tests can stub this to always return true
107
+ def transfer_corrections?(initial_ruby_code, new_ruby_code)
108
+ initial_ruby_code != new_ruby_code
109
+ end
110
+
111
+ def transfer_corrections(coordinator, new_ruby_code)
112
+ begin
113
+ new_haml_lines = coordinator.haml_lines_with_corrections_applied(new_ruby_code)
114
+ rescue HamlLint::RubyExtraction::UnableToTransferCorrections => e
115
+ # Those are lints we couldn't correct. If haml-lint was called without the
116
+ # --auto-correct-only, then this linter will be called again without autocorrect,
117
+ # so the lints will be recorded then.
118
+ @lints = []
119
+
120
+ msg = "Corrections couldn't be transfered: #{e.message} - Consider linting the file " \
121
+ 'without auto-correct and doing the changes manually.'
122
+ if ENV['HAML_LINT_DEBUG'] == 'true'
123
+ msg = "#{msg} DEBUG: Rubocop corrected Ruby code follows:\n#{new_ruby_code}\n------"
124
+ end
125
+
126
+ @lints << HamlLint::Lint.new(self, document.file, nil, msg, :error)
127
+ return
128
+ end
24
129
 
25
- return if extracted_source.source.empty?
130
+ new_haml_string = new_haml_lines.join("\n")
26
131
 
27
- find_lints(extracted_source.source, extracted_source.source_map)
132
+ if new_haml_validity_checks(new_haml_string)
133
+ document.change_source(new_haml_string)
134
+ true
135
+ else
136
+ false
137
+ end
138
+ end
139
+
140
+ def new_haml_validity_checks(new_haml_string)
141
+ new_haml_error = HamlLint::Utils.check_error_when_compiling_haml(new_haml_string)
142
+ return true unless new_haml_error
143
+
144
+ error_message = if new_haml_error.is_a?(::SyntaxError)
145
+ 'Corrections by haml-lint generate Haml that will have Ruby syntax error. Skipping.'
146
+ else
147
+ 'Corrections by haml-lint generate invalid Haml. Skipping.'
148
+ end
149
+
150
+ if ENV['HAML_LINT_DEBUG'] == 'true'
151
+ error_message = error_message.dup
152
+ error_message << "\nDEBUG: Here is the exception:\n#{new_haml_error.full_message}"
153
+
154
+ error_message << "DEBUG: This is the (wrong) HAML after the corrections:\n"
155
+ if new_haml_error.respond_to?(:line)
156
+ error_message << "(DEBUG: Line number of error in the HAML: #{new_haml_error.line})\n"
157
+ end
158
+ error_message << new_haml_string
159
+ else
160
+ # Those are lints we couldn't correct. If haml-lint was called without the
161
+ # --auto-correct-only, then this linter will be called again without autocorrect,
162
+ # so the lints will be recorded then. If it was called with --auto-correct-only,
163
+ # then we did nothing so it makes sense not to show the lints.
164
+ @lints = []
165
+ end
166
+
167
+ @lints << HamlLint::Lint.new(self, document.file, nil, error_message, :error)
168
+ false
28
169
  end
29
170
 
30
171
  # A single CLI instance is shared between files to avoid RuboCop
31
172
  # having to repeatedly reload .rubocop.yml.
32
- def self.rubocop_cli
173
+ def self.rubocop_cli # rubocop:disable Lint/IneffectiveAccessModifier
33
174
  # The ivar is stored on the class singleton rather than the Linter instance
34
175
  # because it can't be Marshal.dump'd (as used by Parallel.map)
35
176
  @rubocop_cli ||= ::RuboCop::CLI.new
36
177
  end
37
178
 
38
- private
179
+ def self.rubocop_config_store # rubocop:disable Lint/IneffectiveAccessModifier
180
+ @rubocop_config_store ||= RubocopConfigStore.new
181
+ end
39
182
 
40
- # Executes RuboCop against the given Ruby code and records the offenses as
41
- # lints.
183
+ # Executes RuboCop against the given Ruby code, records the offenses as
184
+ # lints, runs autocorrect if requested and returns the corrected ruby.
42
185
  #
43
- # @param ruby [String] Ruby code
186
+ # @param ruby_code [String] Ruby code
44
187
  # @param source_map [Hash] map of Ruby code line numbers to original line
45
188
  # numbers in the template
46
- def find_lints(ruby, source_map)
47
- filename =
48
- if document.file
49
- "#{document.file}.rb"
50
- else
51
- 'ruby_script.rb'
52
- end
189
+ # @return [String] The autocorrected Ruby source code
190
+ def process_ruby_source(ruby_code, source_map)
191
+ filename = document.file || 'ruby_script.rb'
53
192
 
54
- with_ruby_from_stdin(ruby) do
55
- extract_lints_from_offenses(lint_file(self.class.rubocop_cli, filename), source_map)
56
- end
193
+ offenses, corrected_ruby = run_rubocop(self.class.rubocop_cli, ruby_code, filename)
194
+
195
+ extract_lints_from_offenses(offenses, source_map)
196
+ corrected_ruby
57
197
  end
58
198
 
59
- # Defined so we can stub the results in tests
199
+ # Runs RuboCop, returning the offenses and corrected code. Raises when RuboCop
200
+ # fails to run correctly.
60
201
  #
61
- # @param rubocop [RuboCop::CLI]
62
- # @param file [String]
63
- # @return [Array<RuboCop::Cop::Offense>]
64
- def lint_file(rubocop, file)
65
- status = rubocop.run(rubocop_flags << file)
66
- unless [::RuboCop::CLI::STATUS_SUCCESS, ::RuboCop::CLI::STATUS_OFFENSES].include?(status)
202
+ # @param rubocop_cli [RuboCop::CLI] There to simplify tests by using a stub
203
+ # @param ruby_code [String] The ruby code to run through RuboCop
204
+ # @param path [String] the path to tell RuboCop we are running
205
+ # @return [Array<RuboCop::Cop::Offense>, String]
206
+ def run_rubocop(rubocop_cli, ruby_code, path) # rubocop:disable Metrics
207
+ rubocop_status = nil
208
+ stdout_str, stderr_str = HamlLint::Utils.with_captured_streams(ruby_code) do
209
+ rubocop_cli.config_store.instance_variable_set(:@options_config, rubocop_config_for(path))
210
+ rubocop_status = rubocop_cli.run(rubocop_flags + ['--stdin', path])
211
+ end
212
+
213
+ if ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
214
+ if OffenseCollector.offenses.empty?
215
+ puts "------ No lints found by RuboCop in #{@document.file}"
216
+ else
217
+ puts "------ Raw lints found by RuboCop in #{@document.file}"
218
+ OffenseCollector.offenses.each do |offense|
219
+ puts offense
220
+ end
221
+ puts '------'
222
+ end
223
+ end
224
+
225
+ unless [::RuboCop::CLI::STATUS_SUCCESS, ::RuboCop::CLI::STATUS_OFFENSES].include?(rubocop_status)
226
+ if stderr_str.start_with?('Infinite loop')
227
+ msg = "RuboCop exited unsuccessfully with status #{rubocop_status}." \
228
+ ' This appears to be due to an autocorrection infinite loop.'
229
+ if ENV['HAML_LINT_DEBUG'] == 'true'
230
+ msg += " DEBUG: RuboCop's output:\n"
231
+ msg += stderr_str.strip
232
+ else
233
+ msg += " First line of RuboCop's output (Use --debug mode to see more):\n"
234
+ msg += stderr_str.each_line.first.strip
235
+ end
236
+
237
+ raise HamlLint::Exceptions::InfiniteLoopError, msg
238
+ end
239
+
67
240
  raise HamlLint::Exceptions::ConfigurationError,
68
- "RuboCop exited unsuccessfully with status #{status}." \
69
- ' Check the stack trace to see if there was a misconfiguration.'
241
+ "RuboCop exited unsuccessfully with status #{rubocop_status}." \
242
+ ' Here is its output to check the stack trace or see if there was' \
243
+ " a misconfiguration:\n#{stderr_str}"
70
244
  end
71
- OffenseCollector.offenses
245
+
246
+ if @autocorrect
247
+ corrected_ruby = stdout_str.partition("#{'=' * 20}\n").last
248
+ end
249
+
250
+ [OffenseCollector.offenses, corrected_ruby]
72
251
  end
73
252
 
74
253
  # Aggregates RuboCop offenses and converts them to {HamlLint::Lint}s
@@ -76,51 +255,92 @@ module HamlLint
76
255
  #
77
256
  # @param offenses [Array<RuboCop::Cop::Offense>]
78
257
  # @param source_map [Hash]
79
- def extract_lints_from_offenses(offenses, source_map)
80
- dummy_node = Struct.new(:line)
258
+ def extract_lints_from_offenses(offenses, source_map) # rubocop:disable Metrics
259
+ offenses.each do |offense|
260
+ next if Array(config['ignored_cops']).include?(offense.cop_name)
261
+ autocorrected = offense.status == :corrected
262
+
263
+ # There will be another execution to deal with not auto-corrected stuff unless
264
+ # we are in autocorrect-only mode, where we don't want not auto-corrected stuff.
265
+ next if @autocorrect && !autocorrected && offense.cop_name != 'Lint/Syntax'
81
266
 
82
- offenses.reject { |offense| Array(config['ignored_cops']).include?(offense.cop_name) }
83
- .each do |offense|
84
- record_lint(dummy_node.new(source_map[offense.line]), offense.message,
85
- offense.severity.name)
267
+ if ENV['HAML_LINT_INTERNAL_DEBUG']
268
+ line = offense.line
269
+ else
270
+ line = source_map[offense.line]
271
+
272
+ if line.nil? && offense.line == source_map.keys.max + 1
273
+ # The sourcemap doesn't include an entry for the line just after the last line,
274
+ # but rubocop sometimes does place offenses there.
275
+ line = source_map[offense.line - 1]
276
+ end
277
+ end
278
+ record_lint(line, offense.message, offense.severity.name,
279
+ corrected: autocorrected)
86
280
  end
87
281
  end
88
282
 
89
283
  # Record a lint for reporting back to the user.
90
284
  #
91
- # @param node [#line] node to extract the line number from
285
+ # @param line [#line] line number of the lint
92
286
  # @param message [String] error/warning to display to the user
93
287
  # @param severity [Symbol] RuboCop severity level for the offense
94
- def record_lint(node, message, severity)
95
- @lints << HamlLint::Lint.new(self, @document.file, node.line, message,
96
- SEVERITY_MAP.fetch(severity, :warning))
288
+ def record_lint(line, message, severity, corrected:)
289
+ # TODO: actual handling for RuboCop's new :info severity
290
+ return if severity == :info
291
+
292
+ @lints << HamlLint::Lint.new(self, @document.file, line, message,
293
+ SEVERITY_MAP.fetch(severity, :warning),
294
+ corrected: corrected)
97
295
  end
98
296
 
99
297
  # Returns flags that will be passed to RuboCop CLI.
100
298
  #
101
299
  # @return [Array<String>]
102
300
  def rubocop_flags
103
- config_file = ENV['HAML_LINT_RUBOCOP_CONF'] || config['config_file']
104
301
  flags = %w[--format HamlLint::OffenseCollector]
105
- flags += ['--config', config_file] if config_file
106
- flags += ['--stdin']
302
+ flags += ignored_cops_flags
303
+ flags += rubocop_autocorrect_flags
107
304
  flags
108
305
  end
109
306
 
110
- # Overrides the global stdin to allow RuboCop to read Ruby code from it.
111
- #
112
- # @param ruby [String] the Ruby code to write to the overridden stdin
113
- # @param _block [Block] the block to perform with the overridden stdin
114
- # @return [void]
115
- def with_ruby_from_stdin(ruby, &_block)
116
- original_stdin = $stdin
117
- stdin = StringIO.new
118
- stdin.write(ruby)
119
- stdin.rewind
120
- $stdin = stdin
121
- yield
122
- ensure
123
- $stdin = original_stdin
307
+ def rubocop_autocorrect_flags
308
+ return [] unless @autocorrect
309
+
310
+ rubocop_version = Gem::Version.new(::RuboCop::Version::STRING)
311
+
312
+ case @autocorrect
313
+ when :safe
314
+ if rubocop_version >= Gem::Version.new('1.30')
315
+ ['--autocorrect']
316
+ else
317
+ ['--auto-correct']
318
+ end
319
+ when :all
320
+ if rubocop_version >= Gem::Version.new('1.30')
321
+ ['--autocorrect-all']
322
+ else
323
+ ['--auto-correct-all']
324
+ end
325
+ else
326
+ raise "Unexpected autocorrect option: #{@autocorrect.inspect}"
327
+ end
328
+ end
329
+
330
+ # Because of autocorrect, we need to pass the ignored cops to RuboCop to
331
+ # prevent it from doing fixes we don't want.
332
+ # Because cop names changed names over time, we cleanup those that don't exist
333
+ # anymore or don't exist yet.
334
+ # This is not exhaustive, it's only for the cops that are in config/default.yml
335
+ def ignored_cops_flags
336
+ ignored_cops = config.fetch('ignored_cops', [])
337
+
338
+ if @autocorrect
339
+ ignored_cops += self.class.cops_names_not_supporting_autocorrect
340
+ end
341
+
342
+ return [] if ignored_cops.empty?
343
+ ['--except', ignored_cops.uniq.join(',')]
124
344
  end
125
345
  end
126
346
 
@@ -147,4 +367,77 @@ module HamlLint
147
367
  self.class.offenses += offenses
148
368
  end
149
369
  end
370
+
371
+ # To handle our need to force some configurations on RuboCop, while still allowing users
372
+ # to customize most of RuboCop using their own rubocop.yml config(s), we need to detect
373
+ # the effective RuboCop configuration for a specific file, and generate a new configuration
374
+ # containing our own "forced configuration" with a `inherit_from` that points on the
375
+ # user's configuration.
376
+ #
377
+ # This class handles all of this logic.
378
+ class RubocopConfigStore
379
+ def initialize
380
+ @dir_path_to_user_config_path = {}
381
+ @user_config_path_to_config_object = {}
382
+ end
383
+
384
+ # Build a RuboCop::Config from config/forced_rubocop_config.yml which inherits from the given
385
+ # user_config_path and return it's path.
386
+ def config_object_pointing_to(user_config_path)
387
+ if @user_config_path_to_config_object[user_config_path]
388
+ return @user_config_path_to_config_object[user_config_path]
389
+ end
390
+
391
+ final_config_hash = forced_rubocop_config_hash.dup
392
+
393
+ if user_config_path != ::RuboCop::ConfigLoader::DEFAULT_FILE
394
+ # If we manually inherit from the default RuboCop config, we may get warnings
395
+ # for deprecated stuff that is in it. We don't when we automatically
396
+ # inherit from it (which always happens)
397
+ final_config_hash['inherit_from'] = user_config_path
398
+ end
399
+
400
+ config_object = Tempfile.create(['.haml-lint-rubocop', '.yml']) do |tempfile|
401
+ tempfile.write(final_config_hash.to_yaml)
402
+ tempfile.close
403
+ ::RuboCop::ConfigLoader.configuration_from_file(tempfile.path)
404
+ end
405
+
406
+ @user_config_path_to_config_object[user_config_path] = config_object
407
+ end
408
+
409
+ # Find the path to the effective RuboCop configuration for a path (file or dir)
410
+ def user_rubocop_config_path_for(path)
411
+ dir = if File.directory?(path)
412
+ path
413
+ else
414
+ File.dirname(path)
415
+ end
416
+
417
+ @dir_path_to_user_config_path[dir] ||= ::RuboCop::ConfigLoader.configuration_file_for(dir)
418
+ end
419
+
420
+ # Returns the content (Hash) of config/forced_rubocop_config.yml after processing it's ERB content.
421
+ # Cached since it doesn't change between files
422
+ def forced_rubocop_config_hash
423
+ return @forced_rubocop_config_hash if @forced_rubocop_config_hash
424
+
425
+ content = File.read(File.join(HamlLint::HOME, 'config', 'forced_rubocop_config.yml'))
426
+ processed_content = HamlLint::Utils.process_erb(content)
427
+ hash = YAML.safe_load(processed_content)
428
+
429
+ if ENV['HAML_LINT_TESTING']
430
+ # In newer RuboCop versions, new cops are not enabled by default, and instead
431
+ # show a message until they are used. We just want a default for them
432
+ # to avoid spamming STDOUT. Making it "disable" reduces the chances of having
433
+ # the test suite start failing after a new cop gets added.
434
+ hash['AllCops'] ||= {}
435
+ if Gem::Version.new(::RuboCop::Version::STRING) >= Gem::Version.new('1')
436
+ hash['AllCops']['NewCops'] = 'disable'
437
+ end
438
+ end
439
+
440
+ @forced_rubocop_config_hash = hash.freeze
441
+ end
442
+ end
150
443
  end
@@ -9,7 +9,7 @@ module HamlLint
9
9
 
10
10
  ALLOWED_SEPARATORS = [' ', '#'].freeze
11
11
 
12
- def visit_tag(node) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
12
+ def visit_tag(node) # rubocop:disable Metrics/CyclomaticComplexity
13
13
  # If this tag has inline script
14
14
  return unless node.contains_script?
15
15
 
@@ -18,15 +18,13 @@ module HamlLint
18
18
 
19
19
  tag_with_text = tag_with_inline_text(node)
20
20
 
21
- unless index = tag_with_text.rindex(text)
22
- # For tags with inline text that contain interpolation, the parser
23
- # converts them to inline script by surrounding them in string quotes,
24
- # e.g. `%p Hello #{name}` becomes `%p= "Hello #{name}"`, causing the
25
- # above search to fail. Check for this case by removing added quotes.
26
- unless (text_without_quotes = strip_surrounding_quotes(text)) &&
27
- (index = tag_with_text.rindex(text_without_quotes))
28
- return
29
- end
21
+ # For tags with inline text that contain interpolation, the parser
22
+ # converts them to inline script by surrounding them in string quotes,
23
+ # e.g. `%p Hello #{name}` becomes `%p= "Hello #{name}"`, causing the
24
+ # above search to fail. Check for this case by removing added quotes.
25
+ if !(index = tag_with_text.rindex(text)) && !((text_without_quotes = strip_surrounding_quotes(text)) &&
26
+ (index = tag_with_text.rindex(text_without_quotes)))
27
+ return
30
28
  end
31
29
 
32
30
  return if tag_with_text[index] == '#' # Ignore code comments
@@ -35,7 +35,7 @@ module HamlLint
35
35
  return unless tree = parse_ruby(script_node.script)
36
36
  %i[str dstr].include?(tree.type) &&
37
37
  !starts_with_reserved_character?(tree.children.first)
38
- rescue ::Parser::SyntaxError # rubocop:disable Lint/SuppressedException
38
+ rescue ::Parser::SyntaxError
39
39
  # Gracefully ignore syntax errors, as that's managed by a different linter
40
40
  end
41
41
 
@@ -11,7 +11,7 @@ module HamlLint
11
11
 
12
12
  def visit_root(root)
13
13
  max = config['max']
14
- line_count = document.source_lines.count
14
+ line_count = document.last_non_empty_line
15
15
  node = root.children.first
16
16
 
17
17
  if line_count > max && !node.disabled?(self)
@@ -24,11 +24,8 @@ module HamlLint
24
24
  # Runs the linter against the given Haml document.
25
25
  #
26
26
  # @param document [HamlLint::Document]
27
- def run(document)
28
- @document = document
29
- @lints = []
30
- visit(document.tree)
31
- @lints
27
+ def run(document, autocorrect: nil) # rubocop:disable Metrics
28
+ run_or_raise(document, autocorrect: autocorrect)
32
29
  rescue Parser::SyntaxError => e
33
30
  location = e.diagnostic.location
34
31
  @lints <<
@@ -39,6 +36,35 @@ module HamlLint
39
36
  e.to_s,
40
37
  :error
41
38
  )
39
+ rescue StandardError => e
40
+ msg = "Couldn't process the file".dup
41
+ if @autocorrect
42
+ # Those lints related to auto-correction were not saved, so don't display them
43
+ @lints = []
44
+ msg << " for autocorrect '#{@autocorrect}'. "
45
+ else
46
+ msg << ' for linting. '
47
+ end
48
+
49
+ msg << "#{e.class.name}: #{e.message}"
50
+ if ENV['HAML_LINT_DEBUG'] == 'true'
51
+ msg << "(DEBUG: Backtrace follows)\n#{e.backtrace.join("\n")}\n------"
52
+ end
53
+
54
+ @lints << HamlLint::Lint.new(self, document.file, nil, msg, :error)
55
+ @lints
56
+ end
57
+
58
+ # Runs the linter against the given Haml document, raises if the file cannot be processed due to
59
+ # Syntax or HAML-Lint internal errors. (For testing purposes)
60
+ #
61
+ # @param document [HamlLint::Document]
62
+ def run_or_raise(document, autocorrect: nil)
63
+ @document = document
64
+ @lints = []
65
+ @autocorrect = autocorrect
66
+ visit(document.tree)
67
+ @lints
42
68
  end
43
69
 
44
70
  # Returns the simple name for this linter.
@@ -48,25 +74,49 @@ module HamlLint
48
74
  self.class.name.to_s.split('::').last
49
75
  end
50
76
 
77
+ # Returns true if this linter supports autocorrect, false otherwise
78
+ #
79
+ # @return [Boolean]
80
+ def self.supports_autocorrect?
81
+ @supports_autocorrect || false
82
+ end
83
+
84
+ def supports_autocorrect?
85
+ self.class.supports_autocorrect?
86
+ end
87
+
51
88
  private
52
89
 
53
90
  attr_reader :config, :document
54
91
 
92
+ # Linters can call supports_autocorrect(true) in their top-level scope to indicate that
93
+ # they supports autocorrect.
94
+ #
95
+ # @params value [Boolean] The new value for supports_autocorrect
96
+ private_class_method def self.supports_autocorrect(value)
97
+ @supports_autocorrect = value
98
+ end
99
+
55
100
  # Record a lint for reporting back to the user.
56
101
  #
57
- # @param node [#line] node to extract the line number from
102
+ # @param node_or_line [#line] line number or node to extract the line number from
58
103
  # @param message [String] error/warning to display to the user
59
- def record_lint(node, message)
60
- @lints << HamlLint::Lint.new(self, @document.file, node.line, message,
61
- config.fetch('severity', :warning).to_sym)
104
+ def record_lint(node_or_line, message, corrected: false)
105
+ line = node_or_line.is_a?(Integer) ? node_or_line : node_or_line.line
106
+ @lints << HamlLint::Lint.new(self, @document.file, line, message,
107
+ config.fetch('severity', :warning).to_sym,
108
+ corrected: corrected)
62
109
  end
63
110
 
64
111
  # Parse Ruby code into an abstract syntax tree.
65
112
  #
66
113
  # @return [AST::Node]
67
114
  def parse_ruby(source)
115
+ self.class.ruby_parser.parse(source)
116
+ end
117
+
118
+ def self.ruby_parser # rubocop:disable Lint/IneffectiveAccessModifier
68
119
  @ruby_parser ||= HamlLint::RubyParser.new
69
- @ruby_parser.parse(source)
70
120
  end
71
121
 
72
122
  # Remove the surrounding double quotes from a string, ignoring any