haml_lint 0.44.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|