haml_lint 0.44.0 → 0.46.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/bin/haml-lint +1 -1
  3. data/config/default.yml +6 -28
  4. data/config/forced_rubocop_config.yml +156 -0
  5. data/lib/haml_lint/adapter/haml_4.rb +18 -0
  6. data/lib/haml_lint/adapter/haml_5.rb +11 -0
  7. data/lib/haml_lint/adapter/haml_6.rb +11 -0
  8. data/lib/haml_lint/cli.rb +8 -3
  9. data/lib/haml_lint/configuration_loader.rb +13 -12
  10. data/lib/haml_lint/document.rb +89 -8
  11. data/lib/haml_lint/exceptions.rb +6 -0
  12. data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +35 -0
  13. data/lib/haml_lint/file_finder.rb +2 -2
  14. data/lib/haml_lint/lint.rb +10 -1
  15. data/lib/haml_lint/linter/final_newline.rb +4 -3
  16. data/lib/haml_lint/linter/implicit_div.rb +1 -1
  17. data/lib/haml_lint/linter/indentation.rb +3 -3
  18. data/lib/haml_lint/linter/no_placeholders.rb +18 -0
  19. data/lib/haml_lint/linter/rubocop.rb +351 -59
  20. data/lib/haml_lint/linter/space_before_script.rb +8 -10
  21. data/lib/haml_lint/linter/unnecessary_string_output.rb +1 -1
  22. data/lib/haml_lint/linter/view_length.rb +1 -1
  23. data/lib/haml_lint/linter.rb +56 -9
  24. data/lib/haml_lint/linter_registry.rb +3 -5
  25. data/lib/haml_lint/logger.rb +2 -2
  26. data/lib/haml_lint/options.rb +26 -2
  27. data/lib/haml_lint/rake_task.rb +2 -2
  28. data/lib/haml_lint/reporter/hash_reporter.rb +1 -3
  29. data/lib/haml_lint/reporter/offense_count_reporter.rb +1 -1
  30. data/lib/haml_lint/reporter/utils.rb +33 -4
  31. data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +20 -0
  32. data/lib/haml_lint/ruby_extraction/base_chunk.rb +113 -0
  33. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +504 -0
  34. data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
  35. data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +54 -0
  36. data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +17 -0
  37. data/lib/haml_lint/ruby_extraction/interpolation_chunk.rb +26 -0
  38. data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +32 -0
  39. data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +40 -0
  40. data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +33 -0
  41. data/lib/haml_lint/ruby_extraction/ruby_source.rb +5 -0
  42. data/lib/haml_lint/ruby_extraction/script_chunk.rb +132 -0
  43. data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +39 -0
  44. data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
  45. data/lib/haml_lint/ruby_extractor.rb +11 -10
  46. data/lib/haml_lint/runner.rb +35 -3
  47. data/lib/haml_lint/spec/matchers/report_lint.rb +22 -7
  48. data/lib/haml_lint/spec/normalize_indent.rb +2 -2
  49. data/lib/haml_lint/spec/shared_linter_context.rb +9 -1
  50. data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +143 -0
  51. data/lib/haml_lint/spec.rb +1 -0
  52. data/lib/haml_lint/tree/filter_node.rb +10 -0
  53. data/lib/haml_lint/tree/node.rb +13 -4
  54. data/lib/haml_lint/tree/tag_node.rb +5 -9
  55. data/lib/haml_lint/utils.rb +130 -5
  56. data/lib/haml_lint/version.rb +1 -1
  57. data/lib/haml_lint/version_comparer.rb +25 -0
  58. data/lib/haml_lint.rb +12 -0
  59. metadata +25 -6
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # Chunk for haml comments. Lines like ` -# Some commenting!`.
5
+ # Only deals with indentation while correcting, but can also be fused to a ScriptChunk.
6
+ class HamlCommentChunk < BaseChunk
7
+ def fuse(following_chunk)
8
+ return unless following_chunk.is_a?(HamlCommentChunk)
9
+
10
+ # We only merge consecutive comments
11
+ # The main reason to want to at least merge those is
12
+ # so that an empty comment doesn't get removed by rubocop by mistake
13
+ return if @haml_line_index + 1 != following_chunk.haml_line_index
14
+
15
+ HamlCommentChunk.new(node, @ruby_lines + following_chunk.ruby_lines, end_marker_indent: end_marker_indent)
16
+ end
17
+
18
+ def fuse_script_chunk(following_chunk)
19
+ return if following_chunk.end_marker_indent.nil?
20
+ return if following_chunk.must_start_chunk
21
+
22
+ nb_blank_lines_between = following_chunk.haml_line_index - haml_line_index - nb_haml_lines
23
+ blank_lines = nb_blank_lines_between > 0 ? [''] * nb_blank_lines_between : []
24
+ new_lines = @ruby_lines + blank_lines + following_chunk.ruby_lines
25
+
26
+ source_map_skips = @skip_line_indexes_in_source_map
27
+ source_map_skips.concat(following_chunk.skip_line_indexes_in_source_map
28
+ .map { |i| i + @ruby_lines.size })
29
+
30
+ ScriptChunk.new(node,
31
+ new_lines,
32
+ haml_line_index: haml_line_index,
33
+ skip_line_indexes_in_source_map: source_map_skips,
34
+ end_marker_indent: following_chunk.end_marker_indent,
35
+ previous_chunk: previous_chunk)
36
+ end
37
+
38
+ def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines)
39
+ if to_ruby_lines.empty?
40
+ haml_lines.slice!(@haml_line_index..haml_end_line_index)
41
+ return
42
+ end
43
+ delta_indent = min_indent_of(to_ruby_lines) - min_indent_of(@ruby_lines)
44
+
45
+ HamlLint::Utils.map_subset!(haml_lines, @haml_line_index..haml_end_line_index) do |l|
46
+ HamlLint::Utils.indent(l, delta_indent)
47
+ end
48
+ end
49
+
50
+ def min_indent_of(lines)
51
+ lines.map { |l| l.index(/\S/) }.compact.min
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # HAML adds a `end` when code gets outdented. We need to add that to the Ruby too, this
5
+ # is the chunk for it.
6
+ # However:
7
+ # * we can't apply fixes to it, so there are no markers
8
+ # * this is a distinct class so that a ScriptChunk can fuse this ImplicitEnd into itself,
9
+ # So that we can generate bigger chunks of uninterrupted Ruby.
10
+ class ImplicitEndChunk < BaseChunk
11
+ def wrap_in_markers
12
+ false
13
+ end
14
+
15
+ def transfer_correction(coordinator, all_corrected_ruby_lines, haml_lines); end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # Deals with interpolation within a plain text, filter, etc.
5
+ # Can only handling single line interpolation, so will be skipped if it takes
6
+ # more than one line or if the correction takes more than one line.
7
+ #
8
+ # Stores the char index to know where in the line to do the replacements.
9
+ class InterpolationChunk < BaseChunk
10
+ def initialize(*args, start_char_index:, **kwargs)
11
+ super(*args, **kwargs)
12
+ @start_char_index = start_char_index
13
+ end
14
+
15
+ def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines)
16
+ return if @ruby_lines.size != 1
17
+ return if to_ruby_lines.size != 1
18
+
19
+ from_ruby_line = @ruby_lines.first.partition(coordinator.script_output_prefix).last
20
+ to_ruby_line = to_ruby_lines.first.partition(coordinator.script_output_prefix).last
21
+
22
+ haml_line = haml_lines[@haml_line_index]
23
+ haml_line[@start_char_index...(@start_char_index + from_ruby_line.size)] = to_ruby_line
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # Chunk for dealing with every HAML filter other than `:ruby`
5
+ # The generated Ruby for these is just a HEREDOC, so interpolation is corrected at
6
+ # the same time by RuboCop.
7
+ class NonRubyFilterChunk < BaseChunk
8
+ def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines)
9
+ delta_indent = to_ruby_lines.first.index(/\S/) - @ruby_lines.first.index(/\S/)
10
+
11
+ haml_lines[@haml_line_index] = HamlLint::Utils.indent(haml_lines[@haml_line_index], delta_indent)
12
+
13
+ # Ignoring the starting <<~HAML_LINT_FILTER and ending end
14
+ to_content_lines = to_ruby_lines[1...-1]
15
+
16
+ to_haml_lines = to_content_lines.map do |line|
17
+ if line !~ /\S/
18
+ # whitespace or empty
19
+ ''
20
+ else
21
+ line
22
+ end
23
+ end
24
+
25
+ haml_lines[(@haml_line_index + 1)..haml_end_line_index] = to_haml_lines
26
+ end
27
+
28
+ def skip_line_indexes_in_source_map
29
+ [@ruby_lines.size - 1]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # This chunk just adds a marker (with a custom name) to the generated Ruby and only attempts to
5
+ # transfer the corrections it receives to the indentation of the associated lines.
6
+ #
7
+ # Also used so that Rubocop doesn't think that there is nothing in `if` and other such structures,
8
+ # so that it does corrections that make sense for the HAML.
9
+ class PlaceholderMarkerChunk < BaseChunk
10
+ def initialize(node, marker_name, indent:, nb_lines: 1, **kwargs)
11
+ @marker_name = marker_name
12
+ @indent = indent
13
+ @nb_lines = nb_lines
14
+ super(node, nil, **kwargs.merge(end_marker_indent: @indent))
15
+ end
16
+
17
+ def full_assemble(coordinator)
18
+ @start_marker_line_number = coordinator.add_marker(@indent, name: @marker_name,
19
+ haml_line_index: haml_line_index)
20
+ end
21
+
22
+ def transfer_correction(coordinator, all_corrected_ruby_lines, haml_lines)
23
+ marker_index = coordinator.find_line_index_of_marker_in_corrections(@start_marker_line_number,
24
+ name: @marker_name)
25
+ new_indent = all_corrected_ruby_lines[marker_index].index(/\S/)
26
+ return if new_indent == @indent
27
+ (haml_line_index..haml_end_line_index).each do |i|
28
+ haml_lines[i] = HamlLint::Utils.indent(haml_lines[i], new_indent - @indent)
29
+ end
30
+ end
31
+
32
+ def haml_end_line_index
33
+ haml_line_index + @nb_lines - 1
34
+ end
35
+
36
+ def end_marker_indent
37
+ @indent
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # Chunk for dealing with `:ruby` filter.
5
+ class RubyFilterChunk < BaseChunk
6
+ attr_reader :start_marker_indent
7
+
8
+ def initialize(*args, start_marker_indent:, **kwargs)
9
+ super(*args, **kwargs)
10
+ @start_marker_indent = start_marker_indent
11
+ end
12
+
13
+ def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines)
14
+ marker_index = coordinator.find_line_index_of_marker_in_corrections(@start_marker_line_number)
15
+
16
+ new_name_indent = coordinator.corrected_ruby_lines[marker_index].index(/\S/)
17
+
18
+ delta_indent = new_name_indent - @start_marker_indent
19
+ haml_lines[@haml_line_index - 1] = HamlLint::Utils.indent(haml_lines[@haml_line_index - 1], delta_indent)
20
+
21
+ to_haml_lines = to_ruby_lines.map do |line|
22
+ if line !~ /\S/
23
+ # whitespace or empty
24
+ ''
25
+ else
26
+ " #{line}"
27
+ end
28
+ end
29
+
30
+ haml_lines[@haml_line_index..haml_end_line_index] = to_haml_lines
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ RubySource = Struct.new(:source, :source_map, :ruby_chunks)
5
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # Chunk for handling outputting and silent scripts, so ` = foo` and ` - bar`
5
+ # Does NOT handle a script beside a tag (ex: `%div= spam`)
6
+ class ScriptChunk < BaseChunk
7
+ MID_BLOCK_KEYWORDS = %w[else elsif when rescue ensure].freeze
8
+
9
+ # @return [Boolean] true if this ScriptChunk must be at the beginning of a chunk.
10
+ # This blocks this ScriptChunk from being fused to a ScriptChunk that is before it.
11
+ # Needed to handle some patterns of outputting script.
12
+ attr_reader :must_start_chunk
13
+
14
+ # @return [Array<Integer>] Line indexes to ignore when building the source_map. For examples,
15
+ # implicit `end` are on their own line in the Ruby file, but in the HAML, they are absent.
16
+ attr_reader :skip_line_indexes_in_source_map
17
+
18
+ # @return [HamlLint::RubyExtraction::BaseChunk] The previous chunk can affect how
19
+ # our starting marker must be indented.
20
+ attr_reader :previous_chunk
21
+
22
+ def initialize(*args, previous_chunk:, must_start_chunk: false,
23
+ skip_line_indexes_in_source_map: [], **kwargs)
24
+ super(*args, **kwargs)
25
+ @must_start_chunk = must_start_chunk
26
+ @skip_line_indexes_in_source_map = skip_line_indexes_in_source_map
27
+ @previous_chunk = previous_chunk
28
+ end
29
+
30
+ def fuse(following_chunk)
31
+ case following_chunk
32
+ when ScriptChunk
33
+ fuse_script_chunk(following_chunk)
34
+ when ImplicitEndChunk
35
+ fuse_implicit_end(following_chunk)
36
+ end
37
+ end
38
+
39
+ def fuse_script_chunk(following_chunk)
40
+ return if following_chunk.end_marker_indent.nil?
41
+ return if following_chunk.must_start_chunk
42
+
43
+ nb_blank_lines_between = following_chunk.haml_line_index - haml_line_index - nb_haml_lines
44
+ blank_lines = nb_blank_lines_between > 0 ? [''] * nb_blank_lines_between : []
45
+ new_lines = @ruby_lines + blank_lines + following_chunk.ruby_lines
46
+
47
+ source_map_skips = @skip_line_indexes_in_source_map
48
+ source_map_skips.concat(following_chunk.skip_line_indexes_in_source_map
49
+ .map { |i| i + @ruby_lines.size })
50
+
51
+ ScriptChunk.new(node,
52
+ new_lines,
53
+ haml_line_index: haml_line_index,
54
+ skip_line_indexes_in_source_map: source_map_skips,
55
+ end_marker_indent: following_chunk.end_marker_indent,
56
+ previous_chunk: previous_chunk)
57
+ end
58
+
59
+ def fuse_implicit_end(following_chunk)
60
+ new_lines = @ruby_lines.dup
61
+ last_non_empty_line_index = new_lines.rindex { |line| line =~ /\S/ }
62
+
63
+ # There is only one line in ImplicitEndChunk
64
+ new_end_index = last_non_empty_line_index + 1
65
+ new_lines.insert(new_end_index, following_chunk.ruby_lines.first)
66
+ source_map_skips = @skip_line_indexes_in_source_map + [new_end_index]
67
+
68
+ ScriptChunk.new(node,
69
+ new_lines,
70
+ haml_line_index: haml_line_index,
71
+ skip_line_indexes_in_source_map: source_map_skips,
72
+ end_marker_indent: following_chunk.end_marker_indent,
73
+ previous_chunk: previous_chunk)
74
+ end
75
+
76
+ def start_marker_indent
77
+ default_indent = super
78
+ default_indent += 2 if MID_BLOCK_KEYWORDS.include?(ChunkExtractor.block_keyword(ruby_lines.first))
79
+ [default_indent, previous_chunk&.end_marker_indent || previous_chunk&.start_marker_indent].compact.max
80
+ end
81
+
82
+ def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines) # rubocop:disable Metrics
83
+ to_ruby_lines.reject! { |l| l.strip == 'end' }
84
+
85
+ output_comment_prefix = ' ' + coordinator.script_output_prefix.rstrip
86
+ to_ruby_lines.map! do |line|
87
+ if line.lstrip.start_with?('#' + output_comment_prefix)
88
+ line = line.dup
89
+ comment_index = line.index('#')
90
+ removal_start_index = comment_index + 1
91
+ removal_end_index = removal_start_index + output_comment_prefix.size
92
+ line[removal_start_index...removal_end_index] = ''
93
+ # It will be removed again below, but will know its suposed to be a =
94
+ line.insert(comment_index, coordinator.script_output_prefix)
95
+ end
96
+ line
97
+ end
98
+
99
+ continued_line_indent_delta = 2
100
+
101
+ to_haml_lines = to_ruby_lines.map.with_index do |line, i|
102
+ if line !~ /\S/
103
+ # whitespace or empty lines, we don't want any indentation
104
+ ''
105
+ elsif line_starts_script?(to_ruby_lines, i)
106
+ code_start = line.index(/\S/)
107
+ if line[code_start..].start_with?(coordinator.script_output_prefix)
108
+ line = line.sub(coordinator.script_output_prefix, '')
109
+ continued_line_indent_delta = 2 - coordinator.script_output_prefix.size
110
+ "#{line[0...code_start]}= #{line[code_start..]}"
111
+ else
112
+ continued_line_indent_delta = 2
113
+ "#{line[0...code_start]}- #{line[code_start..]}"
114
+ end
115
+ else
116
+ HamlLint::Utils.indent(line, continued_line_indent_delta)
117
+ end
118
+ end
119
+
120
+ haml_lines[@haml_line_index..haml_end_line_index] = to_haml_lines
121
+ end
122
+
123
+ def unfinished_script_line?(lines, line_index)
124
+ !!lines[line_index][/,[ \t]*\z/]
125
+ end
126
+
127
+ def line_starts_script?(lines, line_index)
128
+ return true if line_index == 0
129
+ !unfinished_script_line?(lines, line_index - 1)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # Chunk for handling the a tag attributes, such as `%div{style: 'yes_please'}`
5
+ class TagAttributesChunk < BaseChunk
6
+ def initialize(*args, indent_to_remove:, **kwargs)
7
+ super(*args, **kwargs)
8
+ @indent_to_remove = indent_to_remove
9
+ end
10
+
11
+ def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines)
12
+ affected_haml_lines = haml_lines[@haml_line_index..haml_end_line_index]
13
+
14
+ affected_haml = affected_haml_lines.join("\n")
15
+
16
+ from_ruby = unwrap(@ruby_lines).join("\n")
17
+ to_ruby = unwrap(to_ruby_lines).join("\n")
18
+
19
+ affected_start_index = affected_haml.index(from_ruby)
20
+ affected_end_index = affected_start_index + from_ruby.size
21
+ affected_haml[affected_start_index...affected_end_index] = to_ruby
22
+
23
+ haml_lines[@haml_line_index..haml_end_line_index] = affected_haml.split("\n")
24
+ end
25
+
26
+ def unwrap(lines)
27
+ lines = lines.dup
28
+ lines[0] = lines[0].sub(/^\s*/, '').sub(/W+\(/, '')
29
+ lines[-1] = lines[-1].sub(/\)\s*\Z/, '')
30
+
31
+ if @indent_to_remove
32
+ HamlLint::Utils.map_after_first!(lines) do |line|
33
+ line.sub(/^ {1,#{@indent_to_remove}}/, '')
34
+ end
35
+ end
36
+ lines
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # Chunk for handling outputting scripts after a tag, such as `%div= spam`
5
+ class TagScriptChunk < BaseChunk
6
+ def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines) # rubocop:disable Metrics/AbcSize
7
+ # TODO: add checks that we have commas at the end of each line except the last one
8
+
9
+ from_ruby_line = @ruby_lines.first
10
+ to_ruby_line = to_ruby_lines.first
11
+
12
+ to_line_indent = to_ruby_line.index(/\S/)
13
+
14
+ from_ruby_line = from_ruby_line.sub(coordinator.script_output_prefix, '').sub(/^\s+/, '')
15
+ to_ruby_line = to_ruby_line.sub(coordinator.script_output_prefix, '').sub(/^\s+/, '')
16
+
17
+ affected_start_index = haml_lines[@haml_line_index].rindex(from_ruby_line)
18
+
19
+ haml_lines[@haml_line_index][affected_start_index..-1] = to_ruby_line
20
+
21
+ indent_delta = affected_start_index - coordinator.script_output_prefix.size - to_line_indent
22
+
23
+ HamlLint::Utils.map_after_first!(to_ruby_lines) do |line|
24
+ HamlLint::Utils.indent(line, indent_delta)
25
+ end
26
+
27
+ haml_lines[(@haml_line_index + 1)..haml_end_line_index] = to_ruby_lines[1..]
28
+ end
29
+ end
30
+ end
@@ -24,7 +24,7 @@ module HamlLint
24
24
  # The translation won't be perfect, and won't make any real sense, but the
25
25
  # relationship between variable declarations/uses and the flow control graph
26
26
  # will remain intact.
27
- class RubyExtractor # rubocop:disable Metrics/ClassLength
27
+ class RubyExtractor
28
28
  include HamlVisitor
29
29
 
30
30
  # Stores the extracted source and a map of lines of generated source to the
@@ -73,8 +73,9 @@ module HamlLint
73
73
 
74
74
  # Attributes can either be a method call or a literal hash, so wrap it
75
75
  # in a method call itself in order to avoid having to differentiate the
76
- # two.
77
- add_line("{}.merge(#{attributes_code})", node)
76
+ # two. Use the tag name for the method to differentiate different tag types
77
+ # for RuboCop and prevent erroneous warnings.
78
+ add_line("#{node.tag_name}(#{attributes_code})", node)
78
79
  end
79
80
 
80
81
  check_tag_static_hash_source(node)
@@ -128,7 +129,7 @@ module HamlLint
128
129
  def visit_filter(node)
129
130
  if node.filter_type == 'ruby'
130
131
  node.text.split("\n").each_with_index do |line, index|
131
- add_line(line, node.line + index + 1, false)
132
+ add_line(line, node.line + index + 1, discard_blanks: false)
132
133
  end
133
134
  else
134
135
  add_dummy_puts(node, ":#{node.filter_type}")
@@ -161,17 +162,17 @@ module HamlLint
161
162
  @output_count += 1
162
163
  end
163
164
 
164
- def add_line(code, node_or_line, discard_blanks = true)
165
+ def add_line(code, node_or_line, discard_blanks: true)
165
166
  return if code.empty? && discard_blanks
166
167
 
167
168
  indent_level = @indent_level
168
169
 
169
- if node_or_line.respond_to?(:line)
170
+ if node_or_line.respond_to?(:line) && mid_block_keyword?(code)
170
171
  # Since mid-block keywords are children of the corresponding start block
171
172
  # keyword, we need to reduce their indentation level by 1. However, we
172
173
  # don't do this unless this is an actual tag node (a raw line number
173
174
  # means this came from a `:ruby` filter).
174
- indent_level -= 1 if mid_block_keyword?(code)
175
+ indent_level -= 1
175
176
  end
176
177
 
177
178
  indent = (' ' * 2 * indent_level)
@@ -196,7 +197,7 @@ module HamlLint
196
197
  end
197
198
 
198
199
  def anonymous_block?(text)
199
- text =~ /\bdo\s*(\|\s*[^\|]*\s*\|)?(\s*#.*)?\z/
200
+ text =~ /\bdo\s*(\|\s*[^|]*\s*\|)?(\s*#.*)?\z/
200
201
  end
201
202
 
202
203
  START_BLOCK_KEYWORDS = %w[if unless case begin for until while].freeze
@@ -212,8 +213,8 @@ module HamlLint
212
213
  LOOP_KEYWORDS = %w[for until while].freeze
213
214
  def block_keyword(text)
214
215
  # Need to handle 'for'/'while' since regex stolen from HAML parser doesn't
215
- if keyword = text[/\A\s*([^\s]+)\s+/, 1]
216
- return keyword if LOOP_KEYWORDS.include?(keyword)
216
+ if (keyword = text[/\A\s*([^\s]+)\s+/, 1]) && LOOP_KEYWORDS.include?(keyword)
217
+ return keyword
217
218
  end
218
219
 
219
220
  return unless keyword = text.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0]
@@ -24,6 +24,8 @@ module HamlLint
24
24
  @linter_selector = HamlLint::LinterSelector.new(config, options)
25
25
  @fail_fast = options.fetch(:fail_fast, false)
26
26
  @cache = {}
27
+ @autocorrect = options[:autocorrect]
28
+ @autocorrect_only = options[:autocorrect_only]
27
29
 
28
30
  report(options)
29
31
  end
@@ -88,9 +90,39 @@ module HamlLint
88
90
  e.line, e.to_s, :error)]
