haml_lint 0.73.0 → 0.74.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/default.yml +9 -4
- data/lib/haml_lint/cli.rb +5 -1
- data/lib/haml_lint/document.rb +0 -5
- data/lib/haml_lint/linter/class_attribute_with_static_value.rb +51 -11
- data/lib/haml_lint/linter/classes_before_ids.rb +16 -1
- data/lib/haml_lint/linter/consecutive_comments.rb +20 -1
- data/lib/haml_lint/linter/empty_object_reference.rb +16 -1
- data/lib/haml_lint/linter/empty_script.rb +31 -1
- data/lib/haml_lint/linter/final_newline.rb +31 -7
- data/lib/haml_lint/linter/implicit_div.rb +14 -1
- data/lib/haml_lint/linter/leading_comment_space.rb +13 -1
- data/lib/haml_lint/linter/multiline_script.rb +65 -5
- data/lib/haml_lint/linter/rubocop.rb +5 -17
- data/lib/haml_lint/linter/ruby_comments.rb +13 -3
- data/lib/haml_lint/linter/space_before_script.rb +36 -3
- data/lib/haml_lint/linter/space_inside_hash_attributes.rb +41 -3
- data/lib/haml_lint/linter/tag_name.rb +14 -1
- data/lib/haml_lint/linter/trailing_empty_lines.rb +13 -1
- data/lib/haml_lint/linter/trailing_whitespace.rb +15 -2
- data/lib/haml_lint/linter/unescaped_html.rb +27 -0
- data/lib/haml_lint/linter/unnecessary_interpolation.rb +30 -3
- data/lib/haml_lint/linter/unnecessary_string_output.rb +34 -2
- data/lib/haml_lint/linter.rb +111 -0
- data/lib/haml_lint/reporter/disabled_config_reporter.rb +26 -5
- data/lib/haml_lint/reporter/github_reporter.rb +26 -8
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +27 -3
- data/lib/haml_lint/runner.rb +17 -3
- data/lib/haml_lint/source.rb +2 -6
- data/lib/haml_lint/tree/filter_node.rb +13 -0
- data/lib/haml_lint/tree/tag_node.rb +55 -23
- data/lib/haml_lint/version.rb +1 -1
- metadata +3 -2
|
@@ -6,6 +6,8 @@ module HamlLint
|
|
|
6
6
|
class Linter::SpaceInsideHashAttributes < Linter
|
|
7
7
|
include LinterRegistry
|
|
8
8
|
|
|
9
|
+
supports_autocorrect(true)
|
|
10
|
+
|
|
9
11
|
STYLE = {
|
|
10
12
|
'no_space' => {
|
|
11
13
|
start_regex: /\A\{[^ ]/,
|
|
@@ -24,11 +26,47 @@ module HamlLint
|
|
|
24
26
|
def visit_tag(node)
|
|
25
27
|
return unless node.hash_attributes?
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
style_name = config['style'] == 'no_space' ? 'no_space' : 'space'
|
|
30
|
+
style = STYLE[style_name]
|
|
28
31
|
source = node.hash_attributes_source
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
start_ok = source.match?(style[:start_regex])
|
|
34
|
+
end_ok = source.match?(style[:end_regex])
|
|
35
|
+
return if start_ok && end_ok
|
|
36
|
+
|
|
37
|
+
corrected = correct_hash_spacing(node, source, style_name)
|
|
38
|
+
record_lint(node, style[:start_message], corrected: corrected) unless start_ok
|
|
39
|
+
record_lint(node, style[:end_message], corrected: corrected) unless end_ok
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def correct_hash_spacing(node, source, style_name)
|
|
46
|
+
return false unless source
|
|
47
|
+
return false if source.include?("\n") # multi-line hash: detection-only
|
|
48
|
+
|
|
49
|
+
index = node.line - 1
|
|
50
|
+
line = autocorrected_lines[index]
|
|
51
|
+
return false unless line.include?(source)
|
|
52
|
+
|
|
53
|
+
fixed = corrected_hash_source(source, style_name)
|
|
54
|
+
return false if fixed == source
|
|
55
|
+
|
|
56
|
+
correct_line(index, line.sub(source) { fixed })
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [String]
|
|
60
|
+
def corrected_hash_source(source, style_name)
|
|
61
|
+
inner = source[1...-1].strip
|
|
62
|
+
|
|
63
|
+
if style_name == 'no_space'
|
|
64
|
+
"{#{inner}}"
|
|
65
|
+
elsif inner.empty?
|
|
66
|
+
'{ }'
|
|
67
|
+
else
|
|
68
|
+
"{ #{inner} }"
|
|
69
|
+
end
|
|
32
70
|
end
|
|
33
71
|
end
|
|
34
72
|
end
|
|
@@ -5,11 +5,24 @@ module HamlLint
|
|
|
5
5
|
class Linter::TagName < Linter
|
|
6
6
|
include LinterRegistry
|
|
7
7
|
|
|
8
|
+
supports_autocorrect(true)
|
|
9
|
+
|
|
8
10
|
def visit_tag(node)
|
|
9
11
|
tag = node.tag_name
|
|
10
12
|
return unless /[A-Z]/.match?(tag)
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
corrected = correct_tag_name(node, tag)
|
|
15
|
+
record_lint(node, "`#{tag}` should be written in lowercase as `#{tag.downcase}`",
|
|
16
|
+
corrected: corrected)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# @return [Boolean]
|
|
22
|
+
def correct_tag_name(node, tag)
|
|
23
|
+
index = node.line - 1
|
|
24
|
+
line = autocorrected_lines[index]
|
|
25
|
+
correct_line(index, line.sub(/(%)#{Regexp.escape(tag)}/, "\\1#{tag.downcase}"))
|
|
13
26
|
end
|
|
14
27
|
end
|
|
15
28
|
end
|
|
@@ -5,6 +5,8 @@ module HamlLint
|
|
|
5
5
|
class Linter::TrailingEmptyLines < Linter
|
|
6
6
|
include LinterRegistry
|
|
7
7
|
|
|
8
|
+
supports_autocorrect(true)
|
|
9
|
+
|
|
8
10
|
DummyNode = Struct.new(:line)
|
|
9
11
|
|
|
10
12
|
def visit_root(root)
|
|
@@ -16,7 +18,17 @@ module HamlLint
|
|
|
16
18
|
|
|
17
19
|
return unless document.source.end_with?("\n\n")
|
|
18
20
|
|
|
19
|
-
record_lint(line_number, 'Files should not end with trailing empty lines'
|
|
21
|
+
record_lint(line_number, 'Files should not end with trailing empty lines',
|
|
22
|
+
corrected: autocorrect?)
|
|
23
|
+
|
|
24
|
+
apply_autocorrect(corrected_source)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def corrected_source
|
|
30
|
+
last_non_empty_index = document.source_lines.rindex { |line| !line.empty? } || 0
|
|
31
|
+
"#{document.source_lines[0..last_non_empty_index].join("\n")}\n"
|
|
20
32
|
end
|
|
21
33
|
end
|
|
22
34
|
end
|
|
@@ -5,17 +5,30 @@ module HamlLint
|
|
|
5
5
|
class Linter::TrailingWhitespace < Linter
|
|
6
6
|
include LinterRegistry
|
|
7
7
|
|
|
8
|
+
supports_autocorrect(true)
|
|
9
|
+
|
|
8
10
|
DummyNode = Struct.new(:line)
|
|
9
11
|
|
|
10
12
|
def visit_root(root)
|
|
13
|
+
new_lines = document.source_lines.dup
|
|
14
|
+
changed = false
|
|
15
|
+
|
|
11
16
|
document.source_lines.each_with_index do |line, index|
|
|
12
17
|
next unless /\s+$/.match?(line)
|
|
13
18
|
|
|
14
19
|
node = root.node_for_line(index + 1)
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
next if node.disabled?(self)
|
|
21
|
+
|
|
22
|
+
if autocorrect?
|
|
23
|
+
new_lines[index] = line.sub(/\s+$/, '')
|
|
24
|
+
changed = true
|
|
17
25
|
end
|
|
26
|
+
|
|
27
|
+
record_lint(DummyNode.new(index + 1), 'Line contains trailing whitespace',
|
|
28
|
+
corrected: autocorrect?)
|
|
18
29
|
end
|
|
30
|
+
|
|
31
|
+
apply_autocorrect(new_lines.join("\n")) if changed
|
|
19
32
|
end
|
|
20
33
|
end
|
|
21
34
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HamlLint
|
|
4
|
+
# Flags HAML's unescaped-output markers (`!=`, `!~`, and the unescaped
|
|
5
|
+
# plain-text `!`), which bypass HTML escaping.
|
|
6
|
+
#
|
|
7
|
+
# Like `raw`, `html_safe`, and `h()` in Rails, these make it easy to
|
|
8
|
+
# accidentally introduce XSS vulnerabilities when the output includes
|
|
9
|
+
# user-controlled data, e.g.:
|
|
10
|
+
#
|
|
11
|
+
# != "Username: <strong>#{user.name}</strong>"
|
|
12
|
+
class Linter::UnescapedHtml < Linter
|
|
13
|
+
include LinterRegistry
|
|
14
|
+
|
|
15
|
+
MESSAGE =
|
|
16
|
+
'Avoid outputting unescaped HTML with `!`; it bypasses HTML escaping and ' \
|
|
17
|
+
'can introduce XSS vulnerabilities. Sanitize the value instead.'
|
|
18
|
+
|
|
19
|
+
def visit_script(node)
|
|
20
|
+
record_lint(node, MESSAGE) if /\A\s*!/.match?(node.source_code)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visit_tag(node)
|
|
24
|
+
record_lint(node, MESSAGE) if node.unescape_html?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -11,21 +11,48 @@ module HamlLint
|
|
|
11
11
|
class Linter::UnnecessaryInterpolation < Linter
|
|
12
12
|
include LinterRegistry
|
|
13
13
|
|
|
14
|
+
supports_autocorrect(true)
|
|
15
|
+
|
|
14
16
|
def visit_tag(node)
|
|
15
17
|
return if node.script.length <= 2
|
|
16
18
|
|
|
17
19
|
count = 0
|
|
18
20
|
chars = 2 # Include surrounding quote chars
|
|
19
|
-
|
|
21
|
+
interpolated_code = nil
|
|
22
|
+
HamlLint::Utils.extract_interpolated_values(node.script) do |code, _line|
|
|
20
23
|
count += 1
|
|
21
24
|
return if count > 1 # rubocop:disable Lint/NonLocalExitFromIterator
|
|
22
|
-
chars +=
|
|
25
|
+
chars += code.length + 3
|
|
26
|
+
interpolated_code = code
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
if chars == node.script.length
|
|
30
|
+
corrected = correct_interpolation(node, interpolated_code)
|
|
26
31
|
record_lint(node, '`%... \#{expression}` can be written without ' \
|
|
27
|
-
'interpolation as `%...= expression`')
|
|
32
|
+
'interpolation as `%...= expression`', corrected: corrected)
|
|
28
33
|
end
|
|
29
34
|
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def correct_interpolation(node, interpolated_code)
|
|
40
|
+
return false unless interpolated_code
|
|
41
|
+
|
|
42
|
+
index = node.line - 1
|
|
43
|
+
line = autocorrected_lines[index]
|
|
44
|
+
escaped = Regexp.escape(interpolated_code)
|
|
45
|
+
|
|
46
|
+
new_line =
|
|
47
|
+
if inline_content_is_string?(node)
|
|
48
|
+
line.sub(/=\s*"\#\{#{escaped}\}"\s*\z/, "= #{interpolated_code}")
|
|
49
|
+
else
|
|
50
|
+
line.sub(/\s+\#\{#{escaped}\}\s*\z/, "= #{interpolated_code}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return false if new_line == line
|
|
54
|
+
|
|
55
|
+
correct_line(index, new_line)
|
|
56
|
+
end
|
|
30
57
|
end
|
|
31
58
|
end
|
|
@@ -33,8 +33,11 @@ module HamlLint
|
|
|
33
33
|
|
|
34
34
|
def outputs_string_literal?(script_node)
|
|
35
35
|
return unless tree = parse_ruby(script_node.script)
|
|
36
|
-
%i[str dstr].include?(tree.type)
|
|
37
|
-
|
|
36
|
+
return unless %i[str dstr].include?(tree.type)
|
|
37
|
+
|
|
38
|
+
!starts_with_reserved_character?(tree.children.first) &&
|
|
39
|
+
!contains_escape_sequence?(tree) &&
|
|
40
|
+
!contains_significant_whitespace?(tree)
|
|
38
41
|
rescue ::Parser::SyntaxError
|
|
39
42
|
# Gracefully ignore syntax errors, as that's managed by a different linter
|
|
40
43
|
end
|
|
@@ -45,5 +48,34 @@ module HamlLint
|
|
|
45
48
|
string = stringish.respond_to?(:children) ? stringish.children.first : stringish
|
|
46
49
|
string =~ %r{\A\s*[/#-=%~]} if string.is_a?(String)
|
|
47
50
|
end
|
|
51
|
+
|
|
52
|
+
# The ordered segments of a string literal, including any interpolation.
|
|
53
|
+
# A plain `str` node has no interpolation, so it is its own only segment.
|
|
54
|
+
def string_segments(tree)
|
|
55
|
+
tree.type == :dstr ? tree.children : [tree]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns whether any literal portion of the string contains a backslash
|
|
59
|
+
# escape (e.g. `\n`, `\t`, `\u202F`). Such escapes are interpreted inside
|
|
60
|
+
# a Ruby string but would be emitted verbatim as HAML plain text, so the
|
|
61
|
+
# `= "..."` form is not equivalent to the unwrapped plain text.
|
|
62
|
+
def contains_escape_sequence?(tree)
|
|
63
|
+
string_segments(tree).any? do |segment|
|
|
64
|
+
segment.type == :str && segment.location.expression.source.include?('\\')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns whether the string begins or ends with whitespace. HAML strips
|
|
69
|
+
# trailing whitespace from plain text (and leading whitespace denotes
|
|
70
|
+
# indentation), so unwrapping such a string would change the output.
|
|
71
|
+
def contains_significant_whitespace?(tree)
|
|
72
|
+
segments = string_segments(tree)
|
|
73
|
+
bounded_by_whitespace?(segments.first, /\A\s/) ||
|
|
74
|
+
bounded_by_whitespace?(segments.last, /\s\z/)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def bounded_by_whitespace?(segment, pattern)
|
|
78
|
+
segment.type == :str && segment.children.first.to_s.match?(pattern)
|
|
79
|
+
end
|
|
48
80
|
end
|
|
49
81
|
end
|
data/lib/haml_lint/linter.rb
CHANGED
|
@@ -63,6 +63,7 @@ module HamlLint
|
|
|
63
63
|
@document = document
|
|
64
64
|
@lints = []
|
|
65
65
|
@autocorrect = autocorrect
|
|
66
|
+
reset_autocorrect_state
|
|
66
67
|
visit(document.tree)
|
|
67
68
|
@lints
|
|
68
69
|
end
|
|
@@ -85,6 +86,30 @@ module HamlLint
|
|
|
85
86
|
self.class.supports_autocorrect?
|
|
86
87
|
end
|
|
87
88
|
|
|
89
|
+
# Returns whether this linter's autocorrect is safe. Defaults to true (safe)
|
|
90
|
+
# unless the linter declares otherwise via `autocorrect_safe(false)`.
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def self.autocorrect_safe?
|
|
94
|
+
@autocorrect_safe != false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The autocorrect ordering priority for this linter. During autocorrect,
|
|
98
|
+
# linters with a lower priority run first and higher priority run later,
|
|
99
|
+
# against the same (already mutated) document. Linters with the same priority
|
|
100
|
+
# keep their default (alphabetical) order.
|
|
101
|
+
#
|
|
102
|
+
# Called with an argument (e.g. `autocorrect_priority(1)`) in a linter's
|
|
103
|
+
# top-level scope, it sets the priority; called with no argument it reads it.
|
|
104
|
+
# The default, when never set, is 0.
|
|
105
|
+
#
|
|
106
|
+
# @param value [Integer, nil] the new priority, or nil to read the current one
|
|
107
|
+
# @return [Integer]
|
|
108
|
+
def self.autocorrect_priority(value = nil)
|
|
109
|
+
@autocorrect_priority = value unless value.nil?
|
|
110
|
+
@autocorrect_priority || 0
|
|
111
|
+
end
|
|
112
|
+
|
|
88
113
|
private
|
|
89
114
|
|
|
90
115
|
attr_reader :config, :document
|
|
@@ -97,6 +122,92 @@ module HamlLint
|
|
|
97
122
|
@supports_autocorrect = value
|
|
98
123
|
end
|
|
99
124
|
|
|
125
|
+
# Linters can call autocorrect_safe(false) in their top-level scope to declare that their
|
|
126
|
+
# autocorrect is unsafe, meaning it only runs under `--auto-correct-all` (`:all`).
|
|
127
|
+
# The default, when not called, is safe.
|
|
128
|
+
#
|
|
129
|
+
# @params value [Boolean] The new value for autocorrect_safe
|
|
130
|
+
private_class_method def self.autocorrect_safe(value)
|
|
131
|
+
@autocorrect_safe = value
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Resets the per-run autocorrect bookkeeping. Linter instances are reused
|
|
135
|
+
# across files, so subclasses that accumulate extra autocorrect state during
|
|
136
|
+
# a traversal (e.g. a list of lines to merge or delete) must override this,
|
|
137
|
+
# calling `super`, to clear that state between files. Otherwise corrections
|
|
138
|
+
# from one file leak into the next.
|
|
139
|
+
def reset_autocorrect_state
|
|
140
|
+
@autocorrected_lines = nil
|
|
141
|
+
@autocorrect_changed = false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Whether the linter is currently allowed to apply autocorrections, given the active
|
|
145
|
+
# autocorrect mode and this linter's declared safety:
|
|
146
|
+
#
|
|
147
|
+
# * safe linters correct under both `:safe` and `:all`;
|
|
148
|
+
# * unsafe linters correct only under `:all`;
|
|
149
|
+
# * with no mode (`nil`) nothing is corrected (detection only).
|
|
150
|
+
#
|
|
151
|
+
# @return [Boolean]
|
|
152
|
+
def autocorrect?
|
|
153
|
+
case @autocorrect
|
|
154
|
+
when :all
|
|
155
|
+
true
|
|
156
|
+
when :safe
|
|
157
|
+
self.class.autocorrect_safe?
|
|
158
|
+
else
|
|
159
|
+
false
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Applies a corrected, full-document source to the document through the single
|
|
164
|
+
# mutation path (`Document#change_source`), but only when the safety gate permits.
|
|
165
|
+
# No-ops otherwise; `change_source` itself also no-ops when the source is unchanged.
|
|
166
|
+
#
|
|
167
|
+
# @param new_source [String] the corrected HAML source
|
|
168
|
+
def apply_autocorrect(new_source)
|
|
169
|
+
return unless autocorrect?
|
|
170
|
+
document.change_source(new_source)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Lazily-initialized working copy of the document's source lines, used to
|
|
174
|
+
# accumulate line-level corrections during a single tree traversal. Editing
|
|
175
|
+
# this copy (rather than calling `apply_autocorrect` per node) avoids
|
|
176
|
+
# reparsing the document mid-walk, which would invalidate the tree being
|
|
177
|
+
# visited.
|
|
178
|
+
#
|
|
179
|
+
# @return [Array<String>]
|
|
180
|
+
def autocorrected_lines
|
|
181
|
+
@autocorrected_lines ||= document.source_lines.dup
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Replaces a single source line (0-indexed) in the working copy, but only
|
|
185
|
+
# when autocorrect is permitted and the new text actually differs.
|
|
186
|
+
#
|
|
187
|
+
# @param index [Integer] the 0-indexed line to replace
|
|
188
|
+
# @param new_text [String] the corrected line
|
|
189
|
+
# @return [Boolean] true if a change was recorded, false otherwise
|
|
190
|
+
def correct_line(index, new_text)
|
|
191
|
+
return false unless autocorrect?
|
|
192
|
+
return false if autocorrected_lines[index] == new_text
|
|
193
|
+
|
|
194
|
+
autocorrected_lines[index] = new_text
|
|
195
|
+
@autocorrect_changed = true
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Flushes any corrections accumulated via `correct_line` through the single
|
|
199
|
+
# mutation path, once, after the whole tree has been visited. Defined on the
|
|
200
|
+
# base so per-node linters need no boilerplate; no-ops for linters that never
|
|
201
|
+
# accumulated a change (including those that apply within `visit_root`).
|
|
202
|
+
#
|
|
203
|
+
# NOTE: a linter that overrides `after_visit_root` must call `super`, or
|
|
204
|
+
# accumulated corrections will not be applied.
|
|
205
|
+
def after_visit_root(_node)
|
|
206
|
+
return unless @autocorrect_changed
|
|
207
|
+
|
|
208
|
+
apply_autocorrect(autocorrected_lines.join("\n"))
|
|
209
|
+
end
|
|
210
|
+
|
|
100
211
|
# Record a lint for reporting back to the user.
|
|
101
212
|
#
|
|
102
213
|
# @param node_or_line [#line] line number or node to extract the line number from
|
|
@@ -5,10 +5,12 @@ require_relative 'progress_reporter'
|
|
|
5
5
|
module HamlLint
|
|
6
6
|
# Outputs a YAML configuration file based on existing violations.
|
|
7
7
|
class Reporter::DisabledConfigReporter < Reporter::ProgressReporter
|
|
8
|
-
|
|
8
|
+
DEFAULT_EXCLUDE_LIMIT = 15
|
|
9
|
+
|
|
10
|
+
HEADING_TEMPLATE =
|
|
9
11
|
['# This configuration was generated by',
|
|
10
|
-
'# `
|
|
11
|
-
|
|
12
|
+
'# `%<command>s`',
|
|
13
|
+
'# on %<timestamp>s using Haml-Lint version %<version>s.',
|
|
12
14
|
'# The point is for the user to remove these configuration records',
|
|
13
15
|
'# one by one as the lints are removed from the code base.',
|
|
14
16
|
'# Note that changes in the inspected code, or installation of new',
|
|
@@ -25,11 +27,12 @@ module HamlLint
|
|
|
25
27
|
# Create the reporter that will display the report and write the config.
|
|
26
28
|
#
|
|
27
29
|
# @param _log [HamlLint::Logger]
|
|
28
|
-
def initialize(log, limit:
|
|
30
|
+
def initialize(log, limit: DEFAULT_EXCLUDE_LIMIT, options: {})
|
|
29
31
|
super(log)
|
|
30
32
|
@linters_with_lints = Hash.new { |hash, key| hash[key] = [] }
|
|
31
33
|
@linters_lint_count = Hash.new(0)
|
|
32
34
|
@exclude_limit = limit
|
|
35
|
+
@options = options
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# A hash of linters with the files that have that type of lint.
|
|
@@ -81,12 +84,30 @@ module HamlLint
|
|
|
81
84
|
|
|
82
85
|
private
|
|
83
86
|
|
|
87
|
+
# Reconstructs the CLI command used to generate this config.
|
|
88
|
+
#
|
|
89
|
+
# @return [String] the command string
|
|
90
|
+
def command
|
|
91
|
+
cmd = "#{HamlLint::APP_NAME} --auto-gen-config"
|
|
92
|
+
if @options[:auto_gen_exclude_limit]
|
|
93
|
+
cmd += " --auto-gen-exclude-limit #{@options[:auto_gen_exclude_limit]}"
|
|
94
|
+
end
|
|
95
|
+
cmd
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The heading comment for the generated config file.
|
|
99
|
+
#
|
|
100
|
+
# @return [String]
|
|
101
|
+
def heading
|
|
102
|
+
format(HEADING_TEMPLATE, command: command, timestamp: Time.now, version: HamlLint::VERSION)
|
|
103
|
+
end
|
|
104
|
+
|
|
84
105
|
# The contents of the generated configuration file based on captured lint.
|
|
85
106
|
#
|
|
86
107
|
# @return [String] a Yaml-formatted configuration file's contents
|
|
87
108
|
def config_file_contents
|
|
88
109
|
output = []
|
|
89
|
-
output <<
|
|
110
|
+
output << heading
|
|
90
111
|
output << 'linters:' if linters_with_lints.any?
|
|
91
112
|
linters_with_lints.sort.each do |linter, files|
|
|
92
113
|
output << generate_config_for_linter(linter, files)
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
module HamlLint
|
|
4
4
|
# Outputs GitHub workflow commands for GitHub check annotations when run within GitHub actions.
|
|
5
5
|
class Reporter::GithubReporter < Reporter
|
|
6
|
+
# Characters to escape within a command message.
|
|
6
7
|
ESCAPE_MAP = { '%' => '%25', "\n" => '%0A', "\r" => '%0D' }.freeze
|
|
8
|
+
# Property values also escape the `:` and `,` that delimit the command.
|
|
9
|
+
PROPERTY_ESCAPE_MAP = ESCAPE_MAP.merge(':' => '%3A', ',' => '%2C').freeze
|
|
7
10
|
|
|
8
11
|
include Reporter::Utils
|
|
9
12
|
|
|
10
13
|
def added_lint(lint, report)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
else
|
|
14
|
-
print_workflow_command(severity: 'warning', lint: lint)
|
|
15
|
-
end
|
|
14
|
+
severity = lint.severity >= report.fail_level ? 'error' : 'warning'
|
|
15
|
+
print_workflow_command(lint, severity)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def display_report(report)
|
|
@@ -21,12 +21,30 @@ module HamlLint
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
-
def print_workflow_command(lint
|
|
25
|
-
log.log "::#{severity} file=#{lint.filename},
|
|
24
|
+
def print_workflow_command(lint, severity)
|
|
25
|
+
log.log "::#{severity} file=#{github_escape_property(lint.filename)}," \
|
|
26
|
+
"line=#{lint.line},title=#{github_escape_property(annotation_title(lint))}" \
|
|
27
|
+
"::#{github_escape(annotation_message(lint))}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Annotation title, naming the linter that produced the lint when known.
|
|
31
|
+
def annotation_title(lint)
|
|
32
|
+
lint.linter ? "haml-lint #{lint.linter.name}" : 'haml-lint'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Message body, prefixed with the location and linter so it stays useful in the plain log.
|
|
36
|
+
def annotation_message(lint)
|
|
37
|
+
location = "#{lint.filename}:#{lint.line}"
|
|
38
|
+
location += " #{lint.linter.name}:" if lint.linter
|
|
39
|
+
"#{location} #{lint.message}"
|
|
26
40
|
end
|
|
27
41
|
|
|
28
42
|
def github_escape(string)
|
|
29
|
-
string.gsub(Regexp.union(ESCAPE_MAP.keys), ESCAPE_MAP)
|
|
43
|
+
string.to_s.gsub(Regexp.union(ESCAPE_MAP.keys), ESCAPE_MAP)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def github_escape_property(string)
|
|
47
|
+
string.to_s.gsub(Regexp.union(PROPERTY_ESCAPE_MAP.keys), PROPERTY_ESCAPE_MAP)
|
|
30
48
|
end
|
|
31
49
|
end
|
|
32
50
|
end
|
|
@@ -300,7 +300,13 @@ module HamlLint::RubyExtraction
|
|
|
300
300
|
end
|
|
301
301
|
|
|
302
302
|
if attributes_code&.start_with?('{')
|
|
303
|
-
#
|
|
303
|
+
# HTML-style (parens) attributes, e.g. %div(foo=foo), arrive as a synthesized hash string
|
|
304
|
+
# like '{"foo" => foo,}'. That text isn't present verbatim in the HAML, so we can't map
|
|
305
|
+
# RuboCop corrections back and intentionally don't autocorrect it. We still extract the
|
|
306
|
+
# attribute *values* as a non-correctable chunk so RuboCop sees variables used here;
|
|
307
|
+
# otherwise they look unused (false Lint/UselessAssignment, plus unsafe autocorrect that
|
|
308
|
+
# removes the assignment). See issue #648.
|
|
309
|
+
add_html_attributes_value_usage(node, attributes_code, indent: indent)
|
|
304
310
|
attributes_code = nil
|
|
305
311
|
end
|
|
306
312
|
|
|
@@ -345,6 +351,22 @@ module HamlLint::RubyExtraction
|
|
|
345
351
|
final_line_index
|
|
346
352
|
end
|
|
347
353
|
|
|
354
|
+
# For HTML-style (parens) attributes, adds the attribute value expressions as a
|
|
355
|
+
# non-autocorrectable chunk purely so RuboCop counts the variables/methods used there.
|
|
356
|
+
def add_html_attributes_value_usage(node, attributes_code, indent:)
|
|
357
|
+
ast = parse_ruby(attributes_code)
|
|
358
|
+
return unless ast&.type == :hash
|
|
359
|
+
|
|
360
|
+
value_sources = ast.children.filter_map do |pair|
|
|
361
|
+
next unless pair.type == :pair
|
|
362
|
+
pair.children[1]&.source
|
|
363
|
+
end
|
|
364
|
+
return if value_sources.empty?
|
|
365
|
+
|
|
366
|
+
lines = value_sources.map { |value| "#{' ' * indent}#{script_output_prefix}#{value}" }
|
|
367
|
+
@ruby_chunks << AdHocChunk.new(node, lines)
|
|
368
|
+
end
|
|
369
|
+
|
|
348
370
|
# Visiting the script besides tag. The part to the right of the equal sign of
|
|
349
371
|
# lines looking like ` %div= foo(bar)`
|
|
350
372
|
def visit_tag_script(node, line_index:, indent:)
|
|
@@ -392,8 +414,10 @@ module HamlLint::RubyExtraction
|
|
|
392
414
|
filter_name_indent = @original_haml_lines[node.line - 1].index(/\S/)
|
|
393
415
|
if node.filter_type == 'ruby'
|
|
394
416
|
# The indentation in node.text is normalized, so that at least one line
|
|
395
|
-
# is indented by 0.
|
|
396
|
-
|
|
417
|
+
# is indented by 0. Split("\n", -1) + pop keeps trailing blank lines (plain split drops them),
|
|
418
|
+
# which the end marker needs so it doesn't land right after the last code line.
|
|
419
|
+
lines = node.text.split("\n", -1)
|
|
420
|
+
lines.pop
|
|
397
421
|
lines.map! do |line|
|
|
398
422
|
if !/\S/.match?(line)
|
|
399
423
|
# whitespace or empty
|
data/lib/haml_lint/runner.rb
CHANGED
|
@@ -23,7 +23,8 @@ module HamlLint
|
|
|
23
23
|
def run(options = {})
|
|
24
24
|
@config = load_applicable_config(options)
|
|
25
25
|
@sources = extract_applicable_sources(config, options)
|
|
26
|
-
@
|
|
26
|
+
@options = options
|
|
27
|
+
@linter_selector = build_linter_selector
|
|
27
28
|
@fail_fast = options.fetch(:fail_fast, false)
|
|
28
29
|
@cache = {}
|
|
29
30
|
@autocorrect = options[:autocorrect]
|
|
@@ -61,6 +62,17 @@ module HamlLint
|
|
|
61
62
|
# @return [HamlLint::LinterSelector]
|
|
62
63
|
attr_reader :linter_selector
|
|
63
64
|
|
|
65
|
+
# Returns a fresh selector for this run.
|
|
66
|
+
#
|
|
67
|
+
# LinterSelector memoizes linter instances, and linters mutate instance
|
|
68
|
+
# state while processing a document. JRuby runs Parallel.map in threads, so
|
|
69
|
+
# parallel jobs must not share a selector.
|
|
70
|
+
#
|
|
71
|
+
# @return [HamlLint::LinterSelector]
|
|
72
|
+
def build_linter_selector
|
|
73
|
+
HamlLint::LinterSelector.new(config, @options)
|
|
74
|
+
end
|
|
75
|
+
|
|
64
76
|
# Returns the {HamlLint::Configuration} that should be used given the
|
|
65
77
|
# specified options.
|
|
66
78
|
#
|
|
@@ -89,7 +101,6 @@ module HamlLint
|
|
|
89
101
|
begin
|
|
90
102
|
document = HamlLint::Document.new source.contents, file: source.path,
|
|
91
103
|
config: config,
|
|
92
|
-
file_on_disk: !source.stdin?,
|
|
93
104
|
write_to_stdout: @autocorrect_stdout
|
|
94
105
|
rescue HamlLint::Exceptions::ParseError => e
|
|
95
106
|
return [HamlLint::Lint.new(HamlLint::Linter::Syntax.new(config), source.path,
|
|
@@ -122,6 +133,9 @@ module HamlLint
|
|
|
122
133
|
lint_arrays = []
|
|
123
134
|
|
|
124
135
|
autocorrecting_linters = linters.select(&:supports_autocorrect?)
|
|
136
|
+
.each_with_index
|
|
137
|
+
.sort_by { |linter, index| [linter.class.autocorrect_priority, index] }
|
|
138
|
+
.map(&:first)
|
|
125
139
|
lint_arrays << autocorrecting_linters.map do |linter|
|
|
126
140
|
linter.run(document, autocorrect: @autocorrect)
|
|
127
141
|
end
|
|
@@ -191,7 +205,7 @@ module HamlLint
|
|
|
191
205
|
# @return [void]
|
|
192
206
|
def warm_cache
|
|
193
207
|
results = Parallel.map(sources) do |source|
|
|
194
|
-
lints = collect_lints(source,
|
|
208
|
+
lints = collect_lints(source, build_linter_selector, config)
|
|
195
209
|
[source.path, lints]
|
|
196
210
|
end
|
|
197
211
|
@cache = results.to_h
|
data/lib/haml_lint/source.rb
CHANGED
|
@@ -13,17 +13,13 @@ module HamlLint
|
|
|
13
13
|
# @param [IO] io
|
|
14
14
|
def initialize(path: nil, io: nil)
|
|
15
15
|
@path = path
|
|
16
|
+
@file_path = File.expand_path(path) if path
|
|
16
17
|
@io = io
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
# @return [String] Contents of the given IO object.
|
|
20
21
|
def contents
|
|
21
|
-
@contents ||= @io&.read || File.read(
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# @return [boolean] true if we're reading from stdin rather than a file path
|
|
25
|
-
def stdin?
|
|
26
|
-
!@io.nil?
|
|
22
|
+
@contents ||= @io&.read || File.read(@file_path)
|
|
27
23
|
end
|
|
28
24
|
end
|
|
29
25
|
end
|
|
@@ -17,5 +17,18 @@ module HamlLint::Tree
|
|
|
17
17
|
|
|
18
18
|
"#{"\n" * nb_blank_lines}#{super}"
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
# The line numbers that are contained within the node.
|
|
22
|
+
#
|
|
23
|
+
# Unlike most nodes, a filter's content begins on the line *after* the
|
|
24
|
+
# `:filtername` declaration, so the span runs from the declaration line
|
|
25
|
+
# through all of the indented content lines.
|
|
26
|
+
#
|
|
27
|
+
# @return [Range]
|
|
28
|
+
def line_numbers
|
|
29
|
+
return super if lines.empty?
|
|
30
|
+
|
|
31
|
+
(line..(line + lines.count))
|
|
32
|
+
end
|
|
20
33
|
end
|
|
21
34
|
end
|