haml_lint 0.46.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b477cda9580216bbcc7434f0673bd4e8b36fa53cc22695cce4b25d5082921b5f
4
- data.tar.gz: 8d404a3975a3def789672a4629b4bddb38a7b110083a731b801329369fae773b
3
+ metadata.gz: 93d2cec8a3ce45f370d5e8d055ed8f9dc5a988e2c56e8e145f50710f04f40a3f
4
+ data.tar.gz: 59a2d93bd2ab520c4fb1288cf85f784d3d20955293b6167f046bffba3f198cac
5
5
  SHA512:
6
- metadata.gz: 3e5a3e1b34815a356db1eb4ac1f10ca00e7d083a46f7b69cd8d3223ba6785d34eb628737ebd14d1b9867141bec328385794e329e096452df2b8c2868d42c4801
7
- data.tar.gz: 2f27a33785a9f920359767524ab741bcc1edfaf8e52f3b469e264747e7ecf9f5861db308d33efc074634763889c499c187faa0ce8d455eee5618dfa6323d9f5b
6
+ metadata.gz: be4b59ae43a2cab51fd4148e843177b939da14ee7b0dc40a8ed0d9dd7bbcd12d39d674c1d9c7f19f0a524d912e27fc82b882f6412083eda143c5cf60e218c397
7
+ data.tar.gz: 28075057ee4c955a4966d2561ff77680b9d90c24e647dc83386dab4ebfabc404cbcb22913836f987896cc506244a4f612f74e131d8838c91f1e0db027d0f8cdd
@@ -45,6 +45,21 @@ Layout/EndAlignment:
45
45
  Layout/EndOfLine:
46
46
  Enabled: false
47
47
 
48
+ # Turning this cop on can turn
49
+ # = content_tag(:span) do
50
+ # - foo
51
+ # - bar
52
+ #
53
+ # Into
54
+ # - HL.out =
55
+ # - content_tag(:span) do
56
+ # - foo
57
+ # - bar
58
+ #
59
+ # Which is wrong... It would take too much analysis to detect and fix that situation.
60
+ Layout/MultilineAssignmentLayout:
61
+ Enabled: false
62
+
48
63
  Layout/ParameterAlignment:
49
64
  # The alternative, with_fixed_indentation, breaks because we sometimes remove indentation when
50
65
  # dealing with multi-line scripts. (Because a line starting with "=" adds a "HL.out = " to the
@@ -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,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'haml_lint/ruby_extractor'
4
3
  require 'rubocop'
5
4
  require 'tempfile'
6
5
 
@@ -52,11 +51,6 @@ module HamlLint
52
51
  return
53
52
  end
54
53
 
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
54
  @last_extracted_source = nil
61
55
  @last_new_ruby_source = nil
62
56
 
@@ -102,6 +96,13 @@ module HamlLint
102
96
 
103
97
  private
104
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
+
105
106
  # Extracted here so that tests can stub this to always return true
106
107
  def transfer_corrections?(initial_ruby_code, new_ruby_code)
107
108
  initial_ruby_code != new_ruby_code
@@ -205,7 +206,7 @@ module HamlLint
205
206
  def run_rubocop(rubocop_cli, ruby_code, path) # rubocop:disable Metrics
206
207
  rubocop_status = nil
207
208
  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_cli.config_store.instance_variable_set(:@options_config, rubocop_config_for(path))
209
210
  rubocop_status = rubocop_cli.run(rubocop_flags + ['--stdin', path])
210
211
  end
211
212
 
@@ -332,7 +333,7 @@ module HamlLint
332
333
  # anymore or don't exist yet.
333
334
  # This is not exhaustive, it's only for the cops that are in config/default.yml
334
335
  def ignored_cops_flags
335
- ignored_cops = config['ignored_cops']
336
+ ignored_cops = config.fetch('ignored_cops', [])
336
337
 
337
338
  if @autocorrect
338
339
  ignored_cops += self.class.cops_names_not_supporting_autocorrect
@@ -112,8 +112,11 @@ module HamlLint
112
112
  #
113
113
  # @return [AST::Node]
114
114
  def parse_ruby(source)
115
+ self.class.ruby_parser.parse(source)
116
+ end
117
+
118
+ def self.ruby_parser # rubocop:disable Lint/IneffectiveAccessModifier
115
119
  @ruby_parser ||= HamlLint::RubyParser.new
116
- @ruby_parser.parse(source)
117
120
  end
118
121
 
119
122
  # Remove the surrounding double quotes from a string, ignoring any
@@ -16,5 +16,9 @@ module HamlLint::RubyExtraction
16
16
  end
17
17
 
18
18
  def transfer_correction(coordinator, all_corrected_ruby_lines, haml_lines); end
19
+
20
+ def skip_line_indexes_in_source_map
21
+ (0...@ruby_lines.size).to_a
22
+ end
19
23
  end
20
24
  end
@@ -78,7 +78,8 @@ module HamlLint::RubyExtraction
78
78
  end
79
79
 
80
80
  def haml_end_line_index
81
- @haml_line_index + nb_haml_lines - 1
81
+ # the .max is needed to handle cases with 0 nb_haml_lines
82
+ [@haml_line_index + nb_haml_lines - 1, @haml_line_index].max
82
83
  end
83
84
 
84
85
  def nb_haml_lines
@@ -11,6 +11,12 @@ module HamlLint::RubyExtraction
11
11
 
12
12
  attr_reader :script_output_prefix
13
13
 
14
+ HAML_PARSER_INSTANCE = if Haml::VERSION >= '5.0.0'
15
+ ::Haml::Parser.new({})
16
+ else
17
+ ::Haml::Parser.new('', {})
18
+ end
19
+
14
20
  def initialize(document, script_output_prefix:)
15
21
  @document = document
16
22
  @script_output_prefix = script_output_prefix
@@ -19,13 +25,18 @@ module HamlLint::RubyExtraction
19
25
  def extract
20
26
  raise 'Already extracted' if @ruby_chunks
21
27
 
22
- @ruby_chunks = []
23
- @original_haml_lines = @document.source_lines
28
+ prepare_extract
24
29
 
25
30
  visit(@document.tree)
26
31
  @ruby_chunks
27
32
  end
28
33
 
34
+ # Useful for tests
35
+ def prepare_extract
36
+ @ruby_chunks = []
37
+ @original_haml_lines = @document.source_lines
38
+ end
39
+
29
40
  def visit_root(_node)
