scss-lint 0.29.0 → 0.30.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +31 -1
  3. data/data/prefixed-identifiers/base.txt +107 -0
  4. data/data/prefixed-identifiers/bourbon.txt +71 -0
  5. data/lib/scss_lint/cli.rb +34 -7
  6. data/lib/scss_lint/config.rb +12 -1
  7. data/lib/scss_lint/engine.rb +3 -1
  8. data/lib/scss_lint/linter/bang_format.rb +40 -0
  9. data/lib/scss_lint/linter/declaration_order.rb +35 -14
  10. data/lib/scss_lint/linter/import_path.rb +62 -0
  11. data/lib/scss_lint/linter/name_format.rb +1 -1
  12. data/lib/scss_lint/linter/nesting_depth.rb +24 -0
  13. data/lib/scss_lint/linter/property_sort_order.rb +4 -11
  14. data/lib/scss_lint/linter/property_spelling.rb +25 -8
  15. data/lib/scss_lint/linter/qualifying_element.rb +42 -0
  16. data/lib/scss_lint/linter/selector_format.rb +23 -11
  17. data/lib/scss_lint/linter/space_after_property_colon.rb +4 -4
  18. data/lib/scss_lint/linter/space_after_property_name.rb +16 -1
  19. data/lib/scss_lint/linter/space_before_brace.rb +36 -9
  20. data/lib/scss_lint/linter/trailing_semicolon.rb +6 -2
  21. data/lib/scss_lint/linter/vendor_prefixes.rb +64 -0
  22. data/lib/scss_lint/rake_task.rb +1 -0
  23. data/lib/scss_lint/runner.rb +2 -1
  24. data/lib/scss_lint/sass/script.rb +10 -0
  25. data/lib/scss_lint/version.rb +1 -1
  26. data/spec/scss_lint/cli_spec.rb +45 -2
  27. data/spec/scss_lint/linter/bang_format_spec.rb +79 -0
  28. data/spec/scss_lint/linter/declaration_order_spec.rb +466 -0
  29. data/spec/scss_lint/linter/import_path_spec.rb +300 -0
  30. data/spec/scss_lint/linter/nesting_depth_spec.rb +114 -0
  31. data/spec/scss_lint/linter/property_spelling_spec.rb +27 -0
  32. data/spec/scss_lint/linter/qualifying_element_spec.rb +125 -0
  33. data/spec/scss_lint/linter/selector_format_spec.rb +329 -0
  34. data/spec/scss_lint/linter/space_after_property_colon_spec.rb +14 -0
  35. data/spec/scss_lint/linter/space_after_property_name_spec.rb +14 -0
  36. data/spec/scss_lint/linter/space_before_brace_spec.rb +401 -17
  37. data/spec/scss_lint/linter/trailing_semicolon_spec.rb +47 -0
  38. data/spec/scss_lint/linter/vendor_prefixes_spec.rb +350 -0
  39. metadata +19 -2
@@ -0,0 +1,62 @@
1
+ module SCSSLint
2
+ # Checks formatting of the basenames of @imported partials
3
+ class Linter::ImportPath < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_import(node)
7
+ # Ignore CSS imports
8
+ return if File.extname(node.imported_filename) == '.css'
9
+ basename = File.basename(node.imported_filename)
10
+ return if underscore_ok?(basename) && extension_ok?(basename)
11
+ add_lint(node, compose_message(node.imported_filename))
12
+ end
13
+
14
+ private
15
+
16
+ # Checks if the presence or absence of a leading underscore
17
+ # on a string is ok, given config option.
18
+ #
19
+ # @param str [String] the string to check
20
+ # @return [Boolean]
21
+ def underscore_ok?(str)
22
+ underscore_exists = str.start_with?('_')
23
+ config['leading_underscore'] ? underscore_exists : !underscore_exists
24
+ end
25
+
26
+ # Checks if the presence or absence of an `scss` filename
27
+ # extension on a string is ok, given config option.
28
+ #
29
+ # @param str [String] the string to check
30
+ # @return [Boolean]
31
+ def extension_ok?(str)
32
+ extension_exists = str.end_with?('.scss')
33
+ config['filename_extension'] ? extension_exists : !extension_exists
34
+ end
35
+
36
+ # Composes a helpful lint message based on the original filename
37
+ # and the config options.
38
+ #
39
+ # @param orig_filename [String] the original filename
40
+ # @return [String] the helpful lint message
41
+ def compose_message(orig_filename)
42
+ orig_basename = File.basename(orig_filename)
43
+ fixed_basename = orig_basename
44
+
45
+ if config['leading_underscore']
46
+ fixed_basename = '_' + fixed_basename unless fixed_basename.match(/^_/)
47
+ else
48
+ fixed_basename = fixed_basename.sub(/^_/, '')
49
+ end
50
+
51
+ if config['filename_extension']
52
+ fixed_basename += '.scss' unless fixed_basename.match(/\.scss$/)
53
+ else
54
+ fixed_basename = fixed_basename.sub(/\.scss$/, '')
55
+ end
56
+
57
+ fixed_filename = orig_filename.sub(/(.*)#{Regexp.quote(orig_basename)}/,
58
+ "\\1#{fixed_basename}")
59
+ "Imported partial `#{orig_filename}` should be written as `#{fixed_filename}`"
60
+ end
61
+ end
62
+ end
@@ -60,7 +60,7 @@ module SCSSLint
60
60
 
