haml_lint 0.53.0 → 0.76.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +17 -5
  3. data/config/forced_rubocop_config.yml +25 -13
  4. data/lib/haml_lint/adapter.rb +5 -4
  5. data/lib/haml_lint/cli.rb +7 -3
  6. data/lib/haml_lint/constants.rb +1 -1
  7. data/lib/haml_lint/directive.rb +2 -2
  8. data/lib/haml_lint/document.rb +14 -5
  9. data/lib/haml_lint/exceptions.rb +2 -2
  10. data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +2 -2
  11. data/lib/haml_lint/haml_visitor.rb +1 -1
  12. data/lib/haml_lint/lint.rb +8 -3
  13. data/lib/haml_lint/linter/alignment_tabs.rb +21 -4
  14. data/lib/haml_lint/linter/class_attribute_with_static_value.rb +52 -12
  15. data/lib/haml_lint/linter/classes_before_ids.rb +15 -6
  16. data/lib/haml_lint/linter/consecutive_comments.rb +20 -1
  17. data/lib/haml_lint/linter/consecutive_silent_scripts.rb +68 -1
  18. data/lib/haml_lint/linter/empty_object_reference.rb +16 -1
  19. data/lib/haml_lint/linter/empty_script.rb +32 -2
  20. data/lib/haml_lint/linter/final_newline.rb +31 -7
  21. data/lib/haml_lint/linter/html_attributes.rb +71 -2
  22. data/lib/haml_lint/linter/id_names.rb +1 -1
  23. data/lib/haml_lint/linter/implicit_div.rb +14 -1
  24. data/lib/haml_lint/linter/indentation.rb +2 -2
  25. data/lib/haml_lint/linter/instance_variables.rb +28 -3
  26. data/lib/haml_lint/linter/leading_comment_space.rb +14 -2
  27. data/lib/haml_lint/linter/multiline_pipe.rb +3 -3
  28. data/lib/haml_lint/linter/multiline_script.rb +65 -5
  29. data/lib/haml_lint/linter/no_placeholders.rb +2 -2
  30. data/lib/haml_lint/linter/rubocop.rb +87 -76
  31. data/lib/haml_lint/linter/ruby_comments.rb +14 -4
  32. data/lib/haml_lint/linter/space_before_script.rb +37 -4
  33. data/lib/haml_lint/linter/space_inside_hash_attributes.rb +41 -3
  34. data/lib/haml_lint/linter/strict_locals.rb +69 -0
  35. data/lib/haml_lint/linter/tag_name.rb +15 -2
  36. data/lib/haml_lint/linter/trailing_empty_lines.rb +13 -1
  37. data/lib/haml_lint/linter/trailing_whitespace.rb +16 -3
  38. data/lib/haml_lint/linter/unescaped_html.rb +27 -0
  39. data/lib/haml_lint/linter/unnecessary_interpolation.rb +30 -3
  40. data/lib/haml_lint/linter/unnecessary_string_output.rb +108 -12
  41. data/lib/haml_lint/linter.rb +119 -5
  42. data/lib/haml_lint/node_transformer.rb +2 -2
  43. data/lib/haml_lint/options.rb +17 -3
  44. data/lib/haml_lint/rake_task.rb +3 -2
  45. data/lib/haml_lint/reporter/default_reporter.rb +1 -1
  46. data/lib/haml_lint/reporter/disabled_config_reporter.rb +29 -8
  47. data/lib/haml_lint/reporter/github_reporter.rb +50 -0
  48. data/lib/haml_lint/reporter/hash_reporter.rb +2 -0
  49. data/lib/haml_lint/reporter/json_reporter.rb +1 -1
  50. data/lib/haml_lint/reporter/progress_reporter.rb +2 -1
  51. data/lib/haml_lint/reporter/utils.rb +4 -2
  52. data/lib/haml_lint/reporter.rb +1 -1
  53. data/lib/haml_lint/ruby_extraction/base_chunk.rb +4 -4
  54. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +73 -32
  55. data/lib/haml_lint/ruby_extraction/coordinator.rb +9 -4
  56. data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +1 -1
  57. data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +2 -2
  58. data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +1 -1
  59. data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +1 -1
  60. data/lib/haml_lint/ruby_extraction/script_chunk.rb +2 -2
  61. data/lib/haml_lint/runner.rb +70 -30
  62. data/lib/haml_lint/source.rb +25 -0
  63. data/lib/haml_lint/spec/matchers/report_lint.rb +17 -6
  64. data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +34 -4
  65. data/lib/haml_lint/spec.rb +4 -4
  66. data/lib/haml_lint/tree/comment_node.rb +1 -1
  67. data/lib/haml_lint/tree/doctype_node.rb +1 -1
  68. data/lib/haml_lint/tree/filter_node.rb +14 -1
  69. data/lib/haml_lint/tree/haml_comment_node.rb +9 -2
  70. data/lib/haml_lint/tree/node.rb +4 -4
  71. data/lib/haml_lint/tree/root_node.rb +4 -4
  72. data/lib/haml_lint/tree/script_node.rb +1 -1
  73. data/lib/haml_lint/tree/silent_script_node.rb +1 -1
  74. data/lib/haml_lint/tree/tag_node.rb +69 -25
  75. data/lib/haml_lint/utils.rb +4 -31
  76. data/lib/haml_lint/version.rb +1 -1
  77. data/lib/haml_lint.rb +23 -23
  78. metadata +14 -10