30
41
  yield # Collect lines of code from children
31
42
  end
@@ -71,33 +82,44 @@ module HamlLint::RubyExtraction
71
82
  # Visiting comments which are output to HTML. Lines looking like
72
83
  # ` / This will be in the HTML source!`
73
84
  def visit_comment(node)
74
- lines = raw_lines_of_interest(node.line - 1)
75
- indent = lines.first.index(/\S/)
85
+ line = @original_haml_lines[node.line - 1]
86
+ indent = line.index(/\S/)
76
87
  @ruby_chunks << PlaceholderMarkerChunk.new(node, 'comment', indent: indent)
77
88
  end
78
89
 
79
90
  # Visit a script which outputs. Lines looking like ` = foo`
80
91
  def visit_script(node, &block)
81
- lines = raw_lines_of_interest(node.line - 1)
92
+ raw_first_line = @original_haml_lines[node.line - 1]
93
+
94
+ # ==, !, !==, &, &== means interpolation (was needed before HAML 2.2... it's still supported)
95
+ # =, !=, &= mean actual ruby code is coming
96
+ # Anything else is interpolation
97
+ # The regex lists the case for Ruby Code. The 3 cases and making sure they are not followed by another = sign
82
98
 
83
- if lines.first !~ /\A\s*[-=]/
99
+ match = raw_first_line.match(/\A\s*(=|!=|&=)(?!=)/)
100
+ unless match
84
101
  # The line doesn't start with a - or a =, this is actually a "plain"
85
102
  # that contains interpolation.
86
- indent = lines.first.index(/\S/)
103
+ indent = raw_first_line.index(/\S/)
87
104
  @ruby_chunks << PlaceholderMarkerChunk.new(node, 'interpolation', indent: indent)
88
- add_interpolation_chunks(node, lines.first, node.line - 1, indent: indent)
105
+ add_interpolation_chunks(node, raw_first_line, node.line - 1, indent: indent)
89
106
  return
90
107
  end
91
108
 
