haml_lint 0.44.0 → 0.46.0

Sign up to get free protection for your applications and to get access to all the features.
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