scss_lint 0.42.2 → 0.43.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 +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
|