92
- lines[0] = lines[0].sub(/(=[ \t]?)/, '')
109
+ script_prefix = match[1]
110
+ _first_line_offset, lines = extract_raw_ruby_lines(node.script, node.line - 1)
111
+ # We want the actual indentation and prefix for the first line
112
+ first_line = lines[0] = @original_haml_lines[node.line - 1].rstrip
113
+ process_multiline!(first_line)
114
+
115
+ lines[0] = lines[0].sub(/(#{script_prefix}[ \t]?)/, '')
93
116
  line_indentation = Regexp.last_match(1).size
94
117
 
95
118
  raw_code = lines.join("\n")
96
119
 
97
120
  if lines[0][/\S/] == '#'
98
- # a script that only constains a comment... needs special handling
99
- comment_index = lines[0].index(/\S/)
100
- lines[0].insert(comment_index + 1, " #{script_output_prefix.rstrip}")
121
+ # a "=" script that only contains a comment... No need for the "HL.out = " prefix,
122
+ # just treat it as comment which will turn into a "-" comment
101
123
  else
102
124
  lines[0] = HamlLint::Utils.insert_after_indentation(lines[0], script_output_prefix)
103
125
  end
@@ -124,14 +146,25 @@ module HamlLint::RubyExtraction
124
146
  # By forcing this to start a chunk, there will be extra placeholders which
125
147
  # blocks rubocop from merging the lines.
126
148
  must_start_chunk = true
149
+ elsif script_prefix != '='
150
+ # In the few cases where &= and != are used to start the script,
151
+ # We need to remember and put it back in the final HAML. Fusing scripts together
152
+ # would make that basically impossible. Instead, a script has a "first_output_prefix"
153
+ # field for this specific case
154
+ must_start_chunk = true
127
155
  end
128
156
 
129
- finish_visit_any_script(node, lines, raw_code: raw_code, must_start_chunk: must_start_chunk, &block)
157
+ finish_visit_any_script(node, lines, raw_code: raw_code, must_start_chunk: must_start_chunk,
158
+ first_output_prefix: script_prefix, &block)
130
159
  end
131
160
 
132
161
  # Visit a script which doesn't output. Lines looking like ` - foo`
133
162
  def visit_silent_script(node, &block)
134
- lines = raw_lines_of_interest(node.line - 1)
163
+ _first_line_offset, lines = extract_raw_ruby_lines(node.script, node.line - 1)
164
+ # We want the actual indentation and prefix for the first line
165
+ first_line = lines[0] = @original_haml_lines[node.line - 1].rstrip
166
+ process_multiline!(first_line)
167
+
135
168
  lines[0] = lines[0].sub(/(-[ \t]?)/, '')
136
169
  nb_to_deindent = Regexp.last_match(1).size
137
170
 
@@ -145,7 +178,7 @@ module HamlLint::RubyExtraction
145
178
  # Code common to both silent and outputting scripts
146
179
  #
147
180
  # raw_code is the code before we do transformations, such as adding the `HL.out = `
148
- def finish_visit_any_script(node, lines, raw_code: nil, must_start_chunk: false)
181
+ def finish_visit_any_script(node, lines, raw_code: nil, must_start_chunk: false, first_output_prefix: '=')
149
182
  raw_code ||= lines.join("\n")
150
183
  start_nesting = self.class.start_nesting_after?(raw_code)
151
184
 
@@ -158,7 +191,8 @@ module HamlLint::RubyExtraction
158
191
  @ruby_chunks << ScriptChunk.new(node, lines,
159
192
  end_marker_indent: indent_after,
160
193
  must_start_chunk: must_start_chunk,
161
- previous_chunk: @ruby_chunks.last)
194
+ previous_chunk: @ruby_chunks.last,
195
+ first_output_haml_prefix: first_output_prefix)
162
196
 
163
197
  yield
164
198
 
@@ -184,27 +218,34 @@ module HamlLint::RubyExtraction
184
218
  def visit_tag(node)
185
219
  indent = @original_haml_lines[node.line - 1].index(/\S/)
186
220
 
187
- has_children = !node.children.empty?
188
- if has_children
189
- # We don't want to use a block because assignments in a block are local to that block,
190
- # so the semantics of the extracted ruby would be different from the one generated by
191
- # Haml. Those differences can make some cops, such as UselessAssignment, have false
192
- # positives
193
- code = 'begin # rubocop:disable Style/RedundantBegin,Lint/RedundantCopDisableDirective'
194
- @ruby_chunks << AdHocChunk.new(node,
195
- [' ' * indent + code])
196
- indent += 2
197
- end
221
+ # We don't want to use a block because assignments in a block are local to that block,
222
+ # so the semantics of the extracted ruby would be different from the one generated by
223
+ # Haml. Those differences can make some cops, such as UselessAssignment, have false
224
+ # positives
225
+ code = 'begin'
226
+ @ruby_chunks << AdHocChunk.new(node,
227
+ [' ' * indent + code])
228
+ indent += 2
198
229
 
199
- @ruby_chunks << PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
230
+ tag_chunk = PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
231
+ @ruby_chunks << tag_chunk
200
232
 
201
233
  current_line_index = visit_tag_attributes(node, indent: indent)
202
234
  visit_tag_script(node, line_index: current_line_index, indent: indent)
203
235
 
204
- if has_children
205
- yield
206
- indent -= 2
207
- @ruby_chunks << AdHocChunk.new(node, [' ' * indent + 'end'],
236
+ yield
237
+
238
+ indent -= 2
239
+
240
+ if @ruby_chunks.last.equal?(tag_chunk)
241
+ # So there is nothing going "in" the tag, remove the wrapping "begin" and replace the PlaceholderMarkerChunk
242
+ # by one less indented
243
+ @ruby_chunks.pop
244
+ @ruby_chunks.pop
245
+ @ruby_chunks << PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
246
+ else
247
+ @ruby_chunks << AdHocChunk.new(node,
248
+ [' ' * indent + 'ensure', ' ' * indent + ' HL.noop', ' ' * indent + 'end'],
208
249
  haml_line_index: @ruby_chunks.last.haml_end_line_index)
209
250
  end
210
251
  end
@@ -221,11 +262,16 @@ module HamlLint::RubyExtraction
221
262
  attributes_code = additional_attributes.first
222
263
  if !attributes_code && node.hash_attributes? && node.dynamic_attributes_sources.empty?
223
264
  # No idea why .foo{:bar => 123} doesn't get here, but .foo{:bar => '123'} does...
224
- # The code we get for the later is {:bar => '123'}.
265
+ # The code we get for the latter is {:bar => '123'}.
225
266
  # We normalize it by removing the { } so that it matches wha we normally get
226
267
  attributes_code = node.dynamic_attributes_source[:hash][1...-1]
227
268
  end
228
269
 
270
+ if attributes_code&.start_with?('{')
271
+ # Looks like the .foo(bar = 123) case. Ignoring.
272
+ attributes_code = nil
273
+ end
274
+
229
275
  return final_line_index unless attributes_code
230
276
  # Attributes have different ways to be given to us:
231
277
  # .foo{bar: 123} => "bar: 123"
@@ -234,14 +280,13 @@ module HamlLint::RubyExtraction
234
280
  # .foo(bar = 123) => '{"bar" => 123,}'
235
281
  # .foo{html_attrs('fr-fr')} => html_attrs('fr-fr')
236
282
  #
237
- # The (bar = 123) case is extra painful to autocorrect (so is ignored).
283
+ # The (bar = 123) case is extra painful to autocorrect (so is ignored up there).
238
284
  # #raw_ruby_from_haml will "detect" this case by not finding the code.
239
285
  #
240
286
  # We wrap the result in a method to have a valid syntax for all 3 ways
241
287
  # without having to differentiate them.
242
- first_line_offset, raw_attributes_lines = raw_ruby_lines_from_haml(attributes_code,
243
- node.line - 1)
244
-
288
+ first_line_offset, raw_attributes_lines = extract_raw_tag_attributes_ruby_lines(attributes_code,
289
+ node.line - 1)
245
290
  return final_line_index unless raw_attributes_lines
246
291
 
247
292
  final_line_index += raw_attributes_lines.size - 1
@@ -275,7 +320,7 @@ module HamlLint::RubyExtraction
275
320
  # We ignore scripts which are just a comment
276
321
  return if node.script[/\S/] == '#'
277
322
 
278
- first_line_offset, script_lines = raw_ruby_lines_from_haml(node.script, line_index)
323
+ first_line_offset, script_lines = extract_raw_ruby_lines(node.script, line_index)
279
324
 
280
325
  if script_lines.nil?
281
326
  # This is a string with interpolation after a tag
@@ -379,52 +424,91 @@ module HamlLint::RubyExtraction
379
424
  end
380
425
  end
381
426
 
427
+ def process_multiline!(line)
428
+ if HAML_PARSER_INSTANCE.send(:is_multiline?, line)
429
+ line.chop!.rstrip!
430
+ true
431
+ else
432
+ false
433
+ end
434
+ end
435
+
382
436
  # Returns the raw lines from the haml for the given index.
383
437
  # Multiple lines are returned when a line ends with a comma as that is the only
384
438
  # time HAMLs allows Ruby lines to be split.
385
- def raw_lines_of_interest(first_line_index)
386
- line_index = first_line_index
387
- lines_of_interest = [@original_haml_lines[line_index]]
388
-
389
- while @original_haml_lines[line_index].rstrip.end_with?(',')
390
- line_index += 1
391
- lines_of_interest << @original_haml_lines[line_index]
392
- end
393
-
394
- lines_of_interest
395
- end
396
439
 
397
440
  # Haml's line-splitting rules (allowed after comma in scripts and attributes) are handled
398
441
  # at the parser level, so Haml doesn't provide the code as it is actually formatted in the Haml
399
442
  # file. #raw_ruby_from_haml extracts the ruby code as it is exactly in the Haml file.
400
443
  # The first and last lines may not be the complete lines from the Haml, only the Ruby parts
401
444
  # and the indentation between the first and last list.
402
- def raw_ruby_lines_from_haml(code, first_line_index)
403
- stripped_code = code.strip
404
- return if stripped_code.empty?
405
445
 
406
- lines_of_interest = raw_lines_of_interest(first_line_index)
446
+ # HAML transforms the ruby code in many ways as it parses a document. Often removing lines and/or
447
+ # indentation. This is quite annoying for us since we want the exact layout of the code to analyze it.
448
+ #
449
+ # This function receives the code as haml provides it and the line where it starts. It returns
450
+ # the actual code as it is in the haml file, keeping breaks and indentation for the following lines.
451
+ # In addition, the start position of the code in the first line.
452
+ #
453
+ # The rules for handling multiline code in HAML are as follow:
454
+ # * if the line being processed ends with a space and a pipe, then append to the line (without
455
+ # newlines) every following lines that also end with a space and a pipe. This means the last line of
456
+ # the "block" also needs a pipe at the end.
457
+ # * after processing the pipes, when dealing with ruby code (and not in tag attributes' hash), if the line
458
+ # (which maybe span across multiple lines) ends with a comma, add the next line to the current piece of code.
459
+ #
460
+ # @return [first_line_offset, ruby_lines]
461
+ def extract_raw_ruby_lines(haml_processed_ruby_code, first_line_index)
462
+ haml_processed_ruby_code = haml_processed_ruby_code.strip
463
+ first_line = @original_haml_lines[first_line_index]
407
464
 
408
- if lines_of_interest.size == 1
409
- index = lines_of_interest.first.index(stripped_code)
410
- if lines_of_interest.first.include?(stripped_code)
411
- return [index, [stripped_code]]
412
- else
413
- # Sometimes, the code just isn't in the Haml when Haml does transformations to it
414
- return
415
- end
465
+ char_index = first_line.index(haml_processed_ruby_code)
466
+
467
+ if char_index
468
+ return [char_index, [haml_processed_ruby_code]]
469
+ end
470
+
471
+ cur_line_index = first_line_index
472
+ cur_line = first_line.rstrip
473
+ lines = []
474
+
475
+ # The pipes must also be on the last line of the multi-line section
476
+ while cur_line && process_multiline!(cur_line)
477
+ lines << cur_line
478
+ cur_line_index += 1
479
+ cur_line = @original_haml_lines[cur_line_index].rstrip
480
+ end
481
+
482
+ if lines.empty?
483
+ lines << cur_line
484
+ else
485
+ # The pipes must also be on the last line of the multi-line section. So cur_line is not the next line.
486
+ # We want to go back to check for commas
487
+ cur_line_index -= 1
488
+ cur_line = lines.last
489
+ end
490
+
491
+ while HAML_PARSER_INSTANCE.send(:is_ruby_multiline?, cur_line)
492
+ cur_line_index += 1
493
+ cur_line = @original_haml_lines[cur_line_index].rstrip
494
+ lines << cur_line
416
495
  end
417
496
 
418
- raw_haml = lines_of_interest.join("\n")
497
+ joined_lines = lines.join("\n")
498
+
499
+ if haml_processed_ruby_code.include?("\n")
500
+ haml_processed_ruby_code = haml_processed_ruby_code.gsub("\n", ' ')
501
+ end
419
502
 
420
- # Need the gsub because while multiline scripts are turned into a single line,
421
- # by haml, multiline tag attributes are not.
422
- code_parts = stripped_code.gsub("\n", ' ').split(/,\s*/)
503
+ haml_processed_ruby_code.split(/[, ]/)
423
504
 
424
- regexp_code = code_parts.map { |c| Regexp.quote(c) }.join(',\\s*')
425
- regexp = Regexp.new(regexp_code)
505
+ regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/,\s*|\s+/), '(?:,\\s*|\\s+)')
426
506
 
