haml_lint 0.40.0 → 0.51.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 +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
|