89
91
  end
90
92
 
91
- linter_selector.linters_for_file(file).map do |linter|
92
- linter.run(document)
93
- end.flatten
93
+ linters = linter_selector.linters_for_file(file)
94
+ lint_arrays = []
95
+
96
+ if @autocorrect
97
+ lint_arrays << autocorrect_document(document, linters)
98
+ end
99
+
100
+ unless @autocorrect_only
101
+ lint_arrays << linters.map do |linter|
102
+ linter.run(document)
103
+ end
104
+ end
105
+ lint_arrays.flatten
106
+ end
107
+
108
+ # Out of the provided linters, runs those that support autocorrect
109
+ # against the specified document.
110
+ # Updates the document and returns the lints that were corrected.
111
+ #
112
+ # @param document [HamlLint::Document]
113
+ # @param linter_selector [HamlLint::LinterSelector]
114
+ # @return [Array<HamlLint::Lint>]
115
+ def autocorrect_document(document, linters)
116
+ lint_arrays = []
117
+
118
+ autocorrecting_linters = linters.select(&:supports_autocorrect?)
119
+ lint_arrays << autocorrecting_linters.map do |linter|
120
+ linter.run(document, autocorrect: @autocorrect)
121
+ end
122
+
123
+ document.write_to_disk!
124
+
125
+ lint_arrays
94
126
  end