427
- match = raw_haml.match(regexp)
507
+ match = joined_lines.match(regexp)
508
+ # This can happen when pipes are used as marker for multiline parts, and when tag attributes change lines
509
+ # without ending by a comma. This is quite a can of worm and is probably not too frequent, so for now,
510
+ # these cases are not supported.
511
+ return if match.nil?
428
512
 
429
513
  raw_ruby = match[0]
430
514
  ruby_lines = raw_ruby.split("\n")
@@ -433,6 +517,48 @@ module HamlLint::RubyExtraction
433
517
  [first_line_offset, ruby_lines]
434
518
  end
435
519
 
520
+ # Tag attributes actually handle multiline differently than scripts.
521
+ # The basic system basically keeps considering more lines until it meets the closing braces, but still
522
+ # processes pipes too (same as extract_raw_ruby_lines).
523
+ def extract_raw_tag_attributes_ruby_lines(haml_processed_ruby_code, first_line_index)
524
+ haml_processed_ruby_code = haml_processed_ruby_code.strip
525
+ first_line = @original_haml_lines[first_line_index]
526
+
527
+ char_index = first_line.index(haml_processed_ruby_code)
528
+
529
+ if char_index
530
+ return [char_index, [haml_processed_ruby_code]]
531
+ end
532
+
533
+ min_non_white_chars_to_add = haml_processed_ruby_code.scan(/\S/).size
534
+
535
+ regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/\s+/), '\\s+')
536
+
537
+ joined_lines = first_line.rstrip
538
+ process_multiline!(joined_lines)
539
+
540
+ cur_line_index = first_line_index + 1
541
+ while @original_haml_lines[cur_line_index] && min_non_white_chars_to_add > 0
542
+ new_line = @original_haml_lines[cur_line_index].rstrip
543
+ process_multiline!(new_line)
544
+
545
+ min_non_white_chars_to_add -= new_line.scan(/\S/).size
546
+ joined_lines << "\n"
547
+ joined_lines << new_line
548
+ cur_line_index += 1
549
+ end
550
+
551
+ match = joined_lines.match(regexp)
552
+
553
+ return if match.nil?
554
+
555
+ first_line_offset = match.begin(0)
556
+ raw_ruby = match[0]
557
+ ruby_lines = raw_ruby.split("\n")
558
+
559
+ [first_line_offset, ruby_lines]
560
+ end
561
+
436
562
  def wrap_lines(lines, wrap_depth)
437
563
  lines = lines.dup
438
564
  wrapping_prefix = 'W' * (wrap_depth - 1) + '('
@@ -15,26 +15,6 @@ module HamlLint::RubyExtraction
15
15
  HamlCommentChunk.new(node, @ruby_lines + following_chunk.ruby_lines, end_marker_indent: end_marker_indent)
16
16
  end
17
17
 
18
- def fuse_script_chunk(following_chunk)
19
- return if following_chunk.end_marker_indent.nil?
20
- return if following_chunk.must_start_chunk
21
-
22
- nb_blank_lines_between = following_chunk.haml_line_index - haml_line_index - nb_haml_lines
23
- blank_lines = nb_blank_lines_between > 0 ? [''] * nb_blank_lines_between : []
24
- new_lines = @ruby_lines + blank_lines + following_chunk.ruby_lines
25
-
26
- source_map_skips = @skip_line_indexes_in_source_map
27
- source_map_skips.concat(following_chunk.skip_line_indexes_in_source_map
28
- .map { |i| i + @ruby_lines.size })
29
-
30
- ScriptChunk.new(node,
31
- new_lines,
32
- haml_line_index: haml_line_index,
33
- skip_line_indexes_in_source_map: source_map_skips,
34
- end_marker_indent: following_chunk.end_marker_indent,
35
- previous_chunk: previous_chunk)
36
- end
37
-
38
18
  def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines)
