haml_lint 0.44.0 → 0.46.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 +6 -28
- data/config/forced_rubocop_config.yml +156 -0
- data/lib/haml_lint/adapter/haml_4.rb +18 -0
- data/lib/haml_lint/adapter/haml_5.rb +11 -0
- data/lib/haml_lint/adapter/haml_6.rb +11 -0
- data/lib/haml_lint/cli.rb +8 -3
- data/lib/haml_lint/configuration_loader.rb +13 -12
- 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/rubocop.rb +351 -59
- data/lib/haml_lint/linter/space_before_script.rb +8 -10
- 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 +56 -9
- 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 +20 -0
- data/lib/haml_lint/ruby_extraction/base_chunk.rb +113 -0
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +504 -0
- data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
- data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +54 -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 +132 -0
- data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +39 -0
- data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
- data/lib/haml_lint/ruby_extractor.rb +11 -10
- 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 +143 -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/tag_node.rb +5 -9
- data/lib/haml_lint/utils.rb +130 -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 +25 -6
@@ -0,0 +1,504 @@
|
|
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
|
+
def initialize(document, script_output_prefix:)
|
15
|
+
@document = document
|
16
|
+
@script_output_prefix = script_output_prefix
|
17
|
+
end
|
18
|
+
|
19
|
+
def extract
|
20
|
+
raise 'Already extracted' if @ruby_chunks
|
21
|
+
|
22
|
+
@ruby_chunks = []
|
23
|
+
@original_haml_lines = @document.source_lines
|
24
|
+
|
25
|
+
visit(@document.tree)
|
26
|
+
@ruby_chunks
|
27
|
+
end
|
28
|
+
|
29
|
+
def visit_root(_node)
|
30
|
+
yield # Collect lines of code from children
|
31
|
+
end
|
32
|
+
|
33
|
+
# Visiting lines like ` Some raw text to output`
|
34
|
+
def visit_plain(node)
|
35
|
+
indent = @original_haml_lines[node.line - 1].index(/\S/)
|
36
|
+
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'plain', indent: indent)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Visiting lines like ` -# Some commenting!`
|
40
|
+
def visit_haml_comment(node)
|
41
|
+
# We want to preserve leading whitespace if it exists, but add a leading
|
42
|
+
# whitespace if it doesn't exist so that RuboCop's LeadingCommentSpace
|
43
|
+
# doesn't complain
|
44
|
+
line_index = node.line - 1
|
45
|
+
lines = @original_haml_lines[line_index..(line_index + node.text.count("\n"))].dup
|
46
|
+
indent = lines.first.index(/\S/)
|
47
|
+
# Remove only the -, the # will align with regular code
|
48
|
+
# -# comment
|
49
|
+
# - foo()
|
50
|
+
# becomes
|
51
|
+
# # comment
|
52
|
+
# foo()
|
53
|
+
lines[0] = lines[0].sub('-', '')
|
54
|
+
|
55
|
+
# Adding a space before the comment if its missing
|
56
|
+
# We can't fix those, so make sure not to generate warnings for them.
|
57
|
+
lines[0] = lines[0].sub(/\A(\s*)#(\S)/, '\\1# \\2')
|
58
|
+
|
59
|
+
HamlLint::Utils.map_after_first!(lines) do |line|
|
60
|
+
# Since the indent/spaces of the extra line comments isn't exactly in the haml,
|
61
|
+
# it's not RuboCop's job to fix indentation, so just make a reasonable indentation
|
62
|
+
# to avoid offenses.
|
63
|
+
' ' * indent + line.sub(/^\s*/, '# ').rstrip
|
64
|
+
end
|
65
|
+
|
66
|
+
# Using Placeholder instead of script because we can't revert back to the
|
67
|
+
# exact original comment since multiple syntax lead to the exact same comment.
|
68
|
+
@ruby_chunks << HamlCommentChunk.new(node, lines, end_marker_indent: indent)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Visiting comments which are output to HTML. Lines looking like
|
72
|
+
# ` / This will be in the HTML source!`
|
73
|
+
def visit_comment(node)
|
74
|
+
lines = raw_lines_of_interest(node.line - 1)
|
75
|
+
indent = lines.first.index(/\S/)
|
76
|
+
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'comment', indent: indent)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Visit a script which outputs. Lines looking like ` = foo`
|
80
|
+
def visit_script(node, &block)
|
81
|
+
lines = raw_lines_of_interest(node.line - 1)
|
82
|
+
|
83
|
+
if lines.first !~ /\A\s*[-=]/
|
84
|
+
# The line doesn't start with a - or a =, this is actually a "plain"
|
85
|
+
# that contains interpolation.
|
86
|
+
indent = lines.first.index(/\S/)
|
87
|
+
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'interpolation', indent: indent)
|
88
|
+
add_interpolation_chunks(node, lines.first, node.line - 1, indent: indent)
|
89
|
+
return
|
90
|
+
end
|
91
|
+
|
92
|
+
lines[0] = lines[0].sub(/(=[ \t]?)/, '')
|
93
|
+
line_indentation = Regexp.last_match(1).size
|
94
|
+
|
95
|
+
raw_code = lines.join("\n")
|
96
|
+
|
97
|
+
if lines[0][/\S/] == '#'
|
98
|
+
# a script that only constains a comment... needs special handling
|
99
|
+
comment_index = lines[0].index(/\S/)
|
100
|
+
lines[0].insert(comment_index + 1, " #{script_output_prefix.rstrip}")
|
101
|
+
else
|
102
|
+
lines[0] = HamlLint::Utils.insert_after_indentation(lines[0], script_output_prefix)
|
103
|
+
end
|
104
|
+
|
105
|
+
indent_delta = script_output_prefix.size - line_indentation
|
106
|
+
HamlLint::Utils.map_after_first!(lines) do |line|
|
107
|
+
HamlLint::Utils.indent(line, indent_delta)
|
108
|
+
end
|
109
|
+
|
110
|
+
prev_chunk = @ruby_chunks.last
|
111
|
+
if prev_chunk.is_a?(ScriptChunk) &&
|
112
|
+
prev_chunk.node.type == :script &&
|
113
|
+
prev_chunk.node == node.parent
|
114
|
+
# When an outputting script is nested under another outputting script,
|
115
|
+
# we want to block them from being merged together by rubocop, because
|
116
|
+
# this doesn't make sense in HAML.
|
117
|
+
# Example:
|
118
|
+
# = if this_is_short
|
119
|
+
# = this_is_short_too
|
120
|
+
# Could become (after RuboCop):
|
121
|
+
# HL.out = (HL.out = this_is_short_too if this_is_short)
|
122
|
+
# Or in (broken) HAML style:
|
123
|
+
# = this_is_short_too = if this_is_short
|
124
|
+
# By forcing this to start a chunk, there will be extra placeholders which
|
125
|
+
# blocks rubocop from merging the lines.
|
126
|
+
must_start_chunk = true
|
127
|
+
end
|
128
|
+
|
129
|
+
finish_visit_any_script(node, lines, raw_code: raw_code, must_start_chunk: must_start_chunk, &block)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Visit a script which doesn't output. Lines looking like ` - foo`
|
133
|
+
def visit_silent_script(node, &block)
|
134
|
+
lines = raw_lines_of_interest(node.line - 1)
|
135
|
+
lines[0] = lines[0].sub(/(-[ \t]?)/, '')
|
136
|
+
nb_to_deindent = Regexp.last_match(1).size
|
137
|
+
|
138
|
+
HamlLint::Utils.map_after_first!(lines) do |line|
|
139
|
+
line.sub(/^ {1,#{nb_to_deindent}}/, '')
|
140
|
+
end
|
141
|
+
|
142
|
+
finish_visit_any_script(node, lines, &block)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Code common to both silent and outputting scripts
|
146
|
+
#
|
147
|
+
# 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)
|
149
|
+
raw_code ||= lines.join("\n")
|
150
|
+
start_nesting = self.class.start_nesting_after?(raw_code)
|
151
|
+
|
152
|
+
lines = add_following_empty_lines(node, lines)
|
153
|
+
|
154
|
+
my_indent = lines.first.index(/\S/)
|
155
|
+
indent_after = indent_after_line_index(node.line - 1 + lines.size - 1) || 0
|
156
|
+
indent_after = [my_indent, indent_after].max
|
157
|
+
|
158
|
+
@ruby_chunks << ScriptChunk.new(node, lines,
|
159
|
+
end_marker_indent: indent_after,
|
160
|
+
must_start_chunk: must_start_chunk,
|
161
|
+
previous_chunk: @ruby_chunks.last)
|
162
|
+
|
163
|
+
yield
|
164
|
+
|
165
|
+
if start_nesting
|
166
|
+
if node.children.empty?
|
167
|
+
raise "Line #{node.line} should be followed by indentation. This might actually" \
|
168
|
+
" work in Haml, but it's almost a bug that it does. haml-lint cannot process."
|
169
|
+
end
|
170
|
+
|
171
|
+
last_child = node.children.last
|
172
|
+
if last_child.is_a?(HamlLint::Tree::SilentScriptNode) && last_child.keyword == 'end'
|
173
|
+
# This is allowed in Haml 5, gotta handle it!
|
174
|
+
# No need for the implicit end chunk since there is an explicit one
|
175
|
+
else
|
176
|
+
@ruby_chunks << ImplicitEndChunk.new(node, [' ' * my_indent + 'end'],
|
177
|
+
haml_line_index: @ruby_chunks.last.haml_end_line_index,
|
178
|
+
end_marker_indent: my_indent)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Visiting a tag. Lines looking like ` %div`
|
184
|
+
def visit_tag(node)
|
185
|
+
indent = @original_haml_lines[node.line - 1].index(/\S/)
|
186
|
+
|
187
|
+
has_children = !node.children.empty?
|
188
|
+
if has_children
|
189
|
+
# We don't want to use a block because assignments in a block are local to that block,
|
190
|
+
# so the semantics of the extracted ruby would be different from the one generated by
|
191
|
+
# Haml. Those differences can make some cops, such as UselessAssignment, have false
|
192
|
+
# positives
|
193
|
+
code = 'begin # rubocop:disable Style/RedundantBegin,Lint/RedundantCopDisableDirective'
|
194
|
+
@ruby_chunks << AdHocChunk.new(node,
|
195
|
+
[' ' * indent + code])
|
196
|
+
indent += 2
|
197
|
+
end
|
198
|
+
|
199
|
+
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
|
200
|
+
|
201
|
+
current_line_index = visit_tag_attributes(node, indent: indent)
|
202
|
+
visit_tag_script(node, line_index: current_line_index, indent: indent)
|
203
|
+
|
204
|
+
if has_children
|
205
|
+
yield
|
206
|
+
indent -= 2
|
207
|
+
@ruby_chunks << AdHocChunk.new(node, [' ' * indent + 'end'],
|
208
|
+
haml_line_index: @ruby_chunks.last.haml_end_line_index)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# (Called manually form visit_tag)
|
213
|
+
# Visiting the attributes of a tag. Lots of different examples below in the code.
|
214
|
+
# A common syntax is: `%div{style: 'yes_please'}`
|
215
|
+
#
|
216
|
+
# Returns the new line_index we reached, useful to handle the script that follows
|
217
|
+
def visit_tag_attributes(node, indent:)
|
218
|
+
final_line_index = node.line - 1
|
219
|
+
additional_attributes = node.dynamic_attributes_sources
|
220
|
+
|
221
|
+
attributes_code = additional_attributes.first
|
222
|
+
if !attributes_code && node.hash_attributes? && node.dynamic_attributes_sources.empty?
|
223
|
+
# No idea why .foo{:bar => 123} doesn't get here, but .foo{:bar => '123'} does...
|
224
|
+
# The code we get for the later is {:bar => '123'}.
|
225
|
+
# We normalize it by removing the { } so that it matches wha we normally get
|
226
|
+
attributes_code = node.dynamic_attributes_source[:hash][1...-1]
|
227
|
+
end
|
228
|
+
|
229
|
+
return final_line_index unless attributes_code
|
230
|
+
# Attributes have different ways to be given to us:
|
231
|
+
# .foo{bar: 123} => "bar: 123"
|
232
|
+
# .foo{:bar => 123} => ":bar => 123"
|
233
|
+
# .foo{:bar => '123'} => "{:bar => '123'}" # No idea why this is different
|
234
|
+
# .foo(bar = 123) => '{"bar" => 123,}'
|
235
|
+
# .foo{html_attrs('fr-fr')} => html_attrs('fr-fr')
|
236
|
+
#
|
237
|
+
# The (bar = 123) case is extra painful to autocorrect (so is ignored).
|
238
|
+
# #raw_ruby_from_haml will "detect" this case by not finding the code.
|
239
|
+
#
|
240
|
+
# We wrap the result in a method to have a valid syntax for all 3 ways
|
241
|
+
# without having to differentiate them.
|
242
|
+
first_line_offset, raw_attributes_lines = raw_ruby_lines_from_haml(attributes_code,
|
243
|
+
node.line - 1)
|
244
|
+
|
245
|
+
return final_line_index unless raw_attributes_lines
|
246
|
+
|
247
|
+
final_line_index += raw_attributes_lines.size - 1
|
248
|
+
|
249
|
+
# Since .foo{bar: 123} => "bar: 123" needs wrapping (Or it would be a syntax error) and
|
250
|
+
# .foo{html_attrs('fr-fr')} => html_attrs('fr-fr') doesn't care about being
|
251
|
+
# wrapped, we always wrap to place them to a similar offset to how they are in the haml.
|
252
|
+
wrap_by = first_line_offset - indent
|
253
|
+
if wrap_by < 2
|
254
|
+
# Need 2 minimum, for "W(". If we have less, we must indent everything for the difference
|
255
|
+
extra_indent = 2 - wrap_by
|
256
|
+
HamlLint::Utils.map_after_first!(raw_attributes_lines) do |line|
|
257
|
+
HamlLint::Utils.indent(line, extra_indent)
|
258
|
+
end
|
259
|
+
wrap_by = 2
|
260
|
+
end
|
261
|
+
raw_attributes_lines = wrap_lines(raw_attributes_lines, wrap_by)
|
262
|
+
raw_attributes_lines[0] = ' ' * indent + raw_attributes_lines[0]
|
263
|
+
|
264
|
+
@ruby_chunks << TagAttributesChunk.new(node, raw_attributes_lines,
|
265
|
+
end_marker_indent: indent,
|
266
|
+
indent_to_remove: extra_indent)
|
267
|
+
|
268
|
+
final_line_index
|
269
|
+
end
|
270
|
+
|
271
|
+
# Visiting the script besides tag. The part to the right of the equal sign of
|
272
|
+
# lines looking like ` %div= foo(bar)`
|
273
|
+
def visit_tag_script(node, line_index:, indent:)
|
274
|
+
return if node.script.nil? || node.script.empty?
|
275
|
+
# We ignore scripts which are just a comment
|
276
|
+
return if node.script[/\S/] == '#'
|
277
|
+
|
278
|
+
first_line_offset, script_lines = raw_ruby_lines_from_haml(node.script, line_index)
|
279
|
+
|
280
|
+
if script_lines.nil?
|
281
|
+
# This is a string with interpolation after a tag
|
282
|
+
# ex: %tag hello #{world}
|
283
|
+
# Sadly, the text with interpolation is escaped from the original, but this code
|
284
|
+
# needs the original.
|
285
|
+
interpolation_original = @document.unescape_interpolation_to_original_cache[node.script]
|
286
|
+
|
287
|
+
line_start_index = @original_haml_lines[node.line - 1].rindex(interpolation_original)
|
288
|
+
add_interpolation_chunks(node, interpolation_original, node.line - 1,
|
289
|
+
line_start_index: line_start_index, indent: indent)
|
290
|
+
else
|
291
|
+
script_lines[0] = "#{' ' * indent}#{script_output_prefix}#{script_lines[0]}"
|
292
|
+
indent_delta = script_output_prefix.size - first_line_offset + indent
|
293
|
+
HamlLint::Utils.map_after_first!(script_lines) do |line|
|
294
|
+
HamlLint::Utils.indent(line, indent_delta)
|
295
|
+
end
|
296
|
+
|
297
|
+
@ruby_chunks << TagScriptChunk.new(node, script_lines,
|
298
|
+
haml_line_index: line_index,
|
299
|
+
end_marker_indent: indent)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Visiting a HAML filter. Lines looking like ` :javascript` and the following lines
|
304
|
+
# that are nested.
|
305
|
+
def visit_filter(node)
|
306
|
+
# For unknown reasons, haml doesn't escape interpolations in filters.
|
307
|
+
# So we can rely on \n to split / get the number of lines.
|
308
|
+
filter_name_indent = @original_haml_lines[node.line - 1].index(/\S/)
|
309
|
+
if node.filter_type == 'ruby'
|
310
|
+
# The indentation in node.text is normalized, so that at least one line
|
311
|
+
# is indented by 0.
|
312
|
+
lines = node.text.split("\n")
|
313
|
+
lines.map! do |line|
|
314
|
+
if line !~ /\S/
|
315
|
+
# whitespace or empty
|
316
|
+
''
|
317
|
+
else
|
318
|
+
' ' * filter_name_indent + line
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
@ruby_chunks << RubyFilterChunk.new(node, lines,
|
323
|
+
haml_line_index: node.line, # it's one the next line, no need for -1
|
324
|
+
start_marker_indent: filter_name_indent,
|
325
|
+
end_marker_indent: filter_name_indent)
|
326
|
+
elsif node.text.include?('#')
|
327
|
+
name_indentation = ' ' * @original_haml_lines[node.line - 1].index(/\S/)
|
328
|
+
# TODO: HAML_LINT_FILTER could be in the string and mess things up
|
329
|
+
lines = ["#{name_indentation}#{script_output_prefix}<<~HAML_LINT_FILTER"]
|
330
|
+
lines.concat @original_haml_lines[node.line..(node.line + node.text.count("\n") - 1)]
|
331
|
+
lines << "#{name_indentation}HAML_LINT_FILTER"
|
332
|
+
@ruby_chunks << NonRubyFilterChunk.new(node, lines,
|
333
|
+
end_marker_indent: filter_name_indent)
|
334
|
+
# Those could be interpolation. We treat them as a here-doc, which is nice since we can
|
335
|
+
# keep the indentation as-is.
|
336
|
+
else
|
337
|
+
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'filter', indent: filter_name_indent,
|
338
|
+
nb_lines: 1 + node.text.count("\n"))
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# Adds chunks for the interpolation within the given code
|
343
|
+
def add_interpolation_chunks(node, code, haml_line_index, indent:, line_start_index: 0)
|
344
|
+
HamlLint::Utils.handle_interpolation_with_indexes(code) do |scanner, line_index, char_index|
|
345
|
+
escapes = scanner[2].size
|
346
|
+
next if escapes.odd?
|
347
|
+
char = scanner[3] # '{', '@' or '$'
|
348
|
+
if Gem::Version.new(Haml::VERSION) >= Gem::Version.new('5') && (char != '{')
|
349
|
+
# Before Haml 5, scanner didn't have a scanner[3], it only handled `#{}`
|
350
|
+
next
|
351
|
+
end
|
352
|
+
|
353
|
+
start_char_index = char_index
|
354
|
+
start_char_index += line_start_index if line_index == 0
|
355
|
+
|
356
|
+
Haml::Util.balance(scanner, '{', '}', 1)[0][0...-1]
|
357
|
+
|
358
|
+
# Need to manually get the code now that we have positions so that all whitespace is present,
|
359
|
+
# because Haml::Util.balance does a strip...
|
360
|
+
interpolated_code = code[char_index...scanner.charpos - 1]
|
361
|
+
|
362
|
+
interpolated_code = "#{' ' * indent}#{script_output_prefix}#{interpolated_code}"
|
363
|
+
|
364
|
+
if interpolated_code.include?("\n")
|
365
|
+
# We can't correct multiline interpolation.
|
366
|
+
# Finding meaningful code to generate and then transfer back is pretty complex
|
367
|
+
placeholder_code = interpolated_code.gsub(/\s*\n\s*/, ' ').rstrip
|
368
|
+
unless parse_ruby(placeholder_code)
|
369
|
+
placeholder_code = interpolated_code.gsub(/\s*\n\s*/, '; ').rstrip
|
370
|
+
end
|
371
|
+
@ruby_chunks << AdHocChunk.new(node, [placeholder_code],
|
372
|
+
haml_line_index: haml_line_index + line_index)
|
373
|
+
else
|
374
|
+
@ruby_chunks << InterpolationChunk.new(node, [interpolated_code],
|
375
|
+
haml_line_index: haml_line_index + line_index,
|
376
|
+
start_char_index: start_char_index,
|
377
|
+
end_marker_indent: indent)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# Returns the raw lines from the haml for the given index.
|
383
|
+
# Multiple lines are returned when a line ends with a comma as that is the only
|
384
|
+
# time HAMLs allows Ruby lines to be split.
|
385
|
+
def raw_lines_of_interest(first_line_index)
|
386
|
+
line_index = first_line_index
|
387
|
+
lines_of_interest = [@original_haml_lines[line_index]]
|
388
|
+
|
389
|
+
while @original_haml_lines[line_index].rstrip.end_with?(',')
|
390
|
+
line_index += 1
|
391
|
+
lines_of_interest << @original_haml_lines[line_index]
|
392
|
+
end
|
393
|
+
|
394
|
+
lines_of_interest
|
395
|
+
end
|
396
|
+
|
397
|
+
# Haml's line-splitting rules (allowed after comma in scripts and attributes) are handled
|
398
|
+
# at the parser level, so Haml doesn't provide the code as it is actually formatted in the Haml
|
399
|
+
# file. #raw_ruby_from_haml extracts the ruby code as it is exactly in the Haml file.
|
400
|
+
# The first and last lines may not be the complete lines from the Haml, only the Ruby parts
|
401
|
+
# and the indentation between the first and last list.
|
402
|
+
def raw_ruby_lines_from_haml(code, first_line_index)
|
403
|
+
stripped_code = code.strip
|
404
|
+
return if stripped_code.empty?
|
405
|
+
|
406
|
+
lines_of_interest = raw_lines_of_interest(first_line_index)
|
407
|
+
|
408
|
+
if lines_of_interest.size == 1
|
409
|
+
index = lines_of_interest.first.index(stripped_code)
|
410
|
+
if lines_of_interest.first.include?(stripped_code)
|
411
|
+
return [index, [stripped_code]]
|
412
|
+
else
|
413
|
+
# Sometimes, the code just isn't in the Haml when Haml does transformations to it
|
414
|
+
return
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
raw_haml = lines_of_interest.join("\n")
|
419
|
+
|
420
|
+
# Need the gsub because while multiline scripts are turned into a single line,
|
421
|
+
# by haml, multiline tag attributes are not.
|
422
|
+
code_parts = stripped_code.gsub("\n", ' ').split(/,\s*/)
|
423
|
+
|
424
|
+
regexp_code = code_parts.map { |c| Regexp.quote(c) }.join(',\\s*')
|
425
|
+
regexp = Regexp.new(regexp_code)
|
426
|
+
|
427
|
+
match = raw_haml.match(regexp)
|
428
|
+
|
429
|
+
raw_ruby = match[0]
|
430
|
+
ruby_lines = raw_ruby.split("\n")
|
431
|
+
first_line_offset = match.begin(0)
|
432
|
+
|
433
|
+
[first_line_offset, ruby_lines]
|
434
|
+
end
|
435
|
+
|
436
|
+
def wrap_lines(lines, wrap_depth)
|
437
|
+
lines = lines.dup
|
438
|
+
wrapping_prefix = 'W' * (wrap_depth - 1) + '('
|
439
|
+
lines[0] = wrapping_prefix + lines[0]
|
440
|
+
lines[-1] = lines[-1] + ')'
|
441
|
+
lines
|
442
|
+
end
|
443
|
+
|
444
|
+
# Adds empty lines that follow the lines (Used for scripts), so that
|
445
|
+
# RuboCop can receive them too. Some cops are sensitive to empty lines.
|
446
|
+
def add_following_empty_lines(node, lines)
|
447
|
+
first_line_index = node.line - 1 + lines.size
|
448
|
+
extra_lines = []
|
449
|
+
|
450
|
+
extra_lines << '' while HamlLint::Utils.is_blank_line?(@original_haml_lines[first_line_index + extra_lines.size])
|
451
|
+
|
452
|
+
if @original_haml_lines[first_line_index + extra_lines.size].nil?
|
453
|
+
# Since we reached the end of the document without finding content,
|
454
|
+
# then we don't add those lines.
|
455
|
+
return lines
|
456
|
+
end
|
457
|
+
|
458
|
+
lines + extra_lines
|
459
|
+
end
|
460
|
+
|
461
|
+
def parse_ruby(source)
|
462
|
+
@ruby_parser ||= HamlLint::RubyParser.new
|
463
|
+
@ruby_parser.parse(source)
|
464
|
+
end
|
465
|
+
|
466
|
+
def indent_after_line_index(line_index)
|
467
|
+
(line_index + 1..@original_haml_lines.size - 1).each do |i|
|
468
|
+
indent = @original_haml_lines[i].index(/\S/)
|
469
|
+
return indent if indent
|
470
|
+
end
|
471
|
+
nil
|
472
|
+
end
|
473
|
+
|
474
|
+
def self.start_nesting_after?(code)
|
475
|
+
anonymous_block?(code) || start_block_keyword?(code)
|
476
|
+
end
|
477
|
+
|
478
|
+
def self.anonymous_block?(code)
|
479
|
+
# Don't start with a comment and end with a `do`
|
480
|
+
# Definetly not perfect for the comment handling, but otherwise a more advanced parsing system is needed.
|
481
|
+
# Move the comment to its own line if it's annoying.
|
482
|
+
code !~ /\A\s*#/ &&
|
483
|
+
code =~ /\bdo\s*(\|[^|]*\|\s*)?(#.*)?\z/
|
484
|
+
end
|
485
|
+
|
486
|
+
START_BLOCK_KEYWORDS = %w[if unless case begin for until while].freeze
|
487
|
+
def self.start_block_keyword?(code)
|
488
|
+
START_BLOCK_KEYWORDS.include?(block_keyword(code))
|
489
|
+
end
|
490
|
+
|
491
|
+
LOOP_KEYWORDS = %w[for until while].freeze
|
492
|
+
def self.block_keyword(code)
|
493
|
+
# Need to handle 'for'/'while' since regex stolen from HAML parser doesn't
|
494
|
+
if (keyword = code[/\A\s*([^\s]+)\s+/, 1]) && LOOP_KEYWORDS.include?(keyword)
|
495
|
+
return keyword
|
496
|
+
end
|
497
|
+
|
498
|
+
return unless keyword = code.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0]
|
499
|
+
keyword[0] || keyword[1]
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
# rubocop:enable Metrics
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HamlLint::RubyExtraction
|
4
|
+
class UnableToTransferCorrections < StandardError; end
|
5
|
+
|
6
|
+
# Coordinates the entire RubyExtraction system.
|
7
|
+
#
|
8
|
+
# * Uses the extractor to generate chunks.
|
9
|
+
# * Preprocess the chunks to cleanup/fuse some of them.
|
10
|
+
# * Generates the extracted ruby code from the Chunks.
|
11
|
+
# * Handles the markers (see below)
|
12
|
+
# * Use the chunks to transfer corrections from corrected Ruby code back to HAML
|
13
|
+
#
|
14
|
+
# The generated Ruby code uses markers to wrap around the Ruby code from the chunks.
|
15
|
+
# Those markers look like function calls, like: `haml_lint_marker_1`, so are valid ruby.
|
16
|
+
# After RuboCop does it's auto-correction, the markers are used to find the pieces of the
|
17
|
+
# corrected Ruby code that correspond to each Chunk.
|
18
|
+
class Coordinator
|
19
|
+
# @return [String] The prefix used for to handle `= foo` script's in the extracted Ruby code.
|
20
|
+
attr_reader :script_output_prefix
|
21
|
+
|
22
|
+
# @return [String] The prefix used for markers in the Ruby code
|
23
|
+
attr_reader :marker_prefix
|
24
|
+
|
25
|
+
# @return [Array<String>] The ruby lines after extraction from HAML (before RuboCop)
|
26
|
+
attr_reader :assembled_ruby_lines
|
27
|
+
|
28
|
+
# @return [Array<String>] The ruby lines after correction by RuboCop
|
29
|
+
attr_reader :corrected_ruby_lines
|
30
|
+
|
31
|
+
def initialize(document)
|
32
|
+
@document = document
|
33
|
+
@ruby_chunks = nil
|
34
|
+
@assembled_ruby_lines = nil
|
35
|
+
@corrected_ruby_lines = nil
|
36
|
+
@source_map = {}
|
37
|
+
@script_output_prefix = nil
|
38
|
+
|
39
|
+
@haml_lines = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def extract_ruby_source
|
43
|
+
return @ruby_source if @ruby_source
|
44
|
+
|
45
|
+
pick_a_marker_prefix
|
46
|
+
pick_a_script_output_prefix
|
47
|
+
|
48
|
+
@ruby_chunks = HamlLint::RubyExtraction::ChunkExtractor.new(@document,
|
49
|
+
script_output_prefix: @script_output_prefix).extract
|
50
|
+
preprocess_chunks
|
51
|
+
|
52
|
+
@assembled_ruby_lines = []
|
53
|
+
@ruby_chunks.each do |ruby_chunk|
|
54
|
+
ruby_chunk.full_assemble(self)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Making sure the generated source has a final newline
|
58
|
+
@assembled_ruby_lines << '' if @assembled_ruby_lines.last && !@assembled_ruby_lines.last.empty?
|
59
|
+
|
60
|
+
@ruby_source = RubySource.new(@assembled_ruby_lines.join("\n"), @source_map, @ruby_chunks)
|
61
|
+
end
|
62
|
+
|
63
|
+
def preprocess_chunks
|
64
|
+
return if @ruby_chunks.size < 2
|
65
|
+
|
66
|
+
new_chunks = [@ruby_chunks.first]
|
67
|
+
@ruby_chunks[1..].each do |ruby_chunk|
|
68
|
+
fused_chunk = new_chunks.last.fuse(ruby_chunk)
|
69
|
+
if fused_chunk
|
70
|
+
new_chunks[-1] = fused_chunk
|
71
|
+
else
|
72
|
+
new_chunks << ruby_chunk
|
73
|
+
end
|
74
|
+
end
|
75
|
+
@ruby_chunks = new_chunks
|
76
|
+
end
|
77
|
+
|
78
|
+
def haml_lines_with_corrections_applied(corrected_ruby_source)
|
79
|
+
@corrected_ruby_lines = corrected_ruby_source.split("\n")
|
80
|
+
|
81
|
+
@haml_lines = @document.source_lines.dup
|
82
|
+
|
83
|
+
if markers_conflict?(@assembled_ruby_lines, @corrected_ruby_lines)
|
84
|
+
raise UnableToTransferCorrections, 'The changes in the corrected ruby are not supported'
|
85
|
+
end
|
86
|
+
|
87
|
+
finished_with_empty_line = @haml_lines.last.empty?
|
88
|
+
|
89
|
+
# Going in reverse order, so that if we change the number of lines then the
|
90
|
+
# rest of the file will not be offset, which would make things harder
|
91
|
+
@ruby_chunks.reverse_each do |ruby_chunk|
|
92
|
+
ruby_chunk.transfer_correction(self, @corrected_ruby_lines, @haml_lines)
|
93
|
+
end
|
94
|
+
|
95
|
+
if finished_with_empty_line && !@haml_lines.last.empty?
|
96
|
+
@haml_lines << ''
|
97
|
+
end
|
98
|
+
@haml_lines
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_lines(lines, haml_line_index:, skip_indexes_in_source_map: [])
|
102
|
+
nb_skipped_source_map_lines = 0
|
103
|
+
lines.size.times do |i|
|
104
|
+
if skip_indexes_in_source_map.include?(i)
|
105
|
+
nb_skipped_source_map_lines += 1
|
106
|
+
end
|
107
|
+
|
108
|
+
line_number = haml_line_index + 1
|
109
|
+
# If we skip the first line, we want to them to have the number of the following line
|
110
|
+
line_number = [line_number, line_number + i - nb_skipped_source_map_lines].max
|
111
|
+
@source_map[@assembled_ruby_lines.size + i + 1] = line_number
|
112
|
+
end
|
113
|
+
@assembled_ruby_lines.concat(lines)
|
114
|
+
end
|
115
|
+
|
116
|
+
def line_count
|
117
|
+
@assembled_ruby_lines.size
|
118
|
+
end
|
119
|
+
|
120
|
+
def add_marker(indent, haml_line_index:, name: 'marker')
|
121
|
+
add_lines(["#{' ' * indent}#{marker_prefix}_#{name}_#{@assembled_ruby_lines.size + 1}"],
|
122
|
+
haml_line_index: haml_line_index)
|
123
|
+
line_count
|
124
|
+
end
|
125
|
+
|
126
|
+
# If the ruby_lines have different markers in them, or are in a different order,
|
127
|
+
# then RuboCop did not alter them in a way that is compatible with this system.
|
128
|
+
def markers_conflict?(from_ruby_lines, to_ruby_lines)
|
129
|
+
from_markers = from_ruby_lines.grep(/#{marker_prefix}/, &:strip)
|
130
|
+
to_markers = to_ruby_lines.grep(/#{marker_prefix}/, &:strip)
|
131
|
+
from_markers != to_markers
|
132
|
+
end
|
133
|
+
|
134
|
+
def find_line_index_of_marker_in_corrections(line, name: 'marker')
|
135
|
+
marker = "#{marker_prefix}_#{name}_#{line}"
|
136
|
+
|
137
|
+
# In the best cases, the line didn't move
|
138
|
+
# Using end_with? because indentation may have been added
|
139
|
+
return line - 1 if @corrected_ruby_lines[line - 1]&.end_with?(marker)
|
140
|
+
|
141
|
+
@corrected_ruby_lines.index { |l| l.end_with?(marker) }
|
142
|
+
end
|
143
|
+
|
144
|
+
def extract_from_corrected_lines(start_marker_line_number, nb_lines)
|
145
|
+
cur_start_marker_index = find_line_index_of_marker_in_corrections(start_marker_line_number)
|
146
|
+
return if cur_start_marker_index.nil?
|
147
|
+
|
148
|
+
end_marker_line_number = start_marker_line_number + nb_lines + 1
|
149
|
+
cur_end_marker_index = find_line_index_of_marker_in_corrections(end_marker_line_number)
|
150
|
+
return if cur_end_marker_index.nil?
|
151
|
+
|
152
|
+
@corrected_ruby_lines[(cur_start_marker_index + 1)..(cur_end_marker_index - 1)]
|
153
|
+
end
|
154
|
+
|
155
|
+
def pick_a_marker_prefix
|
156
|
+
if @document.source.match?(/\bhaml_lint_/)
|
157
|
+
100.times do
|
158
|
+
suffix = SecureRandom.hex(10)
|
159
|
+
next if @document.source.include?(suffix)
|
160
|
+
@marker_prefix = "haml_lint#{suffix}"
|
161
|
+
return
|
162
|
+
end
|
163
|
+
else
|
164
|
+
@marker_prefix = 'haml_lint'
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def pick_a_script_output_prefix
|
169
|
+
if @document.source.match?(/\bHL\.out\b/)
|
170
|
+
100.times do
|
171
|
+
suffix = SecureRandom.hex(10)
|
172
|
+
next if @document.source.include?(suffix)
|
173
|
+
@script_output_prefix = "HL.out#{suffix} = "
|
174
|
+
return
|
175
|
+
end
|
176
|
+
else
|
177
|
+
@script_output_prefix = 'HL.out = '
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|