haml_lint 0.40.0 → 0.51.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/haml-lint +1 -1
- data/config/default.yml +9 -27
- data/config/forced_rubocop_config.yml +180 -0
- data/lib/haml_lint/adapter/haml_4.rb +20 -0
- data/lib/haml_lint/adapter/haml_5.rb +11 -0
- data/lib/haml_lint/adapter/haml_6.rb +59 -0
- data/lib/haml_lint/adapter.rb +2 -0
- data/lib/haml_lint/cli.rb +8 -3
- data/lib/haml_lint/configuration_loader.rb +49 -13
- data/lib/haml_lint/document.rb +89 -8
- data/lib/haml_lint/exceptions.rb +6 -0
- data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +35 -0
- data/lib/haml_lint/file_finder.rb +2 -2
- data/lib/haml_lint/lint.rb +10 -1
- data/lib/haml_lint/linter/final_newline.rb +4 -3
- data/lib/haml_lint/linter/implicit_div.rb +1 -1
- data/lib/haml_lint/linter/indentation.rb +3 -3
- data/lib/haml_lint/linter/no_placeholders.rb +18 -0
- data/lib/haml_lint/linter/repeated_id.rb +2 -1
- data/lib/haml_lint/linter/rubocop.rb +353 -59
- data/lib/haml_lint/linter/space_before_script.rb +8 -10
- data/lib/haml_lint/linter/trailing_empty_lines.rb +22 -0
- data/lib/haml_lint/linter/unnecessary_string_output.rb +1 -1
- data/lib/haml_lint/linter/view_length.rb +1 -1
- data/lib/haml_lint/linter.rb +60 -10
- data/lib/haml_lint/linter_registry.rb +3 -5
- data/lib/haml_lint/logger.rb +2 -2
- data/lib/haml_lint/options.rb +26 -2
- data/lib/haml_lint/rake_task.rb +2 -2
- data/lib/haml_lint/reporter/hash_reporter.rb +1 -3
- data/lib/haml_lint/reporter/offense_count_reporter.rb +1 -1
- data/lib/haml_lint/reporter/utils.rb +33 -4
- data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +24 -0
- data/lib/haml_lint/ruby_extraction/base_chunk.rb +114 -0
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +672 -0
- data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
- data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +34 -0
- data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +17 -0
- data/lib/haml_lint/ruby_extraction/interpolation_chunk.rb +26 -0
- data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +32 -0
- data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +40 -0
- data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +33 -0
- data/lib/haml_lint/ruby_extraction/ruby_source.rb +5 -0
- data/lib/haml_lint/ruby_extraction/script_chunk.rb +251 -0
- data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +63 -0
- data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
- data/lib/haml_lint/ruby_parser.rb +11 -1
- data/lib/haml_lint/runner.rb +35 -3
- data/lib/haml_lint/spec/matchers/report_lint.rb +22 -7
- data/lib/haml_lint/spec/normalize_indent.rb +2 -2
- data/lib/haml_lint/spec/shared_linter_context.rb +9 -1
- data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +158 -0
- data/lib/haml_lint/spec.rb +1 -0
- data/lib/haml_lint/tree/filter_node.rb +10 -0
- data/lib/haml_lint/tree/node.rb +13 -4
- data/lib/haml_lint/tree/script_node.rb +7 -1
- data/lib/haml_lint/tree/silent_script_node.rb +16 -1
- data/lib/haml_lint/tree/tag_node.rb +5 -9
- data/lib/haml_lint/utils.rb +135 -5
- data/lib/haml_lint/version.rb +1 -1
- data/lib/haml_lint/version_comparer.rb +25 -0
- data/lib/haml_lint.rb +12 -0
- metadata +29 -15
- data/lib/haml_lint/ruby_extractor.rb +0 -222
@@ -0,0 +1,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
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HamlLint::RubyExtraction
|
4
|
+
# Chunk for haml comments. Lines like ` -# Some commenting!`.
|
5
|
+
# Only deals with indentation while correcting, but can also be fused to a ScriptChunk.
|
6
|
+
class HamlCommentChunk < BaseChunk
|
7
|
+
def fuse(following_chunk)
|
8
|
+
return unless following_chunk.is_a?(HamlCommentChunk)
|
9
|
+
|
10
|
+
# We only merge consecutive comments
|
11
|
+
# The main reason to want to at least merge those is
|
12
|
+
# so that an empty comment doesn't get removed by rubocop by mistake
|
13
|
+
return if @haml_line_index + 1 != following_chunk.haml_line_index
|
14
|
+
|
15
|
+
HamlCommentChunk.new(node, @ruby_lines + following_chunk.ruby_lines, end_marker_indent: end_marker_indent)
|
16
|
+
end
|
17
|
+
|
18
|
+
def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines)
|
19
|
+
if to_ruby_lines.empty?
|
20
|
+
haml_lines.slice!(@haml_line_index..haml_end_line_index)
|
21
|
+
return
|
22
|
+
end
|
23
|
+
delta_indent = min_indent_of(to_ruby_lines) - min_indent_of(@ruby_lines)
|
24
|
+
|
25
|
+
HamlLint::Utils.map_subset!(haml_lines, @haml_line_index..haml_end_line_index) do |l|
|
26
|
+
HamlLint::Utils.indent(l, delta_indent)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def min_indent_of(lines)
|
31
|
+
lines.map { |l| l.index(/\S/) }.compact.min
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HamlLint::RubyExtraction
|
4
|
+
# HAML adds a `end` when code gets outdented. We need to add that to the Ruby too, this
|
5
|
+
# is the chunk for it.
|
6
|
+
# However:
|
7
|
+
# * we can't apply fixes to it, so there are no markers
|
8
|
+
# * this is a distinct class so that a ScriptChunk can fuse this ImplicitEnd into itself,
|
9
|
+
# So that we can generate bigger chunks of uninterrupted Ruby.
|
10
|
+
class ImplicitEndChunk < BaseChunk
|
11
|
+
def wrap_in_markers
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def transfer_correction(coordinator, all_corrected_ruby_lines, haml_lines); end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HamlLint::RubyExtraction
|
4
|
+
# Deals with interpolation within a plain text, filter, etc.
|
5
|
+
# Can only handling single line interpolation, so will be skipped if it takes
|
6
|
+
# more than one line or if the correction takes more than one line.
|
7
|
+
#
|
8
|
+
# Stores the char index to know where in the line to do the replacements.
|
9
|
+
class InterpolationChunk < BaseChunk
|
10
|
+
def initialize(*args, start_char_index:, **kwargs)
|
11
|
+
super(*args, **kwargs)
|
12
|
+
@start_char_index = start_char_index
|
13
|
+
end
|
14
|
+
|
15
|
+
def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines)
|
16
|
+
return if @ruby_lines.size != 1
|
17
|
+
return if to_ruby_lines.size != 1
|
18
|
+
|
19
|
+
from_ruby_line = @ruby_lines.first.partition(coordinator.script_output_prefix).last
|
20
|
+
to_ruby_line = to_ruby_lines.first.partition(coordinator.script_output_prefix).last
|
21
|
+
|
22
|
+
haml_line = haml_lines[@haml_line_index]
|
23
|
+
haml_line[@start_char_index...(@start_char_index + from_ruby_line.size)] = to_ruby_line
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HamlLint::RubyExtraction
|
4
|
+
# Chunk for dealing with every HAML filter other than `:ruby`
|
5
|
+
# The generated Ruby for these is just a HEREDOC, so interpolation is corrected at
|
6
|
+
# the same time by RuboCop.
|
7
|
+
class NonRubyFilterChunk < BaseChunk
|
8
|
+
def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines)
|
9
|
+
delta_indent = to_ruby_lines.first.index(/\S/) - @ruby_lines.first.index(/\S/)
|
10
|
+
|
11
|
+
haml_lines[@haml_line_index] = HamlLint::Utils.indent(haml_lines[@haml_line_index], delta_indent)
|
12
|
+
|
13
|
+
# Ignoring the starting <<~HAML_LINT_FILTER and ending end
|
14
|
+
to_content_lines = to_ruby_lines[1...-1]
|
15
|
+
|
16
|
+
to_haml_lines = to_content_lines.map do |line|
|
17
|
+
if line !~ /\S/
|
18
|
+
# whitespace or empty
|
19
|
+
''
|
20
|
+
else
|
21
|
+
line
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
haml_lines[(@haml_line_index + 1)..haml_end_line_index] = to_haml_lines
|
26
|
+
end
|
27
|
+
|
28
|
+
def skip_line_indexes_in_source_map
|
29
|
+
[@ruby_lines.size - 1]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HamlLint::RubyExtraction
|
4
|
+
# This chunk just adds a marker (with a custom name) to the generated Ruby and only attempts to
|
5
|
+
# transfer the corrections it receives to the indentation of the associated lines.
|
6
|
+
#
|
7
|
+
# Also used so that Rubocop doesn't think that there is nothing in `if` and other such structures,
|
8
|
+
# so that it does corrections that make sense for the HAML.
|
9
|
+
class PlaceholderMarkerChunk < BaseChunk
|
10
|
+
def initialize(node, marker_name, indent:, nb_lines: 1, **kwargs)
|
11
|
+
@marker_name = marker_name
|
12
|
+
@indent = indent
|
13
|
+
@nb_lines = nb_lines
|
14
|
+
super(node, nil, **kwargs.merge(end_marker_indent: @indent))
|
15
|
+
end
|
16
|
+
|
17
|
+
def full_assemble(coordinator)
|
18
|
+
@start_marker_line_number = coordinator.add_marker(@indent, name: @marker_name,
|
19
|
+
haml_line_index: haml_line_index)
|
20
|
+
end
|
21
|
+
|
22
|
+
def transfer_correction(coordinator, all_corrected_ruby_lines, haml_lines)
|
23
|
+
marker_index = coordinator.find_line_index_of_marker_in_corrections(@start_marker_line_number,
|
24
|
+
name: @marker_name)
|
25
|
+
new_indent = all_corrected_ruby_lines[marker_index].index(/\S/)
|
26
|
+
return if new_indent == @indent
|
27
|
+
(haml_line_index..haml_end_line_index).each do |i|
|
28
|
+
haml_lines[i] = HamlLint::Utils.indent(haml_lines[i], new_indent - @indent)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def haml_end_line_index
|
33
|
+
haml_line_index + @nb_lines - 1
|
34
|
+
end
|
35
|
+
|
36
|
+
def end_marker_indent
|
37
|
+
@indent
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HamlLint::RubyExtraction
|
4
|
+
# Chunk for dealing with `:ruby` filter.
|
5
|
+
class RubyFilterChunk < BaseChunk
|
6
|
+
attr_reader :start_marker_indent
|
7
|
+
|
8
|
+
def initialize(*args, start_marker_indent:, **kwargs)
|
9
|
+
super(*args, **kwargs)
|
10
|
+
@start_marker_indent = start_marker_indent
|
11
|
+
end
|
12
|
+
|
13
|
+
def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines)
|
14
|
+
marker_index = coordinator.find_line_index_of_marker_in_corrections(@start_marker_line_number)
|
15
|
+
|
16
|
+
new_name_indent = coordinator.corrected_ruby_lines[marker_index].index(/\S/)
|
17
|
+
|
18
|
+
delta_indent = new_name_indent - @start_marker_indent
|
19
|
+
haml_lines[@haml_line_index - 1] = HamlLint::Utils.indent(haml_lines[@haml_line_index - 1], delta_indent)
|
20
|
+
|
21
|
+
to_haml_lines = to_ruby_lines.map do |line|
|
22
|
+
if line !~ /\S/
|
23
|
+
# whitespace or empty
|
24
|
+
''
|
25
|
+
else
|
26
|
+
" #{line}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
haml_lines[@haml_line_index..haml_end_line_index] = to_haml_lines
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ripper'
|
4
|
+
|
5
|
+
module HamlLint::RubyExtraction
|
6
|
+
# Chunk for handling outputting and silent scripts, so ` = foo` and ` - bar`
|
7
|
+
# Does NOT handle a script beside a tag (ex: `%div= spam`)
|
8
|
+
class ScriptChunk < BaseChunk
|
9
|
+
MID_BLOCK_KEYWORDS = %w[else elsif when rescue ensure].freeze
|
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
|
+
|
15
|
+
# @return [Boolean] true if this ScriptChunk must be at the beginning of a chunk.
|
16
|
+
# This blocks this ScriptChunk from being fused to a ScriptChunk that is before it.
|
17
|
+
# Needed to handle some patterns of outputting script.
|
18
|
+
attr_reader :must_start_chunk
|
19
|
+
|
20
|
+
# @return [Array<Integer>] Line indexes to ignore when building the source_map. For examples,
|
21
|
+
# implicit `end` are on their own line in the Ruby file, but in the HAML, they are absent.
|
22
|
+
attr_reader :skip_line_indexes_in_source_map
|
23
|
+
|
24
|
+
# @return [HamlLint::RubyExtraction::BaseChunk] The previous chunk can affect how
|
25
|
+
# our starting marker must be indented.
|
26
|
+
attr_reader :previous_chunk
|
27
|
+
|
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)
|
30
|
+
super(*args, **kwargs)
|
31
|
+
@must_start_chunk = must_start_chunk
|
32
|
+
@skip_line_indexes_in_source_map = skip_line_indexes_in_source_map
|
33
|
+
@previous_chunk = previous_chunk
|
34
|
+
@first_output_haml_prefix = first_output_haml_prefix
|
35
|
+
end
|
36
|
+
|
37
|
+
def fuse(following_chunk)
|
38
|
+
case following_chunk
|
39
|
+
when ScriptChunk
|
40
|
+
fuse_script_chunk(following_chunk)
|
41
|
+
when ImplicitEndChunk
|
42
|
+
fuse_implicit_end(following_chunk)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def fuse_script_chunk(following_chunk)
|
47
|
+
return if following_chunk.end_marker_indent.nil?
|
48
|
+
return if following_chunk.must_start_chunk
|
49
|
+
|
50
|
+
nb_blank_lines_between = following_chunk.haml_line_index - haml_line_index - nb_haml_lines
|
51
|
+
blank_lines = nb_blank_lines_between > 0 ? [''] * nb_blank_lines_between : []
|
52
|
+
new_lines = @ruby_lines + blank_lines + following_chunk.ruby_lines
|
53
|
+
|
54
|
+
source_map_skips = @skip_line_indexes_in_source_map
|
55
|
+
source_map_skips.concat(following_chunk.skip_line_indexes_in_source_map
|
56
|
+
.map { |i| i + @ruby_lines.size })
|
57
|
+
|
58
|
+
ScriptChunk.new(node,
|
59
|
+
new_lines,
|
60
|
+
haml_line_index: haml_line_index,
|
61
|
+
skip_line_indexes_in_source_map: source_map_skips,
|
62
|
+
end_marker_indent: following_chunk.end_marker_indent,
|
63
|
+
previous_chunk: previous_chunk,
|
64
|
+
first_output_haml_prefix: @first_output_haml_prefix)
|
65
|
+
end
|
66
|
+
|
67
|
+
def fuse_implicit_end(following_chunk)
|
68
|
+
new_lines = @ruby_lines.dup
|
69
|
+
last_non_empty_line_index = new_lines.rindex { |line| line =~ /\S/ }
|
70
|
+
|
71
|
+
# There is only one line in ImplicitEndChunk
|
72
|
+
new_end_index = last_non_empty_line_index + 1
|
73
|
+
new_lines.insert(new_end_index, following_chunk.ruby_lines.first)
|
74
|
+
source_map_skips = @skip_line_indexes_in_source_map + [new_end_index]
|
75
|
+
|
76
|
+
ScriptChunk.new(node,
|
77
|
+
new_lines,
|
78
|
+
haml_line_index: haml_line_index,
|
79
|
+
skip_line_indexes_in_source_map: source_map_skips,
|
80
|
+
end_marker_indent: following_chunk.end_marker_indent,
|
81
|
+
previous_chunk: previous_chunk,
|
82
|
+
first_output_haml_prefix: @first_output_haml_prefix)
|
83
|
+
end
|
84
|
+
|
85
|
+
def start_marker_indent
|
86
|
+
default_indent = super
|
87
|
+
default_indent += 2 if MID_BLOCK_KEYWORDS.include?(ChunkExtractor.block_keyword(ruby_lines.first))
|
88
|
+
[default_indent, previous_chunk&.end_marker_indent || previous_chunk&.start_marker_indent].compact.max
|
89
|
+
end
|
90
|
+
|
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
|
104
|
+
to_ruby_lines.reject! { |l| l.strip == 'end' }
|
105
|
+
return [] if to_ruby_lines.empty?
|
106
|
+
|
107
|
+
statement_start_line_indexes = find_statement_start_line_indexes(to_ruby_lines)
|
108
|
+
|
109
|
+
continued_line_indent_delta = 2
|
110
|
+
continued_line_min_indent = 2
|
111
|
+
|
112
|
+
cur_line_start_index = nil
|
113
|
+
line_start_indexes_that_need_pipes = []
|
114
|
+
haml_output_prefix = first_output_haml_prefix
|
115
|
+
to_haml_lines = to_ruby_lines.map.with_index do |line, i| # rubocop:disable Metrics/BlockLength
|
116
|
+
if line !~ /\S/
|
117
|
+
# whitespace or empty lines, we don't want any indentation
|
118
|
+
''
|
119
|
+
elsif statement_start_line_indexes.include?(i)
|
120
|
+
cur_line_start_index = i
|
121
|
+
code_start = line.index(/\S/)
|
122
|
+
continued_line_min_indent = code_start + 2
|
123
|
+
if line[code_start..].start_with?(script_output_ruby_prefix)
|
124
|
+
line = line.sub(script_output_ruby_prefix, '')
|
125
|
+
# The next lines may have been too indented because of the "HL.out = " prefix
|
126
|
+
continued_line_indent_delta = 2 - script_output_ruby_prefix.size
|
127
|
+
new_line = "#{line[0...code_start]}#{haml_output_prefix} #{line[code_start..]}"
|
128
|
+
haml_output_prefix = '='
|
129
|
+
new_line
|
130
|
+
else
|
131
|
+
continued_line_indent_delta = 2
|
132
|
+
"#{line[0...code_start]}- #{line[code_start..]}"
|
133
|
+
end
|
134
|
+
else
|
135
|
+
unless to_ruby_lines[i - 1].end_with?(',')
|
136
|
+
line_start_indexes_that_need_pipes << cur_line_start_index
|
137
|
+
end
|
138
|
+
|
139
|
+
line = HamlLint::Utils.indent(line, continued_line_indent_delta)
|
140
|
+
cur_indent = line[/^ */].size
|
141
|
+
if cur_indent < continued_line_min_indent
|
142
|
+
line = HamlLint::Utils.indent(line, continued_line_min_indent - cur_indent)
|
143
|
+
end
|
144
|
+
line
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Starting from the end because we need to add newlines when 2 groups of lines need pipes, so that they are
|
149
|
+
# separate.
|
150
|
+
line_start_indexes_that_need_pipes.reverse_each do |cur_line_i|
|
151
|
+
loop do
|
152
|
+
cur_line = to_haml_lines[cur_line_i]
|
153
|
+
break if cur_line.nil? || cur_line.empty?
|
154
|
+
to_haml_lines[cur_line_i] = cur_line + ' |'
|
155
|
+
cur_line_i += 1
|
156
|
+
|
157
|
+
break if statement_start_line_indexes.include?(cur_line_i)
|
158
|
+
end
|
159
|
+
|
160
|
+
next_line = to_haml_lines[cur_line_i]
|
161
|
+
if next_line && HamlLint::RubyExtraction::ChunkExtractor::HAML_PARSER_INSTANCE.send(:is_multiline?, next_line)
|
162
|
+
to_haml_lines.insert(cur_line_i, '')
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
to_haml_lines
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.find_statement_start_line_indexes(to_ruby_lines) # rubocop:disable Metrics
|
170
|
+
if to_ruby_lines.size == 1
|
171
|
+
if to_ruby_lines.first[/\S/]
|
172
|
+
return [0]
|
173
|
+
else
|
174
|
+
return []
|
175
|
+
end
|
176
|
+
end
|
177
|
+
statement_start_line_indexes = [] # 0-indexed
|
178
|
+
allow_expression_after_line_number = 0 # 1-indexed
|
179
|
+
last_do_keyword_line_number = nil # 1-indexed, like Ripper.lex
|
180
|
+
|
181
|
+
to_ruby_string = to_ruby_lines.join("\n")
|
182
|
+
if RUBY_VERSION < '3.1'
|
183
|
+
# Ruby 2.6's Ripper has issues when it encounters a else, when, elsif without a matching if/case before.
|
184
|
+
# It literally stop lexing at that point without any error.
|
185
|
+
# Ex from 2.7.8:
|
186
|
+
# require 'ripper'
|
187
|
+
# Ripper.lex("a\nelse\nb")
|
188
|
+
# #=> [[[1, 0], :on_ident, "a", CMDARG], [[1, 1], :on_nl, "\n", BEG], [[2, 0], :on_kw, "else", BEG]]
|
189
|
+
# So we add enough ifs to last quite a few layer. Hopefully enough for all needs. To clarify, there would need
|
190
|
+
# as many "end" keyword in a single ScriptChunk followed by one of the problematic keyword for the problem
|
191
|
+
# to show up.
|
192
|
+
# Considering that a `end` without anything else on the line is removed from to_ruby_lines before getting here
|
193
|
+
# (in format_ruby_lines_to_haml_lines), 10 ifs should be plenty.
|
194
|
+
to_ruby_string = ('if a;' * 10) + to_ruby_string
|
195
|
+
end
|
196
|
+
|
197
|
+
last_line_number_seen = nil
|
198
|
+
Ripper.lex(to_ruby_string).each do |start_loc, token, str|
|
199
|
+
last_line_number_seen = start_loc[0]
|
200
|
+
if token == :on_nl
|
201
|
+
# :on_nl happens when we have a meaningful line change.
|
202
|
+
allow_expression_after_line_number = start_loc[0]
|
203
|
+
next
|
204
|
+
elsif token == :on_ignored_nl
|
205
|
+
# :on_ignored_nl happens for newlines within an expression, or consecutive newlines..
|
206
|
+
# and some cases we care about such as a newline after the pipes after arguments of a block
|
207
|
+
if last_do_keyword_line_number == start_loc[0]
|
208
|
+
# When starting a block, Ripper.lex gives :on_ignored_nl
|
209
|
+
allow_expression_after_line_number = start_loc[0]
|
210
|
+
end
|
211
|
+
next
|
212
|
+
end
|
213
|
+
|
214
|
+
if allow_expression_after_line_number && str[/\S/]
|
215
|
+
if allow_expression_after_line_number < start_loc[0]
|
216
|
+
# Ripper.lex returns line numbers 1-indexed, we want 0-indexed
|
217
|
+
statement_start_line_indexes << start_loc[0] - 1
|
218
|
+
end
|
219
|
+
allow_expression_after_line_number = nil
|
220
|
+
end
|
221
|
+
|
222
|
+
if token == :on_comment
|
223
|
+
# :on_comment contain its own newline at the end of the content
|
224
|
+
allow_expression_after_line_number = start_loc[0]
|
225
|
+
elsif token == :on_kw
|
226
|
+
if str == 'do'
|
227
|
+
# Because of the possible arguments for the block, we can't simply set is_between_expressions to true
|
228
|
+
last_do_keyword_line_number = start_loc[0]
|
229
|
+
elsif ALLOW_EXPRESSION_AFTER_LINE_ENDING_WITH.include?(str)
|
230
|
+
allow_expression_after_line_number = start_loc[0]
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# number is 1-indexed, and we want the line after it, so that's great
|
236
|
+
if last_line_number_seen < to_ruby_lines.size && to_ruby_lines[last_line_number_seen..].any? { |l| l[/\S/] }
|
237
|
+
# There are non-empty lines after the last line Ripper showed us, that's a problem!
|
238
|
+
msg = +'It seems Ripper did not properly process some source code. Please make sure you are on the '
|
239
|
+
msg << 'latest Haml-Lint version, then create an issue at '
|
240
|
+
msg << "https://github.com/sds/haml-lint/issues and include the following information:\n"
|
241
|
+
msg << "Ruby version: #{RUBY_VERSION}\n"
|
242
|
+
msg << "Haml-Lint version: #{HamlLint::VERSION}\n"
|
243
|
+
msg << "HAML version: #{Haml::VERSION}\n"
|
244
|
+
msg << "problematic source code:\n```\n#{to_ruby_lines.join("\n")}\n```"
|
245
|
+
raise msg
|
246
|
+
end
|
247
|
+
|
248
|
+
statement_start_line_indexes
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HamlLint::RubyExtraction
|
4
|
+
# Chunk for handling the a tag attributes, such as `%div{style: 'yes_please'}`
|
5
|
+
class TagAttributesChunk < BaseChunk
|
6
|
+
def initialize(*args, indent_to_remove:, **kwargs)
|
7
|
+
super(*args, **kwargs)
|
8
|
+
@indent_to_remove = indent_to_remove
|
9
|
+
end
|
10
|
+
|
11
|
+
def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines) # rubocop:disable Metrics
|
12
|
+
return if @ruby_lines == to_ruby_lines
|
13
|
+
|
14
|
+
affected_haml_lines = haml_lines[@haml_line_index..haml_end_line_index]
|
15
|
+
|
16
|
+
affected_haml = affected_haml_lines.join("\n")
|
17
|
+
|
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
|
+
|
29
|
+
to_ruby = unwrap(to_ruby_lines).join("\n")
|
30
|
+
|
31
|
+
affected_start_index = affected_haml.index(from_ruby)
|
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
|
+
|
41
|
+
affected_haml[affected_start_index...affected_end_index] = to_ruby
|
42
|
+
|
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
|
48
|
+
end
|
49
|
+
|
50
|
+
def unwrap(lines)
|
51
|
+
lines = lines.dup
|
52
|
+
lines[0] = lines[0].sub(/^\s*/, '').sub(/W+\(/, '')
|
53
|
+
lines[-1] = lines[-1].sub(/\)\s*\Z/, '')
|
54
|
+
|
55
|
+
if @indent_to_remove
|
56
|
+
HamlLint::Utils.map_after_first!(lines) do |line|
|
57
|
+
line.sub(/^ {1,#{@indent_to_remove}}/, '')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
lines
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|