scss-lint 0.29.0 → 0.30.0

Sign up to get free protection for your applications and to get access to all the features.
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