95
127
 
96
128
  # Returns the list of files that should be linted given the specified
@@ -10,9 +10,11 @@ module HamlLint
10
10
  expected_line = options[:line]
11
11
  expected_message = options[:message]
12
12
  expected_severity = options[:severity]
13
+ expected_corrected = options[:corrected]
13
14
 
14
15
  match do |linter|
15
- has_lints?(linter, expected_line, count, expected_message, expected_severity)
16
+ has_lints?(linter, expected_line, count, expected_message, expected_severity,
17
+ expected_corrected)
16
18
  end
17
19
 
18
20
  failure_message do |linter|
@@ -63,13 +65,15 @@ module HamlLint
63
65
  (expected_severity ? " with severity '#{expected_severity}'" : '')
64
66
  end
65
67
 
66
- def has_lints?(linter, expected_line, count, expected_message, expected_severity)
68
+ def has_lints?(linter, expected_line, count, expected_message, expected_severity, # rubocop:disable Metrics/ParameterLists
69
+ expected_corrected)
67
70
  if expected_line
68
71
  has_expected_line_lints?(linter,
69
72
  expected_line,
70
73
  count,
71
74
  expected_message,
72
- expected_severity)
75
+ expected_severity,
76
+ expected_corrected)
73
77
  elsif count
74
78
  linter.lints.count == count
