haml_lint 0.45.0 → 0.48.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/bin/haml-lint +1 -1
  3. data/config/default.yml +6 -28
  4. data/config/forced_rubocop_config.yml +171 -0
  5. data/lib/haml_lint/adapter/haml_4.rb +18 -0
  6. data/lib/haml_lint/adapter/haml_5.rb +11 -0
  7. data/lib/haml_lint/adapter/haml_6.rb +11 -0
  8. data/lib/haml_lint/cli.rb +8 -3
  9. data/lib/haml_lint/configuration_loader.rb +9 -10
  10. data/lib/haml_lint/document.rb +89 -8
  11. data/lib/haml_lint/exceptions.rb +6 -0
  12. data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +35 -0
  13. data/lib/haml_lint/file_finder.rb +2 -2
  14. data/lib/haml_lint/lint.rb +10 -1
  15. data/lib/haml_lint/linter/final_newline.rb +4 -3
  16. data/lib/haml_lint/linter/implicit_div.rb +1 -1
  17. data/lib/haml_lint/linter/indentation.rb +3 -3
  18. data/lib/haml_lint/linter/no_placeholders.rb +18 -0
  19. data/lib/haml_lint/linter/repeated_id.rb +2 -1
  20. data/lib/haml_lint/linter/rubocop.rb +353 -60
  21. data/lib/haml_lint/linter/space_before_script.rb +8 -10
  22. data/lib/haml_lint/linter/unnecessary_string_output.rb +1 -1
  23. data/lib/haml_lint/linter/view_length.rb +1 -1
  24. data/lib/haml_lint/linter.rb +60 -10
  25. data/lib/haml_lint/linter_registry.rb +3 -5
  26. data/lib/haml_lint/logger.rb +2 -2
  27. data/lib/haml_lint/options.rb +26 -2
  28. data/lib/haml_lint/rake_task.rb +2 -2
  29. data/lib/haml_lint/reporter/hash_reporter.rb +1 -3
  30. data/lib/haml_lint/reporter/offense_count_reporter.rb +1 -1
  31. data/lib/haml_lint/reporter/utils.rb +33 -4
  32. data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +24 -0
  33. data/lib/haml_lint/ruby_extraction/base_chunk.rb +114 -0
  34. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +630 -0
  35. data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
  36. data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +34 -0
  37. data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +17 -0
  38. data/lib/haml_lint/ruby_extraction/interpolation_chunk.rb +26 -0
  39. data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +32 -0
  40. data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +40 -0
  41. data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +33 -0
  42. data/lib/haml_lint/ruby_extraction/ruby_source.rb +5 -0
  43. data/lib/haml_lint/ruby_extraction/script_chunk.rb +244 -0
  44. data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +63 -0
  45. data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
  46. data/lib/haml_lint/runner.rb +35 -3
  47. data/lib/haml_lint/spec/matchers/report_lint.rb +22 -7
  48. data/lib/haml_lint/spec/normalize_indent.rb +2 -2
  49. data/lib/haml_lint/spec/shared_linter_context.rb +9 -1
  50. data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +158 -0
  51. data/lib/haml_lint/spec.rb +1 -0
  52. data/lib/haml_lint/tree/filter_node.rb +10 -0
  53. data/lib/haml_lint/tree/node.rb +13 -4
  54. data/lib/haml_lint/tree/tag_node.rb +5 -9
  55. data/lib/haml_lint/utils.rb +135 -5
  56. data/lib/haml_lint/version.rb +1 -1
  57. data/lib/haml_lint/version_comparer.rb +25 -0
  58. data/lib/haml_lint.rb +12 -0
  59. metadata +24 -6
  60. data/lib/haml_lint/ruby_extractor.rb +0 -223
