haml_lint 0.73.0 → 0.74.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +9 -4
  3. data/lib/haml_lint/cli.rb +5 -1
  4. data/lib/haml_lint/document.rb +0 -5
  5. data/lib/haml_lint/linter/class_attribute_with_static_value.rb +51 -11
  6. data/lib/haml_lint/linter/classes_before_ids.rb +16 -1
  7. data/lib/haml_lint/linter/consecutive_comments.rb +20 -1
  8. data/lib/haml_lint/linter/empty_object_reference.rb +16 -1
  9. data/lib/haml_lint/linter/empty_script.rb +31 -1
  10. data/lib/haml_lint/linter/final_newline.rb +31 -7
  11. data/lib/haml_lint/linter/implicit_div.rb +14 -1
  12. data/lib/haml_lint/linter/leading_comment_space.rb +13 -1
  13. data/lib/haml_lint/linter/multiline_script.rb +65 -5
  14. data/lib/haml_lint/linter/rubocop.rb +5 -17
  15. data/lib/haml_lint/linter/ruby_comments.rb +13 -3
  16. data/lib/haml_lint/linter/space_before_script.rb +36 -3
  17. data/lib/haml_lint/linter/space_inside_hash_attributes.rb +41 -3
  18. data/lib/haml_lint/linter/tag_name.rb +14 -1
  19. data/lib/haml_lint/linter/trailing_empty_lines.rb +13 -1
  20. data/lib/haml_lint/linter/trailing_whitespace.rb +15 -2
  21. data/lib/haml_lint/linter/unescaped_html.rb +27 -0
  22. data/lib/haml_lint/linter/unnecessary_interpolation.rb +30 -3
  23. data/lib/haml_lint/linter/unnecessary_string_output.rb +34 -2
  24. data/lib/haml_lint/linter.rb +111 -0
  25. data/lib/haml_lint/reporter/disabled_config_reporter.rb +26 -5
  26. data/lib/haml_lint/reporter/github_reporter.rb +26 -8
  27. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +27 -3
  28. data/lib/haml_lint/runner.rb +17 -3
  29. data/lib/haml_lint/source.rb +2 -6
  30. data/lib/haml_lint/tree/filter_node.rb +13 -0
  31. data/lib/haml_lint/tree/tag_node.rb +55 -23
  32. data/lib/haml_lint/version.rb +1 -1
  33. 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: 50729598b076118b141b2cf9552e8ada142c34ed52e3bd3e1a398948355ebfc2
4
+ data.tar.gz: e03f59f848bfa5a5a2d792e4ee347aaf10c956b7d47920594e87e9f283f8cb1f
5
5
  SHA512:
6
- metadata.gz: 85756d43195b59d6582f2c352ef33084cab05d338cf56338d4b82b405890333894a12ebe1716301d831895fa24293594a506be6fc5ba0b112f7edd953963fd58
7
- data.tar.gz: 0ef90b78b0d81b92bee5ff53a89d360ae831a1cf76b79f746a4a37b52e6a3e6d9a882af1ca206d1a0fe69e9cfceead2bbe5b1ea93ff3e90cef3d317aed2b06fb
6
+ metadata.gz: 7ce36a007738cb26bd4034a16cd00861f64db28ce0eefcb9dcef1edca25ee09d61521eed7dfa9b3b5bb558f0c379dde10fcbfdae3ecb72a3b1b3a24877db5188
7
+ data.tar.gz: 8601c85624f55a50dc33296ea88066eb935b7f5bc5843a2e2179204a7d2a259da9dcc95ee8543bc1b7205991f5d589e83f384d085e1884dfaa37ef8ed15fd9f2
data/config/default.yml CHANGED
@@ -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
 
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
@@ -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
@@ -5,6 +5,9 @@ module HamlLint
5
5
  class Linter::MultilineScript < Linter
6
6
  include LinterRegistry
7
7
 
8
+ supports_autocorrect(true)
9
+ autocorrect_safe(false)
10
+
8
11
  # List of operators that can split a script into two lines that we want to
9
12
  # alert on.
