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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +10 -5
  3. data/config/forced_rubocop_config.yml +13 -13
  4. data/lib/haml_lint/cli.rb +5 -1
  5. data/lib/haml_lint/document.rb +3 -8
  6. data/lib/haml_lint/exceptions.rb +2 -2
  7. data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +2 -2
  8. data/lib/haml_lint/haml_visitor.rb +1 -1
  9. data/lib/haml_lint/lint.rb +1 -1
  10. data/lib/haml_lint/linter/class_attribute_with_static_value.rb +51 -11
  11. data/lib/haml_lint/linter/classes_before_ids.rb +16 -1
  12. data/lib/haml_lint/linter/consecutive_comments.rb +20 -1
  13. data/lib/haml_lint/linter/empty_object_reference.rb +16 -1
  14. data/lib/haml_lint/linter/empty_script.rb +31 -1
  15. data/lib/haml_lint/linter/final_newline.rb +31 -7
  16. data/lib/haml_lint/linter/implicit_div.rb +14 -1
  17. data/lib/haml_lint/linter/leading_comment_space.rb +13 -1
  18. data/lib/haml_lint/linter/multiline_script.rb +65 -5
  19. data/lib/haml_lint/linter/rubocop.rb +10 -22
  20. data/lib/haml_lint/linter/ruby_comments.rb +14 -4
  21. data/lib/haml_lint/linter/space_before_script.rb +37 -4
  22. data/lib/haml_lint/linter/space_inside_hash_attributes.rb +41 -3
  23. data/lib/haml_lint/linter/tag_name.rb +14 -1
  24. data/lib/haml_lint/linter/trailing_empty_lines.rb +13 -1
  25. data/lib/haml_lint/linter/trailing_whitespace.rb +15 -2
  26. data/lib/haml_lint/linter/unescaped_html.rb +27 -0
  27. data/lib/haml_lint/linter/unnecessary_interpolation.rb +30 -3
  28. data/lib/haml_lint/linter/unnecessary_string_output.rb +35 -3
  29. data/lib/haml_lint/linter.rb +112 -1
  30. data/lib/haml_lint/node_transformer.rb +2 -2
  31. data/lib/haml_lint/reporter/disabled_config_reporter.rb +26 -5
  32. data/lib/haml_lint/reporter/github_reporter.rb +26 -8
  33. data/lib/haml_lint/ruby_extraction/base_chunk.rb +3 -3
  34. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +37 -13
  35. data/lib/haml_lint/ruby_extraction/coordinator.rb +2 -2
  36. data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +1 -1
  37. data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +1 -1
  38. data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +1 -1
  39. data/lib/haml_lint/ruby_extraction/script_chunk.rb +1 -1
  40. data/lib/haml_lint/runner.rb +31 -3
  41. data/lib/haml_lint/source.rb +2 -6
  42. data/lib/haml_lint/spec/matchers/report_lint.rb +2 -2
  43. data/lib/haml_lint/tree/comment_node.rb +1 -1
  44. data/lib/haml_lint/tree/doctype_node.rb +1 -1
  45. data/lib/haml_lint/tree/filter_node.rb +14 -1
  46. data/lib/haml_lint/tree/haml_comment_node.rb +2 -2
  47. data/lib/haml_lint/tree/node.rb +3 -3
  48. data/lib/haml_lint/tree/root_node.rb +2 -2
  49. data/lib/haml_lint/tree/silent_script_node.rb +1 -1
  50. data/lib/haml_lint/tree/tag_node.rb +57 -25
  51. data/lib/haml_lint/utils.rb +1 -1
  52. data/lib/haml_lint/version.rb +1 -1
  53. metadata +3 -2
@@ -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 HAML-Lint internal errors. (For testing purposes)
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 HAML has a number of strange cases where certain
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 HAML parse node into its corresponding HAML-Lint parse
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
- HEADING =
8
+ DEFAULT_EXCLUDE_LIMIT = 15
9
+
10
+ HEADING_TEMPLATE =
9
11
  ['# This configuration was generated by',
10
- '# `haml-lint --auto-gen-config`',
11
- "# on #{Time.now} using Haml-Lint version #{HamlLint::VERSION}.",
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: 15)
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 << HEADING
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
- if lint.severity >= report.fail_level
12
- print_workflow_command(lint: lint)
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:, severity: 'error')
25
- log.log "::#{severity} file=#{lint.filename},line=#{lint.line}::#{github_escape(lint.message)}"
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 HAML file that HamlLint::Linter::RuboCop
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 HAML concepts, even if they don't represent Ruby
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 HAML file.
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 HAML file to then be processed by RuboCop.
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
- # HAML strips newlines when handling multi-line statements (using pipes or trailing comma)
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 HAML 2.2... it's still supported)
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 HAML.
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) HAML style:
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 HAML. Fusing scripts together
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
- # Looks like the .foo(bar = 123) case. Ignoring.
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 HAML filter. Lines looking like ` :javascript` and the following lines
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
- lines = node.text.split("\n")
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
- # HAML transforms the ruby code in many ways as it parses a document. Often removing lines and/or
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 HAML are as follow:
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 HAML parser doesn't
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 HAML
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 HAML (before RuboCop)
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
- # HAML adds a `end` when code gets outdented. We need to add that to the Ruby too, this
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 HAML filter other than `:ruby`
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 HAML.
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 HAML, they are absent.
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
@@ -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
- @linter_selector = HamlLint::LinterSelector.new(config, options)
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, linter_selector, config)
222
+ lints = collect_lints(source, parallel_linter_selector, config)
195
223
  [source.path, lints]
196
224
  end
197
225
  @cache = results.to_h
@@ -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(path)
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,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HamlLint::Tree
4
- # Represents a visible XHTML comment in a HAML document.
4
+ # Represents a visible XHTML comment in a Haml document.
5
5
  class CommentNode < Node
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HamlLint::Tree
4
- # Represents a doctype definition for a HAML document.
4
+ # Represents a doctype definition for a Haml document.
5
5
  class DoctypeNode < Node
6
6
  end
7
7
  end
@@ -9,7 +9,7 @@ module HamlLint::Tree
9
9
  end
10
10
 
11
11
  def text
12
- # Seems HAML strips the starting blank lines... without them, line numbers become offset,
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 HAML comment node.
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