scss_lint 0.42.2 → 0.43.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 +9 -1
- data/data/properties.txt +1 -1
- data/lib/scss_lint/cli.rb +11 -5
- data/lib/scss_lint/config.rb +1 -2
- data/lib/scss_lint/control_comment_processor.rb +33 -28
- data/lib/scss_lint/engine.rb +10 -7
- data/lib/scss_lint/linter.rb +56 -12
- data/lib/scss_lint/linter/chained_classes.rb +21 -0
- data/lib/scss_lint/linter/color_variable.rb +0 -7
- data/lib/scss_lint/linter/comment.rb +15 -1
- data/lib/scss_lint/linter/empty_line_between_blocks.rb +10 -0
- data/lib/scss_lint/linter/indentation.rb +47 -53
- data/lib/scss_lint/linter/mergeable_selector.rb +29 -0
- data/lib/scss_lint/linter/property_spelling.rb +18 -6
- data/lib/scss_lint/linter/pseudo_element.rb +18 -0
- data/lib/scss_lint/linter/single_line_per_selector.rb +23 -7
- data/lib/scss_lint/linter/space_after_property_colon.rb +5 -6
- data/lib/scss_lint/linter/space_after_property_name.rb +1 -7
- data/lib/scss_lint/linter/space_after_variable_name.rb +1 -1
- data/lib/scss_lint/linter/space_around_operator.rb +23 -7
- data/lib/scss_lint/linter/string_quotes.rb +2 -9
- data/lib/scss_lint/linter/trailing_semicolon.rb +10 -0
- data/lib/scss_lint/options.rb +5 -0
- data/lib/scss_lint/runner.rb +10 -8
- data/lib/scss_lint/selector_visitor.rb +11 -3
- data/lib/scss_lint/version.rb +1 -1
- data/spec/scss_lint/cli_spec.rb +14 -0
- data/spec/scss_lint/linter/chained_classes_spec.rb +45 -0
- data/spec/scss_lint/linter/comment_spec.rb +16 -0
- data/spec/scss_lint/linter/empty_line_between_blocks_spec.rb +62 -0
- data/spec/scss_lint/linter/indentation_spec.rb +1 -9
- data/spec/scss_lint/linter/leading_zero_spec.rb +12 -0
- data/spec/scss_lint/linter/mergeable_selector_spec.rb +59 -0
- data/spec/scss_lint/linter/property_spelling_spec.rb +28 -0
- data/spec/scss_lint/linter/pseudo_element_spec.rb +71 -0
- data/spec/scss_lint/linter/single_line_per_selector_spec.rb +28 -1
- data/spec/scss_lint/linter/space_after_variable_name_spec.rb +12 -0
- data/spec/scss_lint/linter/space_around_operator_spec.rb +51 -0
- data/spec/scss_lint/linter/trailing_semicolon_spec.rb +28 -0
- data/spec/scss_lint/linter_spec.rb +17 -0
- data/spec/scss_lint/runner_spec.rb +2 -2
- metadata +9 -3
@@ -7,6 +7,8 @@ module SCSSLint
|
|
7
7
|
node.children.each_with_object([]) do |child_node, seen_nodes|
|
8
8
|
next unless child_node.is_a?(Sass::Tree::RuleNode)
|
9
9
|
|
10
|
+
next if whitelist_contains(child_node)
|
11
|
+
|
10
12
|
mergeable_node = find_mergeable_node(child_node, seen_nodes)
|
11
13
|
seen_nodes << child_node
|
12
14
|
next unless mergeable_node
|
@@ -27,12 +29,30 @@ module SCSSLint
|
|
27
29
|
private
|
28
30
|
|
29
31
|
def find_mergeable_node(node, seen_nodes)
|
32
|
+
return if multiple_parent_references?(node)
|
33
|
+
|
30
34
|
seen_nodes.find do |seen_node|
|
31
35
|
equal?(node, seen_node) ||
|
32
36
|
(config['force_nesting'] && nested?(node, seen_node))
|
33
37
|
end
|
34
38
|
end
|
35
39
|
|
40
|
+
def multiple_parent_references?(rule_node)
|
41
|
+
return unless rules = rule_node.parsed_rules
|
42
|
+
|
43
|
+
# Iterate over each sequence counting all parent references
|
44
|
+
total_parent_references = rules.members.inject(0) do |sum, seq|
|
45
|
+
sum + seq.members.inject(0) do |ssum, simple_seq|
|
46
|
+
next ssum unless simple_seq.respond_to?(:members)
|
47
|
+
ssum + simple_seq.members.count do |member|
|
48
|
+
member.is_a?(Sass::Selector::Parent)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
total_parent_references > 1
|
54
|
+
end
|
55
|
+
|
36
56
|
def equal?(node1, node2)
|
37
57
|
node_rule(node1) == node_rule(node2)
|
38
58
|
end
|
@@ -58,5 +78,14 @@ module SCSSLint
|
|
58
78
|
"#{rule1}".start_with?("#{rule2} ") ||
|
59
79
|
"#{rule1}".start_with?("#{rule2}.")
|
60
80
|
end
|
81
|
+
|
82
|
+
def whitelist_contains(node)
|
83
|
+
if @whitelist.nil?
|
84
|
+
@whitelist = config['whitelist'] || []
|
85
|
+
@whitelist = [@whitelist] if @whitelist.is_a? String
|
86
|
+
end
|
87
|
+
|
88
|
+
@whitelist.include?(node_rule(node))
|
89
|
+
end
|
61
90
|
end
|
62
91
|
end
|
@@ -9,7 +9,9 @@ module SCSSLint
|
|
9
9
|
.to_set
|
10
10
|
|
11
11
|
def visit_root(_node)
|
12
|
-
@extra_properties = config['extra_properties'].to_set
|
12
|
+
@extra_properties = Array(config['extra_properties']).to_set
|
13
|
+
@disabled_properties = Array(config['disabled_properties']).to_set
|
14
|
+
|
13
15
|
yield # Continue linting children
|
14
16
|
end
|
15
17
|
|
@@ -32,18 +34,28 @@ module SCSSLint
|
|
32
34
|
private
|
33
35
|
|
34
36
|
def check_property(node, prefix = nil) # rubocop:disable CyclomaticComplexity
|
35
|
-
|
36
|
-
return if node.name.count > 1 || !node.name.first.is_a?(String)
|
37
|
+
return if contains_interpolation?(node)
|
37
38
|
|
38
39
|
name = prefix ? "#{prefix}-" : ''
|
39
40
|
name += node.name.join
|
40
41
|
|
41
42
|
# Ignore vendor-prefixed properties
|
42
43
|
return if name.start_with?('-')
|
43
|
-
return if
|
44
|
-
|
44
|
+
return if known_property?(name) && !@disabled_properties.include?(name)
|
45
|
+
|
46
|
+
if @disabled_properties.include?(name)
|
47
|
+
add_lint(node, "Property #{name} is prohibited")
|
48
|
+
else
|
49
|
+
add_lint(node, "Unknown property #{name}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def known_property?(name)
|
54
|
+
KNOWN_PROPERTIES.include?(name) || @extra_properties.include?(name)
|
55
|
+
end
|
45
56
|
|
46
|
-
|
57
|
+
def contains_interpolation?(node)
|
58
|
+
node.name.count > 1 || !node.name.first.is_a?(String)
|
47
59
|
end
|
48
60
|
end
|
49
61
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks for the use of double colons with pseudo elements.
|
3
|
+
class Linter::PseudoElement < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
PSEUDO_ELEMENTS = %w[after backdrop before first-letter first-line selection]
|
7
|
+
|
8
|
+
def visit_pseudo(pseudo)
|
9
|
+
if PSEUDO_ELEMENTS.include?(pseudo.name)
|
10
|
+
return if pseudo.syntactic_type == :element
|
11
|
+
add_lint(pseudo, 'Begin pseudo elements with double colons: `::`')
|
12
|
+
else
|
13
|
+
return if pseudo.syntactic_type != :element
|
14
|
+
add_lint(pseudo, 'Begin pseudo classes with a single colon: `:`')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -10,28 +10,44 @@ module SCSSLint
|
|
10
10
|
|
11
11
|
check_comma_on_own_line(node)
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
line_offset = 0
|
14
|
+
node.members[1..-1].each do |sequence|
|
15
|
+
line_offset += 1 if sequence_start_of_line?(sequence)
|
16
|
+
check_multiline_sequence(node, sequence, line_offset)
|
17
|
+
check_sequence_commas(node, sequence, line_offset)
|
15
18
|
end
|
16
19
|
end
|
17
20
|
|
18
21
|
def visit_sequence(node)
|
19
|
-
|
20
|
-
|
22
|
+
# Only execute if this is first or only sequence in a comma sequence. If
|
23
|
+
# it is the only sequence, then it won't be in a comma sequence, which is
|
24
|
+
# why we define a separate visit_* method specifically for this case.
|
25
|
+
return if node.members.first == "\n"
|
21
26
|
|
22
|
-
|
23
|
-
end
|
27
|
+
check_multiline_sequence(node, node, 0)
|
24
28
|
end
|
25
29
|
|
26
30
|
private
|
27
31
|
|
32
|
+
def sequence_start_of_line?(sequence)
|
33
|
+
sequence.members[0] == "\n"
|
34
|
+
end
|
35
|
+
|
28
36
|
def check_comma_on_own_line(node)
|
29
37
|
return unless node.members[0].members[1] == "\n"
|
30
38
|
add_lint(node, MESSAGE)
|
31
39
|
end
|
32
40
|
|
41
|
+
# Checks if an individual sequence is split over multiple lines
|
42
|
+
def check_multiline_sequence(node, sequence, index)
|
43
|
+
return unless sequence.members.size > 1
|
44
|
+
return unless sequence.members[2..-1].any? { |member| member == "\n" }
|
45
|
+
|
46
|
+
add_lint(node.line + index, MESSAGE)
|
47
|
+
end
|
48
|
+
|
33
49
|
def check_sequence_commas(node, sequence, index)
|
34
|
-
if sequence
|
50
|
+
if !sequence_start_of_line?(sequence)
|
35
51
|
# Next sequence doesn't reside on its own line
|
36
52
|
add_lint(node.line + index, MESSAGE)
|
37
53
|
elsif sequence.members[1] == "\n"
|
@@ -73,16 +73,15 @@ module SCSSLint
|
|
73
73
|
|
74
74
|
def whitespace_after_colon(node)
|
75
75
|
whitespace = []
|
76
|
-
offset =
|
76
|
+
offset = 0
|
77
|
+
start_pos = node.name_source_range.start_pos
|
77
78
|
|
78
79
|
# Find the colon after the property name
|
79
|
-
|
80
|
-
offset += 1
|
81
|
-
end
|
80
|
+
offset = offset_to(start_pos, ':', offset) + 1
|
82
81
|
|
83
82
|
# Count spaces after the colon
|
84
|
-
while [' ', "\t", "\n"].include? character_at(
|
85
|
-
whitespace << character_at(
|
83
|
+
while [' ', "\t", "\n"].include? character_at(start_pos, offset)
|
84
|
+
whitespace << character_at(start_pos, offset)
|
86
85
|
offset += 1
|
87
86
|
end
|
88
87
|
|
@@ -15,13 +15,7 @@ module SCSSLint
|
|
15
15
|
# Deals with a weird Sass bug where the name_source_range of a PropNode does
|
16
16
|
# not start at the beginning of the property name.
|
17
17
|
def property_name_colon_offset(node)
|
18
|
-
|
19
|
-
|
20
|
-
while character_at(node.name_source_range.start_pos, offset) != ':'
|
21
|
-
offset += 1
|
22
|
-
end
|
23
|
-
|
24
|
-
offset
|
18
|
+
offset_to(node.name_source_range.start_pos, ':')
|
25
19
|
end
|
26
20
|
end
|
27
21
|
end
|
@@ -28,7 +28,7 @@ module SCSSLint
|
|
28
28
|
|
29
29
|
private
|
30
30
|
|
31
|
-
def check(node, operation_sources) # rubocop:disable Metrics/AbcSize, Metrics/
|
31
|
+
def check(node, operation_sources) # rubocop:disable Metrics/AbcSize, Metrics/LineLength, Metrics/MethodLength
|
32
32
|
match = operation_sources.operator_source.match(/
|
33
33
|
(?<left_space>\s*)
|
34
34
|
(?<operator>\S+)
|
@@ -41,17 +41,33 @@ module SCSSLint
|
|
41
41
|
# just don't worry about space with a newline.
|
42
42
|
left_newline = match[:left_space].include?("\n")
|
43
43
|
right_newline = match[:right_space].include?("\n")
|
44
|
-
|
45
|
-
|
46
|
-
|
44
|
+
|
45
|
+
case config['style']
|
46
|
+
when 'one_space'
|
47
|
+
if one_space_exists?(match, left_newline, right_newline)
|
47
48
|
add_lint(node, operation_sources.space_msg(match[:operator]))
|
48
49
|
end
|
49
|
-
|
50
|
-
(match
|
51
|
-
|
50
|
+
when 'at_least_one_space'
|
51
|
+
unless spaces_exist?(match, left_newline, right_newline)
|
52
|
+
add_lint(node, operation_sources.space_msg(match[:operator]))
|
53
|
+
end
|
54
|
+
else
|
55
|
+
if spaces_exist?(match, left_newline, right_newline)
|
56
|
+
add_lint(node, operation_sources.no_space_msg(match[:operator]))
|
57
|
+
end
|
52
58
|
end
|
53
59
|
end
|
54
60
|
|
61
|
+
def one_space_exists?(match, left_newline, right_newline)
|
62
|
+
(match[:left_space] != ' ' && !left_newline) ||
|
63
|
+
(match[:right_space] != ' ' && !right_newline)
|
64
|
+
end
|
65
|
+
|
66
|
+
def spaces_exist?(match, left_newline, right_newline)
|
67
|
+
(match[:left_space] != '' && !left_newline) ||
|
68
|
+
(match[:right_space] != '' && !right_newline)
|
69
|
+
end
|
70
|
+
|
55
71
|
# A helper class for storing and adjusting the sources of the different
|
56
72
|
# components of an Operation node.
|
57
73
|
class OperationSources
|
@@ -3,14 +3,7 @@ module SCSSLint
|
|
3
3
|
class Linter::StringQuotes < Linter
|
4
4
|
include LinterRegistry
|
5
5
|
|
6
|
-
|
7
|
-
# Sass allows you to write Sass Script in non-silent comments (/* ... */).
|
8
|
-
# Unfortunately, it doesn't report correct source ranges for these script
|
9
|
-
# nodes.
|
10
|
-
# It's unlikely that a developer wanted to lint the script they wrote in a
|
11
|
-
# comment, so just ignore this case entirely and stop traversing the
|
12
|
-
# children of comment nodes.
|
13
|
-
end
|
6
|
+
CHARSET_DIRECTIVE_LENGTH = '@charset'.length
|
14
7
|
|
15
8
|
def visit_script_stringinterpolation(node)
|
16
9
|
# We can't statically determine what the resultant string looks like when
|
@@ -34,7 +27,7 @@ module SCSSLint
|
|
34
27
|
|
35
28
|
def visit_charset(node)
|
36
29
|
# `@charset` source range includes entire declaration, so exclude that prefix
|
37
|
-
source = source_from_range(node.source_range)[(
|
30
|
+
source = source_from_range(node.source_range)[(CHARSET_DIRECTIVE_LENGTH)..-1]
|
38
31
|
|
39
32
|
check_quotes(node, source)
|
40
33
|
end
|
@@ -15,6 +15,16 @@ module SCSSLint
|
|
15
15
|
# !default`.
|
16
16
|
return check_semicolon(node) if node.global || node.guarded
|
17
17
|
|
18
|
+
# If the variable is a multi-line ListLiteral or MapLiteral, then
|
19
|
+
# `node.expr` will give us everything except the last right paren, and
|
20
|
+
# the semicolon if it exists. In these cases, use the source range of
|
21
|
+
# `node` as above.
|
22
|
+
if (node.expr.is_a?(Sass::Script::Tree::ListLiteral) ||
|
23
|
+
node.expr.is_a?(Sass::Script::Tree::MapLiteral)) &&
|
24
|
+
!node_on_single_line?(node)
|
25
|
+
return check_semicolon(node)
|
26
|
+
end
|
27
|
+
|
18
28
|
check_semicolon(node.expr)
|
19
29
|
end
|
20
30
|
|
data/lib/scss_lint/options.rb
CHANGED
@@ -77,6 +77,11 @@ module SCSSLint
|
|
77
77
|
@options[:excluded_files] = files
|
78
78
|
end
|
79
79
|
|
80
|
+
parser.on('--stdin-file-path file-path', String,
|
81
|
+
'Specify the path to assume for the file passed via STDIN') do |stdin_file_path|
|
82
|
+
@options[:stdin_file_path] = stdin_file_path
|
83
|
+
end
|
84
|
+
|
80
85
|
parser.on('-o', '--out path', 'Write output to a file instead of STDOUT', String) do |path|
|
81
86
|
define_output_path(path)
|
82
87
|
end
|
data/lib/scss_lint/runner.rb
CHANGED
@@ -12,7 +12,7 @@ module SCSSLint
|
|
12
12
|
@linters.map!(&:new)
|
13
13
|
end
|
14
14
|
|
15
|
-
# @param files [Array]
|
15
|
+
# @param files [Array<Hash>] list of file object/path hashes
|
16
16
|
def run(files)
|
17
17
|
@files = files
|
18
18
|
@files.each do |file|
|
@@ -22,16 +22,18 @@ module SCSSLint
|
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
|
-
# @param file [
|
25
|
+
# @param file [Hash]
|
26
|
+
# @option file [String] File object
|
27
|
+
# @option path [String] path to File (determines which Linter config to apply)
|
26
28
|
def find_lints(file)
|
27
|
-
engine = Engine.new(file
|
29
|
+
engine = Engine.new(file)
|
28
30
|
|
29
31
|
@linters.each do |linter|
|
30
32
|
begin
|
31
|
-
run_linter(linter, engine, file)
|
33
|
+
run_linter(linter, engine, file[:path])
|
32
34
|
rescue => error
|
33
35
|
raise SCSSLint::Exceptions::LinterError,
|
34
|
-
"#{linter.class} raised unexpected error linting file #{file}: " \
|
36
|
+
"#{linter.class} raised unexpected error linting file #{file[:path]}: " \
|
35
37
|
"'#{error.message}'",
|
36
38
|
error.backtrace
|
37
39
|
end
|
@@ -40,12 +42,12 @@ module SCSSLint
|
|
40
42
|
@lints << Lint.new(nil, ex.sass_filename, Location.new(ex.sass_line),
|
41
43
|
"Syntax Error: #{ex}", :error)
|
42
44
|
rescue FileEncodingError => ex
|
43
|
-
@lints << Lint.new(nil, file, Location.new, ex.to_s, :error)
|
45
|
+
@lints << Lint.new(nil, file[:path], Location.new, ex.to_s, :error)
|
44
46
|
end
|
45
47
|
|
46
48
|
# For stubbing in tests.
|
47
|
-
def run_linter(linter, engine,
|
48
|
-
return if @config.excluded_file_for_linter?(
|
49
|
+
def run_linter(linter, engine, file_path)
|
50
|
+
return if @config.excluded_file_for_linter?(file_path, linter)
|
49
51
|
@lints += linter.run(engine, @config.linter_options(linter))
|
50
52
|
end
|
51
53
|
end
|
@@ -22,13 +22,21 @@ module SCSSLint
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
# The class name of a node, in snake_case form, e.g.
|
26
|
+
# `Sass::Selector::SimpleSequence` -> `simple_sequence`.
|
27
|
+
#
|
28
|
+
# The name is memoized as a class variable on the node itself.
|
25
29
|
def selector_node_name(node)
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
if node.class.class_variable_defined?(:@@snake_case_name)
|
31
|
+
return node.class.class_variable_get(:@@snake_case_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
rindex = node.class.name.rindex('::')
|
35
|
+
name = node.class.name[(rindex + 2)..-1]
|
29
36
|
name.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
30
37
|
name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
31
38
|
name.downcase!
|
39
|
+
node.class.class_variable_set(:@@snake_case_name, name)
|
32
40
|
end
|
33
41
|
end
|
34
42
|
end
|
data/lib/scss_lint/version.rb
CHANGED
data/spec/scss_lint/cli_spec.rb
CHANGED
@@ -154,6 +154,20 @@ describe SCSSLint::CLI do
|
|
154
154
|
end
|
155
155
|
end
|
156
156
|
|
157
|
+
context 'when the --stdin-file-path argument is specified' do
|
158
|
+
let(:flags) { ['--stdin-file-path', 'some-fake-file-path.scss'] }
|
159
|
+
|
160
|
+
before do
|
161
|
+
STDIN.stub(:read).and_return('// Nothing interesting')
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'passes STDIN and the file path as a file tuple to the runner' do
|
165
|
+
SCSSLint::Runner.any_instance.should_receive(:run)
|
166
|
+
.with([file: STDIN, path: 'some-fake-file-path.scss'])
|
167
|
+
safe_run
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
157
171
|
context 'when specified SCSS file globs match no files' do
|
158
172
|
before do
|
159
173
|
SCSSLint::FileFinder.any_instance.stub(:find)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SCSSLint::Linter::ChainedClasses do
|
4
|
+
context 'with a single class' do
|
5
|
+
let(:scss) { <<-SCSS }
|
6
|
+
.class {}
|
7
|
+
SCSS
|
8
|
+
|
9
|
+
it { should_not report_lint }
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'with a single class with a descendant ' do
|
13
|
+
let(:scss) { <<-SCSS }
|
14
|
+
.class .descendant {}
|
15
|
+
SCSS
|
16
|
+
|
17
|
+
it { should_not report_lint }
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'with a chained class' do
|
21
|
+
let(:scss) { <<-SCSS }
|
22
|
+
.chained.class {}
|
23
|
+
SCSS
|
24
|
+
|
25
|
+
it { should report_lint line: 1 }
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'with a chained class in a nested rule set' do
|
29
|
+
let(:scss) { <<-SCSS }
|
30
|
+
p {
|
31
|
+
.chained.class {}
|
32
|
+
}
|
33
|
+
SCSS
|
34
|
+
|
35
|
+
it { should report_lint line: 2 }
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'with a chained class in part of a sequence' do
|
39
|
+
let(:scss) { <<-SCSS }
|
40
|
+
.some .sequence .with .chained.class .in .it {}
|
41
|
+
SCSS
|
42
|
+
|
43
|
+
it { should report_lint line: 1 }
|
44
|
+
end
|
45
|
+
end
|