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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4157a366e322124248553b8119eaa33321000123121b8250eee8d0357131c65a
4
- data.tar.gz: 631eb064ae4614ed9682ae88d85c276a87e0d2b6b35f19f51869601bbde31bce
3
+ metadata.gz: 8bb347cd92b5e23b37b8beb59d2c67f23eecd1e5778fdc45a7c4fbd73d14f854
4
+ data.tar.gz: 93836aaa95ab3bde6932969c81fb9e409c9806ab9057a1162580e1771f1edff7
5
5
  SHA512:
6
- metadata.gz: 85756d43195b59d6582f2c352ef33084cab05d338cf56338d4b82b405890333894a12ebe1716301d831895fa24293594a506be6fc5ba0b112f7edd953963fd58
7
- data.tar.gz: 0ef90b78b0d81b92bee5ff53a89d360ae831a1cf76b79f746a4a37b52e6a3e6d9a882af1ca206d1a0fe69e9cfceead2bbe5b1ea93ff3e90cef3d317aed2b06fb
6
+ metadata.gz: 5c5789d9ca6de3d2cae48ef91277b374ed84b477323389ccc7049d1d4e690ef1cb2252781074c902c7b82fa9dd332a36ace0d25652723841e170c89c9c805e15
7
+ data.tar.gz: 325df8db407db7567bf88db824dc2a071af3457c827b5268e96e6e2e17f0a63b6192974082ed8e4bab9ca0b645aacfbec981d7a25e6e9e7330d28f8c60df2123
data/config/default.yml CHANGED
@@ -3,7 +3,7 @@
3
3
  # This is an opinionated list of which hooks are valuable to run and what their
4
4
  # out of the box settings should be.
5
5
 
6
- # Whether to ignore frontmatter at the beginning of HAML documents for
6
+ # Whether to ignore frontmatter at the beginning of Haml documents for
7
7
  # frameworks such as Jekyll/Middleman
8
8
  skip_frontmatter: false
9
9
 
@@ -50,8 +50,8 @@ linters:
50
50
 
51
51
  Indentation:
52
52
  enabled: true
53
- character: space # or tab
54
- width: 2 # ignored if character == tab
53
+ character: space # or tab
54
+ width: 2 # ignored if character == tab
55
55
 
56
56
  InlineStyles:
57
57
  enabled: true
@@ -88,8 +88,10 @@ linters:
88
88
 
89
89
  RuboCop:
90
90
  enabled: true
91
- # Users can ignore cops using this configuration instead of editing their rubocop configuration.
92
- # Mostly there for backward compatibility.
91
+ # Cops listed here are skipped entirely (passed to RuboCop via `--except`), both when
92
+ # reporting and when auto-correcting. This is the way to disable a cop only for Haml files,
93
+ # and the only way to disable a forced cop (see config/forced_rubocop_config.yml), since the
94
+ # forced configuration always overrides `Enabled: false` set in your own .rubocop.yml.
93
95
  ignored_cops: []
94
96
 
95
97
  RubyComments:
@@ -118,6 +120,9 @@ linters:
118
120
  TrailingWhitespace:
119
121
  enabled: true
120
122
 
123
+ UnescapedHtml:
124
+ enabled: true
125
+
121
126
  UnnecessaryInterpolation:
122
127
  enabled: true
123
128
 
@@ -1,7 +1,7 @@
1
1
  # These are some configurations that are required for RuboCop because:
2
- # * HAML-Lint compiles ruby code with a particular format. If the rules mis-match that format,
3
- # HAML-Lint would generate lints that the user cannot fix.
4
- # * HAML-Lint can autocorrect code only if the result matches some specific format
2
+ # * Haml-Lint compiles ruby code with a particular format. If the rules mis-match that format,
3
+ # Haml-Lint would generate lints that the user cannot fix.
4
+ # * Haml-Lint can autocorrect code only if the result matches some specific format
5
5
  #
