haml_lint 0.47.0 → 0.49.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/config/default.yml +3 -0
- data/lib/haml_lint/linter/repeated_id.rb +2 -1
- data/lib/haml_lint/linter/rubocop.rb +0 -1
- data/lib/haml_lint/linter/trailing_empty_lines.rb +22 -0
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +188 -67
- data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +0 -20
- data/lib/haml_lint/ruby_extraction/script_chunk.rb +142 -30
- data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +26 -2
- data/lib/haml_lint/ruby_parser.rb +11 -1
- data/lib/haml_lint/utils.rb +5 -0
- data/lib/haml_lint/version.rb +1 -1
- metadata +3 -3
- data/lib/haml_lint/ruby_extractor.rb +0 -224
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0411721c82b95f3662c12b9bb3eeff8013a81dbef095d1b5756baf529f121fd9
|
4
|
+
data.tar.gz: 2ab1affd25ac7b27278a35bc56a883aeb8626573e082b3ac4a2e725bdff1a90f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 431cc4b6f6dec7e6d910e7406a76fa58a9224fd689446636e02a7cb895137c33090a3fd845ffbeda941650549da069b8ce056eb979494d6442b348939da32ba5
|
7
|
+
data.tar.gz: 9c491a34546d7b437ee53ba5ec4059f8019bcd45dae564c90d230d8faaf69c7e4ec5f07585afc61bd5e0f903b1678567b727d9d82b3a30723e0aa1c8767923a6
|
data/config/default.yml
CHANGED
@@ -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 =
|
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
|
@@ -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
|
-
|
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
|
-
|
75
|
-
indent =
|
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
|
-
|
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
|
-
|
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 =
|
103
|
+
indent = raw_first_line.index(/\S/)
|
87
104
|
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'interpolation', indent: indent)
|
88
|
-
add_interpolation_chunks(node,
|
105
|
+
add_interpolation_chunks(node, raw_first_line, node.line - 1, indent: indent)
|
89
106
|
return
|
90
107
|
end
|
91
108
|
|
92
|
-
|
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
|
99
|
-
|
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,
|
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 =
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
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
|
-
|
205
|
-
|
206
|
-
|
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
|
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 =
|
244
|
-
|
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 =
|
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
|
-
|
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
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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)
|
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
|
-
|
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
|
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?(
|
108
|
-
line = line.sub(
|
109
|
-
|
110
|
-
|
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
|
-
|
121
|
-
|
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
|
-
|
124
|
-
!!lines[line_index][/,[ \t]*\z/]
|
159
|
+
to_haml_lines
|
125
160
|
end
|
126
161
|
|
127
|
-
def
|
128
|
-
|
129
|
-
|
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
|
-
|
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
|
data/lib/haml_lint/utils.rb
CHANGED
data/lib/haml_lint/version.rb
CHANGED
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.
|
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-
|
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
|