forked from sds/scss-lint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
trailing_semicolon.rb
97 lines (84 loc) · 3.34 KB
/
trailing_semicolon.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
module SCSSLint
# Checks for a trailing semicolon on statements within rule sets.
class Linter::TrailingSemicolon < Linter
include LinterRegistry
def visit_extend(node)
check_semicolon(node)
end
def visit_variable(node)
# If the variable is using `!default` or `!global` (e.g. `$foo: bar
# !default;`) then `node.expr` will give us the source range just for the
# value (e.g. `bar`). In these cases, we want to use the source range of
# `node`, which will give us most of the entire line (e.g. `foo: bar
# !default`.
return check_semicolon(node) if node.global || node.guarded
# If the variable is a multi-line ListLiteral or MapLiteral, then
# `node.expr` will give us everything except the last right paren, and
# the semicolon if it exists. In these cases, use the source range of
# `node` as above.
if node.expr.is_a?(Sass::Script::Tree::ListLiteral) ||
node.expr.is_a?(Sass::Script::Tree::MapLiteral)
return check_semicolon(node)
end
check_semicolon(node.expr)
end
def visit_prop(node)
if node.children.any? { |n| n.is_a?(Sass::Tree::PropNode) }
yield # Continue checking children
else
check_semicolon(node)
end
end
def visit_mixin(node)
if node.children.any?
yield # Continue checking children
else
check_semicolon(node)
end
end
def visit_import(node)
# Ignore all but the last import for comma-separated @imports
return if source_from_range(node.source_range) =~ /,\s*$/
check_semicolon(node)
end
private
def check_semicolon(node)
if has_space_before_semicolon?(node)
line = node.source_range.start_pos.line
add_lint line,
'Declaration should not have a space before ' \
'the terminating semicolon'
elsif !ends_with_semicolon?(node)
line = node.source_range.start_pos.line
add_lint line, 'Declaration should be terminated by a semicolon'
elsif ends_with_multiple_semicolons?(node)
line = node.source_range.start_pos.line
add_lint line, 'Declaration should be terminated by a single semicolon'
end
end
# Checks that the node is ended by a semicolon (with no whitespace)
def ends_with_semicolon?(node)
semicolon_after_parenthesis?(node) ||
# Otherwise just check for a semicolon
source_from_range(node.source_range) =~ /;(\s*})?$/
end
# Special case: Sass doesn't include the semicolon after an expression
# in the source range it reports, so we need a helper to check after the
# reported range.
def semicolon_after_parenthesis?(node)
last_char = character_at(node.source_range.end_pos)
char_after = character_at(node.source_range.end_pos, 1)
(last_char == ')' && char_after == ';') ||
([last_char, char_after].include?("\n") &&
engine.lines[node.source_range.end_pos.line] =~ /\);(\s*})?$/)
end
def ends_with_multiple_semicolons?(node)
# Look one character past the end to see if there's another semicolon
character_at(node.source_range.end_pos) == ';' &&
character_at(node.source_range.end_pos, 1) == ';'
end
def has_space_before_semicolon?(node)
source_from_range(node.source_range) =~ /\s;(\s*})?$/
end
end
end