haml_lint 0.53.0 → 0.76.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 +17 -5
- data/config/forced_rubocop_config.yml +25 -13
- data/lib/haml_lint/adapter.rb +5 -4
- data/lib/haml_lint/cli.rb +7 -3
- data/lib/haml_lint/constants.rb +1 -1
- data/lib/haml_lint/directive.rb +2 -2
- data/lib/haml_lint/document.rb +14 -5
- data/lib/haml_lint/exceptions.rb +2 -2
- data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +2 -2
- data/lib/haml_lint/haml_visitor.rb +1 -1
- data/lib/haml_lint/lint.rb +8 -3
- data/lib/haml_lint/linter/alignment_tabs.rb +21 -4
- data/lib/haml_lint/linter/class_attribute_with_static_value.rb +52 -12
- data/lib/haml_lint/linter/classes_before_ids.rb +15 -6
- data/lib/haml_lint/linter/consecutive_comments.rb +20 -1
- data/lib/haml_lint/linter/consecutive_silent_scripts.rb +68 -1
- data/lib/haml_lint/linter/empty_object_reference.rb +16 -1
- data/lib/haml_lint/linter/empty_script.rb +32 -2
- data/lib/haml_lint/linter/final_newline.rb +31 -7
- data/lib/haml_lint/linter/html_attributes.rb +71 -2
- data/lib/haml_lint/linter/id_names.rb +1 -1
- data/lib/haml_lint/linter/implicit_div.rb +14 -1
- data/lib/haml_lint/linter/indentation.rb +2 -2
- data/lib/haml_lint/linter/instance_variables.rb +28 -3
- data/lib/haml_lint/linter/leading_comment_space.rb +14 -2
- data/lib/haml_lint/linter/multiline_pipe.rb +3 -3
- data/lib/haml_lint/linter/multiline_script.rb +65 -5
- data/lib/haml_lint/linter/no_placeholders.rb +2 -2
- data/lib/haml_lint/linter/rubocop.rb +87 -76
- data/lib/haml_lint/linter/ruby_comments.rb +14 -4
- data/lib/haml_lint/linter/space_before_script.rb +37 -4
- data/lib/haml_lint/linter/space_inside_hash_attributes.rb +41 -3
- data/lib/haml_lint/linter/strict_locals.rb +69 -0
- data/lib/haml_lint/linter/tag_name.rb +15 -2
- data/lib/haml_lint/linter/trailing_empty_lines.rb +13 -1
- data/lib/haml_lint/linter/trailing_whitespace.rb +16 -3
- 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 +108 -12
- data/lib/haml_lint/linter.rb +119 -5
- data/lib/haml_lint/node_transformer.rb +2 -2
- data/lib/haml_lint/options.rb +17 -3
- data/lib/haml_lint/rake_task.rb +3 -2
- data/lib/haml_lint/reporter/default_reporter.rb +1 -1
- data/lib/haml_lint/reporter/disabled_config_reporter.rb +29 -8
- data/lib/haml_lint/reporter/github_reporter.rb +50 -0
- data/lib/haml_lint/reporter/hash_reporter.rb +2 -0
- data/lib/haml_lint/reporter/json_reporter.rb +1 -1
- data/lib/haml_lint/reporter/progress_reporter.rb +2 -1
- data/lib/haml_lint/reporter/utils.rb +4 -2
- data/lib/haml_lint/reporter.rb +1 -1
- data/lib/haml_lint/ruby_extraction/base_chunk.rb +4 -4
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +73 -32
- data/lib/haml_lint/ruby_extraction/coordinator.rb +9 -4
- data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +1 -1
- data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +2 -2
- data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +1 -1
- data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +1 -1
- data/lib/haml_lint/ruby_extraction/script_chunk.rb +2 -2
- data/lib/haml_lint/runner.rb +70 -30
- data/lib/haml_lint/source.rb +25 -0
- data/lib/haml_lint/spec/matchers/report_lint.rb +17 -6
- data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +34 -4
- data/lib/haml_lint/spec.rb +4 -4
- data/lib/haml_lint/tree/comment_node.rb +1 -1
- data/lib/haml_lint/tree/doctype_node.rb +1 -1
- data/lib/haml_lint/tree/filter_node.rb +14 -1
- data/lib/haml_lint/tree/haml_comment_node.rb +9 -2
- data/lib/haml_lint/tree/node.rb +4 -4
- data/lib/haml_lint/tree/root_node.rb +4 -4
- data/lib/haml_lint/tree/script_node.rb +1 -1
- data/lib/haml_lint/tree/silent_script_node.rb +1 -1
- data/lib/haml_lint/tree/tag_node.rb +69 -25
- data/lib/haml_lint/utils.rb +4 -31
- data/lib/haml_lint/version.rb +1 -1
- data/lib/haml_lint.rb +23 -23
- metadata +14 -10
|
@@ -6,6 +6,9 @@ module HamlLint
|
|
|
6
6
|
class Linter::ConsecutiveSilentScripts < Linter
|
|
7
7
|
include LinterRegistry
|
|
8
8
|
|
|
9
|
+
supports_autocorrect(true)
|
|
10
|
+
autocorrect_safe(false)
|
|
11
|
+
|
|
9
12
|
SILENT_SCRIPT_DETECTOR = ->(child) do
|
|
10
13
|
child.type == :silent_script && child.children.empty?
|
|
11
14
|
end
|
|
@@ -18,12 +21,21 @@ module HamlLint
|
|
|
18
21
|
SILENT_SCRIPT_DETECTOR,
|
|
19
22
|
config['max_consecutive'] + 1,
|
|
20
23
|
) do |group|
|
|
24
|
+
reported_nodes.concat(group)
|
|
21
25
|
record_lint(group.first,
|
|
22
26
|
"#{group.count} consecutive Ruby scripts can be merged " \
|
|
23
|
-
'into a single `:ruby` filter'
|
|
27
|
+
'into a single `:ruby` filter',
|
|
28
|
+
corrected: collect_merge(group))
|
|
24
29
|
end
|
|
25
30
|
end
|
|
26
31
|
|
|
32
|
+
def after_visit_root(_node)
|
|
33
|
+
super
|
|
34
|
+
return if merges.empty?
|
|
35
|
+
|
|
36
|
+
apply_autocorrect(merged_source)
|
|
37
|
+
end
|
|
38
|
+
|
|
27
39
|
private
|
|
28
40
|
|
|
29
41
|
def possible_group(node)
|
|
@@ -37,5 +49,60 @@ module HamlLint
|
|
|
37
49
|
def reported_nodes
|
|
38
50
|
@reported_nodes ||= []
|
|
39
51
|
end
|
|
52
|
+
|
|
53
|
+
def reset_autocorrect_state
|
|
54
|
+
super
|
|
55
|
+
@merges = []
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Queues a group of consecutive silent scripts to be replaced by an
|
|
59
|
+
# equivalent `:ruby` filter block. Skips (and reports without correcting)
|
|
60
|
+
# groups that aren't a run of single-line scripts on contiguous lines.
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean] whether a correction was queued
|
|
63
|
+
def collect_merge(group)
|
|
64
|
+
return false unless autocorrect?
|
|
65
|
+
return false unless mergeable?(group)
|
|
66
|
+
|
|
67
|
+
merges << {
|
|
68
|
+
start: group.first.line - 1,
|
|
69
|
+
finish: group.last.line - 1,
|
|
70
|
+
lines: ruby_filter_lines(group),
|
|
71
|
+
}
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Boolean] whether every script is a single line and the group
|
|
76
|
+
# occupies a contiguous run of lines
|
|
77
|
+
def mergeable?(group)
|
|
78
|
+
group.none? { |node| node.script.include?("\n") } &&
|
|
79
|
+
group.each_cons(2).all? { |a, b| b.line == a.line + 1 }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Builds the replacement lines: a `:ruby` filter marker at the group's
|
|
83
|
+
# indentation, followed by each script's body indented one level deeper.
|
|
84
|
+
#
|
|
85
|
+
# @return [Array<String>]
|
|
86
|
+
def ruby_filter_lines(group)
|
|
87
|
+
indent = document.source_lines[group.first.line - 1][/\A\s*/]
|
|
88
|
+
inner = indent + (indent.include?("\t") ? "\t" : ' ')
|
|
89
|
+
["#{indent}:ruby", *group.map { |node| "#{inner}#{node.script.strip}" }]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Applies the queued range replacements from the bottom up (so earlier line
|
|
93
|
+
# indices stay valid) and returns the rebuilt source.
|
|
94
|
+
#
|
|
95
|
+
# @return [String]
|
|
96
|
+
def merged_source
|
|
97
|
+
lines = document.source_lines.dup
|
|
98
|
+
merges.sort_by { |merge| -merge[:start] }.each do |merge|
|
|
99
|
+
lines[merge[:start]..merge[:finish]] = merge[:lines]
|
|
100
|
+
end
|
|
101
|
+
lines.join("\n")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def merges
|
|
105
|
+
@merges ||= []
|
|
106
|
+
end
|
|
40
107
|
end
|
|
41
108
|
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
|
-
return unless
|
|
12
|
+
return unless /\A\s*\Z/.match?(node.script)
|
|
13
|
+
|
|
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
|
|
10
38
|
|
|
11
|
-
|
|
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,11 +6,80 @@ module HamlLint
|
|
|
6
6
|
class Linter::HtmlAttributes < Linter
|
|
7
7
|
include LinterRegistry
|
|
8
8
|
|
|
9
|
+
supports_autocorrect(true)
|
|
10
|
+
autocorrect_safe(false)
|
|
11
|
+
|
|
12
|
+
MESSAGE = "Prefer the hash attributes syntax (%tag{ lang: 'en' }) " \
|
|
13
|
+
'over HTML attributes syntax (%tag(lang=en))'
|
|
14
|
+
|
|
9
15
|
def visit_tag(node)
|
|
10
16
|
return unless node.html_attributes?
|
|
11
17
|
|
|
12
|
-
record_lint(node,
|
|
13
|
-
|
|
18
|
+
record_lint(node, MESSAGE, corrected: correct(node))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Rewrites the HTML attribute group `(...)` as a Ruby hash group `{...}`,
|
|
24
|
+
# preserving each attribute's value (static strings, booleans, and Ruby
|
|
25
|
+
# expressions alike).
|
|
26
|
+
#
|
|
27
|
+
# The conversion is skipped (the lint is still reported) when it can't be
|
|
28
|
+
# performed losslessly: when the tag already has a `{...}` hash group, when
|
|
29
|
+
# the `.class`/`#id` shorthand is present (its `class`/`id` are merged into
|
|
30
|
+
# the parsed attributes and can't be told apart from the HTML ones), or when
|
|
31
|
+
# the attribute list spans multiple lines.
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean] whether a correction was applied
|
|
34
|
+
def correct(node)
|
|
35
|
+
return false unless correctable?(node)
|
|
36
|
+
return false unless pairs = hash_pairs(node)
|
|
37
|
+
|
|
38
|
+
hash_source = "{ #{pairs.join(', ')} }"
|
|
39
|
+
return false unless parse_ruby(hash_source)
|
|
40
|
+
|
|
41
|
+
index = node.line - 1
|
|
42
|
+
line = autocorrected_lines[index]
|
|
43
|
+
html_source = node.dynamic_attributes_source[:html]
|
|
44
|
+
return false unless line.include?(html_source)
|
|
45
|
+
|
|
46
|
+
correct_line(index, line.sub(html_source) { hash_source })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Whether the tag's HTML attributes can be losslessly converted in place.
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def correctable?(node)
|
|
53
|
+
return false if node.hash_attributes?
|
|
54
|
+
return false unless node.static_attributes_source.empty?
|
|
55
|
+
|
|
56
|
+
html_source = node.dynamic_attributes_source[:html]
|
|
57
|
+
!html_source.nil? && !html_source.include?("\n")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Builds the `key => value` fragments for the tag's HTML attributes, or +nil+
|
|
61
|
+
# when one of the static values can't be safely serialized.
|
|
62
|
+
#
|
|
63
|
+
# @return [Array<String>, nil]
|
|
64
|
+
def hash_pairs(node)
|
|
65
|
+
pairs = node.static_attributes.map do |key, value|
|
|
66
|
+
return nil unless serializable?(value)
|
|
67
|
+
|
|
68
|
+
"#{key.inspect} => #{value.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
node.dynamic_attributes_sources.each do |literal|
|
|
72
|
+
inner = literal.strip.delete_prefix('{').delete_suffix('}').sub(/,\s*\z/, '').strip
|
|
73
|
+
pairs << inner unless inner.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
pairs
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Boolean] whether the static attribute value round-trips through
|
|
80
|
+
# +#inspect+ as a Ruby literal
|
|
81
|
+
def serializable?(value)
|
|
82
|
+
value.is_a?(String) || [true, false, nil].include?(value) || value.is_a?(Numeric)
|
|
14
83
|
end
|
|
15
84
|
end
|
|
16
85
|
end
|
|
@@ -24,7 +24,7 @@ module HamlLint
|
|
|
24
24
|
|
|
25
25
|
style = config['style'] || 'lisp_case'
|
|
26
26
|
matcher = STYLES[style]
|
|
27
|
-
record_lint(node, "`id` attribute must be in #{STYLIZED_NAMES[style]}") unless id
|
|
27
|
+
record_lint(node, "`id` attribute must be in #{STYLIZED_NAMES[style]}") unless id&.match?(matcher)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
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
|
|
@@ -11,7 +11,7 @@ module HamlLint
|
|
|
11
11
|
tab: /^\t*(?! )/,
|
|
12
12
|
}.freeze
|
|
13
13
|
|
|
14
|
-
LEADING_SPACES_REGEX = /^( +)(?! )
|
|
14
|
+
LEADING_SPACES_REGEX = /^( +)(?! )/
|
|
15
15
|
|
|
16
16
|
def visit_root(root)
|
|
17
17
|
character = config['character'].to_sym
|
|
@@ -30,7 +30,7 @@ module HamlLint
|
|
|
30
30
|
dummy_node = Struct.new(:line)
|
|
31
31
|
|
|
32
32
|
document.source_lines.each_with_index do |line, index|
|
|
33
|
-
next if line
|
|
33
|
+
next if line&.match?(regex)
|
|
34
34
|
|
|
35
35
|
unless root.node_for_line(index).disabled?(self)
|
|
36
36
|
record_lint dummy_node.new(index + 1), "Line contains #{wrong_characters} in indentation"
|
|
@@ -10,7 +10,7 @@ module HamlLint
|
|
|
10
10
|
# @param [HamlLint::Tree::RootNode] the root of a syntax tree
|
|
11
11
|
# @return [true, false] whether the linter is enabled for the tree
|
|
12
12
|
def visit_root(node)
|
|
13
|
-
@enabled = matcher.match(File.basename(node.file))
|
|
13
|
+
@enabled = matcher.match?(File.basename(node.file))
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# Checks for instance variables in script nodes when the linter is enabled.
|
|
@@ -21,7 +21,7 @@ module HamlLint
|
|
|
21
21
|
return unless enabled?
|
|
22
22
|
|
|
23
23
|
if node.parsed_script.contains_instance_variables?
|
|
24
|
-
record_lint(node,
|
|
24
|
+
record_lint(node, failure_message)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -41,10 +41,27 @@ module HamlLint
|
|
|
41
41
|
|
|
42
42
|
visit_script(node) ||
|
|
43
43
|
if node.parsed_attributes.contains_instance_variables?
|
|
44
|
-
record_lint(node,
|
|
44
|
+
record_lint(node, failure_message)
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Checks for instance variables in :ruby filters when the linter is enabled.
|
|
49
|
+
#
|
|
50
|
+
# @param [HamlLint::Tree::FilterNode]
|
|
51
|
+
# @return [void]
|
|
52
|
+
def visit_filter(node)
|
|
53
|
+
return unless enabled?
|
|
54
|
+
return unless node.filter_type == 'ruby'
|
|
55
|
+
return unless ast = parse_ruby(node.text)
|
|
56
|
+
|
|
57
|
+
ast.each_node do |i|
|
|
58
|
+
if i.type == :ivar
|
|
59
|
+
record_lint(node, failure_message)
|
|
60
|
+
break
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
48
65
|
private
|
|
49
66
|
|
|
50
67
|
# Tracks whether the linter is enabled for the file.
|
|
@@ -75,5 +92,13 @@ module HamlLint
|
|
|
75
92
|
def matcher
|
|
76
93
|
@matcher ||= Regexp.new(config['matchers'][file_types] || '\A_.*\.haml\z')
|
|
77
94
|
end
|
|
95
|
+
|
|
96
|
+
# The error message when an ivar is found
|
|
97
|
+
#
|
|
98
|
+
# @api private
|
|
99
|
+
# @return [String]
|
|
100
|
+
def failure_message
|
|
101
|
+
"Avoid using instance variables in #{file_types} views"
|
|
102
|
+
end
|
|
78
103
|
end
|
|
79
104
|
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
|
-
return if
|
|
13
|
+
return if /\A#*(\s*|\s+\S.*)$/.match?(node.text)
|
|
14
|
+
|
|
15
|
+
corrected = correct_leading_space(node)
|
|
16
|
+
record_lint(node, 'Comment should have a space after the `#`', corrected: corrected)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
12
20
|
|
|
13
|
-
|
|
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
|
|
@@ -26,12 +26,12 @@ module HamlLint
|
|
|
26
26
|
# Plain text nodes are allowed to consist of a single pipe
|
|
27
27
|
return if line.strip == '|'
|
|
28
28
|
|
|
29
|
-
record_lint(node, MESSAGE) if line.match(MULTILINE_PIPE_REGEX)
|
|
29
|
+
record_lint(node, MESSAGE) if line.match?(MULTILINE_PIPE_REGEX)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
|
-
MULTILINE_PIPE_REGEX = /\s+\|\s
|
|
34
|
+
MULTILINE_PIPE_REGEX = /\s+\|\s*$/
|
|
35
35
|
|
|
36
36
|
def line_text_for_node(node)
|
|
37
37
|
document.source_lines[node.line - 1]
|
|
@@ -39,7 +39,7 @@ module HamlLint
|
|
|
39
39
|
|
|
40
40
|
def check(node)
|
|
41
41
|
line = line_text_for_node(node)
|
|
42
|
-
record_lint(node, MESSAGE) if line.match(MULTILINE_PIPE_REGEX)
|
|
42
|
+
record_lint(node, MESSAGE) if line.match?(MULTILINE_PIPE_REGEX)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
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
|
|
@@ -6,8 +6,8 @@ module HamlLint
|
|
|
6
6
|
include LinterRegistry
|
|
7
7
|
|
|
8
8
|
MSG = 'Placeholders attributes should not be used.'
|
|
9
|
-
HASH_REGEXP = /:?['"]?placeholder['"]?(?::| *=>)
|
|
10
|
-
HTML_REGEXP = /placeholder
|
|
9
|
+
HASH_REGEXP = /:?['"]?placeholder['"]?(?::| *=>)/
|
|
10
|
+
HTML_REGEXP = /placeholder=/
|
|
11
11
|
|
|
12
12
|
def visit_tag(node)
|
|
13
13
|
return unless node.hash_attributes_source =~ HASH_REGEXP || node.html_attributes_source =~ HTML_REGEXP
|