39
19
  if to_ruby_lines.empty?
40
20
  haml_lines.slice!(@haml_line_index..haml_end_line_index)
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ripper'
4
+
3
5
  module HamlLint::RubyExtraction
4
6
  # Chunk for handling outputting and silent scripts, so ` = foo` and ` - bar`
5
7
  # Does NOT handle a script beside a tag (ex: `%div= spam`)
6
8
  class ScriptChunk < BaseChunk
7
9
  MID_BLOCK_KEYWORDS = %w[else elsif when rescue ensure].freeze
8
10
 
11
+ # @return [String] The prefix for the first outputting string of this script. (One of = != &=)
12
+ # The outputting scripts after the first are always with =
13
+ attr_reader :first_output_haml_prefix
14
+
9
15
  # @return [Boolean] true if this ScriptChunk must be at the beginning of a chunk.
10
16
  # This blocks this ScriptChunk from being fused to a ScriptChunk that is before it.
11
17
  # Needed to handle some patterns of outputting script.
@@ -19,12 +25,13 @@ module HamlLint::RubyExtraction
19
25
  # our starting marker must be indented.
20
26
  attr_reader :previous_chunk
21
27
 
22
- def initialize(*args, previous_chunk:, must_start_chunk: false,
23
- skip_line_indexes_in_source_map: [], **kwargs)
28
+ def initialize(*args, previous_chunk:, must_start_chunk: false, # rubocop:disable Metrics/ParameterLists
29
+ skip_line_indexes_in_source_map: [], first_output_haml_prefix: '=', **kwargs)
24
30
  super(*args, **kwargs)
25
31
  @must_start_chunk = must_start_chunk
26
32
  @skip_line_indexes_in_source_map = skip_line_indexes_in_source_map
27
33
  @previous_chunk = previous_chunk
34
+ @first_output_haml_prefix = first_output_haml_prefix
28
35
  end
29
36
 
30
37
  def fuse(following_chunk)
@@ -53,7 +60,8 @@ module HamlLint::RubyExtraction
53
60
  haml_line_index: haml_line_index,
54
61
  skip_line_indexes_in_source_map: source_map_skips,
55
62
  end_marker_indent: following_chunk.end_marker_indent,
56
- previous_chunk: previous_chunk)
63
+ previous_chunk: previous_chunk,
64
+ first_output_haml_prefix: @first_output_haml_prefix)
57
65
  end
58
66
 
59
67
  def fuse_implicit_end(following_chunk)
@@ -70,7 +78,8 @@ module HamlLint::RubyExtraction
70
78
  haml_line_index: haml_line_index,
71
79
  skip_line_indexes_in_source_map: source_map_skips,
72
80
  end_marker_indent: following_chunk.end_marker_indent,
73
- previous_chunk: previous_chunk)
81
+ previous_chunk: previous_chunk,
82
+ first_output_haml_prefix: @first_output_haml_prefix)
74
83
  end
75
84
 
76
85
  def start_marker_indent
@@ -79,54 +88,157 @@ module HamlLint::RubyExtraction
79
88
  [default_indent, previous_chunk&.end_marker_indent || previous_chunk&.start_marker_indent].compact.max
80
89
  end
81
90
 
82
- def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines) # rubocop:disable Metrics
91
+ def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines)
92
+ to_haml_lines = self.class.format_ruby_lines_to_haml_lines(
93
+ to_ruby_lines,
94
+ script_output_ruby_prefix: coordinator.script_output_prefix,
95
+ first_output_haml_prefix: @first_output_haml_prefix
96
+ )
97
+
98
+ haml_lines[@haml_line_index..haml_end_line_index] = to_haml_lines
99
+ end
100
+
101
+ ALLOW_EXPRESSION_AFTER_LINE_ENDING_WITH = %w[else begin ensure].freeze
102
+
103
+ def self.format_ruby_lines_to_haml_lines(to_ruby_lines, script_output_ruby_prefix:, first_output_haml_prefix: '=') # rubocop:disable Metrics
83
104
  to_ruby_lines.reject! { |l| l.strip == 'end' }
105
+ return [] if to_ruby_lines.empty?
84
106
 
85
- output_comment_prefix = ' ' + coordinator.script_output_prefix.rstrip
86
- to_ruby_lines.map! do |line|
87
- if line.lstrip.start_with?('#' + output_comment_prefix)
88
- line = line.dup
89
- comment_index = line.index('#')
90
- removal_start_index = comment_index + 1
91
- removal_end_index = removal_start_index + output_comment_prefix.size
92
- line[removal_start_index...removal_end_index] = ''
93
- # It will be removed again below, but will know its suposed to be a =
94
- line.insert(comment_index, coordinator.script_output_prefix)
95
- end
96
- line
97
- end
107
+ statement_start_line_indexes = find_statement_start_line_indexes(to_ruby_lines)
98
108
 
99
109
  continued_line_indent_delta = 2
100
110
 
111
+ cur_line_start_index = nil
112
+ line_start_indexes_that_need_pipes = []
113
+ haml_output_prefix = first_output_haml_prefix
101
114
  to_haml_lines = to_ruby_lines.map.with_index do |line, i|
102
115
  if line !~ /\S/
103
116
  # whitespace or empty lines, we don't want any indentation
104
117
  ''
105
- elsif line_starts_script?(to_ruby_lines, i)
118
+ elsif statement_start_line_indexes.include?(i)
119
+ cur_line_start_index = i
106
120
  code_start = line.index(/\S/)
107
- if line[code_start..].start_with?(coordinator.script_output_prefix)
108
- line = line.sub(coordinator.script_output_prefix, '')
109
- continued_line_indent_delta = 2 - coordinator.script_output_prefix.size
110
- "#{line[0...code_start]}= #{line[code_start..]}"
121
+ if line[code_start..].start_with?(script_output_ruby_prefix)
122
+ line = line.sub(script_output_ruby_prefix, '')
123
+ # The line may have been too indented because of the "HL.out = " prefix
124
+ continued_line_indent_delta = 2 - script_output_ruby_prefix.size
125
+ new_line = "#{line[0...code_start]}#{haml_output_prefix} #{line[code_start..]}"
126
+ haml_output_prefix = '='
127
+ new_line
111
128
  else