6
6
  # So these configuration should not be overwritable by users.
7
7
 
@@ -31,17 +31,17 @@ Layout/CaseIndentation:
31
31
  IndentOneStep: false # Need to force the `false`
32
32
 
33
33
  # Need this cop so that code gets formatted similarly to Haml's indentation,
34
- # since HAML-Lint relies on Ruby's indentation being the same as Haml's.
34
+ # since Haml-Lint relies on Ruby's indentation being the same as Haml's.
35
35
  Layout/ElseAlignment:
36
36
  Enabled: true
37
37
 
38
38
  # Need this cop so that code gets formatted similarly to Haml's indentation,
39
- # since HAML-Lint relies on Ruby's indentation being the same as Haml's.
39
+ # since Haml-Lint relies on Ruby's indentation being the same as Haml's.
40
40
  Layout/EndAlignment:
41
41
  EnforcedStyleAlignWith: start_of_line
42
42
  Enabled: true
43
43
 
44
- # We generate the ruby content, this is basically useless and should be a lint in HAML-Lint
44
+ # We generate the ruby content, this is basically useless and should be a lint in Haml-Lint
45
45
  Layout/EndOfLine:
46
46
  Enabled: false
47
47
 
@@ -90,14 +90,14 @@ Metrics/BlockNesting:
90
90
  Naming/FileName:
91
91
  Enabled: false
92
92
 
93
- # HAML doesn't properly support multiline blocks using { }, only using do/end.
93
+ # Haml doesn't properly support multiline blocks using { }, only using do/end.
94
94
  # If you don't consider the { } block for indentation, things "works", but the indentation is misleading.
95
95
  # For example, this works:
96
96
  # - a = lambda {
97
97
  # - if abc
98
98
  # - something
99
99
  # - }
100
- # But if you indented the 2 lines within { }, then HAML would add an extra `end` and the generated
100
+ # But if you indented the 2 lines within { }, then Haml would add an extra `end` and the generated
101
101
  # ruby would be invalid.
102
102
  Style/BlockDelimiters:
103
103
  # So we need this cop to cleanup those cases and turn them to `end`.
@@ -106,7 +106,7 @@ Style/BlockDelimiters:
106
106
  # We don't allow the default "Can be anything" exception for lambda/proc
107
107
  <%= rubocop_version < '1.33' ? 'IgnoredMethods' : 'AllowedMethods' %>: []
108
108
 
109
- # We don't support correcting HAML comments
109
+ # We don't support correcting Haml comments
110
110
  Style/CommentAnnotation:
111
111
  AutoCorrect: false
112
112
 
@@ -144,7 +144,7 @@ Style/RedundantStringEscape:
144
144
  <% end %>
145
145
 
146
146
  # In some case, this cop can cause a change in the spacing.
147
- # In HAML 5.2, going from (absurd example for clarity):
147
+ # In Haml 5.2, going from (absurd example for clarity):
148
148
  # = 'abc' rescue nil
149
149
  # = 'def'
150
150
  # to:
@@ -161,7 +161,7 @@ Style/RescueModifier:
161
161
 
162
162
  # Cops that remove commas can be a problem when lines are split on multiple ones.
163
163
  # If we have a big array on more than one line, the removal of the comma generates
164
- # invalid HAML
164
+ # invalid Haml
165
165
  Style/SymbolArray:
166
166
  Enabled: false
167
167
 
@@ -170,7 +170,7 @@ Style/UnlessElse:
170
170
  AutoCorrect: false
171
171
 
172
172
  # If an array of strings was on multiple lines, this cop will make a %w(...) on multiple lines.
173
- # Without the comma at the end of the first line, there the resulting HAML will be invalid, since the only
173
+ # Without the comma at the end of the first line, there the resulting Haml will be invalid, since the only
174
174
  # case where a script can change line is after a comma.
175
175
  Style/WordArray:
176
176
  AutoCorrect: false
