haml_lint 0.73.0 → 0.75.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 +10 -5
- data/config/forced_rubocop_config.yml +13 -13
- data/lib/haml_lint/cli.rb +5 -1
- data/lib/haml_lint/document.rb +3 -8
- data/lib/haml_lint/exceptions.rb +2 -2
- data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +2 -2
- data/lib/haml_lint/haml_visitor.rb +1 -1
- data/lib/haml_lint/lint.rb +1 -1
- 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 +10 -22
- data/lib/haml_lint/linter/ruby_comments.rb +14 -4
- data/lib/haml_lint/linter/space_before_script.rb +37 -4
- 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 +35 -3
- data/lib/haml_lint/linter.rb +112 -1
- data/lib/haml_lint/node_transformer.rb +2 -2
- 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/base_chunk.rb +3 -3
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +37 -13
- data/lib/haml_lint/ruby_extraction/coordinator.rb +2 -2
- data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +1 -1
- data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +1 -1
- data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +1 -1
- data/lib/haml_lint/ruby_extraction/script_chunk.rb +1 -1
- data/lib/haml_lint/runner.rb +31 -3
- data/lib/haml_lint/source.rb +2 -6
- data/lib/haml_lint/spec/matchers/report_lint.rb +2 -2
- data/lib/haml_lint/tree/comment_node.rb +1 -1
- data/lib/haml_lint/tree/doctype_node.rb +1 -1
- data/lib/haml_lint/tree/filter_node.rb +14 -1
- data/lib/haml_lint/tree/haml_comment_node.rb +2 -2
- data/lib/haml_lint/tree/node.rb +3 -3
- data/lib/haml_lint/tree/root_node.rb +2 -2
- data/lib/haml_lint/tree/silent_script_node.rb +1 -1
- data/lib/haml_lint/tree/tag_node.rb +57 -25
- data/lib/haml_lint/utils.rb +1 -1
- data/lib/haml_lint/version.rb +1 -1
- metadata +3 -2
data/lib/haml_lint/linter.rb
CHANGED
|
@@ -56,13 +56,14 @@ module HamlLint
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Runs the linter against the given Haml document, raises if the file cannot be processed due to
|
|
59
|
-
# Syntax or
|
|
59
|
+
# Syntax or Haml-Lint internal errors. (For testing purposes)
|
|
60
60
|
#
|
|
61
61
|
# @param document [HamlLint::Document]
|
|
62
62
|
def run_or_raise(document, autocorrect: nil)
|
|
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
|
|
@@ -4,7 +4,7 @@ module HamlLint
|
|
|
4
4
|
# Responsible for transforming {Haml::Parser::ParseNode} objects into
|
|
5
5
|
# corresponding {HamlLint::Tree::Node} objects.
|
|
6
6
|
#
|
|
7
|
-
# The parse tree generated by
|
|
7
|
+
# The parse tree generated by Haml has a number of strange cases where certain
|
|
8
8
|
# types of nodes are created that don't necessarily correspond to what one
|
|
9
9
|
# would expect. This class is intended to isolate and handle these cases so
|
|
10
10
|
# that linters don't have to deal with them.
|
|
@@ -16,7 +16,7 @@ module HamlLint
|
|
|
16
16
|
@document = document
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Converts the given
|
|
19
|
+
# Converts the given Haml parse node into its corresponding Haml-Lint parse
|
|
20
20
|
# node.
|
|
21
21
|
#
|
|
22
22
|
# @param haml_node [Haml::Parser::ParseNode]
|
|
@@ -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
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module HamlLint::RubyExtraction
|
|
4
4
|
# This is the base class for all of the Chunks of HamlLint::RubyExtraction.
|
|
5
|
-
# A Chunk represents a part of the
|
|
5
|
+
# A Chunk represents a part of the Haml file that HamlLint::Linter::RuboCop
|
|
6
6
|
# is processing and will insert some Ruby code in a file passed to RuboCop.
|
|
7
7
|
#
|
|
8
|
-
# There are chunks for most
|
|
8
|
+
# There are chunks for most Haml concepts, even if they don't represent Ruby
|
|
9
9
|
# code. For example, there is a chunk that represents a `%div` tag, which
|
|
10
10
|
# uses a `begin` in the generated Ruby to add indentation for the children
|
|
11
|
-
# of the %div in the Ruby file just like there is in the
|
|
11
|
+
# of the %div in the Ruby file just like there is in the Haml file.
|
|
12
12
|
class BaseChunk
|
|
13
13
|
COMMA_CHANGES_LINES = true
|
|
14
14
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
module HamlLint::RubyExtraction
|
|
5
5
|
# Extracts "chunks" of the haml file into instances of subclasses of HamlLint::RubyExtraction::BaseChunk.
|
|
6
6
|
#
|
|
7
|
-
# This is the first step of generating Ruby code from a
|
|
7
|
+
# This is the first step of generating Ruby code from a Haml file to then be processed by RuboCop.
|
|
8
8
|
# See HamlLint::RubyExtraction::BaseChunk for more details.
|
|
9
9
|
class ChunkExtractor
|
|
10
10
|
include HamlLint::HamlVisitor
|
|
@@ -17,7 +17,7 @@ module HamlLint::RubyExtraction
|
|
|
17
17
|
::Haml::Parser.new('', {})
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
#
|
|
20
|
+
# Haml strips newlines when handling multi-line statements (using pipes or trailing comma)
|
|
21
21
|
# We don't. So the regex must be fixed to correctly detect the start of the string.
|
|
22
22
|
BLOCK_KEYWORD_REGEX = Regexp.new(Haml::Parser::BLOCK_KEYWORD_REGEX.source.sub('^', '\A'))
|
|
23
23
|
|
|
@@ -124,7 +124,7 @@ module HamlLint::RubyExtraction
|
|
|
124
124
|
def visit_script(node, &block)
|
|
125
125
|
raw_first_line = @original_haml_lines[node.line - 1]
|
|
126
126
|
|
|
127
|
-
# ==, !, !==, &, &== means interpolation (was needed before
|
|
127
|
+
# ==, !, !==, &, &== means interpolation (was needed before Haml 2.2... it's still supported)
|
|
128
128
|
# =, !=, &= mean actual ruby code is coming
|
|
129
129
|
# Anything else is interpolation
|
|
130
130
|
# The regex lists the case for Ruby Code. The 3 cases and making sure they are not followed by another = sign
|
|
@@ -169,20 +169,20 @@ module HamlLint::RubyExtraction
|
|
|
169
169
|
prev_chunk.node == node.parent
|
|
170
170
|
# When an outputting script is nested under another outputting script,
|
|
171
171
|
# we want to block them from being merged together by rubocop, because
|
|
172
|
-
# this doesn't make sense in
|
|
172
|
+
# this doesn't make sense in Haml.
|
|
173
173
|
# Example:
|
|
174
174
|
# = if this_is_short
|
|
175
175
|
# = this_is_short_too
|
|
176
176
|
# Could become (after RuboCop):
|
|
177
177
|
# HL.out = (HL.out = this_is_short_too if this_is_short)
|
|
178
|
-
# Or in (broken)
|
|
178
|
+
# Or in (broken) Haml style:
|
|
179
179
|
# = this_is_short_too = if this_is_short
|
|
180
180
|
# By forcing this to start a chunk, there will be extra placeholders which
|
|
181
181
|
# blocks rubocop from merging the lines.
|
|
182
182
|
must_start_chunk = true
|
|
183
183
|
elsif script_prefix != '='
|
|
184
184
|
# In the few cases where &= and != are used to start the script,
|
|
185
|
-
# We need to remember and put it back in the final
|
|
185
|
+
# We need to remember and put it back in the final Haml. Fusing scripts together
|
|
186
186
|
# would make that basically impossible. Instead, a script has a "first_output_prefix"
|
|
187
187
|
# field for this specific case
|
|
188
188
|
must_start_chunk = true
|
|
@@ -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:)
|
|
@@ -384,7 +406,7 @@ module HamlLint::RubyExtraction
|
|
|
384
406
|
end
|
|
385
407
|
end
|
|
386
408
|
|
|
387
|
-
# Visiting a
|
|
409
|
+
# Visiting a Haml filter. Lines looking like ` :javascript` and the following lines
|
|
388
410
|
# that are nested.
|
|
389
411
|
def visit_filter(node)
|
|
390
412
|
# For unknown reasons, haml doesn't escape interpolations in filters.
|
|
@@ -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
|
|
@@ -497,14 +521,14 @@ module HamlLint::RubyExtraction
|
|
|
497
521
|
# The first and last lines may not be the complete lines from the Haml, only the Ruby parts
|
|
498
522
|
# and the indentation between the first and last list.
|
|
499
523
|
|
|
500
|
-
#
|
|
524
|
+
# Haml transforms the ruby code in many ways as it parses a document. Often removing lines and/or
|
|
501
525
|
# indentation. This is quite annoying for us since we want the exact layout of the code to analyze it.
|
|
502
526
|
#
|
|
503
527
|
# This function receives the code as haml provides it and the line where it starts. It returns
|
|
504
528
|
# the actual code as it is in the haml file, keeping breaks and indentation for the following lines.
|
|
505
529
|
# In addition, the start position of the code in the first line.
|
|
506
530
|
#
|
|
507
|
-
# The rules for handling multiline code in
|
|
531
|
+
# The rules for handling multiline code in Haml are as follow:
|
|
508
532
|
# * if the line being processed ends with a space and a pipe, then append to the line (without
|
|
509
533
|
# newlines) every following lines that also end with a space and a pipe. This means the last line of
|
|
510
534
|
# the "block" also needs a pipe at the end.
|
|
@@ -703,7 +727,7 @@ module HamlLint::RubyExtraction
|
|
|
703
727
|
|
|
704
728
|
LOOP_KEYWORDS = %w[for until while].freeze
|
|
705
729
|
def self.block_keyword(code)
|
|
706
|
-
# Need to handle 'for'/'while' since regex stolen from
|
|
730
|
+
# Need to handle 'for'/'while' since regex stolen from Haml parser doesn't
|
|
707
731
|
if (keyword = code[/\A\s*([^\s]+)\s+/, 1]) && LOOP_KEYWORDS.include?(keyword)
|
|
708
732
|
return keyword
|
|
709
733
|
end
|
|
@@ -9,7 +9,7 @@ module HamlLint::RubyExtraction
|
|
|
9
9
|
# * Preprocess the chunks to cleanup/fuse some of them.
|
|
10
10
|
# * Generates the extracted ruby code from the Chunks.
|
|
11
11
|
# * Handles the markers (see below)
|
|
12
|
-
# * Use the chunks to transfer corrections from corrected Ruby code back to
|
|
12
|
+
# * Use the chunks to transfer corrections from corrected Ruby code back to Haml
|
|
13
13
|
#
|
|
14
14
|
# The generated Ruby code uses markers to wrap around the Ruby code from the chunks.
|
|
15
15
|
# Those markers look like function calls, like: `haml_lint_marker_1`, so are valid ruby.
|
|
@@ -22,7 +22,7 @@ module HamlLint::RubyExtraction
|
|
|
22
22
|
# @return [String] The prefix used for markers in the Ruby code
|
|
23
23
|
attr_reader :marker_prefix
|
|
24
24
|
|
|
25
|
-
# @return [Array<String>] The ruby lines after extraction from
|
|
25
|
+
# @return [Array<String>] The ruby lines after extraction from Haml (before RuboCop)
|
|
26
26
|
attr_reader :assembled_ruby_lines
|
|
27
27
|
|
|
28
28
|
# @return [Array<String>] The ruby lines after correction by RuboCop
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module HamlLint::RubyExtraction
|
|
4
|
-
#
|
|
4
|
+
# Haml adds a `end` when code gets outdented. We need to add that to the Ruby too, this
|
|
5
5
|
# is the chunk for it.
|
|
6
6
|
# However:
|
|
7
7
|
# * we can't apply fixes to it, so there are no markers
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module HamlLint::RubyExtraction
|
|
4
|
-
# Chunk for dealing with every
|
|
4
|
+
# Chunk for dealing with every Haml filter other than `:ruby`
|
|
5
5
|
# The generated Ruby for these is just a HEREDOC, so interpolation is corrected at
|
|
6
6
|
# the same time by RuboCop.
|
|
7
7
|
class NonRubyFilterChunk < BaseChunk
|
|
@@ -5,7 +5,7 @@ module HamlLint::RubyExtraction
|
|
|
5
5
|
# transfer the corrections it receives to the indentation of the associated lines.
|
|
6
6
|
#
|
|
7
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
|
|
8
|
+
# so that it does corrections that make sense for the Haml.
|
|
9
9
|
class PlaceholderMarkerChunk < BaseChunk
|
|
10
10
|
def initialize(node, marker_name, indent:, nb_lines: 1, **kwargs)
|
|
11
11
|
@marker_name = marker_name
|
|
@@ -18,7 +18,7 @@ module HamlLint::RubyExtraction
|
|
|
18
18
|
attr_reader :must_start_chunk
|
|
19
19
|
|
|
20
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
|
|
21
|
+
# implicit `end` are on their own line in the Ruby file, but in the Haml, they are absent.
|
|
22
22
|
attr_reader :skip_line_indexes_in_source_map
|
|
23
23
|
|
|
24
24
|
# @return [HamlLint::RubyExtraction::BaseChunk] The previous chunk can affect how
|
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,31 @@ module HamlLint
|
|
|
61
62
|
# @return [HamlLint::LinterSelector]
|
|
62
63
|
attr_reader :linter_selector
|
|
63
64
|
|
|
65
|
+
PARALLEL_LINTER_SELECTOR_THREAD_KEY = :haml_lint_runner_parallel_linter_selectors
|
|
66
|
+
|
|
67
|
+
# Returns a fresh selector for this run.
|
|
68
|
+
#
|
|
69
|
+
# LinterSelector memoizes linter instances, and linters mutate instance
|
|
70
|
+
# state while processing a document. JRuby runs Parallel.map in threads, so
|
|
71
|
+
# parallel jobs must not share a selector.
|
|
72
|
+
#
|
|
73
|
+
# @return [HamlLint::LinterSelector]
|
|
74
|
+
def build_linter_selector
|
|
75
|
+
HamlLint::LinterSelector.new(config, @options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns a selector scoped to the current parallel worker.
|
|
79
|
+
#
|
|
80
|
+
# MRI runs Parallel.map in forked processes, so each worker can safely reuse
|
|
81
|
+
# its own linter instances. JRuby runs Parallel.map in threads, so this must
|
|
82
|
+
# be isolated per thread to avoid sharing mutable linter state.
|
|
83
|
+
#
|
|
84
|
+
# @return [HamlLint::LinterSelector]
|
|
85
|
+
def parallel_linter_selector
|
|
86
|
+
selectors = Thread.current[PARALLEL_LINTER_SELECTOR_THREAD_KEY] ||= {}.compare_by_identity
|
|
87
|
+
selectors[self] ||= build_linter_selector
|
|
88
|
+
end
|
|
89
|
+
|
|
64
90
|
# Returns the {HamlLint::Configuration} that should be used given the
|
|
65
91
|
# specified options.
|
|
66
92
|
#
|
|
@@ -89,7 +115,6 @@ module HamlLint
|
|
|
89
115
|
begin
|
|
90
116
|
document = HamlLint::Document.new source.contents, file: source.path,
|
|
91
117
|
config: config,
|
|
92
|
-
file_on_disk: !source.stdin?,
|
|
93
118
|
write_to_stdout: @autocorrect_stdout
|
|
94
119
|
rescue HamlLint::Exceptions::ParseError => e
|
|
95
120
|
return [HamlLint::Lint.new(HamlLint::Linter::Syntax.new(config), source.path,
|
|
@@ -122,6 +147,9 @@ module HamlLint
|
|
|
122
147
|
lint_arrays = []
|
|
123
148
|
|
|
124
149
|
autocorrecting_linters = linters.select(&:supports_autocorrect?)
|
|
150
|
+
.each_with_index
|
|
151
|
+
.sort_by { |linter, index| [linter.class.autocorrect_priority, index] }
|
|
152
|
+
.map(&:first)
|
|
125
153
|
lint_arrays << autocorrecting_linters.map do |linter|
|
|
126
154
|
linter.run(document, autocorrect: @autocorrect)
|
|
127
155
|
end
|
|
@@ -191,7 +219,7 @@ module HamlLint
|
|
|
191
219
|
# @return [void]
|
|
192
220
|
def warm_cache
|
|
193
221
|
results = Parallel.map(sources) do |source|
|
|
194
|
-
lints = collect_lints(source,
|
|
222
|
+
lints = collect_lints(source, parallel_linter_selector, config)
|
|
195
223
|
[source.path, lints]
|
|
196
224
|
end
|
|
197
225
|
@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
|
|
@@ -65,7 +65,7 @@ module HamlLint
|
|
|
65
65
|
(expected_severity ? " with severity '#{expected_severity}'" : '')
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
def has_lints?(linter, expected_line, count, expected_message, expected_severity, # rubocop:disable Metrics/ParameterLists
|
|
68
|
+
def has_lints?(linter, expected_line, count, expected_message, expected_severity, # rubocop:disable Metrics/ParameterLists,Naming
|
|
69
69
|
expected_corrected)
|
|
70
70
|
if expected_line
|
|
71
71
|
has_expected_line_lints?(linter,
|
|
@@ -83,7 +83,7 @@ module HamlLint
|
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
def has_expected_line_lints?(linter, # rubocop:disable Metrics/ParameterLists
|
|
86
|
+
def has_expected_line_lints?(linter, # rubocop:disable Metrics/ParameterLists,Naming
|
|
87
87
|
expected_line,
|
|
88
88
|
count,
|
|
89
89
|
expected_message,
|
|
@@ -9,7 +9,7 @@ module HamlLint::Tree
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def text
|
|
12
|
-
# Seems
|
|
12
|
+
# Seems Haml strips the starting blank lines... without them, line numbers become offset,
|
|
13
13
|
# breaking the source_map and auto-correct
|
|
14
14
|
|
|
15
15
|
nb_blank_lines = 0
|
|
@@ -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
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require_relative '../directive'
|
|
4
4
|
|
|
5
5
|
module HamlLint::Tree
|
|
6
|
-
# Represents a
|
|
6
|
+
# Represents a Haml comment node.
|
|
7
7
|
class HamlCommentNode < Node
|
|
8
8
|
def directives
|
|
9
9
|
directives = super
|
|
@@ -28,7 +28,7 @@ module HamlLint::Tree
|
|
|
28
28
|
# Returns whether this comment contains a `locals` directive.
|
|
29
29
|
#
|
|
30
30
|
# @return [Boolean]
|
|
31
|
-
def is_strict_locals?
|
|
31
|
+
def is_strict_locals? # rubocop:disable Naming
|
|
32
32
|
text.lstrip.start_with?('locals:')
|
|
33
33
|
end
|
|
34
34
|
|