haml_lint 0.46.0 → 0.48.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/config/forced_rubocop_config.yml +15 -0
- data/lib/haml_lint/linter/repeated_id.rb +2 -1
- data/lib/haml_lint/linter/rubocop.rb +9 -8
- data/lib/haml_lint/linter.rb +4 -1
- data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +4 -0
- data/lib/haml_lint/ruby_extraction/base_chunk.rb +2 -1
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +194 -68
- data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +0 -20
- data/lib/haml_lint/ruby_extraction/script_chunk.rb +142 -30
- data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +26 -2
- data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +15 -0
- data/lib/haml_lint/utils.rb +5 -0
- data/lib/haml_lint/version.rb +1 -1
- metadata +3 -4
- data/lib/haml_lint/ruby_extractor.rb +0 -224
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93d2cec8a3ce45f370d5e8d055ed8f9dc5a988e2c56e8e145f50710f04f40a3f
|
4
|
+
data.tar.gz: 59a2d93bd2ab520c4fb1288cf85f784d3d20955293b6167f046bffba3f198cac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be4b59ae43a2cab51fd4148e843177b939da14ee7b0dc40a8ed0d9dd7bbcd12d39d674c1d9c7f19f0a524d912e27fc82b882f6412083eda143c5cf60e218c397
|
7
|
+
data.tar.gz: 28075057ee4c955a4966d2561ff77680b9d90c24e647dc83386dab4ebfabc404cbcb22913836f987896cc506244a4f612f74e131d8838c91f1e0db027d0f8cdd
|
@@ -45,6 +45,21 @@ Layout/EndAlignment:
|
|
45
45
|
Layout/EndOfLine:
|
46
46
|
Enabled: false
|
47
47
|
|
48
|
+
# Turning this cop on can turn
|
49
|
+
# = content_tag(:span) do
|
50
|
+
# - foo
|
51
|
+
# - bar
|
52
|
+
#
|
53
|
+
# Into
|
54
|
+
# - HL.out =
|
55
|
+
# - content_tag(:span) do
|
56
|
+
# - foo
|
57
|
+
# - bar
|
58
|
+
#
|
59
|
+
# Which is wrong... It would take too much analysis to detect and fix that situation.
|
60
|
+
Layout/MultilineAssignmentLayout:
|
61
|
+
Enabled: false
|
62
|
+
|
48
63
|
Layout/ParameterAlignment:
|
49
64
|
# The alternative, with_fixed_indentation, breaks because we sometimes remove indentation when
|
50
65
|
# dealing with multi-line scripts. (Because a line starting with "=" adds a "HL.out = " to the
|
@@ -8,13 +8,14 @@ module HamlLint
|
|
8
8
|
MESSAGE_FORMAT = %{Do not repeat id "#%s" on the page}
|
9
9
|
|
10
10
|
def visit_root(_node)
|
11
|
-
@id_map =
|
11
|
+
@id_map = {}
|
12
12
|
end
|
13
13
|
|
14
14
|
def visit_tag(node)
|
15
15
|
id = node.tag_id
|
16
16
|
return unless id && !id.empty?
|
17
17
|
|
18
|
+
id_map[id] ||= []
|
18
19
|
nodes = (id_map[id] << node)
|
19
20
|
case nodes.size
|
20
21
|
when 1 then nil
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'haml_lint/ruby_extractor'
|
4
3
|
require 'rubocop'
|
5
4
|
require 'tempfile'
|
6
5
|
|
@@ -52,11 +51,6 @@ module HamlLint
|
|
52
51
|
return
|
53
52
|
end
|
54
53
|
|
55
|
-
user_config_path = ENV['HAML_LINT_RUBOCOP_CONF'] || config['config_file']
|
56
|
-
user_config_path ||= self.class.rubocop_config_store.user_rubocop_config_path_for(document.file)
|
57
|
-
user_config_path = File.absolute_path(user_config_path)
|
58
|
-
@rubocop_config = self.class.rubocop_config_store.config_object_pointing_to(user_config_path)
|
59
|
-
|
60
54
|
@last_extracted_source = nil
|
61
55
|
@last_new_ruby_source = nil
|
62
56
|
|
@@ -102,6 +96,13 @@ module HamlLint
|
|
102
96
|
|
103
97
|
private
|
104
98
|
|
99
|
+
def rubocop_config_for(path)
|
100
|
+
user_config_path = ENV['HAML_LINT_RUBOCOP_CONF'] || config['config_file']
|
101
|
+
user_config_path ||= self.class.rubocop_config_store.user_rubocop_config_path_for(path)
|
102
|
+
user_config_path = File.absolute_path(user_config_path)
|
103
|
+
self.class.rubocop_config_store.config_object_pointing_to(user_config_path)
|
104
|
+
end
|
105
|
+
|
105
106
|
# Extracted here so that tests can stub this to always return true
|
106
107
|
def transfer_corrections?(initial_ruby_code, new_ruby_code)
|
107
108
|
initial_ruby_code != new_ruby_code
|
@@ -205,7 +206,7 @@ module HamlLint
|
|
205
206
|
def run_rubocop(rubocop_cli, ruby_code, path) # rubocop:disable Metrics
|
206
207
|
rubocop_status = nil
|
207
208
|
stdout_str, stderr_str = HamlLint::Utils.with_captured_streams(ruby_code) do
|
208
|
-
rubocop_cli.config_store.instance_variable_set(:@options_config,
|
209
|
+
rubocop_cli.config_store.instance_variable_set(:@options_config, rubocop_config_for(path))
|
209
210
|
rubocop_status = rubocop_cli.run(rubocop_flags + ['--stdin', path])
|
210
211
|
end
|
211
212
|
|
@@ -332,7 +333,7 @@ module HamlLint
|
|
332
333
|
# anymore or don't exist yet.
|
333
334
|
# This is not exhaustive, it's only for the cops that are in config/default.yml
|
334
335
|
def ignored_cops_flags
|
335
|
-
ignored_cops = config
|
336
|
+
ignored_cops = config.fetch('ignored_cops', [])
|
336
337
|
|
337
338
|
if @autocorrect
|
338
339
|
ignored_cops += self.class.cops_names_not_supporting_autocorrect
|
data/lib/haml_lint/linter.rb
CHANGED
@@ -112,8 +112,11 @@ module HamlLint
|
|
112
112
|
#
|
113
113
|
# @return [AST::Node]
|
114
114
|
def parse_ruby(source)
|
115
|
+
self.class.ruby_parser.parse(source)
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.ruby_parser # rubocop:disable Lint/IneffectiveAccessModifier
|
115
119
|
@ruby_parser ||= HamlLint::RubyParser.new
|
116
|
-
@ruby_parser.parse(source)
|
117
120
|
end
|
118
121
|
|
119
122
|
# Remove the surrounding double quotes from a string, ignoring any
|
@@ -78,7 +78,8 @@ module HamlLint::RubyExtraction
|
|
78
78
|
end
|
79
79
|
|
80
80
|
def haml_end_line_index
|
81
|
-
|
81
|
+
# the .max is needed to handle cases with 0 nb_haml_lines
|
82
|
+
[@haml_line_index + nb_haml_lines - 1, @haml_line_index].max
|
82
83
|
end
|
83
84
|
|
84
85
|
def nb_haml_lines
|
@@ -11,6 +11,12 @@ module HamlLint::RubyExtraction
|
|
11
11
|
|
12
12
|
attr_reader :script_output_prefix
|
13
13
|
|
14
|
+
HAML_PARSER_INSTANCE = if Haml::VERSION >= '5.0.0'
|
15
|
+
::Haml::Parser.new({})
|
16
|
+
else
|
17
|
+
::Haml::Parser.new('', {})
|
18
|
+
end
|
19
|
+
|
14
20
|
def initialize(document, script_output_prefix:)
|
15
21
|
@document = document
|
16
22
|
@script_output_prefix = script_output_prefix
|
@@ -19,13 +25,18 @@ module HamlLint::RubyExtraction
|
|
19
25
|
def extract
|
20
26
|
raise 'Already extracted' if @ruby_chunks
|
21
27
|
|
22
|
-
|
23
|
-
@original_haml_lines = @document.source_lines
|
28
|
+
prepare_extract
|
24
29
|
|
25
30
|
visit(@document.tree)
|
26
31
|
@ruby_chunks
|
27
32
|
end
|
28
33
|
|
34
|
+
# Useful for tests
|
35
|
+
def prepare_extract
|
36
|
+
@ruby_chunks = []
|
37
|
+
@original_haml_lines = @document.source_lines
|
38
|
+
end
|
39
|
+
|
29
40
|
def visit_root(_node)
|
30
41
|
yield # Collect lines of code from children
|
31
42
|
end
|
@@ -71,33 +82,44 @@ module HamlLint::RubyExtraction
|
|
71
82
|
# Visiting comments which are output to HTML. Lines looking like
|
72
83
|
# ` / This will be in the HTML source!`
|
73
84
|
def visit_comment(node)
|
74
|
-
|
75
|
-
indent =
|
85
|
+
line = @original_haml_lines[node.line - 1]
|
86
|
+
indent = line.index(/\S/)
|
76
87
|
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'comment', indent: indent)
|
77
88
|
end
|
78
89
|
|
79
90
|
# Visit a script which outputs. Lines looking like ` = foo`
|
80
91
|
def visit_script(node, &block)
|
81
|
-
|
92
|
+
raw_first_line = @original_haml_lines[node.line - 1]
|
93
|
+
|
94
|
+
# ==, !, !==, &, &== means interpolation (was needed before HAML 2.2... it's still supported)
|
95
|
+
# =, !=, &= mean actual ruby code is coming
|
96
|
+
# Anything else is interpolation
|
97
|
+
# The regex lists the case for Ruby Code. The 3 cases and making sure they are not followed by another = sign
|
82
98
|
|
83
|
-
|
99
|
+
match = raw_first_line.match(/\A\s*(=|!=|&=)(?!=)/)
|
100
|
+
unless match
|
84
101
|
# The line doesn't start with a - or a =, this is actually a "plain"
|
85
102
|
# that contains interpolation.
|
86
|
-
indent =
|
103
|
+
indent = raw_first_line.index(/\S/)
|
87
104
|
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'interpolation', indent: indent)
|
88
|
-
add_interpolation_chunks(node,
|
105
|
+
add_interpolation_chunks(node, raw_first_line, node.line - 1, indent: indent)
|
89
106
|
return
|
90
107
|
end
|
91
108
|
|
92
|
-
|
109
|
+
script_prefix = match[1]
|
110
|
+
_first_line_offset, lines = extract_raw_ruby_lines(node.script, node.line - 1)
|
111
|
+
# We want the actual indentation and prefix for the first line
|
112
|
+
first_line = lines[0] = @original_haml_lines[node.line - 1].rstrip
|
113
|
+
process_multiline!(first_line)
|
114
|
+
|
115
|
+
lines[0] = lines[0].sub(/(#{script_prefix}[ \t]?)/, '')
|
93
116
|
line_indentation = Regexp.last_match(1).size
|
94
117
|
|
95
118
|
raw_code = lines.join("\n")
|
96
119
|
|
97
120
|
if lines[0][/\S/] == '#'
|
98
|
-
# a script that only
|
99
|
-
|
100
|
-
lines[0].insert(comment_index + 1, " #{script_output_prefix.rstrip}")
|
121
|
+
# a "=" script that only contains a comment... No need for the "HL.out = " prefix,
|
122
|
+
# just treat it as comment which will turn into a "-" comment
|
101
123
|
else
|
102
124
|
lines[0] = HamlLint::Utils.insert_after_indentation(lines[0], script_output_prefix)
|
103
125
|
end
|
@@ -124,14 +146,25 @@ module HamlLint::RubyExtraction
|
|
124
146
|
# By forcing this to start a chunk, there will be extra placeholders which
|
125
147
|
# blocks rubocop from merging the lines.
|
126
148
|
must_start_chunk = true
|
149
|
+
elsif script_prefix != '='
|
150
|
+
# In the few cases where &= and != are used to start the script,
|
151
|
+
# We need to remember and put it back in the final HAML. Fusing scripts together
|
152
|
+
# would make that basically impossible. Instead, a script has a "first_output_prefix"
|
153
|
+
# field for this specific case
|
154
|
+
must_start_chunk = true
|
127
155
|
end
|
128
156
|
|
129
|
-
finish_visit_any_script(node, lines, raw_code: raw_code, must_start_chunk: must_start_chunk,
|
157
|
+
finish_visit_any_script(node, lines, raw_code: raw_code, must_start_chunk: must_start_chunk,
|
158
|
+
first_output_prefix: script_prefix, &block)
|
130
159
|
end
|
131
160
|
|
132
161
|
# Visit a script which doesn't output. Lines looking like ` - foo`
|
133
162
|
def visit_silent_script(node, &block)
|
134
|
-
lines =
|
163
|
+
_first_line_offset, lines = extract_raw_ruby_lines(node.script, node.line - 1)
|
164
|
+
# We want the actual indentation and prefix for the first line
|
165
|
+
first_line = lines[0] = @original_haml_lines[node.line - 1].rstrip
|
166
|
+
process_multiline!(first_line)
|
167
|
+
|
135
168
|
lines[0] = lines[0].sub(/(-[ \t]?)/, '')
|
136
169
|
nb_to_deindent = Regexp.last_match(1).size
|
137
170
|
|
@@ -145,7 +178,7 @@ module HamlLint::RubyExtraction
|
|
145
178
|
# Code common to both silent and outputting scripts
|
146
179
|
#
|
147
180
|
# raw_code is the code before we do transformations, such as adding the `HL.out = `
|
148
|
-
def finish_visit_any_script(node, lines, raw_code: nil, must_start_chunk: false)
|
181
|
+
def finish_visit_any_script(node, lines, raw_code: nil, must_start_chunk: false, first_output_prefix: '=')
|
149
182
|
raw_code ||= lines.join("\n")
|
150
183
|
start_nesting = self.class.start_nesting_after?(raw_code)
|
151
184
|
|
@@ -158,7 +191,8 @@ module HamlLint::RubyExtraction
|
|
158
191
|
@ruby_chunks << ScriptChunk.new(node, lines,
|
159
192
|
end_marker_indent: indent_after,
|
160
193
|
must_start_chunk: must_start_chunk,
|
161
|
-
previous_chunk: @ruby_chunks.last
|
194
|
+
previous_chunk: @ruby_chunks.last,
|
195
|
+
first_output_haml_prefix: first_output_prefix)
|
162
196
|
|
163
197
|
yield
|
164
198
|
|
@@ -184,27 +218,34 @@ module HamlLint::RubyExtraction
|
|
184
218
|
def visit_tag(node)
|
185
219
|
indent = @original_haml_lines[node.line - 1].index(/\S/)
|
186
220
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
[' ' * indent + code])
|
196
|
-
indent += 2
|
197
|
-
end
|
221
|
+
# We don't want to use a block because assignments in a block are local to that block,
|
222
|
+
# so the semantics of the extracted ruby would be different from the one generated by
|
223
|
+
# Haml. Those differences can make some cops, such as UselessAssignment, have false
|
224
|
+
# positives
|
225
|
+
code = 'begin'
|
226
|
+
@ruby_chunks << AdHocChunk.new(node,
|
227
|
+
[' ' * indent + code])
|
228
|
+
indent += 2
|
198
229
|
|
199
|
-
|
230
|
+
tag_chunk = PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
|
231
|
+
@ruby_chunks << tag_chunk
|
200
232
|
|
201
233
|
current_line_index = visit_tag_attributes(node, indent: indent)
|
202
234
|
visit_tag_script(node, line_index: current_line_index, indent: indent)
|
203
235
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
236
|
+
yield
|
237
|
+
|
238
|
+
indent -= 2
|
239
|
+
|
240
|
+
if @ruby_chunks.last.equal?(tag_chunk)
|
241
|
+
# So there is nothing going "in" the tag, remove the wrapping "begin" and replace the PlaceholderMarkerChunk
|
242
|
+
# by one less indented
|
243
|
+
@ruby_chunks.pop
|
244
|
+
@ruby_chunks.pop
|
245
|
+
@ruby_chunks << PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
|
246
|
+
else
|
247
|
+
@ruby_chunks << AdHocChunk.new(node,
|
248
|
+
[' ' * indent + 'ensure', ' ' * indent + ' HL.noop', ' ' * indent + 'end'],
|
208
249
|
haml_line_index: @ruby_chunks.last.haml_end_line_index)
|
209
250
|
end
|
210
251
|
end
|
@@ -221,11 +262,16 @@ module HamlLint::RubyExtraction
|
|
221
262
|
attributes_code = additional_attributes.first
|
222
263
|
if !attributes_code && node.hash_attributes? && node.dynamic_attributes_sources.empty?
|
223
264
|
# No idea why .foo{:bar => 123} doesn't get here, but .foo{:bar => '123'} does...
|
224
|
-
# The code we get for the
|
265
|
+
# The code we get for the latter is {:bar => '123'}.
|
225
266
|
# We normalize it by removing the { } so that it matches wha we normally get
|
226
267
|
attributes_code = node.dynamic_attributes_source[:hash][1...-1]
|
227
268
|
end
|
228
269
|
|
270
|
+
if attributes_code&.start_with?('{')
|
271
|
+
# Looks like the .foo(bar = 123) case. Ignoring.
|
272
|
+
attributes_code = nil
|
273
|
+
end
|
274
|
+
|
229
275
|
return final_line_index unless attributes_code
|
230
276
|
# Attributes have different ways to be given to us:
|
231
277
|
# .foo{bar: 123} => "bar: 123"
|
@@ -234,14 +280,13 @@ module HamlLint::RubyExtraction
|
|
234
280
|
# .foo(bar = 123) => '{"bar" => 123,}'
|
235
281
|
# .foo{html_attrs('fr-fr')} => html_attrs('fr-fr')
|
236
282
|
#
|
237
|
-
# The (bar = 123) case is extra painful to autocorrect (so is ignored).
|
283
|
+
# The (bar = 123) case is extra painful to autocorrect (so is ignored up there).
|
238
284
|
# #raw_ruby_from_haml will "detect" this case by not finding the code.
|
239
285
|
#
|
240
286
|
# We wrap the result in a method to have a valid syntax for all 3 ways
|
241
287
|
# without having to differentiate them.
|
242
|
-
first_line_offset, raw_attributes_lines =
|
243
|
-
|
244
|
-
|
288
|
+
first_line_offset, raw_attributes_lines = extract_raw_tag_attributes_ruby_lines(attributes_code,
|
289
|
+
node.line - 1)
|
245
290
|
return final_line_index unless raw_attributes_lines
|
246
291
|
|
247
292
|
final_line_index += raw_attributes_lines.size - 1
|
@@ -275,7 +320,7 @@ module HamlLint::RubyExtraction
|
|
275
320
|
# We ignore scripts which are just a comment
|
276
321
|
return if node.script[/\S/] == '#'
|
277
322
|
|
278
|
-
first_line_offset, script_lines =
|
323
|
+
first_line_offset, script_lines = extract_raw_ruby_lines(node.script, line_index)
|
279
324
|
|
280
325
|
if script_lines.nil?
|
281
326
|
# This is a string with interpolation after a tag
|
@@ -379,52 +424,91 @@ module HamlLint::RubyExtraction
|
|
379
424
|
end
|
380
425
|
end
|
381
426
|
|
427
|
+
def process_multiline!(line)
|
428
|
+
if HAML_PARSER_INSTANCE.send(:is_multiline?, line)
|
429
|
+
line.chop!.rstrip!
|
430
|
+
true
|
431
|
+
else
|
432
|
+
false
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
382
436
|
# Returns the raw lines from the haml for the given index.
|
383
437
|
# Multiple lines are returned when a line ends with a comma as that is the only
|
384
438
|
# 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
439
|
|
397
440
|
# Haml's line-splitting rules (allowed after comma in scripts and attributes) are handled
|
398
441
|
# at the parser level, so Haml doesn't provide the code as it is actually formatted in the Haml
|
399
442
|
# file. #raw_ruby_from_haml extracts the ruby code as it is exactly in the Haml file.
|
400
443
|
# The first and last lines may not be the complete lines from the Haml, only the Ruby parts
|
401
444
|
# 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
445
|
|
406
|
-
|
446
|
+
# HAML transforms the ruby code in many ways as it parses a document. Often removing lines and/or
|
447
|
+
# indentation. This is quite annoying for us since we want the exact layout of the code to analyze it.
|
448
|
+
#
|
449
|
+
# This function receives the code as haml provides it and the line where it starts. It returns
|
450
|
+
# the actual code as it is in the haml file, keeping breaks and indentation for the following lines.
|
451
|
+
# In addition, the start position of the code in the first line.
|
452
|
+
#
|
453
|
+
# The rules for handling multiline code in HAML are as follow:
|
454
|
+
# * if the line being processed ends with a space and a pipe, then append to the line (without
|
455
|
+
# newlines) every following lines that also end with a space and a pipe. This means the last line of
|
456
|
+
# the "block" also needs a pipe at the end.
|
457
|
+
# * after processing the pipes, when dealing with ruby code (and not in tag attributes' hash), if the line
|
458
|
+
# (which maybe span across multiple lines) ends with a comma, add the next line to the current piece of code.
|
459
|
+
#
|
460
|
+
# @return [first_line_offset, ruby_lines]
|
461
|
+
def extract_raw_ruby_lines(haml_processed_ruby_code, first_line_index)
|
462
|
+
haml_processed_ruby_code = haml_processed_ruby_code.strip
|
463
|
+
first_line = @original_haml_lines[first_line_index]
|
407
464
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
465
|
+
char_index = first_line.index(haml_processed_ruby_code)
|
466
|
+
|
467
|
+
if char_index
|
468
|
+
return [char_index, [haml_processed_ruby_code]]
|
469
|
+
end
|
470
|
+
|
471
|
+
cur_line_index = first_line_index
|
472
|
+
cur_line = first_line.rstrip
|
473
|
+
lines = []
|
474
|
+
|
475
|
+
# The pipes must also be on the last line of the multi-line section
|
476
|
+
while cur_line && process_multiline!(cur_line)
|
477
|
+
lines << cur_line
|
478
|
+
cur_line_index += 1
|
479
|
+
cur_line = @original_haml_lines[cur_line_index].rstrip
|
480
|
+
end
|
481
|
+
|
482
|
+
if lines.empty?
|
483
|
+
lines << cur_line
|
484
|
+
else
|
485
|
+
# The pipes must also be on the last line of the multi-line section. So cur_line is not the next line.
|
486
|
+
# We want to go back to check for commas
|
487
|
+
cur_line_index -= 1
|
488
|
+
cur_line = lines.last
|
489
|
+
end
|
490
|
+
|
491
|
+
while HAML_PARSER_INSTANCE.send(:is_ruby_multiline?, cur_line)
|
492
|
+
cur_line_index += 1
|
493
|
+
cur_line = @original_haml_lines[cur_line_index].rstrip
|
494
|
+
lines << cur_line
|
416
495
|
end
|
417
496
|
|
418
|
-
|
497
|
+
joined_lines = lines.join("\n")
|
498
|
+
|
499
|
+
if haml_processed_ruby_code.include?("\n")
|
500
|
+
haml_processed_ruby_code = haml_processed_ruby_code.gsub("\n", ' ')
|
501
|
+
end
|
419
502
|
|
420
|
-
|
421
|
-
# by haml, multiline tag attributes are not.
|
422
|
-
code_parts = stripped_code.gsub("\n", ' ').split(/,\s*/)
|
503
|
+
haml_processed_ruby_code.split(/[, ]/)
|
423
504
|
|
424
|
-
|
425
|
-
regexp = Regexp.new(regexp_code)
|
505
|
+
regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/,\s*|\s+/), '(?:,\\s*|\\s+)')
|
426
506
|
|
427
|
-
match =
|
507
|
+
match = joined_lines.match(regexp)
|
508
|
+
# This can happen when pipes are used as marker for multiline parts, and when tag attributes change lines
|
509
|
+
# without ending by a comma. This is quite a can of worm and is probably not too frequent, so for now,
|
510
|
+
# these cases are not supported.
|
511
|
+
return if match.nil?
|
428
512
|
|
429
513
|
raw_ruby = match[0]
|
430
514
|
ruby_lines = raw_ruby.split("\n")
|
@@ -433,6 +517,48 @@ module HamlLint::RubyExtraction
|
|
433
517
|
[first_line_offset, ruby_lines]
|
434
518
|
end
|
435
519
|
|
520
|
+
# Tag attributes actually handle multiline differently than scripts.
|
521
|
+
# The basic system basically keeps considering more lines until it meets the closing braces, but still
|
522
|
+
# processes pipes too (same as extract_raw_ruby_lines).
|
523
|
+
def extract_raw_tag_attributes_ruby_lines(haml_processed_ruby_code, first_line_index)
|
524
|
+
haml_processed_ruby_code = haml_processed_ruby_code.strip
|
525
|
+
first_line = @original_haml_lines[first_line_index]
|
526
|
+
|
527
|
+
char_index = first_line.index(haml_processed_ruby_code)
|
528
|
+
|
529
|
+
if char_index
|
530
|
+
return [char_index, [haml_processed_ruby_code]]
|
531
|
+
end
|
532
|
+
|
533
|
+
min_non_white_chars_to_add = haml_processed_ruby_code.scan(/\S/).size
|
534
|
+
|
535
|
+
regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/\s+/), '\\s+')
|
536
|
+
|
537
|
+
joined_lines = first_line.rstrip
|
538
|
+
process_multiline!(joined_lines)
|
539
|
+
|
540
|
+
cur_line_index = first_line_index + 1
|
541
|
+
while @original_haml_lines[cur_line_index] && min_non_white_chars_to_add > 0
|
542
|
+
new_line = @original_haml_lines[cur_line_index].rstrip
|
543
|
+
process_multiline!(new_line)
|
544
|
+
|
545
|
+
min_non_white_chars_to_add -= new_line.scan(/\S/).size
|
546
|
+
joined_lines << "\n"
|
547
|
+
joined_lines << new_line
|
548
|
+
cur_line_index += 1
|
549
|
+
end
|
550
|
+
|
551
|
+
match = joined_lines.match(regexp)
|
552
|
+
|
553
|
+
return if match.nil?
|
554
|
+
|
555
|
+
first_line_offset = match.begin(0)
|
556
|
+
raw_ruby = match[0]
|
557
|
+
ruby_lines = raw_ruby.split("\n")
|
558
|
+
|
559
|
+
[first_line_offset, ruby_lines]
|
560
|
+
end
|
561
|
+
|
436
562
|
def wrap_lines(lines, wrap_depth)
|
437
563
|
lines = lines.dup
|
438
564
|
wrapping_prefix = 'W' * (wrap_depth - 1) + '('
|
@@ -15,26 +15,6 @@ module HamlLint::RubyExtraction
|
|
15
15
|
HamlCommentChunk.new(node, @ruby_lines + following_chunk.ruby_lines, end_marker_indent: end_marker_indent)
|
16
16
|
end
|
17
17
|
|
18
|
-
def fuse_script_chunk(following_chunk)
|
19
|
-
return if following_chunk.end_marker_indent.nil?
|
20
|
-
return if following_chunk.must_start_chunk
|
21
|
-
|
22
|
-
nb_blank_lines_between = following_chunk.haml_line_index - haml_line_index - nb_haml_lines
|
23
|
-
blank_lines = nb_blank_lines_between > 0 ? [''] * nb_blank_lines_between : []
|
24
|
-
new_lines = @ruby_lines + blank_lines + following_chunk.ruby_lines
|
25
|
-
|
26
|
-
source_map_skips = @skip_line_indexes_in_source_map
|
27
|
-
source_map_skips.concat(following_chunk.skip_line_indexes_in_source_map
|
28
|
-
.map { |i| i + @ruby_lines.size })
|
29
|
-
|
30
|
-
ScriptChunk.new(node,
|
31
|
-
new_lines,
|
32
|
-
haml_line_index: haml_line_index,
|
33
|
-
skip_line_indexes_in_source_map: source_map_skips,
|
34
|
-
end_marker_indent: following_chunk.end_marker_indent,
|
35
|
-
previous_chunk: previous_chunk)
|
36
|
-
end
|
37
|
-
|
38
18
|
def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines)
|
39
19
|
if to_ruby_lines.empty?
|
40
20
|
haml_lines.slice!(@haml_line_index..haml_end_line_index)
|
@@ -1,11 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'ripper'
|
4
|
+
|
3
5
|
module HamlLint::RubyExtraction
|
4
6
|
# Chunk for handling outputting and silent scripts, so ` = foo` and ` - bar`
|
5
7
|
# Does NOT handle a script beside a tag (ex: `%div= spam`)
|
6
8
|
class ScriptChunk < BaseChunk
|
7
9
|
MID_BLOCK_KEYWORDS = %w[else elsif when rescue ensure].freeze
|
8
10
|
|
11
|
+
# @return [String] The prefix for the first outputting string of this script. (One of = != &=)
|
12
|
+
# The outputting scripts after the first are always with =
|
13
|
+
attr_reader :first_output_haml_prefix
|
14
|
+
|
9
15
|
# @return [Boolean] true if this ScriptChunk must be at the beginning of a chunk.
|
10
16
|
# This blocks this ScriptChunk from being fused to a ScriptChunk that is before it.
|
11
17
|
# Needed to handle some patterns of outputting script.
|
@@ -19,12 +25,13 @@ module HamlLint::RubyExtraction
|
|
19
25
|
# our starting marker must be indented.
|
20
26
|
attr_reader :previous_chunk
|
21
27
|
|
22
|
-
def initialize(*args, previous_chunk:, must_start_chunk: false,
|
23
|
-
skip_line_indexes_in_source_map: [], **kwargs)
|
28
|
+
def initialize(*args, previous_chunk:, must_start_chunk: false, # rubocop:disable Metrics/ParameterLists
|
29
|
+
skip_line_indexes_in_source_map: [], first_output_haml_prefix: '=', **kwargs)
|
24
30
|
super(*args, **kwargs)
|
25
31
|
@must_start_chunk = must_start_chunk
|
26
32
|
@skip_line_indexes_in_source_map = skip_line_indexes_in_source_map
|
27
33
|
@previous_chunk = previous_chunk
|
34
|
+
@first_output_haml_prefix = first_output_haml_prefix
|
28
35
|
end
|
29
36
|
|
30
37
|
def fuse(following_chunk)
|
@@ -53,7 +60,8 @@ module HamlLint::RubyExtraction
|
|
53
60
|
haml_line_index: haml_line_index,
|
54
61
|
skip_line_indexes_in_source_map: source_map_skips,
|
55
62
|
end_marker_indent: following_chunk.end_marker_indent,
|
56
|
-
previous_chunk: previous_chunk
|
63
|
+
previous_chunk: previous_chunk,
|
64
|
+
first_output_haml_prefix: @first_output_haml_prefix)
|
57
65
|
end
|
58
66
|
|
59
67
|
def fuse_implicit_end(following_chunk)
|
@@ -70,7 +78,8 @@ module HamlLint::RubyExtraction
|
|
70
78
|
haml_line_index: haml_line_index,
|
71
79
|
skip_line_indexes_in_source_map: source_map_skips,
|
72
80
|
end_marker_indent: following_chunk.end_marker_indent,
|
73
|
-
previous_chunk: previous_chunk
|
81
|
+
previous_chunk: previous_chunk,
|
82
|
+
first_output_haml_prefix: @first_output_haml_prefix)
|
74
83
|
end
|
75
84
|
|
76
85
|
def start_marker_indent
|
@@ -79,54 +88,157 @@ module HamlLint::RubyExtraction
|
|
79
88
|
[default_indent, previous_chunk&.end_marker_indent || previous_chunk&.start_marker_indent].compact.max
|
80
89
|
end
|
81
90
|
|
82
|
-
def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines)
|
91
|
+
def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines)
|
92
|
+
to_haml_lines = self.class.format_ruby_lines_to_haml_lines(
|
93
|
+
to_ruby_lines,
|
94
|
+
script_output_ruby_prefix: coordinator.script_output_prefix,
|
95
|
+
first_output_haml_prefix: @first_output_haml_prefix
|
96
|
+
)
|
97
|
+
|
98
|
+
haml_lines[@haml_line_index..haml_end_line_index] = to_haml_lines
|
99
|
+
end
|
100
|
+
|
101
|
+
ALLOW_EXPRESSION_AFTER_LINE_ENDING_WITH = %w[else begin ensure].freeze
|
102
|
+
|
103
|
+
def self.format_ruby_lines_to_haml_lines(to_ruby_lines, script_output_ruby_prefix:, first_output_haml_prefix: '=') # rubocop:disable Metrics
|
83
104
|
to_ruby_lines.reject! { |l| l.strip == 'end' }
|
105
|
+
return [] if to_ruby_lines.empty?
|
84
106
|
|
85
|
-
|
86
|
-
to_ruby_lines.map! do |line|
|
87
|
-
if line.lstrip.start_with?('#' + output_comment_prefix)
|
88
|
-
line = line.dup
|
89
|
-
comment_index = line.index('#')
|
90
|
-
removal_start_index = comment_index + 1
|
91
|
-
removal_end_index = removal_start_index + output_comment_prefix.size
|
92
|
-
line[removal_start_index...removal_end_index] = ''
|
93
|
-
# It will be removed again below, but will know its suposed to be a =
|
94
|
-
line.insert(comment_index, coordinator.script_output_prefix)
|
95
|
-
end
|
96
|
-
line
|
97
|
-
end
|
107
|
+
statement_start_line_indexes = find_statement_start_line_indexes(to_ruby_lines)
|
98
108
|
|
99
109
|
continued_line_indent_delta = 2
|
100
110
|
|
111
|
+
cur_line_start_index = nil
|
112
|
+
line_start_indexes_that_need_pipes = []
|
113
|
+
haml_output_prefix = first_output_haml_prefix
|
101
114
|
to_haml_lines = to_ruby_lines.map.with_index do |line, i|
|
102
115
|
if line !~ /\S/
|
103
116
|
# whitespace or empty lines, we don't want any indentation
|
104
117
|
''
|
105
|
-
elsif
|
118
|
+
elsif statement_start_line_indexes.include?(i)
|
119
|
+
cur_line_start_index = i
|
106
120
|
code_start = line.index(/\S/)
|
107
|
-
if line[code_start..].start_with?(
|
108
|
-
line = line.sub(
|
109
|
-
|
110
|
-
|
121
|
+
if line[code_start..].start_with?(script_output_ruby_prefix)
|
122
|
+
line = line.sub(script_output_ruby_prefix, '')
|
123
|
+
# The line may have been too indented because of the "HL.out = " prefix
|
124
|
+
continued_line_indent_delta = 2 - script_output_ruby_prefix.size
|
125
|
+
new_line = "#{line[0...code_start]}#{haml_output_prefix} #{line[code_start..]}"
|
126
|
+
haml_output_prefix = '='
|
127
|
+
new_line
|
111
128
|
else
|
112
129
|
continued_line_indent_delta = 2
|
113
130
|
"#{line[0...code_start]}- #{line[code_start..]}"
|
114
131
|
end
|
115
132
|
else
|
133
|
+
unless to_ruby_lines[i - 1].end_with?(',')
|
134
|
+
line_start_indexes_that_need_pipes << cur_line_start_index
|
135
|
+
end
|
136
|
+
|
116
137
|
HamlLint::Utils.indent(line, continued_line_indent_delta)
|
117
138
|
end
|
118
139
|
end
|
119
140
|
|
120
|
-
|
121
|
-
|
141
|
+
# Starting from the end because we need to add newlines when 2 groups of lines need pipes, so that they are
|
142
|
+
# separate.
|
143
|
+
line_start_indexes_that_need_pipes.reverse_each do |cur_line_i|
|
144
|
+
loop do
|
145
|
+
cur_line = to_haml_lines[cur_line_i]
|
146
|
+
break if cur_line.nil? || cur_line.empty?
|
147
|
+
to_haml_lines[cur_line_i] = cur_line + ' |'
|
148
|
+
cur_line_i += 1
|
149
|
+
|
150
|
+
break if statement_start_line_indexes.include?(cur_line_i)
|
151
|
+
end
|
152
|
+
|
153
|
+
next_line = to_haml_lines[cur_line_i]
|
154
|
+
if next_line && HamlLint::RubyExtraction::ChunkExtractor::HAML_PARSER_INSTANCE.send(:is_multiline?, next_line)
|
155
|
+
to_haml_lines.insert(cur_line_i, '')
|
156
|
+
end
|
157
|
+
end
|
122
158
|
|
123
|
-
|
124
|
-
!!lines[line_index][/,[ \t]*\z/]
|
159
|
+
to_haml_lines
|
125
160
|
end
|
126
161
|
|
127
|
-
def
|
128
|
-
|
129
|
-
|
162
|
+
def self.find_statement_start_line_indexes(to_ruby_lines) # rubocop:disable Metrics
|
163
|
+
if to_ruby_lines.size == 1
|
164
|
+
if to_ruby_lines.first[/\S/]
|
165
|
+
return [0]
|
166
|
+
else
|
167
|
+
return []
|
168
|
+
end
|
169
|
+
end
|
170
|
+
statement_start_line_indexes = [] # 0-indexed
|
171
|
+
allow_expression_after_line_number = 0 # 1-indexed
|
172
|
+
last_do_keyword_line_number = nil # 1-indexed, like Ripper.lex
|
173
|
+
|
174
|
+
to_ruby_string = to_ruby_lines.join("\n")
|
175
|
+
if RUBY_VERSION < '3.1'
|
176
|
+
# Ruby 2.6's Ripper has issues when it encounters a else, when, elsif without a matching if/case before.
|
177
|
+
# It literally stop lexing at that point without any error.
|
178
|
+
# Ex from 2.7.8:
|
179
|
+
# require 'ripper'
|
180
|
+
# Ripper.lex("a\nelse\nb")
|
181
|
+
# #=> [[[1, 0], :on_ident, "a", CMDARG], [[1, 1], :on_nl, "\n", BEG], [[2, 0], :on_kw, "else", BEG]]
|
182
|
+
# So we add enough ifs to last quite a few layer. Hopefully enough for all needs. To clarify, there would need
|
183
|
+
# as many "end" keyword in a single ScriptChunk followed by one of the problematic keyword for the problem
|
184
|
+
# to show up.
|
185
|
+
# Considering that a `end` without anything else on the line is removed from to_ruby_lines before getting here
|
186
|
+
# (in format_ruby_lines_to_haml_lines), 10 ifs should be plenty.
|
187
|
+
to_ruby_string = ('if a;' * 10) + to_ruby_string
|
188
|
+
end
|
189
|
+
|
190
|
+
last_line_number_seen = nil
|
191
|
+
Ripper.lex(to_ruby_string).each do |start_loc, token, str|
|
192
|
+
last_line_number_seen = start_loc[0]
|
193
|
+
if token == :on_nl
|
194
|
+
# :on_nl happens when we have a meaningful line change.
|
195
|
+
allow_expression_after_line_number = start_loc[0]
|
196
|
+
next
|
197
|
+
elsif token == :on_ignored_nl
|
198
|
+
# :on_ignored_nl happens for newlines within an expression, or consecutive newlines..
|
199
|
+
# and some cases we care about such as a newline after the pipes after arguments of a block
|
200
|
+
if last_do_keyword_line_number == start_loc[0]
|
201
|
+
# When starting a block, Ripper.lex gives :on_ignored_nl
|
202
|
+
allow_expression_after_line_number = start_loc[0]
|
203
|
+
end
|
204
|
+
next
|
205
|
+
end
|
206
|
+
|
207
|
+
if allow_expression_after_line_number && str[/\S/]
|
208
|
+
if allow_expression_after_line_number < start_loc[0]
|
209
|
+
# Ripper.lex returns line numbers 1-indexed, we want 0-indexed
|
210
|
+
statement_start_line_indexes << start_loc[0] - 1
|
211
|
+
end
|
212
|
+
allow_expression_after_line_number = nil
|
213
|
+
end
|
214
|
+
|
215
|
+
if token == :on_comment
|
216
|
+
# :on_comment contain its own newline at the end of the content
|
217
|
+
allow_expression_after_line_number = start_loc[0]
|
218
|
+
elsif token == :on_kw
|
219
|
+
if str == 'do'
|
220
|
+
# Because of the possible arguments for the block, we can't simply set is_between_expressions to true
|
221
|
+
last_do_keyword_line_number = start_loc[0]
|
222
|
+
elsif ALLOW_EXPRESSION_AFTER_LINE_ENDING_WITH.include?(str)
|
223
|
+
allow_expression_after_line_number = start_loc[0]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# number is 1-indexed, and we want the line after it, so that's great
|
229
|
+
if last_line_number_seen < to_ruby_lines.size && to_ruby_lines[last_line_number_seen..].any? { |l| l[/\S/] }
|
230
|
+
# There are non-empty lines after the last line Ripper showed us, that's a problem!
|
231
|
+
msg = +'It seems Ripper did not properly process some source code. Please make sure you are on the '
|
232
|
+
msg << 'latest Haml-Lint version, then create an issue at '
|
233
|
+
msg << "https://github.com/sds/haml-lint/issues and include the following information:\n"
|
234
|
+
msg << "Ruby version: #{RUBY_VERSION}\n"
|
235
|
+
msg << "Haml-Lint version: #{HamlLint::VERSION}\n"
|
236
|
+
msg << "HAML version: #{Haml::VERSION}\n"
|
237
|
+
msg << "problematic source code:\n```\n#{to_ruby_lines.join("\n")}\n```"
|
238
|
+
raise msg
|
239
|
+
end
|
240
|
+
|
241
|
+
statement_start_line_indexes
|
130
242
|
end
|
131
243
|
end
|
132
244
|
end
|
@@ -8,19 +8,43 @@ module HamlLint::RubyExtraction
|
|
8
8
|
@indent_to_remove = indent_to_remove
|
9
9
|
end
|
10
10
|
|
11
|
-
def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines)
|
11
|
+
def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines) # rubocop:disable Metrics
|
12
|
+
return if @ruby_lines == to_ruby_lines
|
13
|
+
|
12
14
|
affected_haml_lines = haml_lines[@haml_line_index..haml_end_line_index]
|
13
15
|
|
14
16
|
affected_haml = affected_haml_lines.join("\n")
|
15
17
|
|
16
18
|
from_ruby = unwrap(@ruby_lines).join("\n")
|
19
|
+
|
20
|
+
if to_ruby_lines.size > 1
|
21
|
+
min_indent = to_ruby_lines.first[/^\s*/]
|
22
|
+
to_ruby_lines.each.with_index do |line, i|
|
23
|
+
next if i == 0
|
24
|
+
next if line.start_with?(min_indent)
|
25
|
+
to_ruby_lines[i] = "#{min_indent}#{line.lstrip}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
17
29
|
to_ruby = unwrap(to_ruby_lines).join("\n")
|
18
30
|
|
19
31
|
affected_start_index = affected_haml.index(from_ruby)
|
20
|
-
|
32
|
+
if affected_start_index
|
33
|
+
affected_end_index = affected_start_index + from_ruby.size
|
34
|
+
else
|
35
|
+
regexp = HamlLint::Utils.regexp_for_parts(from_ruby.split("\n"), "(?:\s*\\|?\n)")
|
36
|
+
mo = affected_haml.match(regexp)
|
37
|
+
affected_start_index = mo.begin(0)
|
38
|
+
affected_end_index = mo.end(0)
|
39
|
+
end
|
40
|
+
|
21
41
|
affected_haml[affected_start_index...affected_end_index] = to_ruby
|
22
42
|
|
23
43
|
haml_lines[@haml_line_index..haml_end_line_index] = affected_haml.split("\n")
|
44
|
+
|
45
|
+
if haml_lines[haml_end_line_index].end_with?(' |')
|
46
|
+
haml_lines[haml_end_line_index].chop!.rstrip!
|
47
|
+
end
|
24
48
|
end
|
25
49
|
|
26
50
|
def unwrap(lines)
|
@@ -32,6 +32,19 @@ module HamlLint
|
|
32
32
|
false
|
33
33
|
end
|
34
34
|
|
35
|
+
let(:supported_haml?) do |example|
|
36
|
+
# Tries to match `{% haml_version >= '5' %}`, extracting the operator and the "number"
|
37
|
+
haml_version_regex = /\{%\s*haml_version\s*([^\w\s]+?)\s*['"]?(\d+(\.\d+)*)['"]?\s*%\}/
|
38
|
+
requirements = example.metadata[:full_description].scan(haml_version_regex)
|
39
|
+
|
40
|
+
# This can be used by the requirements in eval
|
41
|
+
haml_version = HamlLint::VersionComparer.for_haml
|
42
|
+
|
43
|
+
requirements.all? do |(operator, version)|
|
44
|
+
haml_version.send(operator, version)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
35
48
|
before do
|
36
49
|
if stub_rubocop?
|
37
50
|
skip if end_ruby.include?('SKIP')
|
@@ -92,6 +105,8 @@ module HamlLint
|
|
92
105
|
# 4) the corrected haml
|
93
106
|
# Each steps is delimited by a line with ---
|
94
107
|
def follows_steps # rubocop:disable Metrics
|
108
|
+
skip unless supported_haml?
|
109
|
+
|
95
110
|
begin
|
96
111
|
subject.run_or_raise(document, autocorrect: autocorrect)
|
97
112
|
rescue StandardError => e
|
data/lib/haml_lint/utils.rb
CHANGED
data/lib/haml_lint/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: haml_lint
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.48.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shane da Silva
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-07-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: haml
|
@@ -179,7 +179,6 @@ files:
|
|
179
179
|
- lib/haml_lint/ruby_extraction/script_chunk.rb
|
180
180
|
- lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb
|
181
181
|
- lib/haml_lint/ruby_extraction/tag_script_chunk.rb
|
182
|
-
- lib/haml_lint/ruby_extractor.rb
|
183
182
|
- lib/haml_lint/ruby_parser.rb
|
184
183
|
- lib/haml_lint/runner.rb
|
185
184
|
- lib/haml_lint/severity.rb
|
@@ -221,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
221
220
|
- !ruby/object:Gem::Version
|
222
221
|
version: '0'
|
223
222
|
requirements: []
|
224
|
-
rubygems_version: 3.
|
223
|
+
rubygems_version: 3.1.6
|
225
224
|
signing_key:
|
226
225
|
specification_version: 4
|
227
226
|
summary: HAML lint tool
|
@@ -1,224 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module HamlLint
|
4
|
-
# Utility class for extracting Ruby script from a HAML file that can then be
|
5
|
-
# linted with a Ruby linter (i.e. is "legal" Ruby). The goal is to turn this:
|
6
|
-
#
|
7
|
-
# - if signed_in?(viewer)
|
8
|
-
# %span Stuff
|
9
|
-
# = link_to 'Sign Out', sign_out_path
|
10
|
-
# - else
|
11
|
-
# .some-class{ class: my_method }= my_method
|
12
|
-
# = link_to 'Sign In', sign_in_path
|
13
|
-
#
|
14
|
-
# into this:
|
15
|
-
#
|
16
|
-
# if signed_in?(viewer)
|
17
|
-
# link_to 'Sign Out', sign_out_path
|
18
|
-
# else
|
19
|
-
# { class: my_method }
|
20
|
-
# my_method
|
21
|
-
# link_to 'Sign In', sign_in_path
|
22
|
-
# end
|
23
|
-
#
|
24
|
-
# The translation won't be perfect, and won't make any real sense, but the
|
25
|
-
# relationship between variable declarations/uses and the flow control graph
|
26
|
-
# will remain intact.
|
27
|
-
class RubyExtractor
|
28
|
-
include HamlVisitor
|
29
|
-
|
30
|
-
# Stores the extracted source and a map of lines of generated source to the
|
31
|
-
# original source that created them.
|
32
|
-
#
|
33
|
-
# @attr_reader source [String] generated source code
|
34
|
-
# @attr_reader source_map [Hash] map of line numbers from generated source
|
35
|
-
# to original source line number
|
36
|
-
RubySource = Struct.new(:source, :source_map)
|
37
|
-
|
38
|
-
# Extracts Ruby code from Sexp representing a Slim document.
|
39
|
-
#
|
40
|
-
# @param document [HamlLint::Document]
|
41
|
-
# @return [HamlLint::RubyExtractor::RubySource]
|
42
|
-
def extract(document)
|
43
|
-
visit(document.tree)
|
44
|
-
RubySource.new(@source_lines.join("\n"), @source_map)
|
45
|
-
end
|
46
|
-
|
47
|
-
def visit_root(_node)
|
48
|
-
@source_lines = []
|
49
|
-
@source_map = {}
|
50
|
-
@line_count = 0
|
51
|
-
@indent_level = 0
|
52
|
-
@output_count = 0
|
53
|
-
|
54
|
-
yield # Collect lines of code from children
|
55
|
-
end
|
56
|
-
|
57
|
-
def visit_plain(node)
|
58
|
-
# Don't output the text, as we don't want to have to deal with any RuboCop
|
59
|
-
# cops regarding StringQuotes or AsciiComments, and it's not important to
|
60
|
-
# overall document anyway.
|
61
|
-
add_dummy_puts(node)
|
62
|
-
end
|
63
|
-
|
64
|
-
def visit_tag(node)
|
65
|
-
additional_attributes = node.dynamic_attributes_sources
|
66
|
-
|
67
|
-
# Include dummy references to code executed in attributes list
|
68
|
-
# (this forces a "use" of a variable to prevent "assigned but unused
|
69
|
-
# variable" lints)
|
70
|
-
additional_attributes.each do |attributes_code|
|
71
|
-
# Normalize by removing excess whitespace to avoid format lints
|
72
|
-
attributes_code = attributes_code.gsub(/\s*\n\s*/, "\n").strip
|
73
|
-
|
74
|
-
# Attributes can either be a method call or a literal hash, so wrap it
|
75
|
-
# in a method call itself in order to avoid having to differentiate the
|
76
|
-
# two. Use the tag name for the method to differentiate different tag types
|
77
|
-
# for RuboCop and prevent erroneous warnings.
|
78
|
-
add_line("#{node.tag_name}(#{attributes_code})", node)
|
79
|
-
end
|
80
|
-
|
81
|
-
check_tag_static_hash_source(node)
|
82
|
-
|
83
|
-
# We add a dummy puts statement to represent the tag name being output.
|
84
|
-
# This prevents some erroneous RuboCop warnings.
|
85
|
-
add_dummy_puts(node, node.tag_name)
|
86
|
-
|
87
|
-
code = node.script.strip
|
88
|
-
add_line(code, node) unless code.empty?
|
89
|
-
end
|
90
|
-
|
91
|
-
def after_visit_tag(node)
|
92
|
-
# We add a dummy puts statement for closing tag.
|
93
|
-
add_dummy_puts(node, "#{node.tag_name}/")
|
94
|
-
end
|
95
|
-
|
96
|
-
def visit_script(node)
|
97
|
-
code = node.text
|
98
|
-
|
99
|
-
add_line(code.strip, node)
|
100
|
-
|
101
|
-
start_block = anonymous_block?(code) || start_block_keyword?(code)
|
102
|
-
|
103
|
-
if start_block
|
104
|
-
@indent_level += 1
|
105
|
-
end
|
106
|
-
|
107
|
-
yield # Continue extracting code from children
|
108
|
-
|
109
|
-
if start_block
|
110
|
-
@indent_level -= 1
|
111
|
-
add_line('end', node)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def visit_haml_comment(node)
|
116
|
-
# We want to preseve leading whitespace if it exists, but include leading
|
117
|
-
# whitespace if it doesn't exist so that RuboCop's LeadingCommentSpace
|
118
|
-
# doesn't complain
|
119
|
-
comment = node.text
|
120
|
-
.gsub(/\n(\S)/, "\n# \\1")
|
121
|
-
.gsub(/\n(\s)/, "\n#\\1")
|
122
|
-
add_line("##{comment}", node)
|
123
|
-
end
|
124
|
-
|
125
|
-
def visit_silent_script(node, &block)
|
126
|
-
visit_script(node, &block)
|
127
|
-
end
|
128
|
-
|
129
|
-
def visit_filter(node)
|
130
|
-
if node.filter_type == 'ruby'
|
131
|
-
node.text.split("\n").each_with_index do |line, index|
|
132
|
-
add_line(line, node.line + index + 1, discard_blanks: false)
|
133
|
-
end
|
134
|
-
else
|
135
|
-
add_dummy_puts(node, ":#{node.filter_type}")
|
136
|
-
HamlLint::Utils.extract_interpolated_values(node.text) do |interpolated_code, line|
|
137
|
-
add_line(interpolated_code, node.line + line)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
private
|
143
|
-
|
144
|
-
def check_tag_static_hash_source(node)
|
145
|
-
# Haml::Parser converts hashrocket-style hash attributes of strings and symbols
|
146
|
-
# to static attributes, and excludes them from the dynamic attribute sources:
|
147
|
-
# https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L400-L404
|
148
|
-
# https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L540-L554
|
149
|
-
# Here, we add the hash source back in so it can be inspected by rubocop.
|
150
|
-
if node.hash_attributes? && node.dynamic_attributes_sources.empty?
|
151
|
-
normalized_attr_source = node.dynamic_attributes_source[:hash].gsub(/\s*\n\s*/, ' ')
|
152
|
-
|
153
|
-
add_line(normalized_attr_source, node)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
# Adds a dummy method call with a unique name so we don't get
|
158
|
-
# Style/IdenticalConditionalBranches RuboCop warnings
|
159
|
-
def add_dummy_puts(node, annotation = nil)
|
160
|
-
annotation = " # #{annotation}" if annotation
|
161
|
-
add_line("_haml_lint_puts_#{@output_count}#{annotation}", node)
|
162
|
-
@output_count += 1
|
163
|
-
end
|
164
|
-
|
165
|
-
def add_line(code, node_or_line, discard_blanks: true)
|
166
|
-
return if code.empty? && discard_blanks
|
167
|
-
|
168
|
-
indent_level = @indent_level
|
169
|
-
|
170
|
-
if node_or_line.respond_to?(:line) && mid_block_keyword?(code)
|
171
|
-
# Since mid-block keywords are children of the corresponding start block
|
172
|
-
# keyword, we need to reduce their indentation level by 1. However, we
|
173
|
-
# don't do this unless this is an actual tag node (a raw line number
|
174
|
-
# means this came from a `:ruby` filter).
|
175
|
-
indent_level -= 1
|
176
|
-
end
|
177
|
-
|
178
|
-
indent = (' ' * 2 * indent_level)
|
179
|
-
|
180
|
-
@source_lines << indent_code(code, indent)
|
181
|
-
|
182
|
-
original_line =
|
183
|
-
node_or_line.respond_to?(:line) ? node_or_line.line : node_or_line
|
184
|
-
|
185
|
-
# For interpolated code in filters that spans multiple lines, the
|
186
|
-
# resulting code will span multiple lines, so we need to create a
|
187
|
-
# mapping for each line.
|
188
|
-
(code.count("\n") + 1).times do
|
189
|
-
@line_count += 1
|
190
|
-
@source_map[@line_count] = original_line
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
def indent_code(code, indent)
|
195
|
-
codes = code.split("\n")
|
196
|
-
codes.map { |c| indent + c }.join("\n")
|
197
|
-
end
|
198
|
-
|
199
|
-
def anonymous_block?(text)
|
200
|
-
text =~ /\bdo\s*(\|\s*[^|]*\s*\|)?(\s*#.*)?\z/
|
201
|
-
end
|
202
|
-
|
203
|
-
START_BLOCK_KEYWORDS = %w[if unless case begin for until while].freeze
|
204
|
-
def start_block_keyword?(text)
|
205
|
-
START_BLOCK_KEYWORDS.include?(block_keyword(text))
|
206
|
-
end
|
207
|
-
|
208
|
-
MID_BLOCK_KEYWORDS = %w[else elsif when rescue ensure].freeze
|
209
|
-
def mid_block_keyword?(text)
|
210
|
-
MID_BLOCK_KEYWORDS.include?(block_keyword(text))
|
211
|
-
end
|
212
|
-
|
213
|
-
LOOP_KEYWORDS = %w[for until while].freeze
|
214
|
-
def block_keyword(text)
|
215
|
-
# Need to handle 'for'/'while' since regex stolen from HAML parser doesn't
|
216
|
-
if (keyword = text[/\A\s*([^\s]+)\s+/, 1]) && LOOP_KEYWORDS.include?(keyword)
|
217
|
-
return keyword
|
218
|
-
end
|
219
|
-
|
220
|
-
return unless keyword = text.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0]
|
221
|
-
keyword[0] || keyword[1]
|
222
|
-
end
|
223
|
-
end
|
224
|
-
end
|