75
79
  elsif expected_message
@@ -79,17 +83,20 @@ module HamlLint
79
83
  end
80
84
  end
81
85
 
82
- def has_expected_line_lints?(linter,
86
+ def has_expected_line_lints?(linter, # rubocop:disable Metrics/ParameterLists
83
87
  expected_line,
84
88
  count,
85
89
  expected_message,
86
- expected_severity)
90
+ expected_severity,
91
+ expected_corrected)
87
92
  if count
88
93
  multiple_lints_match_line?(linter, expected_line, count)
89
94
  elsif expected_message
90
95
  lint_on_line_matches_message?(linter, expected_line, expected_message)
91
96
  elsif expected_severity
92
97
  lint_on_line_matches_severity?(linter, expected_line, expected_severity)
98
+ elsif !expected_corrected.nil?
99
+ lint_on_line_matches_corrected?(linter, expected_line, expected_corrected)
93
100
  else
94
101
  lint_lines(linter).include?(expected_line)
95
102
  end
@@ -101,9 +108,10 @@ module HamlLint
101
108
  end
102
109
 
103
110
  def lint_on_line_matches_message?(linter, expected_line, expected_message)
111
+ # Using === to support regex to match anywhere in the string
104
112
  linter
105
113
  .lints
106
- .any? { |lint| lint.line == expected_line && lint.message == expected_message }
114
+ .any? { |lint| lint.line == expected_line && expected_message === lint.message } # rubocop:disable Style/CaseEquality
107
115
  end