112
129
  continued_line_indent_delta = 2
113
130
  "#{line[0...code_start]}- #{line[code_start..]}"
114
131
  end
115
132
  else
133
+ unless to_ruby_lines[i - 1].end_with?(',')
134
+ line_start_indexes_that_need_pipes << cur_line_start_index
135
+ end
136
+
116
137
  HamlLint::Utils.indent(line, continued_line_indent_delta)
117
138
  end
118
139
  end
119
140
 
120
- haml_lines[@haml_line_index..haml_end_line_index] = to_haml_lines
121
- end
141
+ # Starting from the end because we need to add newlines when 2 groups of lines need pipes, so that they are
142
+ # separate.
143
+ line_start_indexes_that_need_pipes.reverse_each do |cur_line_i|
144
+ loop do
145
+ cur_line = to_haml_lines[cur_line_i]
146
+ break if cur_line.nil? || cur_line.empty?
147
+ to_haml_lines[cur_line_i] = cur_line + ' |'
148
+ cur_line_i += 1
149
+
150
+ break if statement_start_line_indexes.include?(cur_line_i)
151
+ end
152
+
153
+ next_line = to_haml_lines[cur_line_i]
154
+ if next_line && HamlLint::RubyExtraction::ChunkExtractor::HAML_PARSER_INSTANCE.send(:is_multiline?, next_line)
155
+ to_haml_lines.insert(cur_line_i, '')
156
+ end
157
+ end
122
158
 
123
- def unfinished_script_line?(lines, line_index)
124
- !!lines[line_index][/,[ \t]*\z/]
159
+ to_haml_lines
125
160
  end
126
161
 
127
- def line_starts_script?(lines, line_index)
128
- return true if line_index == 0
129
- !unfinished_script_line?(lines, line_index - 1)
162
+ def self.find_statement_start_line_indexes(to_ruby_lines) # rubocop:disable Metrics
163
+ if to_ruby_lines.size == 1
164
+ if to_ruby_lines.first[/\S/]
165
+ return [0]
166
+ else
167
+ return []
168
+ end
169
+ end
170
+ statement_start_line_indexes = [] # 0-indexed
171
+ allow_expression_after_line_number = 0 # 1-indexed
172
+ last_do_keyword_line_number = nil # 1-indexed, like Ripper.lex
173
+
174
+ to_ruby_string = to_ruby_lines.join("\n")
175
+ if RUBY_VERSION < '3.1'
176
+ # Ruby 2.6's Ripper has issues when it encounters a else, when, elsif without a matching if/case before.
177
+ # It literally stop lexing at that point without any error.
178
+ # Ex from 2.7.8:
179
+ # require 'ripper'
180
+ # Ripper.lex("a\nelse\nb")
181
+ # #=> [[[1, 0], :on_ident, "a", CMDARG], [[1, 1], :on_nl, "\n", BEG], [[2, 0], :on_kw, "else", BEG]]
182
+ # So we add enough ifs to last quite a few layer. Hopefully enough for all needs. To clarify, there would need
183
+ # as many "end" keyword in a single ScriptChunk followed by one of the problematic keyword for the problem
184
+ # to show up.
185
+ # Considering that a `end` without anything else on the line is removed from to_ruby_lines before getting here
186
+ # (in format_ruby_lines_to_haml_lines), 10 ifs should be plenty.
187
+ to_ruby_string = ('if a;' * 10) + to_ruby_string
188
+ end
189
+
190
+ last_line_number_seen = nil
191
+ Ripper.lex(to_ruby_string).each do |start_loc, token, str|
192
+ last_line_number_seen = start_loc[0]
193
+ if token == :on_nl
194
+ # :on_nl happens when we have a meaningful line change.
195
+ allow_expression_after_line_number = start_loc[0]
196
+ next
197
+ elsif token == :on_ignored_nl
198
+ # :on_ignored_nl happens for newlines within an expression, or consecutive newlines..
199
+ # and some cases we care about such as a newline after the pipes after arguments of a block
200
+ if last_do_keyword_line_number == start_loc[0]
201
+ # When starting a block, Ripper.lex gives :on_ignored_nl
202
+ allow_expression_after_line_number = start_loc[0]
203
+ end
204
+ next
205
+ end
206
+
207
+ if allow_expression_after_line_number && str[/\S/]
208
+ if allow_expression_after_line_number < start_loc[0]
209
+ # Ripper.lex returns line numbers 1-indexed, we want 0-indexed
210
+ statement_start_line_indexes << start_loc[0] - 1
211
+ end
212
+ allow_expression_after_line_number = nil
213
+ end
214
+
215
+ if token == :on_comment
216
+ # :on_comment contain its own newline at the end of the content
217
+ allow_expression_after_line_number = start_loc[0]
218
+ elsif token == :on_kw
219
+ if str == 'do'
220
+ # Because of the possible arguments for the block, we can't simply set is_between_expressions to true
221
+ last_do_keyword_line_number = start_loc[0]
222
+ elsif ALLOW_EXPRESSION_AFTER_LINE_ENDING_WITH.include?(str)
223
+ allow_expression_after_line_number = start_loc[0]
224
+ end
225
+ end
226
+ end
227
+
228
+ # number is 1-indexed, and we want the line after it, so that's great
229
+ if last_line_number_seen < to_ruby_lines.size && to_ruby_lines[last_line_number_seen..].any? { |l| l[/\S/] }
230
+ # There are non-empty lines after the last line Ripper showed us, that's a problem!
231
+ msg = +'It seems Ripper did not properly process some source code. Please make sure you are on the '
232
+ msg << 'latest Haml-Lint version, then create an issue at '
233
+ msg << "https://github.com/sds/haml-lint/issues and include the following information:\n"
234
+ msg << "Ruby version: #{RUBY_VERSION}\n"
235
+ msg << "Haml-Lint version: #{HamlLint::VERSION}\n"
236
+ msg << "HAML version: #{Haml::VERSION}\n"
237
+ msg << "problematic source code:\n```\n#{to_ruby_lines.join("\n")}\n```"
238
+ raise msg
239
+ end
240
+
241
+ statement_start_line_indexes
130
242
  end
131
243
  end
132
244
  end
@@ -8,19 +8,43 @@ module HamlLint::RubyExtraction
8
8
  @indent_to_remove = indent_to_remove
9
9
  end
10
10
 
