haml_lint 0.44.0 → 0.46.0

Sign up to get free protection for your applications and to get access to all the features.
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.