haml_lint 0.45.0 → 0.48.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 (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