scss_lint 0.40.1 → 0.41.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/config/default.yml +20 -10
- data/data/property-sort-orders/recess.txt +12 -0
- data/data/property-sort-orders/smacss.txt +13 -0
- data/lib/scss_lint/cli.rb +4 -3
- data/lib/scss_lint/control_comment_processor.rb +34 -25
- data/lib/scss_lint/linter/border_zero.rb +1 -1
- data/lib/scss_lint/linter/disable_linter_reason.rb +39 -0
- data/lib/scss_lint/linter/mergeable_selector.rb +3 -1
- data/lib/scss_lint/linter/nesting_depth.rb +1 -0
- data/lib/scss_lint/linter/selector_depth.rb +1 -1
- data/lib/scss_lint/linter/single_line_per_property.rb +1 -1
- data/lib/scss_lint/linter/single_line_per_selector.rb +9 -1
- data/lib/scss_lint/linter/space_after_variable_name.rb +1 -1
- data/lib/scss_lint/linter/space_around_operator.rb +86 -0
- data/lib/scss_lint/linter/space_between_parens.rb +96 -20
- data/lib/scss_lint/linter/transition_all.rb +30 -0
- data/lib/scss_lint/linter/unnecessary_mantissa.rb +1 -0
- data/lib/scss_lint/reporter/clean_files_reporter.rb +10 -0
- data/lib/scss_lint/reporter.rb +5 -2
- data/lib/scss_lint/runner.rb +3 -2
- data/lib/scss_lint/sass/script.rb +19 -0
- data/lib/scss_lint/version.rb +1 -1
- data/spec/scss_lint/linter/bang_format_spec.rb +1 -1
- data/spec/scss_lint/linter/disable_linter_reason_spec.rb +63 -0
- data/spec/scss_lint/linter/nesting_depth_spec.rb +16 -0
- data/spec/scss_lint/linter/single_line_per_selector_spec.rb +23 -0
- data/spec/scss_lint/linter/space_after_variable_name_spec.rb +1 -1
- data/spec/scss_lint/linter/space_around_operator_spec.rb +240 -0
- data/spec/scss_lint/linter/space_between_parens_spec.rb +195 -1
- data/spec/scss_lint/linter/transition_all_spec.rb +81 -0
- data/spec/scss_lint/linter/unnecessary_mantissa_spec.rb +20 -0
- data/spec/scss_lint/linter_spec.rb +21 -0
- data/spec/scss_lint/report_lint_spec.rb +268 -0
- data/spec/scss_lint/reporter/clean_files_reporter_spec.rb +73 -0
- data/spec/scss_lint/reporter/config_reporter_spec.rb +1 -1
- data/spec/scss_lint/reporter/default_reporter_spec.rb +1 -1
- data/spec/scss_lint/reporter/files_reporter_spec.rb +3 -2
- data/spec/scss_lint/reporter/json_reporter_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/support/matchers/report_lint.rb +11 -2
- metadata +20 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 478b23880809f4fb5eeb95543b34534de05edf5a
|
4
|
+
data.tar.gz: 97f5d37498b751b7bdae0dd23032acc92de56fdf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4808204c5756753ab99d9485590e034c6711f5b4ce67024c09dcfd281f6fa4199eb12331dd4a272933428c5991093bb4e6d2b5cf77743886df83488acc35651d
|
7
|
+
data.tar.gz: 951df4e1c90579ff4ebc55b61693bab102ba66761875781f6e0baf7d64232c0e95004f0de6eaacdc2db483a1c0d5d287aa79dd14d75d52220540f7cfbc129ee4
|
data/config/default.yml
CHANGED
@@ -36,6 +36,9 @@ linters:
|
|
36
36
|
DeclarationOrder:
|
37
37
|
enabled: true
|
38
38
|
|
39
|
+
DisableLinterReason:
|
40
|
+
enabled: false
|
41
|
+
|
39
42
|
DuplicateProperty:
|
40
43
|
enabled: true
|
41
44
|
|
@@ -111,6 +114,16 @@ linters:
|
|
111
114
|
include_nested: false
|
112
115
|
max_properties: 10
|
113
116
|
|
117
|
+
PropertySortOrder:
|
118
|
+
enabled: true
|
119
|
+
ignore_unspecified: false
|
120
|
+
min_properties: 2
|
121
|
+
separate_groups: false
|
122
|
+
|
123
|
+
PropertySpelling:
|
124
|
+
enabled: true
|
125
|
+
extra_properties: []
|
126
|
+
|
114
127
|
PropertyUnits:
|
115
128
|
enabled: true
|
116
129
|
global: [
|
@@ -124,16 +137,6 @@ linters:
|
|
124
137
|
'%'] # Other
|
125
138
|
properties: {}
|
126
139
|
|
127
|
-
PropertySortOrder:
|
128
|
-
enabled: true
|
129
|
-
ignore_unspecified: false
|
130
|
-
min_properties: 2
|
131
|
-
separate_groups: false
|
132
|
-
|
133
|
-
PropertySpelling:
|
134
|
-
enabled: true
|
135
|
-
extra_properties: []
|
136
|
-
|
137
140
|
QualifyingElement:
|
138
141
|
enabled: true
|
139
142
|
allow_element_with_attribute: false
|
@@ -172,6 +175,10 @@ linters:
|
|
172
175
|
SpaceAfterVariableName:
|
173
176
|
enabled: true
|
174
177
|
|
178
|
+
SpaceAroundOperator:
|
179
|
+
enabled: true
|
180
|
+
style: one_space # or 'no_space'
|
181
|
+
|
175
182
|
SpaceBeforeBrace:
|
176
183
|
enabled: true
|
177
184
|
style: space # or 'new_line'
|
@@ -194,6 +201,9 @@ linters:
|
|
194
201
|
TrailingZero:
|
195
202
|
enabled: false
|
196
203
|
|
204
|
+
TransitionAll:
|
205
|
+
enabled: false
|
206
|
+
|
197
207
|
UnnecessaryMantissa:
|
198
208
|
enabled: true
|
199
209
|
|
data/lib/scss_lint/cli.rb
CHANGED
@@ -56,7 +56,7 @@ module SCSSLint
|
|
56
56
|
def scan_for_lints(options, config)
|
57
57
|
runner = Runner.new(config)
|
58
58
|
runner.run(FileFinder.new(config).find(options[:files]))
|
59
|
-
report_lints(options, runner.lints)
|
59
|
+
report_lints(options, runner.lints, runner.files)
|
60
60
|
|
61
61
|
if runner.lints.any?(&:error?)
|
62
62
|
halt :error
|
@@ -159,10 +159,11 @@ module SCSSLint
|
|
159
159
|
|
160
160
|
# @param options [Hash]
|
161
161
|
# @param lints [Array<Lint>]
|
162
|
-
|
162
|
+
# @param files [Array<String>]
|
163
|
+
def report_lints(options, lints, files)
|
163
164
|
sorted_lints = lints.sort_by { |l| [l.filename, l.location] }
|
164
165
|
options.fetch(:reporters).each do |reporter, output|
|
165
|
-
results = reporter.new(sorted_lints).report_lints
|
166
|
+
results = reporter.new(sorted_lints, files).report_lints
|
166
167
|
io = (output == :stdout ? $stdout : File.new(output, 'w+'))
|
167
168
|
io.print results if results
|
168
169
|
end
|
@@ -25,11 +25,11 @@ module SCSSLint
|
|
25
25
|
linters = command[:linters]
|
26
26
|
return unless linters.include?('all') || linters.include?(@linter.name)
|
27
27
|
|
28
|
-
process_command(command
|
28
|
+
process_command(command, node)
|
29
29
|
|
30
30
|
# Is the control comment the only thing on this line?
|
31
31
|
return if node.is_a?(Sass::Tree::RuleNode) ||
|
32
|
-
%r{^\s*(//|/\*)}.match(@linter.engine.lines[
|
32
|
+
%r{^\s*(//|/\*)}.match(@linter.engine.lines[command[:line] - 1])
|
33
33
|
|
34
34
|
# Otherwise, pop since we only want comment to apply to the single line
|
35
35
|
pop_control_comment_stack(node)
|
@@ -39,7 +39,7 @@ module SCSSLint
|
|
39
39
|
#
|
40
40
|
# @param node [Sass::Tree::Node]
|
41
41
|
def after_node_visit(node)
|
42
|
-
while @disable_stack.any? && @disable_stack.last.node_parent == node
|
42
|
+
while @disable_stack.any? && @disable_stack.last[:node].node_parent == node
|
43
43
|
pop_control_comment_stack(node)
|
44
44
|
end
|
45
45
|
end
|
@@ -47,41 +47,50 @@ module SCSSLint
|
|
47
47
|
private
|
48
48
|
|
49
49
|
def extract_command(node)
|
50
|
-
comment =
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
50
|
+
return unless comment = retrieve_comment_text(node)
|
51
|
+
|
52
|
+
comment.split(/(?<=\n)/).each_with_index do |comment_line, line_no|
|
53
|
+
if match = %r{
|
54
|
+
(/|\*|^ \*)\s* # Comment start marker
|
55
|
+
scss-lint:
|
56
|
+
(?<action>disable|enable)\s+
|
57
|
+
(?<linters>.*?)
|
58
|
+
\s*(?:\*/|\n) # Comment end marker or end of line
|
59
|
+
}x.match(comment_line)
|
60
|
+
return {
|
61
|
+
action: match[:action],
|
62
|
+
linters: match[:linters].split(/\s*,\s*|\s+/),
|
63
|
+
line: node.line + line_no
|
64
|
+
}
|
56
65
|
end
|
66
|
+
end
|
67
|
+
|
68
|
+
false
|
69
|
+
end
|
57
70
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
{
|
67
|
-
action: match[:action],
|
68
|
-
linters: match[:linters].split(/\s*,\s*|\s+/),
|
69
|
-
}
|
71
|
+
def retrieve_comment_text(node)
|
72
|
+
case node
|
73
|
+
when Sass::Tree::CommentNode
|
74
|
+
node.value.first
|
75
|
+
when Sass::Tree::RuleNode
|
76
|
+
node.rule.select { |chunk| chunk.is_a?(String) }.join
|
77
|
+
end
|
70
78
|
end
|
71
79
|
|
72
80
|
def process_command(command, node)
|
73
|
-
case command
|
81
|
+
case command[:action]
|
74
82
|
when 'disable'
|
75
|
-
@disable_stack << node
|
83
|
+
@disable_stack << { node: node, line: command[:line] }
|
76
84
|
when 'enable'
|
77
85
|
pop_control_comment_stack(node)
|
78
86
|
end
|
79
87
|
end
|
80
88
|
|
81
89
|
def pop_control_comment_stack(node)
|
82
|
-
return unless
|
90
|
+
return unless command = @disable_stack.pop
|
83
91
|
|
84
|
-
|
92
|
+
comment_node = command[:node]
|
93
|
+
start_line = command[:line]
|
85
94
|
if comment_node.class.node_name == :rule
|
86
95
|
end_line = start_line
|
87
96
|
elsif node.class.node_name == :root
|
@@ -32,7 +32,7 @@ module SCSSLint
|
|
32
32
|
return unless %w[0 none].include?(border)
|
33
33
|
return if @preference[0] == border
|
34
34
|
|
35
|
-
add_lint(node, "`border: #{@preference[0]} is preferred over " \
|
35
|
+
add_lint(node, "`border: #{@preference[0]}` is preferred over " \
|
36
36
|
"`border: #{@preference[1]}`")
|
37
37
|
end
|
38
38
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks for "reason" comments above linter-disabling comments.
|
3
|
+
class Linter::DisableLinterReason < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_comment(node)
|
7
|
+
# No lint if the first line of the comment is not a command (because then
|
8
|
+
# either this comment has no commands, or the first line serves as a the
|
9
|
+
# reason for a command on a later line).
|
10
|
+
return unless comment_lines(node).first.match(COMMAND_REGEX)
|
11
|
+
|
12
|
+
# Maybe the previous node is the "reason" comment.
|
13
|
+
prev = previous_node(node)
|
14
|
+
|
15
|
+
if prev && prev.is_a?(Sass::Tree::CommentNode)
|
16
|
+
# No lint if the last line of the previous comment is not a command.
|
17
|
+
return unless comment_lines(prev).last.match(COMMAND_REGEX)
|
18
|
+
end
|
19
|
+
|
20
|
+
add_lint(node,
|
21
|
+
'scss-lint:disable control comments should be preceded by a ' \
|
22
|
+
'comment explaining why the linters need to be disabled.')
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
COMMAND_REGEX = %r{
|
28
|
+
(/|\*)\s* # Comment start marker
|
29
|
+
scss-lint:
|
30
|
+
(?<action>disable)\s+
|
31
|
+
(?<linters>.*?)
|
32
|
+
\s*(?:\*/|\n) # Comment end marker or end of line
|
33
|
+
}x
|
34
|
+
|
35
|
+
def comment_lines(node)
|
36
|
+
node.value.join.split("\n")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -11,8 +11,10 @@ module SCSSLint
|
|
11
11
|
seen_nodes << child_node
|
12
12
|
next unless mergeable_node
|
13
13
|
|
14
|
+
rule_text = node_rule(child_node).gsub(/(\r?\n)+/, ' ')
|
15
|
+
|
14
16
|
add_lint child_node.line,
|
15
|
-
"Merge rule `#{
|
17
|
+
"Merge rule `#{rule_text}` with rule " \
|
16
18
|
"on line #{mergeable_node.line}"
|
17
19
|
end
|
18
20
|
|
@@ -47,7 +47,7 @@ module SCSSLint
|
|
47
47
|
# combinator, as these "combine" simple sequences such that they do not
|
48
48
|
# increase depth.
|
49
49
|
depth = simple_sequences.size -
|
50
|
-
separators.count { |item| item == '~' || item == '+'
|
50
|
+
separators.count { |item| item == '~' || item == '+' }
|
51
51
|
|
52
52
|
if parent_selectors > 0
|
53
53
|
# If parent selectors are present, add the current depth for each
|
@@ -3,7 +3,7 @@ module SCSSLint
|
|
3
3
|
class Linter::SingleLinePerProperty < Linter
|
4
4
|
include LinterRegistry
|
5
5
|
|
6
|
-
def visit_rule(node)
|
6
|
+
def visit_rule(node)
|
7
7
|
single_line = single_line_rule_set?(node)
|
8
8
|
return if single_line && config['allow_single_line_rule_sets']
|
9
9
|
|
@@ -3,7 +3,7 @@ module SCSSLint
|
|
3
3
|
class Linter::SingleLinePerSelector < Linter
|
4
4
|
include LinterRegistry
|
5
5
|
|
6
|
-
MESSAGE = 'Each selector in a comma sequence should be on its own line'
|
6
|
+
MESSAGE = 'Each selector in a comma sequence should be on its own single line'
|
7
7
|
|
8
8
|
def visit_comma_sequence(node)
|
9
9
|
return unless node.members.count > 1
|
@@ -15,6 +15,14 @@ module SCSSLint
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
def visit_sequence(node)
|
19
|
+
node.members[1..-1].each_with_index do |item, index|
|
20
|
+
next unless item == "\n"
|
21
|
+
|
22
|
+
add_lint(node.line + index, MESSAGE)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
18
26
|
private
|
19
27
|
|
20
28
|
def check_comma_on_own_line(node)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module SCSSLint
|
2
2
|
# Checks for spaces following the name of a variable and before the colon
|
3
3
|
# separating the variables's name from its value.
|
4
|
-
class SpaceAfterVariableName < Linter
|
4
|
+
class Linter::SpaceAfterVariableName < Linter
|
5
5
|
include LinterRegistry
|
6
6
|
|
7
7
|
def visit_variable(node)
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks for space around operators on values.
|
3
|
+
class Linter::SpaceAroundOperator < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_script_operation(node) # rubocop:disable Metrics/AbcSize
|
7
|
+
source = normalize_source(source_from_range(node.source_range))
|
8
|
+
left_range = node.operand1.source_range
|
9
|
+
right_range = node.operand2.source_range
|
10
|
+
|
11
|
+
# We need to #chop at the end because an operation's operand1 _always_
|
12
|
+
# includes one character past the actual operand (which is either a
|
13
|
+
# whitespace character, or the first character of the operation).
|
14
|
+
left_source = normalize_source(source_from_range(left_range))
|
15
|
+
right_source = normalize_source(source_from_range(right_range))
|
16
|
+
operator_source = source_between(left_range, right_range)
|
17
|
+
left_source, operator_source = adjust_left_boundary(left_source, operator_source)
|
18
|
+
|
19
|
+
match = operator_source.match(/
|
20
|
+
(?<left_space>\s*)
|
21
|
+
(?<operator>\S+)
|
22
|
+
(?<right_space>\s*)
|
23
|
+
/x)
|
24
|
+
|
25
|
+
if config['style'] == 'one_space'
|
26
|
+
if match[:left_space] != ' ' || match[:right_space] != ' '
|
27
|
+
add_lint(node, SPACE_MSG % [source, left_source, match[:operator], right_source])
|
28
|
+
end
|
29
|
+
elsif match[:left_space] != '' || match[:right_space] != ''
|
30
|
+
add_lint(node, NO_SPACE_MSG % [source, left_source, match[:operator], right_source])
|
31
|
+
end
|
32
|
+
|
33
|
+
yield
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
SPACE_MSG = '`%s` should be written with a single space on each side of ' \
|
39
|
+
'the operator: `%s %s %s`'
|
40
|
+
NO_SPACE_MSG = '`%s` should be written without spaces around the ' \
|
41
|
+
'operator: `%s%s%s`'
|
42
|
+
|
43
|
+
def source_between(range1, range2)
|
44
|
+
# We don't want to add 1 to range1.end_pos.offset for the same reason as
|
45
|
+
# the #chop comment above.
|
46
|
+
between_start = Sass::Source::Position.new(
|
47
|
+
range1.end_pos.line,
|
48
|
+
range1.end_pos.offset,
|
49
|
+
)
|
50
|
+
between_end = Sass::Source::Position.new(
|
51
|
+
range2.start_pos.line,
|
52
|
+
range2.start_pos.offset - 1,
|
53
|
+
)
|
54
|
+
|
55
|
+
source_from_range(Sass::Source::Range.new(between_start,
|
56
|
+
between_end,
|
57
|
+
range1.file,
|
58
|
+
range1.importer))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Removes trailing parentheses and compacts newlines into a single space
|
62
|
+
def normalize_source(source)
|
63
|
+
source.chop.gsub(/\s*\n\s*/, ' ')
|
64
|
+
end
|
65
|
+
|
66
|
+
def adjust_left_boundary(left, operator)
|
67
|
+
# If the left operand is wrapped in parentheses, any right parens end up
|
68
|
+
# in the operator source. Here, we move them into the left operand
|
69
|
+
# source, which is awkward in any messaging, but it works.
|
70
|
+
if match = operator.match(/^(\s*\))+/)
|
71
|
+
left += match[0]
|
72
|
+
operator = operator[match.end(0)..-1]
|
73
|
+
end
|
74
|
+
|
75
|
+
# If the left operand is a nested operation, Sass includes any whitespace
|
76
|
+
# before the (outer) operator in the left operator's source_range's
|
77
|
+
# end_pos, which is not the case with simple, non-operation operands.
|
78
|
+
if match = left.match(/\s+$/)
|
79
|
+
left = left[0..match.begin(0)]
|
80
|
+
operator = match[0] + operator
|
81
|
+
end
|
82
|
+
|
83
|
+
[left, operator]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -1,36 +1,112 @@
|
|
1
1
|
module SCSSLint
|
2
|
+
# rubocop:disable Metrics/AbcSize
|
3
|
+
|
2
4
|
# Checks for the presence of spaces between parentheses.
|
3
5
|
class Linter::SpaceBetweenParens < Linter
|
4
6
|
include LinterRegistry
|
5
7
|
|
6
|
-
def
|
8
|
+
def check_node(node)
|
9
|
+
check(node, source_from_range(node.source_range))
|
10
|
+
yield
|
11
|
+
end
|
12
|
+
|
13
|
+
alias_method :visit_atroot, :check_node
|
14
|
+
alias_method :visit_cssimport, :check_node
|
15
|
+
alias_method :visit_function, :check_node
|
16
|
+
alias_method :visit_media, :check_node
|
17
|
+
alias_method :visit_mixindef, :check_node
|
18
|
+
alias_method :visit_mixin, :check_node
|
19
|
+
alias_method :visit_script_funcall, :check_node
|
20
|
+
|
21
|
+
def feel_for_parens_and_check_node(node)
|
22
|
+
source = feel_for_enclosing_parens(node)
|
23
|
+
check(node, source)
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :visit_script_listliteral, :feel_for_parens_and_check_node
|
28
|
+
alias_method :visit_script_mapliteral, :feel_for_parens_and_check_node
|
29
|
+
alias_method :visit_script_operation, :feel_for_parens_and_check_node
|
30
|
+
alias_method :visit_script_string, :feel_for_parens_and_check_node
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
TRAILING_WHITESPACE = /\s*$/
|
35
|
+
|
36
|
+
def check(node, source) # rubocop:disable Metrics/MethodLength
|
7
37
|
@spaces = config['spaces']
|
38
|
+
source = trim_right_paren(source)
|
39
|
+
return if source.count('(') != source.count(')')
|
40
|
+
source.scan(/
|
41
|
+
\(
|
42
|
+
(?<left>\s*)
|
43
|
+
(?<contents>.*)
|
44
|
+
(?<right>\s*)
|
45
|
+
\)
|
46
|
+
/x) do |left, contents, right|
|
47
|
+
right = contents.match(TRAILING_WHITESPACE)[0] + right
|
48
|
+
contents.gsub(TRAILING_WHITESPACE, '')
|
49
|
+
|
50
|
+
# We don't lint on multiline parenthetical source.
|
51
|
+
break if (left + contents + right).include? "\n"
|
8
52
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
53
|
+
if contents.empty?
|
54
|
+
# If we're looking at empty parens (like `()`, `( )`, `( )`, etc.),
|
55
|
+
# only report a possible lint on the left side.
|
56
|
+
right = ' ' * @spaces
|
57
|
+
end
|
58
|
+
|
59
|
+
if left != ' ' * @spaces
|
60
|
+
message = "#{expected_spaces} after `(` instead of `#{left}`"
|
61
|
+
add_lint(node, message)
|
62
|
+
end
|
63
|
+
|
64
|
+
if right != ' ' * @spaces
|
65
|
+
message = "#{expected_spaces} before `)` instead of `#{right}`"
|
66
|
+
add_lint(node, message)
|
19
67
|
end
|
20
68
|
end
|
21
|
-
yield
|
22
69
|
end
|
23
70
|
|
24
|
-
|
71
|
+
# An expression enclosed in parens will include or not include each paren, depending
|
72
|
+
# on whitespace. Here we feel out for enclosing parens, and return them as the new
|
73
|
+
# source for the node.
|
74
|
+
def feel_for_enclosing_parens(node) # rubocop:disable Metrics/CyclomaticComplexity
|
75
|
+
range = node.source_range
|
76
|
+
original_source = source_from_range(range)
|
77
|
+
left_offset = -1
|
78
|
+
right_offset = 0
|
79
|
+
|
80
|
+
if original_source[-1] != ')'
|
81
|
+
right_offset += 1 while character_at(range.end_pos, right_offset) =~ /\s/
|
25
82
|
|
26
|
-
|
27
|
-
|
28
|
-
|
83
|
+
return original_source if character_at(range.end_pos, right_offset) != ')'
|
84
|
+
end
|
85
|
+
|
86
|
+
# At this point, we know that we're wrapped on the right by a ')'.
|
87
|
+
# Are we wrapped on the left by a '('?
|
88
|
+
left_offset -= 1 while character_at(range.start_pos, left_offset) =~ /\s/
|
89
|
+
return original_source if character_at(range.start_pos, left_offset) != '('
|
90
|
+
|
91
|
+
# At this point, we know we're wrapped on both sides by parens. However,
|
92
|
+
# those parens may be part of a parent function call. We don't care about
|
93
|
+
# such parens. This depends on whether the preceding character is part of
|
94
|
+
# a function name.
|
95
|
+
return original_source if character_at(range.start_pos, left_offset - 1) =~ /[A-Za-z0-9_]/
|
96
|
+
|
97
|
+
range.start_pos.offset += left_offset
|
98
|
+
range.end_pos.offset += right_offset
|
99
|
+
source_from_range(range)
|
100
|
+
end
|
101
|
+
|
102
|
+
# An unrelated right paren will sneak into the source of a node if there is no
|
103
|
+
# whitespace between the node and the right paren.
|
104
|
+
def trim_right_paren(source)
|
105
|
+
(source.count(')') == source.count('(') + 1) ? source[0..-2] : source
|
106
|
+
end
|
29
107
|
|
30
|
-
|
31
|
-
|
32
|
-
"between parentheses instead of #{spaces}"
|
33
|
-
add_lint(location, message)
|
108
|
+
def expected_spaces
|
109
|
+
"Expected #{pluralize(@spaces, 'space')}"
|
34
110
|
end
|
35
111
|
end
|
36
112
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks for explicitly transitioned properties instead of transition all.
|
3
|
+
class Linter::TransitionAll < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
TRANSITION_PROPERTIES = %w[
|
7
|
+
transition
|
8
|
+
transition-property
|
9
|
+
]
|
10
|
+
|
11
|
+
def visit_prop(node)
|
12
|
+
property = node.name.first.to_s
|
13
|
+
return unless TRANSITION_PROPERTIES.include?(property)
|
14
|
+
|
15
|
+
check_transition(node, property, node.value.to_sass)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def check_transition(node, property, value)
|
21
|
+
return unless offset = value =~ /\ball\b/
|
22
|
+
|
23
|
+
pos = node.value_source_range.start_pos.after(value[0, offset])
|
24
|
+
|
25
|
+
add_lint(Location.new(pos.line, pos.offset, 3),
|
26
|
+
"#{property} should contain explicit properties " \
|
27
|
+
'instead of using the keyword all')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Reports a single line for each clean file (having zero lints).
|
3
|
+
class Reporter::CleanFilesReporter < Reporter
|
4
|
+
def report_lints
|
5
|
+
dirty_files = lints.map(&:filename).uniq
|
6
|
+
clean_files = files - dirty_files
|
7
|
+
clean_files.sort.join("\n") + "\n" if clean_files.any?
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/lib/scss_lint/reporter.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
module SCSSLint
|
2
2
|
# Responsible for displaying lints to the user in some format.
|
3
3
|
class Reporter
|
4
|
-
attr_reader :lints
|
4
|
+
attr_reader :lints, :files
|
5
5
|
|
6
6
|
def self.descendants
|
7
7
|
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
# @param lints [List<Lint>] a list of Lints sorted by file and line number
|
11
|
+
# @param files [List<String>] a list of the files that were linted
|
12
|
+
def initialize(lints, files)
|
11
13
|
@lints = lints
|
14
|
+
@files = files
|
12
15
|
end
|
13
16
|
|
14
17
|
def report_lints
|
data/lib/scss_lint/runner.rb
CHANGED
@@ -2,7 +2,7 @@ module SCSSLint
|
|
2
2
|
# Finds and aggregates all lints found by running the registered linters
|
3
3
|
# against a set of SCSS files.
|
4
4
|
class Runner
|
5
|
-
attr_reader :lints
|
5
|
+
attr_reader :lints, :files
|
6
6
|
|
7
7
|
# @param config [Config]
|
8
8
|
def initialize(config)
|
@@ -13,7 +13,8 @@ module SCSSLint
|
|
13
13
|
|
14
14
|
# @param files [Array]
|
15
15
|
def run(files)
|
16
|
-
files
|
16
|
+
@files = files
|
17
|
+
@files.each do |file|
|
17
18
|
find_lints(file)
|
18
19
|
end
|
19
20
|
end
|