10
13
  SPLIT_OPERATORS = %w[
@@ -31,8 +34,40 @@ module HamlLint
31
34
  check(node)
32
35
  end
33
36
 
37
+ def after_visit_root(node)
38
+ super
39
+ return if merges.empty?
40
+
41
+ apply_autocorrect(merged_source)
42
+ end
43
+
34
44
  private
35
45
 
46
+ def reset_autocorrect_state
47
+ super
48
+ @merges = []
49
+ end
50
+
51
+ def merged_source
52
+ lines = document.source_lines.dup
53
+ to_delete = apply_merges(lines)
54
+ lines.reject.with_index { |_, index| to_delete.include?(index) }.join("\n")
55
+ end
56
+
57
+ # Mutates +lines+ in place, folding each continuation script onto its chain
58
+ # root, and returns the line indices that should be deleted.
59
+ #
60
+ # @return [Array<Integer>]
61
+ def apply_merges(lines)
62
+ redirect = {}
63
+ merges.sort_by { |merge| merge[:from] }.map do |merge|
64
+ root = redirect.fetch(merge[:from], merge[:from])
65
+ redirect[merge[:succ_line]] = root
66
+ lines[root] = "#{lines[root].rstrip} #{merge[:succ_script]}"
67
+ merge[:succ_line]
68
+ end
69
+ end
70
+
36
71
  def check(node)
37
72
  # Condition occurs when scripts do not contain nested content, e.g.
38
73
  #
@@ -48,11 +83,36 @@ module HamlLint
48
83
  return unless node.children.empty?
49
84
 
50
85
  operator = node.script[/\s+(\S+)\z/, 1]
51
- if SPLIT_OPERATORS.include?(operator)
52
- record_lint(node,
53
- "Script with trailing operator `#{operator}` should be " \
54
- 'merged with the script on the following line')
55
- end
86
+ return unless SPLIT_OPERATORS.include?(operator)
87
+
88
+ corrected = collect_merge(node)
89
+ record_lint(node,
90
+ "Script with trailing operator `#{operator}` should be " \
91
+ 'merged with the script on the following line',
92
+ corrected: corrected)
93
+ end
94
+
95
+ def collect_merge(node)
96
+ return false unless autocorrect?
97
+
98
+ # Only merge with the immediately following sibling on the next line.
99
+ # `node.successor` would climb to an ancestor's sibling when this script is
100
+ # the last child of its block, which would pull a line from outside the
101
+ # block into it and change the semantics.
102
+ succ = node.subsequents.first
103
+ return false unless succ && %i[script silent_script].include?(succ.type)
104
+ return false unless succ.line == node.line + 1
105
+
106
+ merges << {
107
+ from: node.line - 1,
108
+ succ_line: succ.line - 1,
109
+ succ_script: succ.script.strip,
110
+ }
111
+ true
112
+ end
113
+
114
+ def merges
115
+ @merges ||= []
56
116
  end
57
117
  end
58
118
  end
@@ -20,10 +20,12 @@ module HamlLint
20
20
  class Runner < ::RuboCop::Runner
21
21
  attr_reader :offenses
22
22
 
23
- def run(haml_path, ruby_code, config:, allow_cache: false)
24
- @allow_cache = allow_cache
23
+ def run(haml_path, ruby_code, config:)
25
24
  @offenses = []
26
25
  @config_store.instance_variable_set(:@options_config, config)
26
+ # Using stdin also disables RuboCop's result cache, which is intentional:
27
+ # it reconstructs offense positions from the on-disk HAML, not the Ruby we
28
+ # inspected, so reusing it misreports lines (sds/haml-lint#593).
27
29
  @options[:stdin] = ruby_code
28
30
  super([haml_path])
29
31
  end
@@ -40,20 +42,6 @@ module HamlLint
40
42
  def file_finished(_file, offenses)
41
43
  @offenses = offenses
42
44
  end
43
-
44
- # RuboCop caches results by taking a hash of the file contents & path, among other things.
45
- # It disables its cache when working on file-content from stdin.
46
- # Unfortunately we always use RuboCop's stdin, even when we're linting a file on-disk.
47
- # So, override RuboCop::Runner#cached_run? so that it'll allow caching results, so long
48
- # as haml-lint itself isn't being invoked with files on stdin.
49
- def cached_run?
50
- return false unless @allow_cache
51
-
52
- @cached_run ||=
53
- (@options[:cache] == 'true' ||
54
- (@options[:cache] != 'false' && @config_store.for_pwd.for_all_cops['UseCache'])) &&
55
- !@options[:auto_gen_config]
56
- end
57
45
  end
58
46
 
59
47
  include LinterRegistry
@@ -242,7 +230,7 @@ module HamlLint
242
230
  # @param path [String] the path to tell RuboCop we are running
243
231
  # @return [Array<RuboCop::Cop::Offense>, String]
244
232
  def run_rubocop(rubocop_runner, ruby_code, path) # rubocop:disable Metrics
245
- rubocop_runner.run(path, ruby_code, config: rubocop_config_for(path), allow_cache: @document&.file_on_disk)
233
+ rubocop_runner.run(path, ruby_code, config: rubocop_config_for(path))
246
234
 
247
235
  if ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
248
236
  if rubocop_runner.offenses.empty?
@@ -5,10 +5,13 @@ module HamlLint
5
5
  class Linter::RubyComments < Linter
6
6
  include LinterRegistry
7
7
 
8
+ supports_autocorrect(true)
9
+
8
10
  def visit_silent_script(node)
9
- if code_comment?(node)
10
- record_lint(node, 'Use `-#` for comments instead of `- #`')
11
- end
11
+ return unless code_comment?(node)
12
+
13
+ corrected = correct_comment(node)
14
+ record_lint(node, 'Use `-#` for comments instead of `- #`', corrected: corrected)
12
15
  end
13
16
 
14
17
  private
@@ -16,5 +19,12 @@ module HamlLint
16
19
  def code_comment?(node)
17
20
  node.script =~ /\A\s+#/
18
21
  end
22
+
23
+ # @return [Boolean]
24
+ def correct_comment(node)
25
+ index = node.line - 1
26
+ line = autocorrected_lines[index]
27
+ correct_line(index, line.sub(/\A(\s*)-\s+#/, '\1-#'))
28
+ end
19
29
  end
20
30
  end
@@ -5,6 +5,8 @@ module HamlLint
5
5
  class Linter::SpaceBeforeScript < Linter
6
6
  include LinterRegistry
7
7
 
8
+ supports_autocorrect(true)
9
+
8
10
  MESSAGE_FORMAT = 'The %s symbol should have one space separating it from code'
9
11
 
10
12
  ALLOWED_SEPARATORS = [' ', '#'].freeze
@@ -33,18 +35,23 @@ module HamlLint
33
35
  # (need to do it this way as the parser strips whitespace from node)
34
36
  return unless tag_with_text[index - 1] != ' '
35
37
 
36
- record_lint(node, MESSAGE_FORMAT % '=')
38
+ corrected = correct_inline_script(node, text)
39
+ record_lint(node, MESSAGE_FORMAT % '=', corrected: corrected)
37
40
  end
38
41
 
39
42
  def visit_script(node)
40
43
  # Plain text nodes with interpolation are converted to script nodes, so we
41
44
  # need to ignore them here.
42
45
  return unless document.source_lines[node.line - 1].lstrip.start_with?('=')
43
- record_lint(node, MESSAGE_FORMAT % '=') if missing_space?(node)
46
+ return unless missing_space?(node)
47
+
48
+ record_lint(node, MESSAGE_FORMAT % '=', corrected: correct_leading_marker(node, '='))
44
49
  end
45
50
 
46
51
  def visit_silent_script(node)
47
- record_lint(node, MESSAGE_FORMAT % '-') if missing_space?(node)
52
+ return unless missing_space?(node)
53
+
54
+ record_lint(node, MESSAGE_FORMAT % '-', corrected: correct_leading_marker(node, '-'))
48
55
  end
49
56
 
50
57
  private
@@ -53,5 +60,31 @@ module HamlLint
53
60
  text = node.script
54
61
  !ALLOWED_SEPARATORS.include?(text[0]) if text
55
62
  end
63
+
64
+ # Inserts one space after a leading `=`/`-` marker.
65
+ #
66
+ # @return [Boolean]
67
+ def correct_leading_marker(node, marker)
68
+ index = node.line - 1
69
+ line = autocorrected_lines[index]
70
+ escaped = Regexp.escape(marker)
71
+ correct_line(index, line.sub(/\A(\s*#{escaped})(?=[^\s#{escaped}])/, '\1 '))
72
+ end
73
+
74
+ # Inserts a space after the `=` marker introducing a tag's inline script.
75
+ #
76
+ # @return [Boolean]
77
+ def correct_inline_script(node, text)
78
+ index = node.line - 1
79
+ line = autocorrected_lines[index]
80
+
81
+ pos = line.rindex("=#{text}")
82
+ if pos.nil? && (unquoted = strip_surrounding_quotes(text))
83
+ pos = line.rindex("=#{unquoted}")
84
+ end
85
+ return false unless pos
86
+
87
+ correct_line(index, "#{line[0..pos]} #{line[(pos + 1)..]}")
88
+ end
56
89
  end
57
90
  end