61
61
  CONVENTIONS = {
62
62
  'hyphenated_lowercase' => {
63
- explanation: 'in lowercase with hyphens instead of underscores',
63
+ explanation: 'in all lowercase letters with hyphens instead of underscores',
64
64
  validator: ->(name) { name !~ /[_A-Z]/ },
65
65
  },
66
66
  'BEM' => {
@@ -0,0 +1,24 @@
1
+ module SCSSLint
2
+ # Checks for rule sets nested deeper than a specified maximum depth.
3
+ class Linter::NestingDepth < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ @max_depth = config['max_depth']
8
+ @depth = 1
9
+ yield # Continue linting children
10
+ end
11
+
12
+ def visit_rule(node)
13
+ if @depth > @max_depth
14
+ add_lint(node, "Nesting should be no greater than #{@max_depth}, but was #{@depth}")
15
+ else
16
+ # Only continue if we didn't exceed the max depth already (this makes
17
+ # the lint less noisy)
18
+ @depth += 1
19
+ yield # Continue linting children
20
+ @depth -= 1
21
+ end
22
+ end
23
+ end
24
+ end
@@ -26,7 +26,7 @@ module SCSSLint
26
26
  sorted_props.each_with_index do |prop, index|
27
27
  next unless prop != sortable_prop_info[index]
28
28
 
29
- add_lint(sortable_props[index], lint_message)
29
+ add_lint(sortable_props[index], lint_message(sorted_props))
30
30
  break
31
31
  end
32
32
 
@@ -127,16 +127,9 @@ module SCSSLint
127
127
  config['order'].is_a?(String)
128
128
  end
129
129
 
130
- def lint_message
131
- if preset_order?
132
- "Properties should be sorted according to the #{config['order']} sort order"
133
- elsif @preferred_order
134
- 'Properties should be sorted according to the custom order ' \
135
- 'specified by the configuration'
136
- else
137
- 'Properties should be sorted in order, with vendor-prefixed ' \
138
- 'extensions before the standardized CSS property'
139
- end
130
+ def lint_message(sortable_prop_info)
131
+ props = sortable_prop_info.map { |prop| prop[:name] }.join(', ')
132
+ "Properties should be ordered #{props}"
140
133
  end
141
134
  end
142
135
  end
@@ -3,6 +3,11 @@ module SCSSLint
3
3
  class Linter::PropertySpelling < Linter
4
4
  include LinterRegistry
5
5
 
6
+ KNOWN_PROPERTIES = File.open(File.join(SCSS_LINT_DATA, 'properties.txt'))
7
+ .read
8
+ .split
9
+ .to_set
10
+
6
11
  def visit_root(_node)
7
12
  @extra_properties = config['extra_properties'].to_set
8
13
  yield # Continue linting children
@@ -12,7 +17,26 @@ module SCSSLint
12
17
  # Ignore properties with interpolation
13
18
  return if node.name.count > 1 || !node.name.first.is_a?(String)
14
19
 
15
- name = node.name.join
20
+ nested_properties = node.children.select { |child| child.is_a?(Sass::Tree::PropNode) }
21
+ if nested_properties.any?
22
+ # Treat nested properties specially, as they are a concatenation of the
23
+ # parent with child property
24
+ nested_properties.each do |nested_prop|
25
+ check_property(nested_prop, node.name.join)
26
+ end
27
+ else
28
+ check_property(node)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ 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
+
38
+ name = prefix ? "#{prefix}-" : ''
39
+ name += node.name.join
16
40
 
17
41
  # Ignore vendor-prefixed properties
18
42
  return if name.start_with?('-')
@@ -21,12 +45,5 @@ module SCSSLint
21
45
 
22
46
  add_lint(node, "Unknown property #{name}")
23
47
  end
24
-
25
- private
26
-
27
- KNOWN_PROPERTIES = File.open(File.join(SCSS_LINT_DATA, 'properties.txt'))
28
- .read
29
- .split
30
- .to_set
31
48
  end
32
49
  end
@@ -0,0 +1,42 @@
1
+ module SCSSLint
2
+ # Checks for element selectors qualifying id, classe, or attribute selectors.
3
+ class Linter::QualifyingElement < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_simple_sequence(seq)
7
+ return unless seq_contains_sel_class?(seq, Sass::Selector::Element)
8
+ check_id(seq) unless config['allow_element_with_id']
9
+ check_class(seq) unless config['allow_element_with_class']
10
+ check_attribute(seq) unless config['allow_element_with_attribute']
11
+ end
12
+
13
+ private
14
+
15
+ # Checks if a simple sequence contains a
16
+ # simple selector of a certain class.
17
+ #
18
+ # @param seq [Sass::Selector::SimpleSequence]
19
+ # @param selector_class [Sass::Selector::Simple]
20
+ # @returns [Boolean]
21
+ def seq_contains_sel_class?(seq, selector_class)
22
+ seq.members.any? do |simple|
23
+ simple.is_a?(selector_class)
24
+ end
25
+ end
26
+
27
+ def check_id(seq)
28
+ return unless seq_contains_sel_class?(seq, Sass::Selector::Id)
29
+ add_lint(seq.line, 'Avoid qualifying id selectors with an element.')
30
+ end
31
+
32
+ def check_class(seq)
33
+ return unless seq_contains_sel_class?(seq, Sass::Selector::Class)
34
+ add_lint(seq.line, 'Avoid qualifying class selectors with an element.')
35
+ end
36
+
37
+ def check_attribute(seq)
38
+ return unless seq_contains_sel_class?(seq, Sass::Selector::Attribute)
39
+ add_lint(seq.line, 'Avoid qualifying attribute selectors with an element.')
40
+ end
41
+ end
42
+ end
@@ -10,36 +10,36 @@ module SCSSLint
10
10
  end
11
11
 
12
12
  def visit_attribute(attribute)
13
- check(attribute) unless @ignored_types.include?('attribute')
13
+ check(attribute, 'attribute') unless @ignored_types.include?('attribute')
14
14
  end
15
15
 
16
16
  def visit_class(klass)
17
- check(klass) unless @ignored_types.include?('class')
17
+ check(klass, 'class') unless @ignored_types.include?('class')
18
18
  end
19
19
 
20
20
  def visit_element(element)
21
- check(element) unless @ignored_types.include?('element')
21
+ check(element, 'element') unless @ignored_types.include?('element')
22
22
  end
23
23
 
24
24
  def visit_id(id)
25
- check(id) unless @ignored_types.include?('id')
25
+ check(id, 'id') unless @ignored_types.include?('id')
26
26
  end
27
27
 
28
28
  def visit_placeholder(placeholder)
29
- check(placeholder) unless @ignored_types.include?('placeholder')
29
+ check(placeholder, 'placeholder') unless @ignored_types.include?('placeholder')
30
30
  end
31
31
 
32
32
  def visit_pseudo(pseudo)
33
- check(pseudo) unless @ignored_types.include?('pseudo-selector')
33
+ check(pseudo, 'pseudo') unless @ignored_types.include?('pseudo-selector')
34
34
  end
35
35
 
36
36
  private
37
37
 
38
- def check(node)
38
+ def check(node, type)
39
39
  name = node.name
40
40
 
41
41
  return if @ignored_names.include?(name)
42
- return unless violation = violated_convention(name)
42
+ return unless violation = violated_convention(name, type)
43
43
 
44
44
  add_lint(node, "Selector `#{name}` should be " \
45
45
  "written #{violation[:explanation]}")
@@ -58,15 +58,27 @@ module SCSSLint
58
58
  explanation: 'has no spaces with capitalized words except first',
59
59
  validator: ->(name) { name =~ /^[a-z][a-zA-Z0-9]*$/ },
60
60
  },
61
+ 'hyphenated_BEM' => {
62
+ explanation: 'in hyphenated BEM (Block Element Modifier) format',
63
+ validator: ->(name) { name !~ /[A-Z]|-{3}|_{3}|[^_]_[^_]/ },
64
+ },
61
65
  'BEM' => {
62
66
  explanation: 'in BEM (Block Element Modifier) format',
63
- validator: ->(name) { name !~ /[A-Z]|-{3}|_{3}|[^_]_[^_]/ },
67
+ validator: lambda do |name|
68
+ name =~ /
69
+ ^[a-z]([-]?[a-z0-9]+)*
70
+ (__[a-z0-9]([-]?[a-z0-9]+)*)?
71
+ ((_[a-z0-9]([-]?[a-z0-9]+)*){2})?$
72
+ /x
73
+ end,
64
74
  },
65
75
  }
