scss_lint 0.40.1 → 0.41.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 +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
|