108
116
 
109
117
  def lint_on_line_matches_severity?(linter, expected_line, expected_severity)
@@ -112,8 +120,15 @@ module HamlLint
112
120
  .any? { |lint| lint.line == expected_line && lint.severity == expected_severity }
113
121
  end
114
122
 
123
+ def lint_on_line_matches_corrected?(linter, expected_line, expected_corrected)
124
+ linter
125
+ .lints
126
+ .any? { |lint| lint.line == expected_line && lint.corrected == expected_corrected }
127
+ end
128
+
115
129
  def lint_messages_match?(linter, expected_message)
116
- lint_messages(linter).all? { |message| message == expected_message }
130
+ # Using === to support regex to match anywhere in the string
131
+ lint_messages(linter).all? { |message| expected_message === message } # rubocop:disable Style/CaseEquality
117
132
  end
118
133
 
119
134
  def lint_lines(linter)
@@ -6,8 +6,8 @@ module HamlLint
6
6
  # for writing code without having the leading indentation count.
7
7
  module IndentNormalizer
8
8
  def normalize_indent(code)
9
- leading_indent = code[/^(\s*)/, 1]
10
- code.lstrip.gsub(/\n#{leading_indent}/, "\n")
9
+ leading_indent = code[/([ \t]*)/, 1]
10
+ code.gsub(/^#{leading_indent}/, '')
11
11
  end
12
12
  end
13
13
  end
@@ -14,13 +14,21 @@ module HamlLint
14
14
  }
15
15
  end
16
16
 
17
+ let(:autocorrect) { nil }
18
+
17
19
  let(:config) { options[:config].for_linter(described_class) }
18
20
 
19
21
  let(:document) { HamlLint::Document.new(normalize_indent(haml), options) }
20
22
 
23
+ # :run_or_raise, :run, or nil to not auto-call something
24
+ let(:run_method_to_use) { :run_or_raise }
25
+
21
26
  subject { described_class.new(config) }
22
27
 
23
- before { subject.run(document) }
28
+ before do
29
+ next unless run_method_to_use
30
+ subject.send(run_method_to_use, document, autocorrect: autocorrect)
31
+ end
24
32
  end
25
33
  end
26
34
  end