66
76
 
67
77
  # Checks the given name and returns the violated convention if it failed.
68
- def violated_convention(name_string)
69
- convention_name = config['convention'] || 'hyphenated_lowercase'
78
+ def violated_convention(name_string, type)
79
+ convention_name = config["#{type}_convention"] ||
80
+ config['convention'] ||
81
+ 'hyphenated_lowercase'
70
82
 
71
83
  convention = CONVENTIONS[convention_name] || {
72
84
  explanation: "must match regex /#{convention_name}/",
@@ -65,13 +65,13 @@ module SCSSLint
65
65
  spaces = 0
66
66
  offset = 1
67
67
 
68
- # Handle quirk where Sass parser doesn't include colon in source range
69
- # when property name is followed by spaces
70
- if character_at(node.name_source_range.end_pos, offset) == ':'
68
+ # Find the colon after the property name
69
+ while character_at(node.name_source_range.start_pos, offset - 1) != ':'
71
70
  offset += 1
72
71
  end
73
72
 
74
- while character_at(node.name_source_range.end_pos, offset) == ' '
73
+ # Count spaces after the colon
74
+ while character_at(node.name_source_range.start_pos, offset) == ' '
75
75
  spaces += 1
76
76
  offset += 1
77
77
  end
@@ -5,8 +5,23 @@ module SCSSLint
5
5
  include LinterRegistry
6
6
 
7
7
  def visit_prop(node)
8
- return unless character_at(node.name_source_range.end_pos) != ':'
8
+ offset = property_name_colon_offset(node)
9
+ return unless character_at(node.name_source_range.start_pos, offset - 1) == ' '
9
10
  add_lint node, 'Property name should be immediately followed by a colon'
10
11
  end
12
+
13
+ private
14
+
15
+ # Deals with a weird Sass bug where the name_source_range of a PropNode does
16
+ # not start at the beginning of the property name.
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
25
+ end
11
26
  end
12
27
  end
@@ -27,19 +27,46 @@ module SCSSLint
27
27
 
28
28
  def check_for_space(node, string)
29
29
  line = node.source_range.end_pos.line
30
- char_before_is_whitespace = ["\n", ' '].include?(string[-2])
31
30
 
32
31
  if config['allow_single_line_padding'] && node_on_single_line?(node)
33
- unless char_before_is_whitespace
34
- add_lint(line, 'Opening curly brace `{` should be ' \
35
- 'preceded by at least one space')
36
- end
32
+ return unless string[-2] != ' '
33
+ add_lint(line, 'Opening curly brace in a single line rule set '\
34
+ '`{` should be preceded by at least one space')
37
35
  else
38
- if !char_before_is_whitespace || string[-3] == ' '
39
- add_lint(line, 'Opening curly brace `{` should be ' \
40
- 'preceded by one space')
41
- end
36
+ return unless chars_before_incorrect(string)
37
+ style_message = (config['style'] == 'new_line') ? 'a new line' : 'one space'
38
+ add_lint(line, 'Opening curly brace `{` should be ' \
39
+ "preceded by #{style_message}")
42
40
  end
43
41
  end
42
+
43
+ # Check if the characters before the end of the string
44
+ # are not what they should be
45
+ def chars_before_incorrect(string)
46
+ if config['style'] != 'new_line'
47
+ return !single_space_before(string)
48
+ end
49
+ !newline_before_nonwhitespace(string)
50
+ end
51
+
52
+ # Check if there is one space and only one
53
+ # space before the end of the string
54
+ def single_space_before(string)
55
+ return false if string[-2] != ' '
56
+ return false if string[-3] == ' '
57
+ true
58
+ end
59
+
60
+ # Check if, starting from the end of a string
61
+ # and moving backwards, towards the beginning,
62
+ # we find a new line before any non-whitespace characters
63
+ def newline_before_nonwhitespace(string)
64
+ offset = -2
65
+ while /\S/.match(string[offset]).nil?
66
+ return true if string[offset] == "\n"
67
+ offset -= 1
68
+ end
69
+ false
70
+ end
44
71
  end
45
72
  end
@@ -27,6 +27,10 @@ module SCSSLint
27
27
  end
28
28
  end
29
29
 
30
+ def visit_import(node)
31
+ check_semicolon(node)
32
+ end
33
+
30
34
  private
31
35
 
32
36
  def check_semicolon(node)
@@ -46,7 +50,7 @@ module SCSSLint
46
50
 
47
51
  # Checks that the node is ended by a semicolon (with no whitespace)
48
52
  def ends_with_semicolon?(node)
49
- source_from_range(node.source_range) =~ /;$/
53
+ source_from_range(node.source_range) =~ /;(\s*})?$/
50
54
  end
