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