@@ -0,0 +1,630 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics
4
+ module HamlLint::RubyExtraction
5
+ # Extracts "chunks" of the haml file into instances of subclasses of HamlLint::RubyExtraction::BaseChunk.
6
+ #
7
+ # This is the first step of generating Ruby code from a HAML file to then be processed by RuboCop.
8
+ # See HamlLint::RubyExtraction::BaseChunk for more details.
9
+ class ChunkExtractor
10
+ include HamlLint::HamlVisitor
11
+
12
+ attr_reader :script_output_prefix
13
+
14
+ HAML_PARSER_INSTANCE = if Haml::VERSION >= '5.0.0'
15
+ ::Haml::Parser.new({})
16
+ else
17
+ ::Haml::Parser.new('', {})
18
+ end
19
+
20
+ def initialize(document, script_output_prefix:)
21
+ @document = document
22
+ @script_output_prefix = script_output_prefix
23
+ end
24
+
25
+ def extract
26
+ raise 'Already extracted' if @ruby_chunks
27
+
28
+ prepare_extract
29
+
30
+ visit(@document.tree)
31
+ @ruby_chunks
32
+ end
33
+
34
+ # Useful for tests
35
+ def prepare_extract
36
+ @ruby_chunks = []
37
+ @original_haml_lines = @document.source_lines
38
+ end
39
+
40
+ def visit_root(_node)
41
+ yield # Collect lines of code from children
42
+ end
43
+
44
+ # Visiting lines like ` Some raw text to output`
45
+ def visit_plain(node)
46
+ indent = @original_haml_lines[node.line - 1].index(/\S/)
47
+ @ruby_chunks << PlaceholderMarkerChunk.new(node, 'plain', indent: indent)
48
+ end
49
+
50
+ # Visiting lines like ` -# Some commenting!`
51
+ def visit_haml_comment(node)
52
+ # We want to preserve leading whitespace if it exists, but add a leading
53
+ # whitespace if it doesn't exist so that RuboCop's LeadingCommentSpace
54
+ # doesn't complain
55
+ line_index = node.line - 1
56
+ lines = @original_haml_lines[line_index..(line_index + node.text.count("\n"))].dup
57
+ indent = lines.first.index(/\S/)
58
+ # Remove only the -, the # will align with regular code
59
+ # -# comment
60
+ # - foo()
61
+ # becomes
62
+ # # comment
63
+ # foo()
64
+ lines[0] = lines[0].sub('-', '')
65
+
66
+ # Adding a space before the comment if its missing
67
+ # We can't fix those, so make sure not to generate warnings for them.
68
+ lines[0] = lines[0].sub(/\A(\s*)#(\S)/, '\\1# \\2')
69
+
70
+ HamlLint::Utils.map_after_first!(lines) do |line|
71
+ # Since the indent/spaces of the extra line comments isn't exactly in the haml,
72
+ # it's not RuboCop's job to fix indentation, so just make a reasonable indentation
73
+ # to avoid offenses.
74
+ ' ' * indent + line.sub(/^\s*/, '# ').rstrip
75
+ end
76
+
77
+ # Using Placeholder instead of script because we can't revert back to the
78
+ # exact original comment since multiple syntax lead to the exact same comment.
79
+ @ruby_chunks << HamlCommentChunk.new(node, lines, end_marker_indent: indent)
80
+ end
81
+
82
+ # Visiting comments which are output to HTML. Lines looking like
83
+ # ` / This will be in the HTML source!`
84
+ def visit_comment(node)
85
+ line = @original_haml_lines[node.line - 1]
86
+ indent = line.index(/\S/)
87
+ @ruby_chunks << PlaceholderMarkerChunk.new(node, 'comment', indent: indent)
88
+ end
89
+
90
+ # Visit a script which outputs. Lines looking like ` = foo`
91
+ def visit_script(node, &block)
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
98
+
99
+ match = raw_first_line.match(/\A\s*(=|!=|&=)(?!=)/)
100
+ unless match
101
+ # The line doesn't start with a - or a =, this is actually a "plain"
102
+ # that contains interpolation.
103
+ indent = raw_first_line.index(/\S/)
104
+ @ruby_chunks << PlaceholderMarkerChunk.new(node, 'interpolation', indent: indent)
105
+ add_interpolation_chunks(node, raw_first_line, node.line - 1, indent: indent)
106
+ return
107
+ end
108
+
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]?)/, '')
116
+ line_indentation = Regexp.last_match(1).size
117
+
118
+ raw_code = lines.join("\n")
119
+
120
+ if lines[0][/\S/] == '#'
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
123
+ else
124
+ lines[0] = HamlLint::Utils.insert_after_indentation(lines[0], script_output_prefix)
125
+ end
126
+
127
+ indent_delta = script_output_prefix.size - line_indentation
128
+ HamlLint::Utils.map_after_first!(lines) do |line|
129
+ HamlLint::Utils.indent(line, indent_delta)
130
+ end
131
+
132
+ prev_chunk = @ruby_chunks.last
133
+ if prev_chunk.is_a?(ScriptChunk) &&
134
+ prev_chunk.node.type == :script &&
135
+ prev_chunk.node == node.parent
136
+ # When an outputting script is nested under another outputting script,
137
+ # we want to block them from being merged together by rubocop, because
138
+ # this doesn't make sense in HAML.
139
+ # Example:
140
+ # = if this_is_short
141
+ # = this_is_short_too
142
+ # Could become (after RuboCop):
143
+ # HL.out = (HL.out = this_is_short_too if this_is_short)
144
+ # Or in (broken) HAML style:
145
+ # = this_is_short_too = if this_is_short
146
+ # By forcing this to start a chunk, there will be extra placeholders which
147
+ # blocks rubocop from merging the lines.
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
155
+ end
156
+
157
+ finish_visit_any_script(node, lines, raw_code: raw_code, must_start_chunk: must_start_chunk,
158
+ first_output_prefix: script_prefix, &block)
159
+ end
160
+
161
+ # Visit a script which doesn't output. Lines looking like ` - foo`
162
+ def visit_silent_script(node, &block)
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
+
168
+ lines[0] = lines[0].sub(/(-[ \t]?)/, '')
169
+ nb_to_deindent = Regexp.last_match(1).size
170
+
171
+ HamlLint::Utils.map_after_first!(lines) do |line|
172
+ line.sub(/^ {1,#{nb_to_deindent}}/, '')
173
+ end
174
+
175
+ finish_visit_any_script(node, lines, &block)
176
+ end
177
+
178
+ # Code common to both silent and outputting scripts
179
+ #
180
+ # raw_code is the code before we do transformations, such as adding the `HL.out = `
181
+ def finish_visit_any_script(node, lines, raw_code: nil, must_start_chunk: false, first_output_prefix: '=')
182
+ raw_code ||= lines.join("\n")
183
+ start_nesting = self.class.start_nesting_after?(raw_code)
184
+
185
+ lines = add_following_empty_lines(node, lines)
186
+
187
+ my_indent = lines.first.index(/\S/)
188
+ indent_after = indent_after_line_index(node.line - 1 + lines.size - 1) || 0
189
+ indent_after = [my_indent, indent_after].max
190
+
191
+ @ruby_chunks << ScriptChunk.new(node, lines,
192
+ end_marker_indent: indent_after,
193
+ must_start_chunk: must_start_chunk,
194
+ previous_chunk: @ruby_chunks.last,
195
+ first_output_haml_prefix: first_output_prefix)
196
+
197
+ yield
198
+
199
+ if start_nesting
200
+ if node.children.empty?
201
+ raise "Line #{node.line} should be followed by indentation. This might actually" \
202
+ " work in Haml, but it's almost a bug that it does. haml-lint cannot process."
203
+ end
204
+
205
+ last_child = node.children.last
206
+ if last_child.is_a?(HamlLint::Tree::SilentScriptNode) && last_child.keyword == 'end'
207
+ # This is allowed in Haml 5, gotta handle it!
208
+ # No need for the implicit end chunk since there is an explicit one
209
+ else
210
+ @ruby_chunks << ImplicitEndChunk.new(node, [' ' * my_indent + 'end'],
211
+ haml_line_index: @ruby_chunks.last.haml_end_line_index,
212
+ end_marker_indent: my_indent)
213
+ end
214
+ end
215
+ end
216
+
217
+ # Visiting a tag. Lines looking like ` %div`
218
+ def visit_tag(node)
219
+ indent = @original_haml_lines[node.line - 1].index(/\S/)
220
+
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
229
+
230
+ tag_chunk = PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
231
+ @ruby_chunks << tag_chunk
232
+
233
+ current_line_index = visit_tag_attributes(node, indent: indent)
234
+ visit_tag_script(node, line_index: current_line_index, indent: indent)
235
+
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'],
249
+ haml_line_index: @ruby_chunks.last.haml_end_line_index)
250
+ end
251
+ end
252
+
253
+ # (Called manually form visit_tag)
254
+ # Visiting the attributes of a tag. Lots of different examples below in the code.
255
+ # A common syntax is: `%div{style: 'yes_please'}`
256
+ #
257
+ # Returns the new line_index we reached, useful to handle the script that follows
258
+ def visit_tag_attributes(node, indent:)
259
+ final_line_index = node.line - 1
260
+ additional_attributes = node.dynamic_attributes_sources
261
+
262
+ attributes_code = additional_attributes.first
263
+ if !attributes_code && node.hash_attributes? && node.dynamic_attributes_sources.empty?
264
+ # No idea why .foo{:bar => 123} doesn't get here, but .foo{:bar => '123'} does...
265
+ # The code we get for the latter is {:bar => '123'}.
266
+ # We normalize it by removing the { } so that it matches wha we normally get
267
+ attributes_code = node.dynamic_attributes_source[:hash][1...-1]
268
+ end
269
+
270
+ if attributes_code&.start_with?('{')
271
+ # Looks like the .foo(bar = 123) case. Ignoring.
272
+ attributes_code = nil
273
+ end
274
+
275
+ return final_line_index unless attributes_code
276
+ # Attributes have different ways to be given to us:
277
+ # .foo{bar: 123} => "bar: 123"
278
+ # .foo{:bar => 123} => ":bar => 123"
279
+ # .foo{:bar => '123'} => "{:bar => '123'}" # No idea why this is different
280
+ # .foo(bar = 123) => '{"bar" => 123,}'
281
+ # .foo{html_attrs('fr-fr')} => html_attrs('fr-fr')
282
+ #
283
+ # The (bar = 123) case is extra painful to autocorrect (so is ignored up there).
284
+ # #raw_ruby_from_haml will "detect" this case by not finding the code.
285
+ #
286
+ # We wrap the result in a method to have a valid syntax for all 3 ways
287
+ # without having to differentiate them.
288
+ first_line_offset, raw_attributes_lines = extract_raw_tag_attributes_ruby_lines(attributes_code,
289
+ node.line - 1)
290
+ return final_line_index unless raw_attributes_lines
291
+
292
+ final_line_index += raw_attributes_lines.size - 1
293
+
294
+ # Since .foo{bar: 123} => "bar: 123" needs wrapping (Or it would be a syntax error) and
295
+ # .foo{html_attrs('fr-fr')} => html_attrs('fr-fr') doesn't care about being
296
+ # wrapped, we always wrap to place them to a similar offset to how they are in the haml.
297
+ wrap_by = first_line_offset - indent
298
+ if wrap_by < 2
299
+ # Need 2 minimum, for "W(". If we have less, we must indent everything for the difference
300
+ extra_indent = 2 - wrap_by
301
+ HamlLint::Utils.map_after_first!(raw_attributes_lines) do |line|
302
+ HamlLint::Utils.indent(line, extra_indent)
303
+ end
304
+ wrap_by = 2
305
+ end
306
+ raw_attributes_lines = wrap_lines(raw_attributes_lines, wrap_by)
307
+ raw_attributes_lines[0] = ' ' * indent + raw_attributes_lines[0]
308
+
309
+ @ruby_chunks << TagAttributesChunk.new(node, raw_attributes_lines,
310
+ end_marker_indent: indent,
311
+ indent_to_remove: extra_indent)
312
+
313
+ final_line_index
314
+ end
315
+
316
+ # Visiting the script besides tag. The part to the right of the equal sign of
317
+ # lines looking like ` %div= foo(bar)`
318
+ def visit_tag_script(node, line_index:, indent:)
319
+ return if node.script.nil? || node.script.empty?
320
+ # We ignore scripts which are just a comment
321
+ return if node.script[/\S/] == '#'
322
+
323
+ first_line_offset, script_lines = extract_raw_ruby_lines(node.script, line_index)
324
+
325
+ if script_lines.nil?
326
+ # This is a string with interpolation after a tag
327
+ # ex: %tag hello #{world}
328
+ # Sadly, the text with interpolation is escaped from the original, but this code
329
+ # needs the original.
330
+ interpolation_original = @document.unescape_interpolation_to_original_cache[node.script]
331
+
332
+ line_start_index = @original_haml_lines[node.line - 1].rindex(interpolation_original)
333
+ add_interpolation_chunks(node, interpolation_original, node.line - 1,
334
+ line_start_index: line_start_index, indent: indent)
335
+ else
336
+ script_lines[0] = "#{' ' * indent}#{script_output_prefix}#{script_lines[0]}"
337
+ indent_delta = script_output_prefix.size - first_line_offset + indent
338
+ HamlLint::Utils.map_after_first!(script_lines) do |line|
339
+ HamlLint::Utils.indent(line, indent_delta)
340
+ end
341
+
342
+ @ruby_chunks << TagScriptChunk.new(node, script_lines,
343
+ haml_line_index: line_index,
344
+ end_marker_indent: indent)
345
+ end
346
+ end
347
+
348
+ # Visiting a HAML filter. Lines looking like ` :javascript` and the following lines
349
+ # that are nested.
350
+ def visit_filter(node)
351
+ # For unknown reasons, haml doesn't escape interpolations in filters.
352
+ # So we can rely on \n to split / get the number of lines.
353
+ filter_name_indent = @original_haml_lines[node.line - 1].index(/\S/)
354
+ if node.filter_type == 'ruby'
355
+ # The indentation in node.text is normalized, so that at least one line
356
+ # is indented by 0.
357
+ lines = node.text.split("\n")
358
+ lines.map! do |line|
359
+ if line !~ /\S/
360
+ # whitespace or empty
361
+ ''
362
+ else
363
+ ' ' * filter_name_indent + line
364
+ end
365
+ end
366
+
367
+ @ruby_chunks << RubyFilterChunk.new(node, lines,
368
+ haml_line_index: node.line, # it's one the next line, no need for -1
369
+ start_marker_indent: filter_name_indent,
370
+ end_marker_indent: filter_name_indent)
371
+ elsif node.text.include?('#')
372
+ name_indentation = ' ' * @original_haml_lines[node.line - 1].index(/\S/)
373
+ # TODO: HAML_LINT_FILTER could be in the string and mess things up
374
+ lines = ["#{name_indentation}#{script_output_prefix}<<~HAML_LINT_FILTER"]
375
+ lines.concat @original_haml_lines[node.line..(node.line + node.text.count("\n") - 1)]
376
+ lines << "#{name_indentation}HAML_LINT_FILTER"
377
+ @ruby_chunks << NonRubyFilterChunk.new(node, lines,
378
+ end_marker_indent: filter_name_indent)
379
+ # Those could be interpolation. We treat them as a here-doc, which is nice since we can
380
+ # keep the indentation as-is.
381
+ else
382
+ @ruby_chunks << PlaceholderMarkerChunk.new(node, 'filter', indent: filter_name_indent,
383
+ nb_lines: 1 + node.text.count("\n"))
384
+ end
385
+ end
386
+
387
+ # Adds chunks for the interpolation within the given code
388
+ def add_interpolation_chunks(node, code, haml_line_index, indent:, line_start_index: 0)
389
+ HamlLint::Utils.handle_interpolation_with_indexes(code) do |scanner, line_index, char_index|
390
+ escapes = scanner[2].size
391
+ next if escapes.odd?
392
+ char = scanner[3] # '{', '@' or '$'
393
+ if Gem::Version.new(Haml::VERSION) >= Gem::Version.new('5') && (char != '{')
394
+ # Before Haml 5, scanner didn't have a scanner[3], it only handled `#{}`
395
+ next
396
+ end
397
+
398
+ start_char_index = char_index
399
+ start_char_index += line_start_index if line_index == 0
400
+
401
+ Haml::Util.balance(scanner, '{', '}', 1)[0][0...-1]
402
+
403
+ # Need to manually get the code now that we have positions so that all whitespace is present,
404
+ # because Haml::Util.balance does a strip...
405
+ interpolated_code = code[char_index...scanner.charpos - 1]
406
+
407
+ interpolated_code = "#{' ' * indent}#{script_output_prefix}#{interpolated_code}"
408
+
409
+ if interpolated_code.include?("\n")
410
+ # We can't correct multiline interpolation.
411
+ # Finding meaningful code to generate and then transfer back is pretty complex
412
+ placeholder_code = interpolated_code.gsub(/\s*\n\s*/, ' ').rstrip
413
+ unless parse_ruby(placeholder_code)
414
+ placeholder_code = interpolated_code.gsub(/\s*\n\s*/, '; ').rstrip
415
+ end
416
+ @ruby_chunks << AdHocChunk.new(node, [placeholder_code],
417
+ haml_line_index: haml_line_index + line_index)
418
+ else
419
+ @ruby_chunks << InterpolationChunk.new(node, [interpolated_code],
420
+ haml_line_index: haml_line_index + line_index,
421
+ start_char_index: start_char_index,
422
+ end_marker_indent: indent)
423
+ end
424
+ end
425
+ end
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
+
436
+ # Returns the raw lines from the haml for the given index.
437
+ # Multiple lines are returned when a line ends with a comma as that is the only
438
+ # time HAMLs allows Ruby lines to be split.
439
+
440
+ # Haml's line-splitting rules (allowed after comma in scripts and attributes) are handled
441
+ # at the parser level, so Haml doesn't provide the code as it is actually formatted in the Haml
442
+ # file. #raw_ruby_from_haml extracts the ruby code as it is exactly in the Haml file.
443
+ # The first and last lines may not be the complete lines from the Haml, only the Ruby parts
444
+ # and the indentation between the first and last list.
445
+
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]
464
+
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
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
502
+
503
+ haml_processed_ruby_code.split(/[, ]/)
504
+
505
+ regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/,\s*|\s+/), '(?:,\\s*|\\s+)')
506
+
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?
512
+
513
+ raw_ruby = match[0]
514
+ ruby_lines = raw_ruby.split("\n")
515
+ first_line_offset = match.begin(0)
516
+
517
+ [first_line_offset, ruby_lines]
518
+ end
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
+
562
+ def wrap_lines(lines, wrap_depth)
563
+ lines = lines.dup
564
+ wrapping_prefix = 'W' * (wrap_depth - 1) + '('
565
+ lines[0] = wrapping_prefix + lines[0]
566
+ lines[-1] = lines[-1] + ')'
567
+ lines
568
+ end
569
+
570
+ # Adds empty lines that follow the lines (Used for scripts), so that
571
+ # RuboCop can receive them too. Some cops are sensitive to empty lines.
572
+ def add_following_empty_lines(node, lines)
573
+ first_line_index = node.line - 1 + lines.size
574
+ extra_lines = []
575
+
576
+ extra_lines << '' while HamlLint::Utils.is_blank_line?(@original_haml_lines[first_line_index + extra_lines.size])
577
+
578
+ if @original_haml_lines[first_line_index + extra_lines.size].nil?
579
+ # Since we reached the end of the document without finding content,
580
+ # then we don't add those lines.
581
+ return lines
582
+ end
583
+
584
+ lines + extra_lines
585
+ end
586
+
587
+ def parse_ruby(source)
588
+ @ruby_parser ||= HamlLint::RubyParser.new
589
+ @ruby_parser.parse(source)
590
+ end
591
+
592
+ def indent_after_line_index(line_index)
593
+ (line_index + 1..@original_haml_lines.size - 1).each do |i|
594
+ indent = @original_haml_lines[i].index(/\S/)
595
+ return indent if indent
596
+ end
597
+ nil
598
+ end
599
+
600
+ def self.start_nesting_after?(code)
601
+ anonymous_block?(code) || start_block_keyword?(code)
602
+ end
603
+
604
+ def self.anonymous_block?(code)
605
+ # Don't start with a comment and end with a `do`
606
+ # Definetly not perfect for the comment handling, but otherwise a more advanced parsing system is needed.
607
+ # Move the comment to its own line if it's annoying.
608
+ code !~ /\A\s*#/ &&
609
+ code =~ /\bdo\s*(\|[^|]*\|\s*)?(#.*)?\z/
610
+ end
611
+
612
+ START_BLOCK_KEYWORDS = %w[if unless case begin for until while].freeze
613
+ def self.start_block_keyword?(code)
614
+ START_BLOCK_KEYWORDS.include?(block_keyword(code))
615
+ end
616
+
617
+ LOOP_KEYWORDS = %w[for until while].freeze
618
+ def self.block_keyword(code)
619
+ # Need to handle 'for'/'while' since regex stolen from HAML parser doesn't
620
+ if (keyword = code[/\A\s*([^\s]+)\s+/, 1]) && LOOP_KEYWORDS.include?(keyword)
621
+ return keyword
622
+ end
623
+
624
+ return unless keyword = code.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0]
625
+ keyword[0] || keyword[1]
626
+ end
627
+ end
628
+ end
629
+
630
+ # rubocop:enable Metrics