haml_lint 0.40.0 → 0.51.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/haml-lint +1 -1
- data/config/default.yml +9 -27
- data/config/forced_rubocop_config.yml +180 -0
- data/lib/haml_lint/adapter/haml_4.rb +20 -0
- data/lib/haml_lint/adapter/haml_5.rb +11 -0
- data/lib/haml_lint/adapter/haml_6.rb +59 -0
- data/lib/haml_lint/adapter.rb +2 -0
- data/lib/haml_lint/cli.rb +8 -3
- data/lib/haml_lint/configuration_loader.rb +49 -13
- data/lib/haml_lint/document.rb +89 -8
- data/lib/haml_lint/exceptions.rb +6 -0
- data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +35 -0
- data/lib/haml_lint/file_finder.rb +2 -2
- data/lib/haml_lint/lint.rb +10 -1
- data/lib/haml_lint/linter/final_newline.rb +4 -3
- data/lib/haml_lint/linter/implicit_div.rb +1 -1
- data/lib/haml_lint/linter/indentation.rb +3 -3
- data/lib/haml_lint/linter/no_placeholders.rb +18 -0
- data/lib/haml_lint/linter/repeated_id.rb +2 -1
- data/lib/haml_lint/linter/rubocop.rb +353 -59
- data/lib/haml_lint/linter/space_before_script.rb +8 -10
- data/lib/haml_lint/linter/trailing_empty_lines.rb +22 -0
- data/lib/haml_lint/linter/unnecessary_string_output.rb +1 -1
- data/lib/haml_lint/linter/view_length.rb +1 -1
- data/lib/haml_lint/linter.rb +60 -10
- data/lib/haml_lint/linter_registry.rb +3 -5
- data/lib/haml_lint/logger.rb +2 -2
- data/lib/haml_lint/options.rb +26 -2
- data/lib/haml_lint/rake_task.rb +2 -2
- data/lib/haml_lint/reporter/hash_reporter.rb +1 -3
- data/lib/haml_lint/reporter/offense_count_reporter.rb +1 -1
- data/lib/haml_lint/reporter/utils.rb +33 -4
- data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +24 -0
- data/lib/haml_lint/ruby_extraction/base_chunk.rb +114 -0
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +672 -0
- data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
- data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +34 -0
- data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +17 -0
- data/lib/haml_lint/ruby_extraction/interpolation_chunk.rb +26 -0
- data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +32 -0
- data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +40 -0
- data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +33 -0
- data/lib/haml_lint/ruby_extraction/ruby_source.rb +5 -0
- data/lib/haml_lint/ruby_extraction/script_chunk.rb +251 -0
- data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +63 -0
- data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
- data/lib/haml_lint/ruby_parser.rb +11 -1
- data/lib/haml_lint/runner.rb +35 -3
- data/lib/haml_lint/spec/matchers/report_lint.rb +22 -7
- data/lib/haml_lint/spec/normalize_indent.rb +2 -2
- data/lib/haml_lint/spec/shared_linter_context.rb +9 -1
- data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +158 -0
- data/lib/haml_lint/spec.rb +1 -0
- data/lib/haml_lint/tree/filter_node.rb +10 -0
- data/lib/haml_lint/tree/node.rb +13 -4
- data/lib/haml_lint/tree/script_node.rb +7 -1
- data/lib/haml_lint/tree/silent_script_node.rb +16 -1
- data/lib/haml_lint/tree/tag_node.rb +5 -9
- data/lib/haml_lint/utils.rb +135 -5
- data/lib/haml_lint/version.rb +1 -1
- data/lib/haml_lint/version_comparer.rb +25 -0
- data/lib/haml_lint.rb +12 -0
- metadata +29 -15
- data/lib/haml_lint/ruby_extractor.rb +0 -222
@@ -0,0 +1,672 @@
|
|
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
|
+
lines = extract_piped_plain_multilines(node.line - 1)
|
106
|
+
add_interpolation_chunks(node, lines.join("\n"), node.line - 1, indent: indent)
|
107
|
+
return
|
108
|
+
end
|
109
|
+
|
110
|
+
script_prefix = match[1]
|
111
|
+
_first_line_offset, lines = extract_raw_ruby_lines(node.script, node.line - 1)
|
112
|
+
# We want the actual indentation and prefix for the first line
|
113
|
+
first_line = lines[0] = @original_haml_lines[node.line - 1].rstrip
|
114
|
+
process_multiline!(first_line)
|
115
|
+
|
116
|
+
lines[0] = lines[0].sub(/(#{script_prefix}[ \t]?)/, '')
|
117
|
+
line_indentation = Regexp.last_match(1).size
|
118
|
+
|
119
|
+
raw_code = lines.join("\n")
|
120
|
+
|
121
|
+
if lines[0][/\S/] == '#'
|
122
|
+
# a "=" script that only contains a comment... No need for the "HL.out = " prefix,
|
123
|
+
# just treat it as comment which will turn into a "-" comment
|
124
|
+
else
|
125
|
+
lines[0] = HamlLint::Utils.insert_after_indentation(lines[0], script_output_prefix)
|
126
|
+
end
|
127
|
+
|
128
|
+
indent_delta = script_output_prefix.size - line_indentation
|
129
|
+
HamlLint::Utils.map_after_first!(lines) do |line|
|
130
|
+
HamlLint::Utils.indent(line, indent_delta)
|
131
|
+
end
|
132
|
+
|
133
|
+
prev_chunk = @ruby_chunks.last
|
134
|
+
if prev_chunk.is_a?(ScriptChunk) &&
|
135
|
+
prev_chunk.node.type == :script &&
|
136
|
+
prev_chunk.node == node.parent
|
137
|
+
# When an outputting script is nested under another outputting script,
|
138
|
+
# we want to block them from being merged together by rubocop, because
|
139
|
+
# this doesn't make sense in HAML.
|
140
|
+
# Example:
|
141
|
+
# = if this_is_short
|
142
|
+
# = this_is_short_too
|
143
|
+
# Could become (after RuboCop):
|
144
|
+
# HL.out = (HL.out = this_is_short_too if this_is_short)
|
145
|
+
# Or in (broken) HAML style:
|
146
|
+
# = this_is_short_too = if this_is_short
|
147
|
+
# By forcing this to start a chunk, there will be extra placeholders which
|
148
|
+
# blocks rubocop from merging the lines.
|
149
|
+
must_start_chunk = true
|
150
|
+
elsif script_prefix != '='
|
151
|
+
# In the few cases where &= and != are used to start the script,
|
152
|
+
# We need to remember and put it back in the final HAML. Fusing scripts together
|
153
|
+
# would make that basically impossible. Instead, a script has a "first_output_prefix"
|
154
|
+
# field for this specific case
|
155
|
+
must_start_chunk = true
|
156
|
+
end
|
157
|
+
|
158
|
+
finish_visit_any_script(node, lines, raw_code: raw_code, must_start_chunk: must_start_chunk,
|
159
|
+
first_output_prefix: script_prefix, &block)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Visit a script which doesn't output. Lines looking like ` - foo`
|
163
|
+
def visit_silent_script(node, &block)
|
164
|
+
_first_line_offset, lines = extract_raw_ruby_lines(node.script, node.line - 1)
|
165
|
+
# We want the actual indentation and prefix for the first line
|
166
|
+
first_line = lines[0] = @original_haml_lines[node.line - 1].rstrip
|
167
|
+
process_multiline!(first_line)
|
168
|
+
|
169
|
+
lines[0] = lines[0].sub(/(-[ \t]?)/, '')
|
170
|
+
nb_to_deindent = Regexp.last_match(1).size
|
171
|
+
|
172
|
+
HamlLint::Utils.map_after_first!(lines) do |line|
|
173
|
+
line.sub(/^ {1,#{nb_to_deindent}}/, '')
|
174
|
+
end
|
175
|
+
|
176
|
+
finish_visit_any_script(node, lines, &block)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Code common to both silent and outputting scripts
|
180
|
+
#
|
181
|
+
# raw_code is the code before we do transformations, such as adding the `HL.out = `
|
182
|
+
def finish_visit_any_script(node, lines, raw_code: nil, must_start_chunk: false, first_output_prefix: '=')
|
183
|
+
raw_code ||= lines.join("\n")
|
184
|
+
start_nesting = self.class.start_nesting_after?(raw_code)
|
185
|
+
|
186
|
+
lines = add_following_empty_lines(node, lines)
|
187
|
+
|
188
|
+
my_indent = lines.first.index(/\S/)
|
189
|
+
indent_after = indent_after_line_index(node.line - 1 + lines.size - 1) || 0
|
190
|
+
indent_after = [my_indent, indent_after].max
|
191
|
+
|
192
|
+
@ruby_chunks << ScriptChunk.new(node, lines,
|
193
|
+
end_marker_indent: indent_after,
|
194
|
+
must_start_chunk: must_start_chunk,
|
195
|
+
previous_chunk: @ruby_chunks.last,
|
196
|
+
first_output_haml_prefix: first_output_prefix)
|
197
|
+
|
198
|
+
yield
|
199
|
+
|
200
|
+
if start_nesting
|
201
|
+
if node.children.empty?
|
202
|
+
raise "Line #{node.line} should be followed by indentation. This might actually" \
|
203
|
+
" work in Haml, but it's almost a bug that it does. haml-lint cannot process."
|
204
|
+
end
|
205
|
+
|
206
|
+
last_child = node.children.last
|
207
|
+
if last_child.is_a?(HamlLint::Tree::SilentScriptNode) && last_child.keyword == 'end'
|
208
|
+
# This is allowed in Haml 5, gotta handle it!
|
209
|
+
# No need for the implicit end chunk since there is an explicit one
|
210
|
+
else
|
211
|
+
@ruby_chunks << ImplicitEndChunk.new(node, [' ' * my_indent + 'end'],
|
212
|
+
haml_line_index: @ruby_chunks.last.haml_end_line_index,
|
213
|
+
end_marker_indent: my_indent)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Visiting a tag. Lines looking like ` %div`
|
219
|
+
def visit_tag(node)
|
220
|
+
indent = @original_haml_lines[node.line - 1].index(/\S/)
|
221
|
+
|
222
|
+
# We don't want to use a block because assignments in a block are local to that block,
|
223
|
+
# so the semantics of the extracted ruby would be different from the one generated by
|
224
|
+
# Haml. Those differences can make some cops, such as UselessAssignment, have false
|
225
|
+
# positives
|
226
|
+
code = 'begin'
|
227
|
+
@ruby_chunks << AdHocChunk.new(node,
|
228
|
+
[' ' * indent + code])
|
229
|
+
indent += 2
|
230
|
+
|
231
|
+
tag_chunk = PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
|
232
|
+
@ruby_chunks << tag_chunk
|
233
|
+
|
234
|
+
current_line_index = visit_tag_attributes(node, indent: indent)
|
235
|
+
visit_tag_script(node, line_index: current_line_index, indent: indent)
|
236
|
+
|
237
|
+
yield
|
238
|
+
|
239
|
+
indent -= 2
|
240
|
+
|
241
|
+
if @ruby_chunks.last.equal?(tag_chunk)
|
242
|
+
# So there is nothing going "in" the tag, remove the wrapping "begin" and replace the PlaceholderMarkerChunk
|
243
|
+
# by one less indented
|
244
|
+
@ruby_chunks.pop
|
245
|
+
@ruby_chunks.pop
|
246
|
+
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
|
247
|
+
else
|
248
|
+
@ruby_chunks << AdHocChunk.new(node,
|
249
|
+
[' ' * indent + 'ensure', ' ' * indent + ' HL.noop', ' ' * indent + 'end'],
|
250
|
+
haml_line_index: @ruby_chunks.last.haml_end_line_index)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# (Called manually form visit_tag)
|
255
|
+
# Visiting the attributes of a tag. Lots of different examples below in the code.
|
256
|
+
# A common syntax is: `%div{style: 'yes_please'}`
|
257
|
+
#
|
258
|
+
# Returns the new line_index we reached, useful to handle the script that follows
|
259
|
+
def visit_tag_attributes(node, indent:)
|
260
|
+
final_line_index = node.line - 1
|
261
|
+
additional_attributes = node.dynamic_attributes_sources
|
262
|
+
|
263
|
+
attributes_code = additional_attributes.first
|
264
|
+
if !attributes_code && node.hash_attributes? && node.dynamic_attributes_sources.empty?
|
265
|
+
# No idea why .foo{:bar => 123} doesn't get here, but .foo{:bar => '123'} does...
|
266
|
+
# The code we get for the latter is {:bar => '123'}.
|
267
|
+
# We normalize it by removing the { } so that it matches wha we normally get
|
268
|
+
attributes_code = node.dynamic_attributes_source[:hash][1...-1]
|
269
|
+
end
|
270
|
+
|
271
|
+
if attributes_code&.start_with?('{')
|
272
|
+
# Looks like the .foo(bar = 123) case. Ignoring.
|
273
|
+
attributes_code = nil
|
274
|
+
end
|
275
|
+
|
276
|
+
return final_line_index unless attributes_code
|
277
|
+
# Attributes have different ways to be given to us:
|
278
|
+
# .foo{bar: 123} => "bar: 123"
|
279
|
+
# .foo{:bar => 123} => ":bar => 123"
|
280
|
+
# .foo{:bar => '123'} => "{:bar => '123'}" # No idea why this is different
|
281
|
+
# .foo(bar = 123) => '{"bar" => 123,}'
|
282
|
+
# .foo{html_attrs('fr-fr')} => html_attrs('fr-fr')
|
283
|
+
#
|
284
|
+
# The (bar = 123) case is extra painful to autocorrect (so is ignored up there).
|
285
|
+
# #raw_ruby_from_haml will "detect" this case by not finding the code.
|
286
|
+
#
|
287
|
+
# We wrap the result in a method to have a valid syntax for all 3 ways
|
288
|
+
# without having to differentiate them.
|
289
|
+
first_line_offset, raw_attributes_lines = extract_raw_tag_attributes_ruby_lines(attributes_code,
|
290
|
+
node.line - 1)
|
291
|
+
return final_line_index unless raw_attributes_lines
|
292
|
+
|
293
|
+
final_line_index += raw_attributes_lines.size - 1
|
294
|
+
|
295
|
+
# Since .foo{bar: 123} => "bar: 123" needs wrapping (Or it would be a syntax error) and
|
296
|
+
# .foo{html_attrs('fr-fr')} => html_attrs('fr-fr') doesn't care about being
|
297
|
+
# wrapped, we always wrap to place them to a similar offset to how they are in the haml.
|
298
|
+
wrap_by = first_line_offset - indent
|
299
|
+
if wrap_by < 2
|
300
|
+
# Need 2 minimum, for "W(". If we have less, we must indent everything for the difference
|
301
|
+
extra_indent = 2 - wrap_by
|
302
|
+
HamlLint::Utils.map_after_first!(raw_attributes_lines) do |line|
|
303
|
+
HamlLint::Utils.indent(line, extra_indent)
|
304
|
+
end
|
305
|
+
wrap_by = 2
|
306
|
+
end
|
307
|
+
raw_attributes_lines = wrap_lines(raw_attributes_lines, wrap_by)
|
308
|
+
raw_attributes_lines[0] = ' ' * indent + raw_attributes_lines[0]
|
309
|
+
|
310
|
+
@ruby_chunks << TagAttributesChunk.new(node, raw_attributes_lines,
|
311
|
+
end_marker_indent: indent,
|
312
|
+
indent_to_remove: extra_indent)
|
313
|
+
|
314
|
+
final_line_index
|
315
|
+
end
|
316
|
+
|
317
|
+
# Visiting the script besides tag. The part to the right of the equal sign of
|
318
|
+
# lines looking like ` %div= foo(bar)`
|
319
|
+
def visit_tag_script(node, line_index:, indent:)
|
320
|
+
return if node.script.nil? || node.script.empty?
|
321
|
+
# We ignore scripts which are just a comment
|
322
|
+
return if node.script[/\S/] == '#'
|
323
|
+
|
324
|
+
first_line_offset, script_lines = extract_raw_ruby_lines(node.script, line_index)
|
325
|
+
|
326
|
+
if script_lines.nil?
|
327
|
+
# This is a string with interpolation after a tag
|
328
|
+
# ex: %tag hello #{world}
|
329
|
+
# Sadly, the text with interpolation is escaped from the original, but this code
|
330
|
+
# needs the original.
|
331
|
+
|
332
|
+
interpolation_original = @document.unescape_interpolation_to_original_cache[node.script]
|
333
|
+
line_start_index = @original_haml_lines[node.line - 1].rindex(interpolation_original)
|
334
|
+
if line_start_index.nil?
|
335
|
+
raw_lines = extract_piped_plain_multilines(node.line - 1)
|
336
|
+
equivalent_haml_code = "#{raw_lines.first} #{raw_lines[1..].map(&:lstrip).join(' ')}"
|
337
|
+
line_start_index = equivalent_haml_code.rindex(interpolation_original)
|
338
|
+
|
339
|
+
interpolation_original = raw_lines.join("\n")
|
340
|
+
end
|
341
|
+
add_interpolation_chunks(node, interpolation_original, node.line - 1,
|
342
|
+
line_start_index: line_start_index, indent: indent)
|
343
|
+
else
|
344
|
+
script_lines[0] = "#{' ' * indent}#{script_output_prefix}#{script_lines[0]}"
|
345
|
+
indent_delta = script_output_prefix.size - first_line_offset + indent
|
346
|
+
HamlLint::Utils.map_after_first!(script_lines) do |line|
|
347
|
+
HamlLint::Utils.indent(line, indent_delta)
|
348
|
+
end
|
349
|
+
|
350
|
+
@ruby_chunks << TagScriptChunk.new(node, script_lines,
|
351
|
+
haml_line_index: line_index,
|
352
|
+
end_marker_indent: indent)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Visiting a HAML filter. Lines looking like ` :javascript` and the following lines
|
357
|
+
# that are nested.
|
358
|
+
def visit_filter(node)
|
359
|
+
# For unknown reasons, haml doesn't escape interpolations in filters.
|
360
|
+
# So we can rely on \n to split / get the number of lines.
|
361
|
+
filter_name_indent = @original_haml_lines[node.line - 1].index(/\S/)
|
362
|
+
if node.filter_type == 'ruby'
|
363
|
+
# The indentation in node.text is normalized, so that at least one line
|
364
|
+
# is indented by 0.
|
365
|
+
lines = node.text.split("\n")
|
366
|
+
lines.map! do |line|
|
367
|
+
if line !~ /\S/
|
368
|
+
# whitespace or empty
|
369
|
+
''
|
370
|
+
else
|
371
|
+
' ' * filter_name_indent + line
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
@ruby_chunks << RubyFilterChunk.new(node, lines,
|
376
|
+
haml_line_index: node.line, # it's one the next line, no need for -1
|
377
|
+
start_marker_indent: filter_name_indent,
|
378
|
+
end_marker_indent: filter_name_indent)
|
379
|
+
elsif node.text.include?('#')
|
380
|
+
name_indentation = ' ' * @original_haml_lines[node.line - 1].index(/\S/)
|
381
|
+
# TODO: HAML_LINT_FILTER could be in the string and mess things up
|
382
|
+
lines = ["#{name_indentation}#{script_output_prefix}<<~HAML_LINT_FILTER"]
|
383
|
+
lines.concat @original_haml_lines[node.line..(node.line + node.text.count("\n") - 1)]
|
384
|
+
lines << "#{name_indentation}HAML_LINT_FILTER"
|
385
|
+
@ruby_chunks << NonRubyFilterChunk.new(node, lines,
|
386
|
+
end_marker_indent: filter_name_indent)
|
387
|
+
# Those could be interpolation. We treat them as a here-doc, which is nice since we can
|
388
|
+
# keep the indentation as-is.
|
389
|
+
else
|
390
|
+
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'filter', indent: filter_name_indent,
|
391
|
+
nb_lines: 1 + node.text.count("\n"))
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Adds chunks for the interpolation within the given code
|
396
|
+
def add_interpolation_chunks(node, code, haml_line_index, indent:, line_start_index: 0)
|
397
|
+
HamlLint::Utils.handle_interpolation_with_indexes(code) do |scanner, line_index, line_char_index|
|
398
|
+
escapes = scanner[2].size
|
399
|
+
next if escapes.odd?
|
400
|
+
char = scanner[3] # '{', '@' or '$'
|
401
|
+
if Gem::Version.new(Haml::VERSION) >= Gem::Version.new('5') && (char != '{')
|
402
|
+
# Before Haml 5, scanner didn't have a scanner[3], it only handled `#{}`
|
403
|
+
next
|
404
|
+
end
|
405
|
+
|
406
|
+
line_start_char_index = line_char_index
|
407
|
+
line_start_char_index += line_start_index if line_index == 0
|
408
|
+
code_start_char_index = scanner.charpos
|
409
|
+
|
410
|
+
# This moves the scanner
|
411
|
+
Haml::Util.balance(scanner, '{', '}', 1)
|
412
|
+
|
413
|
+
# Need to manually get the code now that we have positions so that all whitespace is present,
|
414
|
+
# because Haml::Util.balance does a strip...
|
415
|
+
interpolated_code = code[code_start_char_index...scanner.charpos - 1]
|
416
|
+
|
417
|
+
if interpolated_code.include?("\n")
|
418
|
+
# We can't correct multiline interpolation.
|
419
|
+
# Finding meaningful code to generate and then transfer back is pretty complex
|
420
|
+
|
421
|
+
# Since we can't fix it, strip around the code to reduce RuboCop lints that we won't be able to fix.
|
422
|
+
interpolated_code = interpolated_code.strip
|
423
|
+
interpolated_code = "#{' ' * indent}#{script_output_prefix}#{interpolated_code}"
|
424
|
+
|
425
|
+
placeholder_code = interpolated_code.gsub(/\s*\n\s*/, ' ').rstrip
|
426
|
+
unless parse_ruby(placeholder_code)
|
427
|
+
placeholder_code = interpolated_code.gsub(/\s*\n\s*/, '; ').rstrip
|
428
|
+
end
|
429
|
+
@ruby_chunks << AdHocChunk.new(node, [placeholder_code],
|
430
|
+
haml_line_index: haml_line_index + line_index)
|
431
|
+
else
|
432
|
+
interpolated_code = "#{' ' * indent}#{script_output_prefix}#{interpolated_code}"
|
433
|
+
@ruby_chunks << InterpolationChunk.new(node, [interpolated_code],
|
434
|
+
haml_line_index: haml_line_index + line_index,
|
435
|
+
start_char_index: line_start_char_index,
|
436
|
+
end_marker_indent: indent)
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def process_multiline!(line)
|
442
|
+
if HAML_PARSER_INSTANCE.send(:is_multiline?, line)
|
443
|
+
line.chop!.rstrip!
|
444
|
+
true
|
445
|
+
else
|
446
|
+
false
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
def process_plain_multiline!(line)
|
451
|
+
if line&.end_with?(' |')
|
452
|
+
line[-2..] = ''
|
453
|
+
true
|
454
|
+
else
|
455
|
+
false
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
# Returns the raw lines from the haml for the given index.
|
460
|
+
# Multiple lines are returned when a line ends with a comma as that is the only
|
461
|
+
# time HAMLs allows Ruby lines to be split.
|
462
|
+
|
463
|
+
# Haml's line-splitting rules (allowed after comma in scripts and attributes) are handled
|
464
|
+
# at the parser level, so Haml doesn't provide the code as it is actually formatted in the Haml
|
465
|
+
# file. #raw_ruby_from_haml extracts the ruby code as it is exactly in the Haml file.
|
466
|
+
# The first and last lines may not be the complete lines from the Haml, only the Ruby parts
|
467
|
+
# and the indentation between the first and last list.
|
468
|
+
|
469
|
+
# HAML transforms the ruby code in many ways as it parses a document. Often removing lines and/or
|
470
|
+
# indentation. This is quite annoying for us since we want the exact layout of the code to analyze it.
|
471
|
+
#
|
472
|
+
# This function receives the code as haml provides it and the line where it starts. It returns
|
473
|
+
# the actual code as it is in the haml file, keeping breaks and indentation for the following lines.
|
474
|
+
# In addition, the start position of the code in the first line.
|
475
|
+
#
|
476
|
+
# The rules for handling multiline code in HAML are as follow:
|
477
|
+
# * if the line being processed ends with a space and a pipe, then append to the line (without
|
478
|
+
# newlines) every following lines that also end with a space and a pipe. This means the last line of
|
479
|
+
# the "block" also needs a pipe at the end.
|
480
|
+
# * after processing the pipes, when dealing with ruby code (and not in tag attributes' hash), if the line
|
481
|
+
# (which maybe span across multiple lines) ends with a comma, add the next line to the current piece of code.
|
482
|
+
#
|
483
|
+
# @return [first_line_offset, ruby_lines]
|
484
|
+
def extract_raw_ruby_lines(haml_processed_ruby_code, first_line_index)
|
485
|
+
haml_processed_ruby_code = haml_processed_ruby_code.strip
|
486
|
+
first_line = @original_haml_lines[first_line_index]
|
487
|
+
|
488
|
+
char_index = first_line.index(haml_processed_ruby_code)
|
489
|
+
|
490
|
+
if char_index
|
491
|
+
return [char_index, [haml_processed_ruby_code]]
|
492
|
+
end
|
493
|
+
|
494
|
+
cur_line_index = first_line_index
|
495
|
+
cur_line = first_line.rstrip
|
496
|
+
lines = []
|
497
|
+
|
498
|
+
# The pipes must also be on the last line of the multi-line section
|
499
|
+
while cur_line && process_multiline!(cur_line)
|
500
|
+
lines << cur_line
|
501
|
+
cur_line_index += 1
|
502
|
+
cur_line = @original_haml_lines[cur_line_index].rstrip
|
503
|
+
end
|
504
|
+
|
505
|
+
if lines.empty?
|
506
|
+
lines << cur_line
|
507
|
+
else
|
508
|
+
# The pipes must also be on the last line of the multi-line section. So cur_line is not the next line.
|
509
|
+
# We want to go back to check for commas
|
510
|
+
cur_line_index -= 1
|
511
|
+
cur_line = lines.last
|
512
|
+
end
|
513
|
+
|
514
|
+
while HAML_PARSER_INSTANCE.send(:is_ruby_multiline?, cur_line)
|
515
|
+
cur_line_index += 1
|
516
|
+
cur_line = @original_haml_lines[cur_line_index].rstrip
|
517
|
+
lines << cur_line
|
518
|
+
end
|
519
|
+
|
520
|
+
joined_lines = lines.join("\n")
|
521
|
+
|
522
|
+
if haml_processed_ruby_code.include?("\n")
|
523
|
+
haml_processed_ruby_code = haml_processed_ruby_code.gsub("\n", ' ')
|
524
|
+
end
|
525
|
+
|
526
|
+
haml_processed_ruby_code.split(/[, ]/)
|
527
|
+
|
528
|
+
regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/,\s*|\s+/), '(?:,\\s*|\\s+)')
|
529
|
+
|
530
|
+
match = joined_lines.match(regexp)
|
531
|
+
# This can happen when pipes are used as marker for multiline parts, and when tag attributes change lines
|
532
|
+
# without ending by a comma. This is quite a can of worm and is probably not too frequent, so for now,
|
533
|
+
# these cases are not supported.
|
534
|
+
return if match.nil?
|
535
|
+
|
536
|
+
raw_ruby = match[0]
|
537
|
+
ruby_lines = raw_ruby.split("\n")
|
538
|
+
first_line_offset = match.begin(0)
|
539
|
+
|
540
|
+
[first_line_offset, ruby_lines]
|
541
|
+
end
|
542
|
+
|
543
|
+
def extract_piped_plain_multilines(first_line_index)
|
544
|
+
lines = []
|
545
|
+
|
546
|
+
cur_line = @original_haml_lines[first_line_index].rstrip
|
547
|
+
cur_line_index = first_line_index
|
548
|
+
|
549
|
+
# The pipes must also be on the last line of the multi-line section
|
550
|
+
while cur_line && process_plain_multiline!(cur_line)
|
551
|
+
lines << cur_line
|
552
|
+
cur_line_index += 1
|
553
|
+
cur_line = @original_haml_lines[cur_line_index].rstrip
|
554
|
+
end
|
555
|
+
|
556
|
+
if lines.empty?
|
557
|
+
lines << cur_line
|
558
|
+
end
|
559
|
+
lines
|
560
|
+
end
|
561
|
+
|
562
|
+
# Tag attributes actually handle multiline differently than scripts.
|
563
|
+
# The basic system basically keeps considering more lines until it meets the closing braces, but still
|
564
|
+
# processes pipes too (same as extract_raw_ruby_lines).
|
565
|
+
def extract_raw_tag_attributes_ruby_lines(haml_processed_ruby_code, first_line_index)
|
566
|
+
haml_processed_ruby_code = haml_processed_ruby_code.strip
|
567
|
+
first_line = @original_haml_lines[first_line_index]
|
568
|
+
|
569
|
+
char_index = first_line.index(haml_processed_ruby_code)
|
570
|
+
|
571
|
+
if char_index
|
572
|
+
return [char_index, [haml_processed_ruby_code]]
|
573
|
+
end
|
574
|
+
|
575
|
+
min_non_white_chars_to_add = haml_processed_ruby_code.scan(/\S/).size
|
576
|
+
|
577
|
+
regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/\s+/), '\\s+')
|
578
|
+
|
579
|
+
joined_lines = first_line.rstrip
|
580
|
+
process_multiline!(joined_lines)
|
581
|
+
|
582
|
+
cur_line_index = first_line_index + 1
|
583
|
+
while @original_haml_lines[cur_line_index] && min_non_white_chars_to_add > 0
|
584
|
+
new_line = @original_haml_lines[cur_line_index].rstrip
|
585
|
+
process_multiline!(new_line)
|
586
|
+
|
587
|
+
min_non_white_chars_to_add -= new_line.scan(/\S/).size
|
588
|
+
joined_lines << "\n"
|
589
|
+
joined_lines << new_line
|
590
|
+
cur_line_index += 1
|
591
|
+
end
|
592
|
+
|
593
|
+
match = joined_lines.match(regexp)
|
594
|
+
|
595
|
+
return if match.nil?
|
596
|
+
|
597
|
+
first_line_offset = match.begin(0)
|
598
|
+
raw_ruby = match[0]
|
599
|
+
ruby_lines = raw_ruby.split("\n")
|
600
|
+
|
601
|
+
[first_line_offset, ruby_lines]
|
602
|
+
end
|
603
|
+
|
604
|
+
def wrap_lines(lines, wrap_depth)
|
605
|
+
lines = lines.dup
|
606
|
+
wrapping_prefix = 'W' * (wrap_depth - 1) + '('
|
607
|
+
lines[0] = wrapping_prefix + lines[0]
|
608
|
+
lines[-1] = lines[-1] + ')'
|
609
|
+
lines
|
610
|
+
end
|
611
|
+
|
612
|
+
# Adds empty lines that follow the lines (Used for scripts), so that
|
613
|
+
# RuboCop can receive them too. Some cops are sensitive to empty lines.
|
614
|
+
def add_following_empty_lines(node, lines)
|
615
|
+
first_line_index = node.line - 1 + lines.size
|
616
|
+
extra_lines = []
|
617
|
+
|
618
|
+
extra_lines << '' while HamlLint::Utils.is_blank_line?(@original_haml_lines[first_line_index + extra_lines.size])
|
619
|
+
|
620
|
+
if @original_haml_lines[first_line_index + extra_lines.size].nil?
|
621
|
+
# Since we reached the end of the document without finding content,
|
622
|
+
# then we don't add those lines.
|
623
|
+
return lines
|
624
|
+
end
|
625
|
+
|
626
|
+
lines + extra_lines
|
627
|
+
end
|
628
|
+
|
629
|
+
def parse_ruby(source)
|
630
|
+
@ruby_parser ||= HamlLint::RubyParser.new
|
631
|
+
@ruby_parser.parse(source)
|
632
|
+
end
|
633
|
+
|
634
|
+
def indent_after_line_index(line_index)
|
635
|
+
(line_index + 1..@original_haml_lines.size - 1).each do |i|
|
636
|
+
indent = @original_haml_lines[i].index(/\S/)
|
637
|
+
return indent if indent
|
638
|
+
end
|
639
|
+
nil
|
640
|
+
end
|
641
|
+
|
642
|
+
def self.start_nesting_after?(code)
|
643
|
+
anonymous_block?(code) || start_block_keyword?(code)
|
644
|
+
end
|
645
|
+
|
646
|
+
def self.anonymous_block?(code)
|
647
|
+
# Don't start with a comment and end with a `do`
|
648
|
+
# Definetly not perfect for the comment handling, but otherwise a more advanced parsing system is needed.
|
649
|
+
# Move the comment to its own line if it's annoying.
|
650
|
+
code !~ /\A\s*#/ &&
|
651
|
+
code =~ /\bdo\s*(\|[^|]*\|\s*)?(#.*)?\z/
|
652
|
+
end
|
653
|
+
|
654
|
+
START_BLOCK_KEYWORDS = %w[if unless case begin for until while].freeze
|
655
|
+
def self.start_block_keyword?(code)
|
656
|
+
START_BLOCK_KEYWORDS.include?(block_keyword(code))
|
657
|
+
end
|
658
|
+
|
659
|
+
LOOP_KEYWORDS = %w[for until while].freeze
|
660
|
+
def self.block_keyword(code)
|
661
|
+
# Need to handle 'for'/'while' since regex stolen from HAML parser doesn't
|
662
|
+
if (keyword = code[/\A\s*([^\s]+)\s+/, 1]) && LOOP_KEYWORDS.include?(keyword)
|
663
|
+
return keyword
|
664
|
+
end
|
665
|
+
|
666
|
+
return unless keyword = code.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0]
|
667
|
+
keyword[0] || keyword[1]
|
668
|
+
end
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
# rubocop:enable Metrics
|