@@ -4,12 +4,12 @@
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
11
11
 
12
- attr_reader :script_output_prefix
12
+ attr_reader :script_output_prefix, :autocorrect
13
13
 
14
14
  HAML_PARSER_INSTANCE = if Haml::VERSION >= '5.0.0'
15
15
  ::Haml::Parser.new({})
@@ -17,9 +17,14 @@ module HamlLint::RubyExtraction
17
17
  ::Haml::Parser.new('', {})
18
18
  end
19
19
 
20
- def initialize(document, script_output_prefix:)
20
+ # Haml strips newlines when handling multi-line statements (using pipes or trailing comma)
21
+ # We don't. So the regex must be fixed to correctly detect the start of the string.
22
+ BLOCK_KEYWORD_REGEX = Regexp.new(Haml::Parser::BLOCK_KEYWORD_REGEX.source.sub('^', '\A'))
23
+
24
+ def initialize(document, script_output_prefix:, autocorrect: nil)
21
25
  @document = document
22
26
  @script_output_prefix = script_output_prefix
27
+ @autocorrect = autocorrect
23
28
  end
24
29
 
25
30
  def extract
@@ -119,7 +124,7 @@ module HamlLint::RubyExtraction
119
124
  def visit_script(node, &block)
120
125
  raw_first_line = @original_haml_lines[node.line - 1]
121
126
 
122
- # ==, !, !==, &, &== 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)
123
128
  # =, !=, &= mean actual ruby code is coming
124
129
  # Anything else is interpolation
125
130
  # The regex lists the case for Ruby Code. The 3 cases and making sure they are not followed by another = sign
@@ -164,20 +169,20 @@ module HamlLint::RubyExtraction
164
169
  prev_chunk.node == node.parent
165
170
  # When an outputting script is nested under another outputting script,
166
171
  # we want to block them from being merged together by rubocop, because
167
- # this doesn't make sense in HAML.
172
+ # this doesn't make sense in Haml.
168
173
  # Example:
169
174
  # = if this_is_short
170
175
  # = this_is_short_too
171
176
  # Could become (after RuboCop):
172
177
  # HL.out = (HL.out = this_is_short_too if this_is_short)
173
- # Or in (broken) HAML style:
178
+ # Or in (broken) Haml style:
174
179
  # = this_is_short_too = if this_is_short
175
180
  # By forcing this to start a chunk, there will be extra placeholders which
176
181
  # blocks rubocop from merging the lines.
177
182
  must_start_chunk = true
178
183
  elsif script_prefix != '='
179
184
  # In the few cases where &= and != are used to start the script,
180
- # 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
181
186
  # would make that basically impossible. Instead, a script has a "first_output_prefix"
182
187
  # field for this specific case
183
188
  must_start_chunk = true
@@ -247,31 +252,29 @@ module HamlLint::RubyExtraction
247
252
  def visit_tag(node)
248
253
  indent = @original_haml_lines[node.line - 1].index(/\S/)
249
254
 
255
+ @ruby_chunks << PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
256
+
257
+ current_line_index = visit_tag_attributes(node, indent: indent)
258
+ visit_tag_script(node, line_index: current_line_index, indent: indent)
259
+
250
260
  # We don't want to use a block because assignments in a block are local to that block,
