haml_lint 0.47.0 → 0.49.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: e7a818da3ed1d58db0ad35b864c6c90d8036173109f6609da452da8b496dd1d5
4
- data.tar.gz: ca045650d3373d2fa48d840c684c14b3d46a497f39e91eb9058b1827f3bd74b7
3
+ metadata.gz: 0411721c82b95f3662c12b9bb3eeff8013a81dbef095d1b5756baf529f121fd9
4
+ data.tar.gz: 2ab1affd25ac7b27278a35bc56a883aeb8626573e082b3ac4a2e725bdff1a90f
5
5
  SHA512:
6
- metadata.gz: 0e2d83720d795e4589d534087c4db2be681eeae95cecda6432917345dea5e4c59e8ba7d65633fb2d4cd0ea86eafc0ea89b8fc51f994cc0b7973a50b0fa0a0abd
7
- data.tar.gz: 11fc0ea04b7dae506bb0e264d204bdcdcaa96a65230ade8416ae56094a46f95deb3a1006200919804769dcc1c7adf7ab407bbfdf863355248eea9b0e7858308a
6
+ metadata.gz: 431cc4b6f6dec7e6d910e7406a76fa58a9224fd689446636e02a7cb895137c33090a3fd845ffbeda941650549da069b8ce056eb979494d6442b348939da32ba5
7
+ data.tar.gz: 9c491a34546d7b437ee53ba5ec4059f8019bcd45dae564c90d230d8faaf69c7e4ec5f07585afc61bd5e0f903b1678567b727d9d82b3a30723e0aa1c8767923a6
data/config/default.yml CHANGED
@@ -105,6 +105,9 @@ linters:
105
105
  TagName:
106
106
  enabled: true
107
107
 
108
+ TrailingEmptyLines:
109
+ enabled: true
110
+
108
111
  TrailingWhitespace:
109
112
  enabled: true
110
113
 
@@ -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
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint
4
+ # Checks for trailing empty lines.
5
+ class Linter::TrailingEmptyLines < Linter
6
+ include LinterRegistry
7
+
8
+ DummyNode = Struct.new(:line)
9
+
10
+ def visit_root(root)
11
+ return if document.source.empty?
12
+ line_number = document.last_non_empty_line
13
+
14
+ node = root.node_for_line(line_number)
15
+ return if node.disabled?(self)
16
+
17
+ return unless document.source.end_with?("\n\n")
18
+
19
+ record_lint(line_number, 'Files should not end with trailing empty lines')
20
+ end
21
+ end
22
+ end
@@ -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,26 +218,32 @@ 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'
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
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
207
247
  @ruby_chunks << AdHocChunk.new(node,
208
248
  [' ' * indent + 'ensure', ' ' * indent + ' HL.noop', ' ' * indent + 'end'],
209
249
  haml_line_index: @ruby_chunks.last.haml_end_line_index)
@@ -222,11 +262,16 @@ module HamlLint::RubyExtraction
222
262
  attributes_code = additional_attributes.first
223
263
  if !attributes_code && node.hash_attributes? && node.dynamic_attributes_sources.empty?
224
264
  # No idea why .foo{:bar => 123} doesn't get here, but .foo{:bar => '123'} does...
225
- # The code we get for the later is {:bar => '123'}.
265
+ # The code we get for the latter is {:bar => '123'}.
226
266
  # We normalize it by removing the { } so that it matches wha we normally get
227
267
  attributes_code = node.dynamic_attributes_source[:hash][1...-1]
228
268
  end
229
269
 
270
+ if attributes_code&.start_with?('{')
271
+ # Looks like the .foo(bar = 123) case. Ignoring.
272
+ attributes_code = nil
273
+ end
274
+
230
275
  return final_line_index unless attributes_code
231
276
  # Attributes have different ways to be given to us:
232
277
  # .foo{bar: 123} => "bar: 123"
@@ -235,14 +280,13 @@ module HamlLint::RubyExtraction
235
280
  # .foo(bar = 123) => '{"bar" => 123,}'
236
281
  # .foo{html_attrs('fr-fr')} => html_attrs('fr-fr')
237
282
  #
238
- # 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).
239
284
  # #raw_ruby_from_haml will "detect" this case by not finding the code.
240
285
  #
241
286
  # We wrap the result in a method to have a valid syntax for all 3 ways
242
287
  # without having to differentiate them.
243
- first_line_offset, raw_attributes_lines = raw_ruby_lines_from_haml(attributes_code,
244
- node.line - 1)
245
-
288
+ first_line_offset, raw_attributes_lines = extract_raw_tag_attributes_ruby_lines(attributes_code,
289
+ node.line - 1)
246
290
  return final_line_index unless raw_attributes_lines
247
291
 
248
292
  final_line_index += raw_attributes_lines.size - 1
@@ -276,7 +320,7 @@ module HamlLint::RubyExtraction
276
320
  # We ignore scripts which are just a comment
277
321
  return if node.script[/\S/] == '#'
278
322
 
279
- 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)
280
324
 
281
325
  if script_lines.nil?
282
326
  # This is a string with interpolation after a tag