@@ -181,7 +181,7 @@ Layout/TrailingEmptyLines:
181
181
 
182
182
  <% if rubocop_version < '1.8.1' %>
183
183
  # There were a few bugs with this cop that got fixed in this version.
184
- # Before, those bugs would generate invalid Ruby code and that would make it look like HAML-lint is
184
+ # Before, those bugs would generate invalid Ruby code and that would make it look like Haml-lint is
185
185
  # responsible, at least from the user's point of view.
186
186
  Style/StringConcatenation:
187
187
  Enabled: false
data/lib/haml_lint/cli.rb CHANGED
@@ -100,7 +100,11 @@ module HamlLint
100
100
  # @return [HamlLint::Reporter]
101
101
  def reporter_from_options(options)
102
102
  if options[:auto_gen_config]
103
- HamlLint::Reporter::DisabledConfigReporter.new(log, limit: options[:auto_gen_exclude_limit] || 15)
103
+ HamlLint::Reporter::DisabledConfigReporter.new(
104
+ log,
105
+ limit: options[:auto_gen_exclude_limit] || HamlLint::Reporter::DisabledConfigReporter::DEFAULT_EXCLUDE_LIMIT,
106
+ options: options
107
+ )
104
108
  else
105
109
  options.fetch(:reporter, HamlLint::Reporter::DefaultReporter).new(log)
106
110
  end
@@ -14,9 +14,6 @@ module HamlLint
14
14
  # @return [String] Haml template file path
15
15
  attr_reader :file
16
16
 
17
- # @return [Boolean] true if source was read directly from `file` on-disk (rather than from stdin)
18
- attr_reader :file_on_disk
19
-
20
17
  # @return [Boolean] true if source changes (from autocorrect) should be written to stdout instead of disk
21
18
  attr_reader :write_to_stdout
22
19
 
@@ -42,14 +39,12 @@ module HamlLint
42
39
  # @param source [String] Haml code to parse
43
40
  # @param options [Hash]
44
41
  # @option options :file [String] file name of document that was parsed
45
- # @option options :file_on_disk [Boolean] true if source was read straight from `file` on disk
46
42
  # @option options :write_to_stdout [Boolean] true if source changes should be written to stdout
47
43
  # @raise [Haml::Parser::Error] if there was a problem parsing the document
48
44
  def initialize(source, options)
49
45
  @config = options[:config]
50
46
  @file = options.fetch(:file, STRING_SOURCE)
51
47
  @write_to_stdout = options[:write_to_stdout]
52
- @file_on_disk = options[:file_on_disk] && @file != STRING_SOURCE
53
48
  @source_was_changed = false
54
49
  process_source(source)
55
50
  end
@@ -137,8 +132,8 @@ module HamlLint
137
132
  # @param original_tree [Haml::Parser::ParseNode]
138
133
  # @return [Haml::Tree::Node]
139
134
  def process_tree(original_tree)
140
- # Remove the trailing empty HAML comment that the parser creates to signal
141
- # the end of the HAML document
135
+ # Remove the trailing empty Haml comment that the parser creates to signal
136
+ # the end of the Haml document
142
137
  if Gem::Requirement.new('~> 4.0.0').satisfied_by?(Gem.loaded_specs['haml'].version)
143
138
  original_tree.children.pop
144
139
  end
@@ -147,7 +142,7 @@ module HamlLint
147
142
  convert_tree(original_tree)
148
143
  end
149
144
 
150
- # Converts a HAML parse tree to a tree of {HamlLint::Tree::Node} objects.
145
+ # Converts a Haml parse tree to a tree of {HamlLint::Tree::Node} objects.
151
146
  #
152
147
  # This provides a cleaner interface with which the linters can interact with
153
148
  # the parse tree.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Collection of exceptions that can be raised by the HAML Lint application.
3
+ # Collection of exceptions that can be raised by the Haml Lint application.
4
4
  module HamlLint::Exceptions
5
5
  # Raised when a {Configuration} could not be loaded from a file.