51
55
 
52
56
  def ends_with_multiple_semicolons?(node)
@@ -55,7 +59,7 @@ module SCSSLint
55
59
  end
56
60
 
57
61
  def has_space_before_semicolon?(node)
58
- source_from_range(node.source_range) =~ /\s;$/
62
+ source_from_range(node.source_range) =~ /\s;(\s*})?$/
59
63
  end
60
64
  end
61
65
  end
@@ -0,0 +1,64 @@
1
+ module SCSSLint
2
+ # Checks for vendor prefixes.
3
+ class Linter::VendorPrefixes < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ @identifiers = Set.new(extract_identifiers_from_config)
8
+ @identifiers.merge(Set.new(config['include']))
9
+ @exclusions = Set.new(config['exclude'])
10
+ yield
11
+ end
12
+
13
+ def check_node(node)
14
+ name = node.name.is_a?(Array) ? node.name.join : node.name
15
+ # Ignore '@' from @keyframes node name
16
+ check_identifier(node, name.gsub(/^@/, ''))
17
+
18
+ # Check for values
19
+ return unless node.respond_to?(:value) && node.value.respond_to?(:to_sass)
20
+ check_identifier(node, node.value.to_sass)
21
+ end
22
+
23
+ alias_method :visit_prop, :check_node
24
+ alias_method :visit_pseudo, :check_node
25
+ alias_method :visit_directive, :check_node
26
+
27
+ private
28
+
29
+ def check_identifier(node, identifier)
30
+ return unless identifier =~ /^[_-]/
31
+
32
+ # Strip vendor prefix to check against identifiers.
33
+ # (Also strip closing parentheticals from values like linear-gradient.)
34
+ stripped_identifier = identifier.gsub(/(^[_-][a-zA-Z0-9_]+-|\(.*\))/, '')
35
+ return if @exclusions.include?(stripped_identifier)
36
+ return unless @identifiers.include?(stripped_identifier)
37
+
38
+ add_lint(node, 'Avoid vendor prefixes.')
39
+ end
40
+
41
+ def extract_identifiers_from_config
42
+ case config['identifier_list']
43
+ when nil
44
+ nil
45
+ when Array
46
+ config['identifier_list']
47
+ when String
48
+ begin
49
+ file = File.open(File.join(SCSS_LINT_DATA,
50
+ 'prefixed-identifiers',
51
+ "#{config['identifier_list']}.txt"))
52
+ file.read.split("\n").reject { |line| line =~ /^(#|\s*$)/ }
53
+ rescue Errno::ENOENT
54
+ raise SCSSLint::Exceptions::LinterError,
55
+ "Identifier list '#{config['identifier_list']}' does not exist"
56
+ end
57
+ else
58
+ raise SCSSLint::Exceptions::LinterError,
59
+ 'Invalid identifier list specified -- must be the name of a '\
60
+ 'preset or an array of strings'
61
+ end
62
+ end
63
+ end
64
+ end