@@ -380,52 +424,87 @@ module HamlLint::RubyExtraction
380
424
  end
381
425
  end
382
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
+
383
436
  # Returns the raw lines from the haml for the given index.
384
437
  # Multiple lines are returned when a line ends with a comma as that is the only
385
438
  # time HAMLs allows Ruby lines to be split.
386
- def raw_lines_of_interest(first_line_index)
387
- line_index = first_line_index
388
- lines_of_interest = [@original_haml_lines[line_index]]
389
-
390
- while @original_haml_lines[line_index].rstrip.end_with?(',')
391
- line_index += 1
392
- lines_of_interest << @original_haml_lines[line_index]
393
- end
394
-
395
- lines_of_interest
396
- end
397
439
 
398
440
  # Haml's line-splitting rules (allowed after comma in scripts and attributes) are handled
399
441
  # at the parser level, so Haml doesn't provide the code as it is actually formatted in the Haml
400
442
  # file. #raw_ruby_from_haml extracts the ruby code as it is exactly in the Haml file.
401
443
  # The first and last lines may not be the complete lines from the Haml, only the Ruby parts
402
444
  # and the indentation between the first and last list.
403
- def raw_ruby_lines_from_haml(code, first_line_index)
404
- stripped_code = code.strip
405
- return if stripped_code.empty?
406
445
 
407
- 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]
408
464
 
409
- if lines_of_interest.size == 1
410
- index = lines_of_interest.first.index(stripped_code)
411
- if lines_of_interest.first.include?(stripped_code)
412
- return [index, [stripped_code]]
413
- else
414
- # Sometimes, the code just isn't in the Haml when Haml does transformations to it
415
- return
416
- 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
417
489
  end
418
490
 
419
- raw_haml = lines_of_interest.join("\n")
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
495
+ end
496
+
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
420
502
 
421
- # Need the gsub because while multiline scripts are turned into a single line,
422
- # by haml, multiline tag attributes are not.
423
- code_parts = stripped_code.gsub("\n", ' ').split(/,\s*/)
503
+ haml_processed_ruby_code.split(/[, ]/)
424
504
 
425
- regexp_code = code_parts.map { |c| Regexp.quote(c) }.join(',\\s*')
426
- regexp = Regexp.new(regexp_code)
505
+ regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/,\s*|\s+/), '(?:,\\s*|\\s+)')
427
506
 
428
- match = raw_haml.match(regexp)
507
+ match = joined_lines.match(regexp)
429
508
  # This can happen when pipes are used as marker for multiline parts, and when tag attributes change lines
430
509
  # without ending by a comma. This is quite a can of worm and is probably not too frequent, so for now,
431
510
  # these cases are not supported.
@@ -438,6 +517,48 @@ module HamlLint::RubyExtraction
438
517
  [first_line_offset, ruby_lines]
439
518
  end
440
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
+
441
562
  def wrap_lines(lines, wrap_depth)
442
563
  lines = lines.dup
443
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)
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'rubocop'
4
4
  require 'rubocop/ast/builder'
5
- require 'parser/current'
6
5
 
7
6
  module HamlLint
8
7
  # Parser for the Ruby language.
@@ -14,10 +13,21 @@ module HamlLint
14
13
  class RubyParser
15
14
  # Creates a reusable parser.
16
15
  def initialize
16
+ require_parser
17
17
  @builder = ::RuboCop::AST::Builder.new
18
18
  @parser = ::Parser::CurrentRuby.new(@builder)
19
19
  end
20
20
 
21
+ # Require the current parser version while suppressing the
22
+ # compliancy warning for minor version differences.
23
+ def require_parser
24
+ prev = $VERBOSE
25
+ $VERBOSE = nil
26
+ require 'parser/current'
27
+ ensure
28
+ $VERBOSE = prev
29
+ end
30
+
21
31
  # Parse the given Ruby source into an abstract syntax tree.
22
32
  #
23
33
  # @param source [String] Ruby source code
@@ -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.47.0'
5
+ VERSION = '0.49.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.47.0
4
+ version: 0.49.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-07-05 00:00:00.000000000 Z
11
+ date: 2023-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: haml
@@ -143,6 +143,7 @@ files:
143
143
  - lib/haml_lint/linter/space_inside_hash_attributes.rb
144
144
  - lib/haml_lint/linter/syntax.rb
145
145
  - lib/haml_lint/linter/tag_name.rb
146
+ - lib/haml_lint/linter/trailing_empty_lines.rb
146
147
  - lib/haml_lint/linter/trailing_whitespace.rb
147
148
  - lib/haml_lint/linter/unnecessary_interpolation.rb
148
149
  - lib/haml_lint/linter/unnecessary_string_output.rb
@@ -179,7 +180,6 @@ files:
179
180
  - lib/haml_lint/ruby_extraction/script_chunk.rb
180
181
  - lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb
181
182
  - lib/haml_lint/ruby_extraction/tag_script_chunk.rb
182
- - lib/haml_lint/ruby_extractor.rb
183
183
  - lib/haml_lint/ruby_parser.rb
184
184
  - lib/haml_lint/runner.rb
185
185
  - lib/haml_lint/severity.rb
@@ -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