6
6
  class ConfigurationError < StandardError; end
@@ -17,7 +17,7 @@ module HamlLint::Exceptions
17
17
  # Raised when an invalid file path is specified
18
18
  class InvalidFilePath < StandardError; end
19
19
 
20
- # Raised when a problem occurs parsing a HAML document.
20
+ # Raised when a problem occurs parsing a Haml document.
21
21
  class ParseError < ::Haml::SyntaxError; end
22
22
 
23
23
  # Raised when attempting to execute `Runner` with options that would result in
@@ -14,8 +14,8 @@ module Haml::Util
14
14
  Thread.current[:haml_lint_unescape_interpolation_to_original_cache] ||= {}
15
15
  end
16
16
 
17
- # As soon as a HamlLint::Document has finished processing a HAML source, this gets called to
18
- # get a copy of this cache and clear up for the next HAML processing
17
+ # As soon as a HamlLint::Document has finished processing a Haml source, this gets called to
18
+ # get a copy of this cache and clear up for the next Haml processing
19
19
  def self.unescape_interpolation_to_original_cache_take_and_wipe
20
20
  value = unescape_interpolation_to_original_cache.dup
21
21
  unescape_interpolation_to_original_cache.clear
@@ -2,7 +2,7 @@
2
2
 
3
3
  module HamlLint
4
4
  # Provides an interface which when included allows a class to visit nodes in
5
- # the parse tree of a HAML document.
5
+ # the parse tree of a Haml document.
6
6
  module HamlVisitor
7
7
  def visit(node)
8
8
  # Keep track of whether this block was consumed by the visitor. This
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HamlLint
4
- # Contains information about a problem or issue with a HAML document.
4
+ # Contains information about a problem or issue with a Haml document.
5
5
  class Lint
6
6
  # @return [Boolean] If the error was corrected by auto-correct
7
7
  attr_reader :corrected
@@ -18,15 +18,19 @@ module HamlLint
18
18
  class Linter::ClassAttributeWithStaticValue < Linter
19
19
  include LinterRegistry
20
20
 
21
+ supports_autocorrect(true)
22
+
21
23
  STATIC_TYPES = %i[str sym].freeze
22
24
 
23
25
  VALID_CLASS_REGEX = /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/
24
26
 
25
27
  def visit_tag(node)
26
- return unless contains_class_attribute?(node.dynamic_attributes_sources)
28
+ class_value = static_class_value(node.dynamic_attributes_sources)
29
+ return unless class_value
27
30
 
31
+ corrected = correct_class_attribute(node, class_value)
28
32
  record_lint(node, 'Avoid defining `class` in attributes hash ' \
29
- 'for static class names')
33
+ 'for static class names', corrected: corrected)
30
34
  end
31
35
 
32
36
  private
@@ -35,28 +39,64 @@ module HamlLint
35
39
  code.start_with?('{') && code.end_with?('}')
36
40
  end
37
41
 
38
- def contains_class_attribute?(attributes_sources)
42
+ # @return [String, nil]
43
+ def static_class_value(attributes_sources)
39
44
  attributes_sources.each do |code|
40
45
  ast_root = parse_ruby(surrounded_by_braces?(code) ? code : "{#{code}}")
41
46
  next unless ast_root # RuboCop linter will report syntax errors
42
47
 
43
48
  ast_root.children.each do |pair|
44
- return true if static_class_attribute_value?(pair)
49
+ value = static_class_attribute_value(pair)
50
+ return value if value
45
51
  end
46
52
  end
47
53
 
48
- false
54
+ nil
49
55
  end
50
56
 
51
- def static_class_attribute_value?(pair)
52
- return false if (children = pair.children).empty?
57
+ # @return [String, nil]
58
+ def static_class_attribute_value(pair)
59
+ return nil if (children = pair.children).empty?
53
60
 
54
61
  key, value = children
55
62
 
