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