11
- def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines)
11
+ def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines) # rubocop:disable Metrics
12
+ return if @ruby_lines == to_ruby_lines
13
+
12
14
  affected_haml_lines = haml_lines[@haml_line_index..haml_end_line_index]
13
15
 
14
16
  affected_haml = affected_haml_lines.join("\n")
15
17
 
16
18
  from_ruby = unwrap(@ruby_lines).join("\n")
19
+
20
+ if to_ruby_lines.size > 1
21
+ min_indent = to_ruby_lines.first[/^\s*/]
22
+ to_ruby_lines.each.with_index do |line, i|
23
+ next if i == 0
24
+ next if line.start_with?(min_indent)
25
+ to_ruby_lines[i] = "#{min_indent}#{line.lstrip}"
26
+ end
27
+ end
28
+
17
29
  to_ruby = unwrap(to_ruby_lines).join("\n")
18
30
 
19
31
  affected_start_index = affected_haml.index(from_ruby)
20
- affected_end_index = affected_start_index + from_ruby.size
32
+ if affected_start_index
33
+ affected_end_index = affected_start_index + from_ruby.size
34
+ else
35
+ regexp = HamlLint::Utils.regexp_for_parts(from_ruby.split("\n"), "(?:\s*\\|?\n)")
36
+ mo = affected_haml.match(regexp)
37
+ affected_start_index = mo.begin(0)
38
+ affected_end_index = mo.end(0)
39
+ end
40
+
21
41
  affected_haml[affected_start_index...affected_end_index] = to_ruby
22
42
 
23
43
  haml_lines[@haml_line_index..haml_end_line_index] = affected_haml.split("\n")
44
+
45
+ if haml_lines[haml_end_line_index].end_with?(' |')
46
+ haml_lines[haml_end_line_index].chop!.rstrip!
47
+ end
24
48
  end
25
49
 
26
50
  def unwrap(lines)
@@ -32,6 +32,19 @@ module HamlLint
32
32
  false
33
33
  end
34
34
 