251
261
  # so the semantics of the extracted ruby would be different from the one generated by
252
262
  # Haml. Those differences can make some cops, such as UselessAssignment, have false
253
263
  # positives
254
264
  code = 'begin'
255
- @ruby_chunks << AdHocChunk.new(node,
256
- [' ' * indent + code])
257
- indent += 2
265
+ begin_chunk = AdHocChunk.new(node, [' ' * indent + code])
266
+ @ruby_chunks << begin_chunk
258
267
 
259
- tag_chunk = PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
260
- @ruby_chunks << tag_chunk
261
-
262
- current_line_index = visit_tag_attributes(node, indent: indent)
263
- visit_tag_script(node, line_index: current_line_index, indent: indent)
268
+ indent += 2
264
269
 
265
270
  yield
266
271
 
267
272
  indent -= 2
268
273
 
269
- if @ruby_chunks.last.equal?(tag_chunk)
274
+ if @ruby_chunks.last.equal?(begin_chunk)
270
275
  # So there is nothing going "in" the tag, remove the wrapping "begin" and replace the PlaceholderMarkerChunk
271
276
  # by one less indented
272
277
  @ruby_chunks.pop
273
- @ruby_chunks.pop
274
- @ruby_chunks << PlaceholderMarkerChunk.new(node, 'tag', indent: indent)
275
278
  else
276
279
  @ruby_chunks << AdHocChunk.new(node,
277
280
  [' ' * indent + 'ensure', ' ' * indent + ' HL.noop', ' ' * indent + 'end'],
@@ -297,7 +300,13 @@ module HamlLint::RubyExtraction
297
300
  end
298
301
 
299
302
  if attributes_code&.start_with?('{')
300
- # 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)
301
310
  attributes_code = nil
302
311
  end
303
312
 
@@ -342,6 +351,22 @@ module HamlLint::RubyExtraction
342
351
  final_line_index
343
352
  end
344
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
+
345
370
  # Visiting the script besides tag. The part to the right of the equal sign of
346
371
  # lines looking like ` %div= foo(bar)`
347
372
  def visit_tag_script(node, line_index:, indent:)
@@ -381,7 +406,7 @@ module HamlLint::RubyExtraction
381
406
  end
382
407
  end
383
408
 
384
- # 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
385
410
  # that are nested.
386
411
  def visit_filter(node)
387
412
  # For unknown reasons, haml doesn't escape interpolations in filters.
@@ -389,10 +414,12 @@ module HamlLint::RubyExtraction
389
414
  filter_name_indent = @original_haml_lines[node.line - 1].index(/\S/)
390
415
  if node.filter_type == 'ruby'
391
416
  # The indentation in node.text is normalized, so that at least one line
392
- # is indented by 0.
393
- 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
394
421
  lines.map! do |line|
395
- if line !~ /\S/
422
+ if !/\S/.match?(line)
396
423
  # whitespace or empty
397
424
  ''
398
425
  else
@@ -494,14 +521,14 @@ module HamlLint::RubyExtraction
494
521
  # The first and last lines may not be the complete lines from the Haml, only the Ruby parts
495
522
  # and the indentation between the first and last list.
496
523
 
497
- # 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
498
525
  # indentation. This is quite annoying for us since we want the exact layout of the code to analyze it.
499
526
  #
500
527
  # This function receives the code as haml provides it and the line where it starts. It returns
501
528
  # the actual code as it is in the haml file, keeping breaks and indentation for the following lines.
502
529
  # In addition, the start position of the code in the first line.
503
530
  #
504
- # The rules for handling multiline code in HAML are as follow:
531
+ # The rules for handling multiline code in Haml are as follow:
505
532
  # * if the line being processed ends with a space and a pipe, then append to the line (without
506
533
  # newlines) every following lines that also end with a space and a pipe. This means the last line of
507
534
  # the "block" also needs a pipe at the end.
@@ -548,7 +575,7 @@ module HamlLint::RubyExtraction
548
575
  joined_lines = lines.join("\n")
549
576
 
550
577
  if haml_processed_ruby_code.include?("\n")
551
- haml_processed_ruby_code = haml_processed_ruby_code.gsub("\n", ' ')
578
+ haml_processed_ruby_code = haml_processed_ruby_code.tr("\n", ' ')
552
579
  end
553
580
 
554
581
  haml_processed_ruby_code.split(/[, ]/)
@@ -600,9 +627,12 @@ module HamlLint::RubyExtraction
600
627
  return [char_index, [haml_processed_ruby_code]]
601
628
  end
602
629
 
603
- min_non_white_chars_to_add = haml_processed_ruby_code.scan(/\S/).size
630
+ # The +1 is for the closing brace, which we need
631
+ min_non_white_chars_to_add = haml_processed_ruby_code.scan(/\S/).size + 1
604
632
 
605
- regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/\s+/), '\\s+')
633
+ regexp = HamlLint::Utils.regexp_for_parts(
634
+ haml_processed_ruby_code.split(/\s+/), '\\s+', prefix: '\s*', suffix: '\s*(?=[)}])'
635
+ )
606
636
 
