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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +9 -1
  3. data/data/properties.txt +1 -1
  4. data/lib/scss_lint/cli.rb +11 -5
  5. data/lib/scss_lint/config.rb +1 -2
  6. data/lib/scss_lint/control_comment_processor.rb +33 -28
  7. data/lib/scss_lint/engine.rb +10 -7
  8. data/lib/scss_lint/linter.rb +56 -12
  9. data/lib/scss_lint/linter/chained_classes.rb +21 -0
  10. data/lib/scss_lint/linter/color_variable.rb +0 -7
  11. data/lib/scss_lint/linter/comment.rb +15 -1
  12. data/lib/scss_lint/linter/empty_line_between_blocks.rb +10 -0
  13. data/lib/scss_lint/linter/indentation.rb +47 -53
  14. data/lib/scss_lint/linter/mergeable_selector.rb +29 -0
  15. data/lib/scss_lint/linter/property_spelling.rb +18 -6
  16. data/lib/scss_lint/linter/pseudo_element.rb +18 -0
  17. data/lib/scss_lint/linter/single_line_per_selector.rb +23 -7
  18. data/lib/scss_lint/linter/space_after_property_colon.rb +5 -6
  19. data/lib/scss_lint/linter/space_after_property_name.rb +1 -7
  20. data/lib/scss_lint/linter/space_after_variable_name.rb +1 -1
  21. data/lib/scss_lint/linter/space_around_operator.rb +23 -7
  22. data/lib/scss_lint/linter/string_quotes.rb +2 -9
  23. data/lib/scss_lint/linter/trailing_semicolon.rb +10 -0
  24. data/lib/scss_lint/options.rb +5 -0
  25. data/lib/scss_lint/runner.rb +10 -8
  26. data/lib/scss_lint/selector_visitor.rb +11 -3
  27. data/lib/scss_lint/version.rb +1 -1
  28. data/spec/scss_lint/cli_spec.rb +14 -0
  29. data/spec/scss_lint/linter/chained_classes_spec.rb +45 -0
  30. data/spec/scss_lint/linter/comment_spec.rb +16 -0
  31. data/spec/scss_lint/linter/empty_line_between_blocks_spec.rb +62 -0
  32. data/spec/scss_lint/linter/indentation_spec.rb +1 -9
  33. data/spec/scss_lint/linter/leading_zero_spec.rb +12 -0
  34. data/spec/scss_lint/linter/mergeable_selector_spec.rb +59 -0
  35. data/spec/scss_lint/linter/property_spelling_spec.rb +28 -0
  36. data/spec/scss_lint/linter/pseudo_element_spec.rb +71 -0
  37. data/spec/scss_lint/linter/single_line_per_selector_spec.rb +28 -1
  38. data/spec/scss_lint/linter/space_after_variable_name_spec.rb +12 -0
  39. data/spec/scss_lint/linter/space_around_operator_spec.rb +51 -0
  40. data/spec/scss_lint/linter/trailing_semicolon_spec.rb +28 -0
  41. data/spec/scss_lint/linter_spec.rb +17 -0
  42. data/spec/scss_lint/runner_spec.rb +2 -2
  43. 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
- # Ignore properties with interpolation
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 KNOWN_PROPERTIES.include?(name) ||
44
- @extra_properties.include?(name)
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
- add_lint(node, "Unknown property #{name}")
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
- node.members[1..-1].each_with_index do |sequence, index|
14
- check_sequence_commas(node, sequence, index)
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
- node.members[1..-1].each_with_index do |item, index|
20
- next unless item == "\n"
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
- add_lint(node.line + index, MESSAGE)
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.members[0] != "\n"
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 = 1
76
+ offset = 0
77
+ start_pos = node.name_source_range.start_pos
77
78
 
78
79
  # Find the colon after the property name
79
- while character_at(node.name_source_range.start_pos, offset - 1) != ':'
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(node.name_source_range.start_pos, offset)
85
- whitespace << character_at(node.name_source_range.start_pos, offset)
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
- offset = 0
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
@@ -12,7 +12,7 @@ module SCSSLint
12
12
  private
13
13
 
14
14
  def spaces_before_colon?(node)
15
- source_from_range(node.source_range) =~ /\s+:/
15
+ source_from_range(node.source_range) =~ /\A[^:]+\s+:/
16
16
  end
17
17
  end
18
18
  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/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/LineLength
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
- if config['style'] == 'one_space'
45
- if (match[:left_space] != ' ' && !left_newline) ||
46
- (match[:right_space] != ' ' && !right_newline)
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
- elsif (match[:left_space] != '' && !left_newline) ||
50
- (match[:right_space] != '' && !right_newline)
51
- add_lint(node, operation_sources.no_space_msg(match[:operator]))
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
- def visit_comment(_node)
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)[('@charset'.length)..-1]
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
 
@@ -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
@@ -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 [String]
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: 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, file)
48
- return if @config.excluded_file_for_linter?(file, 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
- # Converts the class name of a node into snake_case form, e.g.
27
- # `Sass::Selector::SimpleSequence` -> `simple_sequence`
28
- name = node.class.name.gsub(/.*::(.*?)$/, '\\1')
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
@@ -1,4 +1,4 @@
1
1
  # Defines the gem version.
2
2
  module SCSSLint
3
- VERSION = '0.42.2'
3
+ VERSION = '0.43.0'
4
4
  end
@@ -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