56
- STATIC_TYPES.include?(key.type) &&
57
- key.children.first.to_sym == :class &&
58
- STATIC_TYPES.include?(value.type) &&
59
- value.children.first =~ VALID_CLASS_REGEX
63
+ return nil unless STATIC_TYPES.include?(key.type) &&
64
+ key.children.first.to_sym == :class &&
65
+ STATIC_TYPES.include?(value.type)
66
+
67
+ class_name = value.children.first.to_s
68
+ VALID_CLASS_REGEX.match?(class_name) ? class_name : nil
69
+ end
70
+
71
+ # @return [Boolean]
72
+ def correct_class_attribute(node, class_value)
73
+ hash_source = node.hash_attributes_source
74
+ return false unless hash_source
75
+ return false if hash_source.include?("\n")
76
+ return false unless node.dynamic_attributes_source.keys == [:hash]
77
+ return false unless sole_pair?(node.dynamic_attributes_sources)
78
+ return false if node.static_classes.flatten.include?(class_value)
79
+
80
+ index = node.line - 1
81
+ line = autocorrected_lines[index]
82
+ old = "#{node.static_attributes_source}#{hash_source}"
83
+ return false unless line.include?(old)
84
+
85
+ new = "#{node.static_attributes_source}.#{class_value}"
86
+ correct_line(index, line.sub(old, new))
87
+ end
88
+
89
+ # Whether the attribute hash contains exactly one key/value pair (the class).
90
+ #
91
+ # @return [Boolean]
92
+ def sole_pair?(attributes_sources)
93
+ return false unless attributes_sources.size == 1
94
+
95
+ code = attributes_sources.first
96
+ ast_root = parse_ruby(surrounded_by_braces?(code) ? code : "{#{code}}")
97
+ return false unless ast_root
98
+
99
+ ast_root.children.size == 1
60
100
  end
61
101
  end
62
102
  end
@@ -5,6 +5,8 @@ module HamlLint
5
5
  class Linter::ClassesBeforeIds < Linter
6
6
  include LinterRegistry
7
7
 
8
+ supports_autocorrect(true)
9
+
8
10
  MSG = '%s should be listed before %s (%s should precede %s)'
9
11
 
10
12
  def visit_tag(node)
@@ -17,14 +19,27 @@ module HamlLint
17
19
  next unless next_val.start_with?(first) &&
18
20
  current_val.start_with?(second)
19
21
 
22
+ corrected = correct_attribute_order(node, components)
20
23
  failure_message = format(MSG, *(attribute_type_order + [next_val, current_val]))
21
- record_lint(node, failure_message)
24
+ record_lint(node, failure_message, corrected: corrected)
22
25
  break
23
26
  end
24
27
  end
25
28
 
26
29
  private
27
30
 
31
+ # @param node [HamlLint::Tree::TagNode]
32
+ # @param components [Array<String>] the `.class`/`#id` components in source order
33
+ # @return [Boolean]
34
+ def correct_attribute_order(node, components)
35
+ classes, ids = components.partition { |component| component.start_with?('.') }
36
+ new_source = (ids_first? ? ids + classes : classes + ids).join
37
+
38
+ index = node.line - 1
39
+ line = autocorrected_lines[index]
40
+ correct_line(index, line.sub(node.static_attributes_source, new_source))
41
+ end
42
+
28
43
  def attribute_prefix_order
29
44
  default = %w[. #]
30
45
  default.reverse! if ids_first?
@@ -5,6 +5,9 @@ module HamlLint
5
5
  class Linter::ConsecutiveComments < Linter
6
6
  include LinterRegistry
7
7
 
8
+ supports_autocorrect(true)
9
+ autocorrect_safe(false)
10
+
8
11
  COMMENT_DETECTOR = ->(child) { child.type == :haml_comment }
9
12
 
10
13
  def visit_haml_comment(node)
@@ -16,13 +19,29 @@ module HamlLint
16
19
  config['max_consecutive'] + 1,
17
20
  ) do |group|