607
637
  joined_lines = first_line.rstrip
608
638
  process_multiline!(joined_lines)
@@ -624,7 +654,7 @@ module HamlLint::RubyExtraction
624
654
 
625
655
  first_line_offset = match.begin(0)
626
656
  raw_ruby = match[0]
627
- ruby_lines = raw_ruby.split("\n")
657
+ ruby_lines = raw_ruby.split("\n", -1)
628
658
 
629
659
  [first_line_offset, ruby_lines]
630
660
  end
@@ -632,8 +662,19 @@ module HamlLint::RubyExtraction
632
662
  def wrap_lines(lines, wrap_depth)
633
663
  lines = lines.dup
634
664
  wrapping_prefix = 'W' * (wrap_depth - 1) + '('
665
+
666
+ # Strip leading/trailing space only when linting (not autocorrecting) to avoid SpaceInsideParens violations.
667
+ # Preserve spaces for fully nested attribute hashes that need no adjustment.
668
+ # Preserve spaces during autocorrect, so RuboCop can fix them and map corrections back correctly.
669
+ unless lines[0] =~ /^\s*$/ || autocorrect
670
+ leading_spaces = lines[0][/^\s*/].length
671
+ lines = lines.map { |line| line.gsub(/^\s{,#{leading_spaces}}/, '') }
672
+ lines[-1] = lines[-1].gsub(/\s+\z/, '')
673
+ end
674
+
635
675
  lines[0] = wrapping_prefix + lines[0]
636
676
  lines[-1] = lines[-1] + ')'
677
+
637
678
  lines
638
679
  end
639
680
 
@@ -673,7 +714,7 @@ module HamlLint::RubyExtraction
673
714
 
674
715
  def self.anonymous_block?(code)
675
716
  # Don't start with a comment and end with a `do`
676
- # Definetly not perfect for the comment handling, but otherwise a more advanced parsing system is needed.
717
+ # Definitely not perfect for the comment handling, but otherwise a more advanced parsing system is needed.
677
718
  # Move the comment to its own line if it's annoying.
678
719
  code !~ /\A\s*#/ &&
679
720
  code =~ /\bdo\s*(\|[^|]*\|\s*)?(#.*)?\z/
@@ -686,12 +727,12 @@ module HamlLint::RubyExtraction
686
727
 
687
728
  LOOP_KEYWORDS = %w[for until while].freeze
688
729
  def self.block_keyword(code)
689
- # 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
690
731
  if (keyword = code[/\A\s*([^\s]+)\s+/, 1]) && LOOP_KEYWORDS.include?(keyword)
691
732
  return keyword
692
733
  end
693
734
 
694
- return unless keyword = code.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0]
735
+ return unless keyword = code.scan(BLOCK_KEYWORD_REGEX)[0]
695
736
  keyword[0] || keyword[1]
696
737
  end
697
738
  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,14 +22,18 @@ 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
29
29
  attr_reader :corrected_ruby_lines
30
30
 
31
- def initialize(document)
31
+ # @return [Symbol, nil] The autocorrect mode (:safe, :all, or nil)
32
+ attr_reader :autocorrect
33
+
34
+ def initialize(document, autocorrect:)
32
35
  @document = document
36
+ @autocorrect = autocorrect
33
37
  @ruby_chunks = nil
34
38
  @assembled_ruby_lines = nil
35
39
  @corrected_ruby_lines = nil
@@ -46,7 +50,8 @@ module HamlLint::RubyExtraction
46
50
  pick_a_script_output_prefix
47
51
 
48
52
  @ruby_chunks = HamlLint::RubyExtraction::ChunkExtractor.new(@document,
49
- script_output_prefix: @script_output_prefix).extract
53
+ script_output_prefix: @script_output_prefix,
54
+ autocorrect: autocorrect).extract
50
55
  preprocess_chunks
51
56
 
52
57
  @assembled_ruby_lines = []
@@ -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
@@ -14,7 +14,7 @@ module HamlLint::RubyExtraction
14
14
  to_content_lines = to_ruby_lines[1...-1]
15
15
 
16
16
  to_haml_lines = to_content_lines.map do |line|
17
- if line !~ /\S/
17
+ if !/\S/.match?(line)
18
18
  # whitespace or empty
19
19
  ''
20
20
  else
@@ -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
@@ -19,7 +19,7 @@ module HamlLint::RubyExtraction
19
19
  haml_lines[@haml_line_index - 1] = HamlLint::Utils.indent(haml_lines[@haml_line_index - 1], delta_indent)
20
20
 
21
21
  to_haml_lines = to_ruby_lines.map do |line|
22
- if line !~ /\S/
22
+ if !/\S/.match?(line)
23
23
  # whitespace or empty
24
24
  ''
25
25
  else
@@ -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
@@ -113,7 +113,7 @@ module HamlLint::RubyExtraction
113
113
  line_start_indexes_that_need_pipes = []
114
114
  haml_output_prefix = first_output_haml_prefix
115
115
  to_haml_lines = to_ruby_lines.map.with_index do |line, i| # rubocop:disable Metrics/BlockLength
116
- if line !~ /\S/
116
+ if !/\S/.match?(line)
117
117
  # whitespace or empty lines, we don't want any indentation
118
118
  ''
119
119
  elsif statement_start_line_indexes.include?(i)
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'parallel'
4
4
 
5
+ require_relative 'source'
6
+
5
7
  module HamlLint
6
8
  # Responsible for running the applicable linters against the desired files.
7
9
  class Runner
@@ -20,12 +22,14 @@ module HamlLint
20
22
  # @return [HamlLint::Report] a summary of all lints found
21
23
  def run(options = {})
22
24
  @config = load_applicable_config(options)
23
- @files = extract_applicable_files(config, options)
24
- @linter_selector = HamlLint::LinterSelector.new(config, options)
25
+ @sources = extract_applicable_sources(config, options)
26
+ @options = options
27
+ @linter_selector = build_linter_selector
25
28
  @fail_fast = options.fetch(:fail_fast, false)
26
29
  @cache = {}
27
30
  @autocorrect = options[:autocorrect]
28
31
  @autocorrect_only = options[:autocorrect_only]
32
+ @autocorrect_stdout = options[:stdin] && options[:stderr]
29
33
 
30
34
  report(options)
31
35
  end
@@ -48,16 +52,41 @@ module HamlLint
48
52
  # @return [true, false]
49
53
  alias fail_fast? fail_fast
50
54
 
51
- # The list of files to lint during this run.
55
+ # The list of sources to lint during this run.
52
56
  #
53
- # @return [Array<String>]
54
- attr_reader :files
57
+ # @return [Array<HamlLint::Source>]
58
+ attr_reader :sources
55
59
 
56
60
  # The selector for which linters to run during this run.
57
61
  #
58
62
  # @return [HamlLint::LinterSelector]
59
63
  attr_reader :linter_selector
60
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
+
61
90
  # Returns the {HamlLint::Configuration} that should be used given the
62
91
  # specified options.
63
92
  #
@@ -79,18 +108,20 @@ module HamlLint
79
108
  # Runs all provided linters using the specified config against the given
80
109
  # file.
81
110
  #
82
- # @param file [String] path to file to lint
111
+ # @param source [HamlLint::Source] source to lint
83
112
  # @param linter_selector [HamlLint::LinterSelector]
84
113
  # @param config [HamlLint::Configuration]
85
- def collect_lints(file, linter_selector, config)
114
+ def collect_lints(source, linter_selector, config)
86
115
  begin
87
- document = HamlLint::Document.new(File.read(file), file: file, config: config)
116
+ document = HamlLint::Document.new source.contents, file: source.path,
117
+ config: config,
118
+ write_to_stdout: @autocorrect_stdout
88
119
  rescue HamlLint::Exceptions::ParseError => e
89
- return [HamlLint::Lint.new(HamlLint::Linter::Syntax.new(config), file,
120
+ return [HamlLint::Lint.new(HamlLint::Linter::Syntax.new(config), source.path,
90
121
  e.line, e.to_s, :error)]
91
122
  end
92
123
 
93
- linters = linter_selector.linters_for_file(file)
124
+ linters = linter_selector.linters_for_file(source.path)
94
125
  lint_arrays = []
95
126
 
96
127
  if @autocorrect
@@ -116,6 +147,9 @@ module HamlLint
116
147
  lint_arrays = []
117
148
 
118
149
  autocorrecting_linters = linters.select(&:supports_autocorrect?)
150
+ .each_with_index
151
+ .sort_by { |linter, index| [linter.class.autocorrect_priority, index] }
152
+ .map(&:first)
119
153
  lint_arrays << autocorrecting_linters.map do |linter|
120
154
  linter.run(document, autocorrect: @autocorrect)
121
155
  end
@@ -125,40 +159,46 @@ module HamlLint
125
159
  lint_arrays
126
160
  end
127
161
 
128
- # Returns the list of files that should be linted given the specified
162
+ # Returns the list of sources that should be linted given the specified
129
163
  # configuration and options.
130
164
  #
131
165
  # @param config [HamlLint::Configuration]
132
166
  # @param options [Hash]
133
- # @return [Array<String>]
134
- def extract_applicable_files(config, options)
135
- included_patterns = options[:files]
136
- excluded_patterns = config['exclude']
137
- excluded_patterns += options.fetch(:excluded_files, [])
167
+ # @return [Array<HamlLint::Source>]
168
+ def extract_applicable_sources(config, options)
169
+ if options[:stdin]
170
+ [HamlLint::Source.new(io: $stdin, path: options[:stdin])]
171
+ else
172
+ included_patterns = options[:files]
173
+ excluded_patterns = config['exclude']
174
+ excluded_patterns += options.fetch(:excluded_files, [])
138
175
 
139
- HamlLint::FileFinder.new(config).find(included_patterns, excluded_patterns)
176
+ HamlLint::FileFinder.new(config).find(included_patterns, excluded_patterns).map do |file_path|
177
+ HamlLint::Source.new path: file_path
178
+ end
179
+ end
140
180
  end
141
181
 
142
- # Process the files and add them to the given report.
182
+ # Process the sources and add them to the given report.
143
183
  #
144
184
  # @param report [HamlLint::Report]
145
185
  # @return [void]
146
- def process_files(report)
147
- files.each do |file|
148
- process_file(file, report)
186
+ def process_sources(report)
187
+ sources.each do |source|
188
+ process_source(source, report)
149
189
  break if report.failed? && fail_fast?
150
190
  end
151
191
  end
152
192
 
153
193
  # Process a file and add it to the given report.
154
194
  #
155
- # @param file [String] the name of the file to process
195
+ # @param source [HamlLint::Source] the source to process
156
196
  # @param report [HamlLint::Report]
157
197
  # @return [void]
158
- def process_file(file, report)
159
- lints = @cache[file] || collect_lints(file, linter_selector, config)
198
+ def process_source(source, report)
199
+ lints = @cache[source.path] || collect_lints(source, linter_selector, config)
160
200
  lints.each { |lint| report.add_lint(lint) }
161
- report.finish_file(file, lints)
201
+ report.finish_file(source.path, lints)
162
202
  end
163
203
 
164
204
  # Generates a report based on the given options.
@@ -168,9 +208,9 @@ module HamlLint
168
208
  # @return [HamlLint::Report]
169
209
  def report(options)
170
210
  report = HamlLint::Report.new(reporter: options[:reporter], fail_level: options[:fail_level])
171
- report.start(@files)
211
+ report.start(sources.map(&:path))
172
212
  warm_cache if options[:parallel]
173
- process_files(report)
213
+ process_sources(report)
174
214
  report
175
215
  end
176
216
 
@@ -178,9 +218,9 @@ module HamlLint
178
218
  #
179
219
  # @return [void]
180
220
  def warm_cache
181
- results = Parallel.map(files) do |file|
182
- lints = collect_lints(file, linter_selector, config)
183
- [file, lints]
221
+ results = Parallel.map(sources) do |source|
222
+ lints = collect_lints(source, parallel_linter_selector, config)
223
+ [source.path, lints]
184
224
  end
185
225
  @cache = results.to_h
186
226
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint
4
+ # Wrapper class representing a single target for HamlLint::Runner to run against, comprised of a file path to
5
+ # eventually read from, and an optional IO argument to override with.
6
+ class Source
7
+ # @return [String] File path associated with the given IO object.
8
+ attr_reader :path
9
+
10
+ # Wraps an optional IO object and file path to a source object.
11
+ #
12
+ # @param [String] path
13
+ # @param [IO] io
14
+ def initialize(path: nil, io: nil)
15
+ @path = path
16
+ @file_path = File.expand_path(path) if path
17
+ @io = io
18
+ end
19
+
20
+ # @return [String] Contents of the given IO object.
21
+ def contents
22
+ @contents ||= @io&.read || File.read(@file_path)
23
+ end
24
+ end
25
+ end
@@ -11,10 +11,11 @@ module HamlLint
11
11
  expected_message = options[:message]
12
12
  expected_severity = options[:severity]
13
13
  expected_corrected = options[:corrected]
14
+ expected_correctable = options[:correctable]
14
15
 
15
16
  match do |linter|
16
17
  has_lints?(linter, expected_line, count, expected_message, expected_severity,
17
- expected_corrected)
18
+ expected_corrected, expected_correctable)
18
19
  end
19
20
 
20
21
  failure_message do |linter|
@@ -65,15 +66,16 @@ module HamlLint
65
66
  (expected_severity ? " with severity '#{expected_severity}'" : '')
66
67
  end
67
68
 
68
- def has_lints?(linter, expected_line, count, expected_message, expected_severity, # rubocop:disable Metrics/ParameterLists
69
- expected_corrected)
69
+ def has_lints?(linter, expected_line, count, expected_message, expected_severity, # rubocop:disable Metrics/ParameterLists,Naming
70
+ expected_corrected, expected_correctable)
70
71
  if expected_line
71
72
  has_expected_line_lints?(linter,
72
73
  expected_line,
73
74
  count,
74
75
  expected_message,
75
76
  expected_severity,
76
- expected_corrected)
77
+ expected_corrected,
78
+ expected_correctable)
77
79
  elsif count
78
80
  linter.lints.count == count
79
81
  elsif expected_message
@@ -83,12 +85,13 @@ module HamlLint
83
85
  end
84
86
  end
85
87
 
86
- def has_expected_line_lints?(linter, # rubocop:disable Metrics/ParameterLists
88
+ def has_expected_line_lints?(linter, # rubocop:disable Metrics/ParameterLists,Naming
87
89
  expected_line,
88
90
  count,
89
91
  expected_message,
90
92
  expected_severity,
91
- expected_corrected)
93
+ expected_corrected,
94
+ expected_correctable)
92
95
  if count
93
96
  multiple_lints_match_line?(linter, expected_line, count)
94
97
  elsif expected_message
@@ -97,6 +100,8 @@ module HamlLint
97
100
  lint_on_line_matches_severity?(linter, expected_line, expected_severity)
98
101
  elsif !expected_corrected.nil?
99
102
  lint_on_line_matches_corrected?(linter, expected_line, expected_corrected)
103
+ elsif !expected_correctable.nil?
104
+ lint_on_line_matches_correctable?(linter, expected_line, expected_correctable)
100
105
  else
101
106
  lint_lines(linter).include?(expected_line)
102
107
  end
@@ -126,6 +131,12 @@ module HamlLint
126
131
  .any? { |lint| lint.line == expected_line && lint.corrected == expected_corrected }
127
132
  end
128
133
 
134
+ def lint_on_line_matches_correctable?(linter, expected_line, expected_correctable)
135
+ linter
136
+ .lints
137
+ .any? { |lint| lint.line == expected_line && lint.correctable == expected_correctable }
138
+ end
139
+
129
140
  def lint_messages_match?(linter, expected_message)
130
141
  # Using === to support regex to match anywhere in the string
131
142
  lint_messages(linter).all? { |message| expected_message === message } # rubocop:disable Style/CaseEquality