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.
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