18
21
  group.each { |group_node| reported_nodes << group_node }
22
+ corrected = correct_group(group)
19
23
  record_lint(group.first,
20
- "#{group.count} consecutive comments can be merged into one")
24
+ "#{group.count} consecutive comments can be merged into one",
25
+ corrected: corrected)
21
26
  end
22
27
  end
23
28
 
24
29
  private
25
30
 
31
+ # @return [Boolean]
32
+ def correct_group(group)
33
+ first_index = group.first.line - 1
34
+ continuation_indent = "#{autocorrected_lines[first_index][/\A\s*/]} "
35
+
36
+ changed = false
37
+ group[1..].each do |group_node|
38
+ index = group_node.line - 1
39
+ line = autocorrected_lines[index]
40
+ changed = true if correct_line(index, continuation_indent + line.sub(/\A\s*-#\s?/, ''))
41
+ end
42
+ changed
43
+ end
44
+
26
45
  def possible_group(node)
27
46
  node.subsequents.unshift(node)
28
47
  end
@@ -5,11 +5,26 @@ module HamlLint
5
5
  class Linter::EmptyObjectReference < Linter
6
6
  include LinterRegistry
7
7
 
8
+ supports_autocorrect(true)
9
+
8
10
  def visit_tag(node)
9
11
  return unless node.object_reference? &&
10
12
  node.object_reference_source.strip.empty?
11
13
 
12
- record_lint(node, 'Empty object reference should be removed')
14
+ corrected = correct_object_reference(node)
15
+ record_lint(node, 'Empty object reference should be removed', corrected: corrected)
16
+ end
17
+
18
+ private
19
+
20
+ # @return [Boolean]
21
+ def correct_object_reference(node)
22
+ index = node.line - 1
23
+ line = autocorrected_lines[index]
24
+ # `static_attributes_source` is just the `.class`/`#id` part (e.g. `.foo`),
25
+ # so the optional `%tag` group captures an explicit tag name when present.
26
+ static = Regexp.escape(node.static_attributes_source)
27
+ correct_line(index, line.sub(/\A(\s*(?:%[-:\w]+)?#{static})\[\s*\]/, '\1'))
13
28
  end
14
29
  end
15
30
  end
@@ -5,10 +5,40 @@ module HamlLint
5
5
  class Linter::EmptyScript < Linter
6
6
  include LinterRegistry
7
7
 
8
+ supports_autocorrect(true)
9
+ autocorrect_safe(false)
10
+
8
11
  def visit_silent_script(node)
9
12
  return unless /\A\s*\Z/.match?(node.script)
10
13
 
11
- record_lint(node, 'Empty script should be removed')
14
+ # Only a childless `-` can be deleted; a `-` with children is degenerate
15
+ # but is still reported.
16
+ deletable = node.children.empty?
17
+ deleted_lines << (node.line - 1) if autocorrect? && deletable
18
+ record_lint(node, 'Empty script should be removed',
19
+ corrected: autocorrect? && deletable)
20
+ end
21
+
22
+ def after_visit_root(node)
23
+ super
24
+ return if deleted_lines.empty?
25
+
26
+ kept = document.source_lines.reject.with_index do |_, index|
27
+ deleted_lines.include?(index)
28
+ end
29
+ apply_autocorrect(kept.join("\n"))
30
+ end
31
+
32
+ private
33
+
34
+ def reset_autocorrect_state
35
+ super
36
+ @deleted_lines = []
37
+ end
38
+
39
+ # @return [Array<Integer>]
40
+ def deleted_lines
41
+ @deleted_lines ||= []
12
42
  end
13
43
  end
14
44
  end
@@ -5,6 +5,12 @@ module HamlLint
5
5
  class Linter::FinalNewline < Linter
6
6
  include LinterRegistry
7
7
 
8
+ supports_autocorrect(true)
9
+
10
+ # Run last during autocorrect, so any line-level corrections from other
11
+ # linters are applied before the trailing newline is normalized.
12
+ autocorrect_priority(1)
13
+
8
14
  def visit_root(root)
9
15
  return if document.source.empty?
10
16
  line_number = document.last_non_empty_line
@@ -12,14 +18,32 @@ module HamlLint
12
18
  node = root.node_for_line(line_number)
13
19
  return if node.disabled?(self)
14
20
 
15
- ends_with_newline = document.source.end_with?("\n")
21
+ present = config['present'] ? true : false
22
+ corrected = corrected_source(present)
23
+ return if document.source == corrected
24
+
25
+ record_lint(line_number, message_for(present), corrected: autocorrect?)
26
+ apply_autocorrect(corrected)
27
+ end
28
+
29
+ private
30
+
31
+ def message_for(present)
32
+ if present
33
+ 'Files should end with a trailing newline'
34
+ else
35
+ 'Files should not end with a trailing newline'
36
+ end
37
+ end
16
38
 
17
- if config['present']
18
- unless ends_with_newline
19
- record_lint(line_number, 'Files should end with a trailing newline')
20
- end
21
- elsif ends_with_newline
22
- record_lint(line_number, 'Files should not end with a trailing newline')
39
+ # Normalizes only the single final newline. Collapsing multiple trailing
40
+ # newlines is `TrailingEmptyLines`' job, so a file that already ends with a
41
+ # newline is left untouched here under `present`.
42
+ def corrected_source(present)
43
+ if present
44
+ document.source.end_with?("\n") ? document.source : "#{document.source}\n"
45
+ else
46
+ document.source.sub(/\n+\z/, '')
23
47
  end
24
48
  end
25
49
  end
@@ -6,6 +6,8 @@ module HamlLint
6
6
  class Linter::ImplicitDiv < Linter
7
7
  include LinterRegistry
8
8
 
9
+ supports_autocorrect(true)
10
+
9
11
  def visit_tag(node)
10
12
  return unless node.tag_name == 'div'
11
13
 
@@ -14,9 +16,20 @@ module HamlLint
14
16
  tag = node.source_code[/\s*([^\s={(\[]+)/, 1]
15
17
  return unless tag.start_with?('%div')
16
18
 
19
+ corrected = correct_implicit_div(node)
17
20
  record_lint(node,
18
21
  "`#{tag}` can be written as `#{node.static_attributes_source}` " \
19
- 'since `%div` is implicit')
22
+ 'since `%div` is implicit',
23
+ corrected: corrected)
24
+ end
25
+
26
+ private
27
+
28
+ # @return [Boolean]
29
+ def correct_implicit_div(node)
30
+ index = node.line - 1
31
+ line = autocorrected_lines[index]
32
+ correct_line(index, line.sub(/\A(\s*)%div(?=[.#])/, '\1'))
20
33
  end
21
34
  end
22
35
  end
@@ -5,12 +5,24 @@ module HamlLint
5
5
  class Linter::LeadingCommentSpace < Linter
6
6
  include LinterRegistry
7
7
 
8
+ supports_autocorrect(true)
9
+
8
10
  def visit_haml_comment(node)
9
11
  # Skip if the node spans multiple lines starting on the second line,
10
12
  # or starts with a space
11
13
  return if /\A#*(\s*|\s+\S.*)$/.match?(node.text)
12
14
 
13
- record_lint(node, 'Comment should have a space after the `#`')
15
+ corrected = correct_leading_space(node)
16
+ record_lint(node, 'Comment should have a space after the `#`', corrected: corrected)
17
+ end
18
+
19
+ private
20
+
21
+ # @return [Boolean]
22
+ def correct_leading_space(node)
23
+ index = node.line - 1
24
+ line = autocorrected_lines[index]
25
+ correct_line(index, line.sub(/\A(\s*-#+)(?=\S)/, '\1 '))
14
26
  end
15
27
  end
16
28
  end