35
+ let(:supported_haml?) do |example|
36
+ # Tries to match `{% haml_version >= '5' %}`, extracting the operator and the "number"
37
+ haml_version_regex = /\{%\s*haml_version\s*([^\w\s]+?)\s*['"]?(\d+(\.\d+)*)['"]?\s*%\}/
38
+ requirements = example.metadata[:full_description].scan(haml_version_regex)
39
+
40
+ # This can be used by the requirements in eval
41
+ haml_version = HamlLint::VersionComparer.for_haml
42
+
43
+ requirements.all? do |(operator, version)|
44
+ haml_version.send(operator, version)
45
+ end
46
+ end
47
+
35
48
  before do
36
49
  if stub_rubocop?
37
50
  skip if end_ruby.include?('SKIP')
@@ -92,6 +105,8 @@ module HamlLint
92
105
  # 4) the corrected haml
93
106
  # Each steps is delimited by a line with ---
94
107
  def follows_steps # rubocop:disable Metrics
108
+ skip unless supported_haml?
109
+
95
110
  begin
96
111
  subject.run_or_raise(document, autocorrect: autocorrect)
97
112
  rescue StandardError => e
@@ -276,5 +276,10 @@ module HamlLint
276
276
  ensure
277
277
  $stdin = original_stdin
278
278
  end
279
+
280
+ def regexp_for_parts(parts, join_regexp)
281
+ regexp_code = parts.map { |c| Regexp.quote(c) }.join(join_regexp)
282
+ Regexp.new(regexp_code)
283
+ end
279
284
  end
280
285
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the gem version.
4
4
  module HamlLint
5
- VERSION = '0.46.0'
5
+ VERSION = '0.48.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haml_lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.46.0
4
+ version: 0.48.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane da Silva
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-21 00:00:00.000000000 Z
11
+ date: 2023-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: haml
@@ -179,7 +179,6 @@ files:
179
179
  - lib/haml_lint/ruby_extraction/script_chunk.rb
180
180
  - lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb
181
181
  - lib/haml_lint/ruby_extraction/tag_script_chunk.rb
182
- - lib/haml_lint/ruby_extractor.rb
183
182
  - lib/haml_lint/ruby_parser.rb
184
183
  - lib/haml_lint/runner.rb
185
184
  - lib/haml_lint/severity.rb
@@ -221,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
220
  - !ruby/object:Gem::Version
222
221
  version: '0'
223
222
  requirements: []
224
- rubygems_version: 3.0.3.1
223
+ rubygems_version: 3.1.6
225
224
  signing_key:
226
225
  specification_version: 4
227
226
  summary: HAML lint tool
@@ -1,224 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module HamlLint
4
- # Utility class for extracting Ruby script from a HAML file that can then be
5
- # linted with a Ruby linter (i.e. is "legal" Ruby). The goal is to turn this:
6
- #
7
- # - if signed_in?(viewer)
8
- # %span Stuff
9
- # = link_to 'Sign Out', sign_out_path
10
- # - else
11
- # .some-class{ class: my_method }= my_method
12
- # = link_to 'Sign In', sign_in_path
13
- #
14
- # into this:
15
- #
16
- # if signed_in?(viewer)
17
- # link_to 'Sign Out', sign_out_path
18
- # else
19
- # { class: my_method }
20
- # my_method
21
- # link_to 'Sign In', sign_in_path
22
- # end
23
- #
24
- # The translation won't be perfect, and won't make any real sense, but the
25
- # relationship between variable declarations/uses and the flow control graph
26
- # will remain intact.
27
- class RubyExtractor
28
- include HamlVisitor
29
-
30
- # Stores the extracted source and a map of lines of generated source to the
31
- # original source that created them.
32
- #
33
- # @attr_reader source [String] generated source code
34
- # @attr_reader source_map [Hash] map of line numbers from generated source
35
- # to original source line number
36
- RubySource = Struct.new(:source, :source_map)
37
-
38
- # Extracts Ruby code from Sexp representing a Slim document.
39
- #
40
- # @param document [HamlLint::Document]
41
- # @return [HamlLint::RubyExtractor::RubySource]
42
- def extract(document)
43
- visit(document.tree)
44
- RubySource.new(@source_lines.join("\n"), @source_map)
45
- end
46
-
47
- def visit_root(_node)
48
- @source_lines = []
49
- @source_map = {}
50
- @line_count = 0
51
- @indent_level = 0
52
- @output_count = 0
53
-
54
- yield # Collect lines of code from children
55
- end
56
-
57
- def visit_plain(node)
58
- # Don't output the text, as we don't want to have to deal with any RuboCop
59
- # cops regarding StringQuotes or AsciiComments, and it's not important to
60
- # overall document anyway.
61
- add_dummy_puts(node)
62
- end
63
-
64
- def visit_tag(node)
65
- additional_attributes = node.dynamic_attributes_sources
66
-
67
- # Include dummy references to code executed in attributes list
68
- # (this forces a "use" of a variable to prevent "assigned but unused
69
- # variable" lints)
70
- additional_attributes.each do |attributes_code|
71
- # Normalize by removing excess whitespace to avoid format lints
72
- attributes_code = attributes_code.gsub(/\s*\n\s*/, "\n").strip
73
-
74
- # Attributes can either be a method call or a literal hash, so wrap it
75
- # in a method call itself in order to avoid having to differentiate the
76
- # two. Use the tag name for the method to differentiate different tag types
77
- # for RuboCop and prevent erroneous warnings.
78
- add_line("#{node.tag_name}(#{attributes_code})", node)
79
- end
80
-
81
- check_tag_static_hash_source(node)
82
-
83
- # We add a dummy puts statement to represent the tag name being output.
84
- # This prevents some erroneous RuboCop warnings.
85
- add_dummy_puts(node, node.tag_name)
86
-
87
- code = node.script.strip
88
- add_line(code, node) unless code.empty?
89
- end
90
-
91
- def after_visit_tag(node)
92
- # We add a dummy puts statement for closing tag.
93
- add_dummy_puts(node, "#{node.tag_name}/")
94
- end
95
-
96
- def visit_script(node)
97
- code = node.text
98
-
99
- add_line(code.strip, node)
100
-
101
- start_block = anonymous_block?(code) || start_block_keyword?(code)
102
-
103
- if start_block
104
- @indent_level += 1
105
- end
106
-
107
- yield # Continue extracting code from children
108
-
109
- if start_block
110
- @indent_level -= 1
111
- add_line('end', node)
112
- end
113
- end
114
-
115
- def visit_haml_comment(node)
116
- # We want to preseve leading whitespace if it exists, but include leading
117
- # whitespace if it doesn't exist so that RuboCop's LeadingCommentSpace
118
- # doesn't complain
119
- comment = node.text
120
- .gsub(/\n(\S)/, "\n# \\1")
121
- .gsub(/\n(\s)/, "\n#\\1")
122
- add_line("##{comment}", node)
123
- end
124
-
125
- def visit_silent_script(node, &block)
126
- visit_script(node, &block)
127
- end
128
-
129
- def visit_filter(node)
130
- if node.filter_type == 'ruby'
131
- node.text.split("\n").each_with_index do |line, index|
132
- add_line(line, node.line + index + 1, discard_blanks: false)
133
- end
134
- else
135
- add_dummy_puts(node, ":#{node.filter_type}")
136
- HamlLint::Utils.extract_interpolated_values(node.text) do |interpolated_code, line|
137
- add_line(interpolated_code, node.line + line)
138
- end
139
- end
140
- end
141
-
142
- private
143
-
144
- def check_tag_static_hash_source(node)
145
- # Haml::Parser converts hashrocket-style hash attributes of strings and symbols
146
- # to static attributes, and excludes them from the dynamic attribute sources:
147
- # https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L400-L404
148
- # https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L540-L554
149
- # Here, we add the hash source back in so it can be inspected by rubocop.
150
- if node.hash_attributes? && node.dynamic_attributes_sources.empty?
151
- normalized_attr_source = node.dynamic_attributes_source[:hash].gsub(/\s*\n\s*/, ' ')
152
-
153
- add_line(normalized_attr_source, node)
154
- end
155
- end
156
-
157
- # Adds a dummy method call with a unique name so we don't get
158
- # Style/IdenticalConditionalBranches RuboCop warnings
159
- def add_dummy_puts(node, annotation = nil)
160
- annotation = " # #{annotation}" if annotation
161
- add_line("_haml_lint_puts_#{@output_count}#{annotation}", node)
162
- @output_count += 1
163
- end
164
-
165
- def add_line(code, node_or_line, discard_blanks: true)
166
- return if code.empty? && discard_blanks
167
-
168
- indent_level = @indent_level
169
-
170
- if node_or_line.respond_to?(:line) && mid_block_keyword?(code)
171
- # Since mid-block keywords are children of the corresponding start block
172
- # keyword, we need to reduce their indentation level by 1. However, we
173
- # don't do this unless this is an actual tag node (a raw line number
174
- # means this came from a `:ruby` filter).
175
- indent_level -= 1
176
- end
177
-
178
- indent = (' ' * 2 * indent_level)
179
-
180
- @source_lines << indent_code(code, indent)
181
-
182
- original_line =
183
- node_or_line.respond_to?(:line) ? node_or_line.line : node_or_line
184
-
185
- # For interpolated code in filters that spans multiple lines, the
186
- # resulting code will span multiple lines, so we need to create a
187
- # mapping for each line.
188
- (code.count("\n") + 1).times do
189
- @line_count += 1
190
- @source_map[@line_count] = original_line
191
- end
192
- end
193
-
194
- def indent_code(code, indent)
195
- codes = code.split("\n")
196
- codes.map { |c| indent + c }.join("\n")
197
- end
198
-
199
- def anonymous_block?(text)
200
- text =~ /\bdo\s*(\|\s*[^|]*\s*\|)?(\s*#.*)?\z/
201
- end
202
-
203
- START_BLOCK_KEYWORDS = %w[if unless case begin for until while].freeze
204
- def start_block_keyword?(text)
205
- START_BLOCK_KEYWORDS.include?(block_keyword(text))
206
- end
207
-
208
- MID_BLOCK_KEYWORDS = %w[else elsif when rescue ensure].freeze
209
- def mid_block_keyword?(text)
210
- MID_BLOCK_KEYWORDS.include?(block_keyword(text))
211
- end
212
-
213
- LOOP_KEYWORDS = %w[for until while].freeze
214
- def block_keyword(text)
215
- # Need to handle 'for'/'while' since regex stolen from HAML parser doesn't
216
- if (keyword = text[/\A\s*([^\s]+)\s+/, 1]) && LOOP_KEYWORDS.include?(keyword)
217
- return keyword
218
- end
219
-
220
- return unless keyword = text.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0]
221
- keyword[0] || keyword[1]
222
- end
223
- end
224
- end