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.
- checksums.yaml +4 -4
- data/config/default.yml +9 -4
- data/lib/haml_lint/cli.rb +5 -1
- data/lib/haml_lint/document.rb +0 -5
- data/lib/haml_lint/linter/class_attribute_with_static_value.rb +51 -11
- data/lib/haml_lint/linter/classes_before_ids.rb +16 -1
- data/lib/haml_lint/linter/consecutive_comments.rb +20 -1
- data/lib/haml_lint/linter/empty_object_reference.rb +16 -1
- data/lib/haml_lint/linter/empty_script.rb +31 -1
- data/lib/haml_lint/linter/final_newline.rb +31 -7
- data/lib/haml_lint/linter/implicit_div.rb +14 -1
- data/lib/haml_lint/linter/leading_comment_space.rb +13 -1
- data/lib/haml_lint/linter/multiline_script.rb +65 -5
- data/lib/haml_lint/linter/rubocop.rb +5 -17
- data/lib/haml_lint/linter/ruby_comments.rb +13 -3
- data/lib/haml_lint/linter/space_before_script.rb +36 -3
- data/lib/haml_lint/linter/space_inside_hash_attributes.rb +41 -3
- data/lib/haml_lint/linter/tag_name.rb +14 -1
- data/lib/haml_lint/linter/trailing_empty_lines.rb +13 -1
- data/lib/haml_lint/linter/trailing_whitespace.rb +15 -2
- data/lib/haml_lint/linter/unescaped_html.rb +27 -0
- data/lib/haml_lint/linter/unnecessary_interpolation.rb +30 -3
- data/lib/haml_lint/linter/unnecessary_string_output.rb +34 -2
- data/lib/haml_lint/linter.rb +111 -0
- data/lib/haml_lint/reporter/disabled_config_reporter.rb +26 -5
- data/lib/haml_lint/reporter/github_reporter.rb +26 -8
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +27 -3
- data/lib/haml_lint/runner.rb +17 -3
- data/lib/haml_lint/source.rb +2 -6
- data/lib/haml_lint/tree/filter_node.rb +13 -0
- data/lib/haml_lint/tree/tag_node.rb +55 -23
- data/lib/haml_lint/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50729598b076118b141b2cf9552e8ada142c34ed52e3bd3e1a398948355ebfc2
|
|
4
|
+
data.tar.gz: e03f59f848bfa5a5a2d792e4ee347aaf10c956b7d47920594e87e9f283f8cb1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
54
|
-
width: 2
|
|
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
|
-
#
|
|
92
|
-
#
|
|
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(
|
|
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
|
data/lib/haml_lint/document.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
value = static_class_attribute_value(pair)
|
|
50
|
+
return value if value
|
|
45
51
|
end
|
|
46
52
|
end
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
nil
|
|
49
55
|
end
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|