scss-lint 0.23.1 → 0.24.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +13 -4
  3. data/lib/scss_lint/cli.rb +18 -0
  4. data/lib/scss_lint/config.rb +4 -5
  5. data/lib/scss_lint/lint.rb +4 -2
  6. data/lib/scss_lint/linter.rb +22 -12
  7. data/lib/scss_lint/linter/border_zero.rb +2 -3
  8. data/lib/scss_lint/linter/capitalization_in_selector.rb +5 -5
  9. data/lib/scss_lint/linter/color_keyword.rb +3 -4
  10. data/lib/scss_lint/linter/compass/property_with_mixin.rb +17 -10
  11. data/lib/scss_lint/linter/empty_line_between_blocks.rb +4 -5
  12. data/lib/scss_lint/linter/final_newline.rb +1 -1
  13. data/lib/scss_lint/linter/hex_length.rb +56 -0
  14. data/lib/scss_lint/linter/hex_notation.rb +38 -0
  15. data/lib/scss_lint/linter/hex_validation.rb +23 -0
  16. data/lib/scss_lint/linter/id_with_extraneous_selector.rb +5 -6
  17. data/lib/scss_lint/linter/indentation.rb +8 -7
  18. data/lib/scss_lint/linter/leading_zero.rb +5 -9
  19. data/lib/scss_lint/linter/mergeable_selector.rb +59 -0
  20. data/lib/scss_lint/linter/name_format.rb +4 -4
  21. data/lib/scss_lint/linter/placeholder_in_extend.rb +3 -4
  22. data/lib/scss_lint/linter/property_sort_order.rb +4 -4
  23. data/lib/scss_lint/linter/property_spelling.rb +4 -4
  24. data/lib/scss_lint/linter/selector_depth.rb +1 -1
  25. data/lib/scss_lint/linter/shorthand.rb +10 -12
  26. data/lib/scss_lint/linter/single_line_per_selector.rb +1 -1
  27. data/lib/scss_lint/linter/space_after_comma.rb +2 -3
  28. data/lib/scss_lint/linter/space_after_property_name.rb +2 -3
  29. data/lib/scss_lint/linter/space_between_parens.rb +8 -9
  30. data/lib/scss_lint/linter/string_quotes.rb +2 -3
  31. data/lib/scss_lint/linter/unnecessary_mantissa.rb +5 -5
  32. data/lib/scss_lint/linter/url_quotes.rb +2 -3
  33. data/lib/scss_lint/linter/zero_unit.rb +11 -9
  34. data/lib/scss_lint/reporter.rb +4 -0
  35. data/lib/scss_lint/reporter/config_reporter.rb +26 -0
  36. data/lib/scss_lint/reporter/default_reporter.rb +7 -7
  37. data/lib/scss_lint/reporter/xml_reporter.rb +4 -4
  38. data/lib/scss_lint/runner.rb +2 -2
  39. data/lib/scss_lint/sass/tree.rb +1 -1
  40. data/lib/scss_lint/utils.rb +0 -11
  41. data/lib/scss_lint/version.rb +1 -1
  42. metadata +11 -8
  43. data/lib/scss_lint/linter/duplicate_root.rb +0 -34
  44. data/lib/scss_lint/linter/hex_format.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2d1a01eec7c1dd954e81749c6a668a660013b228
4
- data.tar.gz: 05988d04580327776387a94171c4297f6a7493d1
3
+ metadata.gz: 17fa291ee89e19fe0f6103299f2615c7e3a83633
4
+ data.tar.gz: e52fcd5476a7f944884cb7d8714649dc51ea4e89
5
5
  SHA512:
6
- metadata.gz: f82ac7a86ac50b2f172f0bb08879ec3fef8fc20f6fbdbf35816ea6cab6feae0963062da65ae550a86bf7106d3e44e755c3d510baf81a2ed43f779b3248d0ba2e
7
- data.tar.gz: a47bd582bb1208992367eb5f7f3f61fde36a9d308f8be3745532333a08c70d0e04f90a14adb00e71e409ee3ea6547b46bb1d9d27909e345fe332f3082fe67474
6
+ metadata.gz: 19b28041ce7fa7c14ff869de3be4193543a9c1e43285c2f150e23dc22cdaac3aeaebf4764803a61880646fb4baf5e5d9a5d78e6a970956b0eeae7763f14c0d8e
7
+ data.tar.gz: b87d1c50bbd9805083d687de07e8eaafbd90e931c85171a58fa6ca09cc344d57da30b6fd85ab55d928b633ff4cf7c4c680a2b5671c310c79f7f9cec03e6181c6
@@ -21,9 +21,6 @@ linters:
21
21
  DuplicateProperty:
22
22
  enabled: true
23
23
 
24
- DuplicateRoot:
25
- enabled: true
26
-
27
24
  EmptyLineBetweenBlocks:
28
25
  enabled: true
29
26
  ignore_single_line_blocks: true
@@ -35,7 +32,15 @@ linters:
35
32
  enabled: true
36
33
  present: true
37
34
 
38
- HexFormat:
35
+ HexLength:
36
+ enabled: true
37
+ style: short # or 'long'
38
+
39
+ HexNotation:
40
+ enabled: true
41
+ style: lowercase # or 'uppercase'
42
+
43
+ HexValidation:
39
44
  enabled: true
40
45
 
41
46
  IdWithExtraneousSelector:
@@ -49,6 +54,10 @@ linters:
49
54
  enabled: true
50
55
  style: exclude_zero # or 'include_zero'
51
56
 
57
+ MergeableSelector:
58
+ enabled: true
59
+ force_nesting: true
60
+
52
61
  NameFormat:
53
62
  enabled: true
54
63
  convention: hyphenated_lowercase # or 'BEM', or a regex pattern
@@ -65,6 +65,10 @@ module SCSSLint
65
65
  define_output_format(format)
66
66
  end
67
67
 
68
+ opts.on_tail('--show-formatters', 'Shows available formatters') do
69
+ print_formatters
70
+ end
71
+
68
72
  opts.on('-i', '--include-linter linter,...', Array,
69
73
  'Specify which linters you want to run') do |linters|
70
74
  @options[:included_linters] = linters
@@ -196,6 +200,20 @@ module SCSSLint
196
200
  halt :config
197
201
  end
198
202
 
203
+ def print_formatters
204
+ puts 'Installed formatters:'
205
+
206
+ reporter_names = SCSSLint::Reporter.descendants.map do |reporter|
207
+ reporter.name.split('::').last.split('Reporter').first
208
+ end
209
+
210
+ reporter_names.sort.each do |reporter_name|
211
+ puts " - #{reporter_name}"
212
+ end
213
+
214
+ halt
215
+ end
216
+
199
217
  def print_linters
200
218
  puts 'Installed linters:'
201
219
 
@@ -128,11 +128,10 @@ module SCSSLint
128
128
 
129
129
  LinterRegistry.linters.each do |linter_class|
130
130
  name = linter_name(linter_class)
131
+ next unless name.match(class_name_regex)
131
132
 
132
- if name.match(class_name_regex)
133
- old_options = options['linters'].fetch(name, {})
134
- options['linters'][name] = smart_merge(old_options, wildcard_options)
135
- end
133
+ old_options = options['linters'].fetch(name, {})
134
+ options['linters'][name] = smart_merge(old_options, wildcard_options)
136
135
  end
137
136
  end
138
137
 
@@ -187,7 +186,7 @@ module SCSSLint
187
186
 
188
187
  # Merge two hashes, concatenating lists and further merging nested hashes.
189
188
  def smart_merge(parent, child)
190
- parent.merge(child) do |key, old, new|
189
+ parent.merge(child) do |_key, old, new|
191
190
  case old
192
191
  when Array
193
192
  old + new
@@ -1,13 +1,15 @@
1
1
  module SCSSLint
2
2
  # Stores information about a single problem that was detected by a [Linter].
3
3
  class Lint
4
- attr_reader :filename, :location, :description, :severity
4
+ attr_reader :linter, :filename, :location, :description, :severity
5
5
 
6
+ # @param linter [SCSSLint::Linter]
6
7
  # @param filename [String]
7
8
  # @param location [SCSSLint::Location]
8
9
  # @param description [String]
9
10
  # @param severity [Symbol]
10
- def initialize(filename, location, description, severity = :warning)
11
+ def initialize(linter, filename, location, description, severity = :warning)
12
+ @linter = linter
11
13
  @filename = filename
12
14
  @location = location
13
15
  @description = description
@@ -18,21 +18,16 @@ module SCSSLint
18
18
  visit(engine.tree)
19
19
  end
20
20
 
21
+ protected
22
+
21
23
  # Helper for creating lint from a parse tree node
22
24
  #
23
- # @param node_or_line [Sass::Script::Tree::Node, Sass::Engine::Line]
25
+ # @param node_or_line_or_location [Sass::Script::Tree::Node, Fixnum, SCSSLint::Location]
24
26
  # @param message [String]
25
- def add_lint(node_or_line, message)
26
- location = if node_or_line.respond_to?(:source_range) && node_or_line.source_range
27
- location_from_range(node_or_line.source_range)
28
- elsif node_or_line.respond_to?(:line)
29
- Location.new(node_or_line.line)
30
- else
31
- Location.new(node_or_line)
32
- end
33
-
34
- @lints << Lint.new(engine.filename,
35
- location,
27
+ def add_lint(node_or_line_or_location, message)
28
+ @lints << Lint.new(self,
29
+ engine.filename,
30
+ extract_location(node_or_line_or_location),
36
31
  message)
37
32
  end
38
33
 
@@ -130,5 +125,20 @@ module SCSSLint
130
125
  visit(child)
131
126
  end
132
127
  end
128
+
129
+ private
130
+
131
+ def extract_location(node_or_line_or_location)
132
+ if node_or_line_or_location.is_a?(Location)
133
+ node_or_line_or_location
134
+ elsif node_or_line_or_location.respond_to?(:source_range) &&
135
+ node_or_line_or_location.source_range
136
+ location_from_range(node_or_line_or_location.source_range)
137
+ elsif node_or_line_or_location.respond_to?(:line)
138
+ Location.new(node_or_line_or_location.line)
139
+ else
140
+ Location.new(node_or_line_or_location)
141
+ end
142
+ end
133
143
  end
134
144
  end
@@ -5,10 +5,9 @@ module SCSSLint
5
5
 
6
6
  def visit_prop(node)
7
7
  return unless BORDER_PROPERTIES.include?(node.name.first.to_s)
8
+ return unless node.value.to_sass.strip == 'none'
8
9
 
9
- if node.value.to_sass.strip == 'none'
10
- add_lint(node, '`border: 0;` is preferred over `border: none;`')
11
- end
10
+ add_lint(node, '`border: 0;` is preferred over `border: none;`')
12
11
  end
13
12
 
14
13
  private
@@ -31,11 +31,11 @@ module SCSSLint
31
31
 
32
32
  def check(node, selector_name = nil)
33
33
  name = node.name.join
34
- if name =~ /[A-Z]/
35
- selector_name ||= node.class.name.split('::').last
36
- add_lint(node, "#{selector_name} `#{name}` in selector should be " \
37
- "written in all lowercase as `#{name.downcase}`")
38
- end
34
+ return unless name =~ /[A-Z]/
35
+
36
+ selector_name ||= node.class.name.split('::').last
37
+ add_lint(node, "#{selector_name} `#{name}` in selector should be " \
38
+ "written in all lowercase as `#{name.downcase}`")
39
39
  end
40
40
  end
41
41
  end
@@ -4,9 +4,10 @@ module SCSSLint
4
4
  class Linter::ColorKeyword < Linter
5
5
  include LinterRegistry
6
6
 
7
+ COLOR_REGEX = /(#?[a-f0-9]{3,6}|[a-z]+)/i
8
+
7
9
  def visit_script_color(node)
8
10
  color = source_from_range(node.source_range)[COLOR_REGEX, 1]
9
-
10
11
  add_color_lint(node, color) if color_keyword?(color)
11
12
  end
12
13
 
@@ -20,8 +21,6 @@ module SCSSLint
20
21
 
21
22
  private
22
23
 
23
- COLOR_REGEX = /(#?[a-f0-9]{3,6}|[a-z]+)/i
24
-
25
24
  def add_color_lint(node, original)
26
25
  hex_form = Sass::Script::Value::Color.new(color_rgb(original)).tap do |color|
27
26
  color.options = {} # `inspect` requires options to be set
@@ -29,7 +28,7 @@ module SCSSLint
29
28
 
30
29
  add_lint(node,
31
30
  "Color `#{original}` should be written in hexadecimal form " \
32
- "as `#{shortest_hex_form(hex_form)}`")
31
+ "as `#{hex_form}`")
33
32
  end
34
33
 
35
34
  def color_keyword?(string)
@@ -4,16 +4,8 @@ module SCSSLint
4
4
  include LinterRegistry
5
5
 
6
6
  def visit_prop(node)
7
- prop_name = node.name.join
8
-
9
- if PROPERTIES_WITH_MIXINS.include?(prop_name)
10
- add_lint node, "Use the Compass `#{prop_name}` mixin instead of the property"
11
- end
12
-
13
- if prop_name == 'display' && node.value.to_sass == 'inline-block'
14
- add_lint node,
15
- 'Use the Compass `inline-block` mixin instead of `display: inline-block`'
16
- end
7
+ check_for_properties_with_mixins(node)
8
+ check_for_inline_block(node)
17
9
  end
18
10
 
19
11
  private
@@ -29,5 +21,20 @@ module SCSSLint
29
21
  text-shadow
30
22
  transform
31
23
  ].to_set
24
+
25
+ def check_for_properties_with_mixins(node)
26
+ prop_name = node.name.join
27
+ return unless PROPERTIES_WITH_MIXINS.include?(prop_name)
28
+
29
+ add_lint node, "Use the Compass `#{prop_name}` mixin instead of the property"
30
+ end
31
+
32
+ def check_for_inline_block(node)
33
+ prop_name = node.name.join
34
+ return unless prop_name == 'display' && node.value.to_sass == 'inline-block'
35
+
36
+ add_lint node,
37
+ 'Use the Compass `inline-block` mixin instead of `display: inline-block`'
38
+ end
32
39
  end
33
40
  end
@@ -36,11 +36,10 @@ module SCSSLint
36
36
  end
37
37
 
38
38
  def check_following_node(node, type)
39
- if (following_node = next_node(node)) && (next_start_line = following_node.line)
40
- unless engine.lines[next_start_line - 2].strip.empty?
41
- add_lint(next_start_line - 1, MESSAGE_FORMAT % [type, 'followed'])
42
- end
43
- end
39
+ return unless (following_node = next_node(node)) && (next_start_line = following_node.line)
40
+ return if engine.lines[next_start_line - 2].strip.empty?
41
+
42
+ add_lint(next_start_line - 1, MESSAGE_FORMAT % [type, 'followed'])
44
43
  end
45
44
 
46
45
  # In cases where the previous node is not a block declaration, we won't
@@ -3,7 +3,7 @@ module SCSSLint
3
3
  class Linter::FinalNewline < Linter
4
4
  include LinterRegistry
5
5
 
6
- def visit_root(node)
6
+ def visit_root(_node)
7
7
  return if engine.lines.empty?
8
8
 
9
9
  ends_with_newline = engine.lines[-1][-1] == "\n"
@@ -0,0 +1,56 @@
1
+ module SCSSLint
2
+ # Checks that hexadecimal colors are written in the desired number of
3
+ # characters.
4
+ class Linter::HexLength < Linter
5
+ include LinterRegistry
6
+
7
+ HEX_REGEX = /(#(\h{3}|\h{6}))(?!\h)/
8
+
9
+ def visit_script_color(node)
10
+ return unless hex = source_from_range(node.source_range)[HEX_REGEX, 1]
11
+ check_hex(hex, node)
12
+ end
13
+
14
+ def visit_script_string(node)
15
+ return unless node.type == :identifier
16
+
17
+ node.value.scan(HEX_REGEX) do |match|
18
+ check_hex(match.first, node)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def check_hex(hex, node)
25
+ return if expected(hex) == hex
26
+
27
+ add_lint(node, "Color `#{hex}` should be written as `#{expected(hex)}`")
28
+ end
29
+
30
+ def expected(hex)
31
+ return short_hex_form(hex) if can_be_shorter?(hex) && short_style?
32
+ return long_hex_form(hex) if hex.length == 4 && !short_style?
33
+
34
+ hex
35
+ end
36
+
37
+ def can_be_shorter?(hex)
38
+ hex.length == 7 &&
39
+ hex[1] == hex[2] &&
40
+ hex[3] == hex[4] &&
41
+ hex[5] == hex[6]
42
+ end
43
+
44
+ def short_hex_form(hex)
45
+ [hex[0..1], hex[3], hex[5]].join
46
+ end
47
+
48
+ def long_hex_form(hex)
49
+ [hex[0..1], hex[1], hex[2], hex[2], hex[3], hex[3]].join
50
+ end
51
+
52
+ def short_style?
53
+ config['style'] == 'short'
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ module SCSSLint
2
+ # Checks if hexadecimal colors are written lowercase / uppercase.
3
+ class Linter::HexNotation < Linter
4
+ include LinterRegistry
5
+
6
+ HEX_REGEX = /(#(\h{3}|\h{6}))(?!\h)/
7
+
8
+ def visit_script_color(node)
9
+ return unless hex = source_from_range(node.source_range)[HEX_REGEX, 1]
10
+ check_hex(hex, node)
11
+ end
12
+
13
+ def visit_script_string(node)
14
+ return unless node.type == :identifier
15
+
16
+ node.value.scan(HEX_REGEX) do |match|
17
+ check_hex(match.first, node)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def check_hex(hex, node)
24
+ return if expected(hex) == hex
25
+
26
+ add_lint(node, "Color `#{hex}` should be written as `#{expected(hex)}`")
27
+ end
28
+
29
+ def expected(color)
30
+ return color.downcase if lowercase_style?
31
+ color.upcase
32
+ end
33
+
34
+ def lowercase_style?
35
+ config['style'] == 'lowercase'
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ module SCSSLint
2
+ # Checks for invalid hexadecimal colors.
3
+ class Linter::HexValidation < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_script_string(node)
7
+ return unless node.type == :identifier
8
+
9
+ node.value.scan(/(#\h+)/) do |match|
10
+ check_hex(match.first, node)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ HEX_REGEX = /(#(\h{3}|\h{6}))(?!\h)/
17
+
18
+ def check_hex(hex, node)
19
+ return if HEX_REGEX.match(hex)
20
+ add_lint(node, "Colors must have either three or six digits: `#{hex}`")
21
+ end
22
+ end
23
+ end
@@ -11,13 +11,12 @@ module SCSSLint
11
11
  !simple.is_a?(Sass::Selector::Id) &&
12
12
  !simple.is_a?(Sass::Selector::Pseudo)
13
13
  end
14
+ return unless can_be_simplified
14
15
 
15
- if can_be_simplified
16
- # TODO: Sass::Selector::SimpleSequence#source_range sometimes lies about
17
- # its line, so reference `#line` directly
18
- add_lint(seq.line, "Selector `#{seq}` can be simplified to `#{id_sel}`, " \
19
- 'since IDs should be uniquely identifying')
20
- end
16
+ # TODO: Sass::Selector::SimpleSequence#source_range sometimes lies about
17
+ # its line, so reference `#line` directly
18
+ add_lint(seq.line, "Selector `#{seq}` can be simplified to `#{id_sel}`, " \
19
+ 'since IDs should be uniquely identifying')
21
20
  end
22
21
  end
23
22
  end
@@ -3,7 +3,7 @@ module SCSSLint
3
3
  class Linter::Indentation < Linter
4
4
  include LinterRegistry
5
5
 
6
- def visit_root(node)
6
+ def visit_root(_node)
7
7
  @indent_width = config['width']
8
8
  @indent = 0
9
9
  yield
@@ -29,12 +29,12 @@ module SCSSLint
29
29
 
30
30
  actual_indent = engine.lines[node.line - 1][/^(\s*)/, 1]
31
31
 
32
- if actual_indent.length != @indent
33
- add_lint(node.line,
34
- "Line should be indented #{@indent} spaces, " \
35
- "but was indented #{actual_indent.length} spaces")
36
- return true
37
- end
32
+ return if actual_indent.length == @indent
33
+
34
+ add_lint(node.line,
35
+ "Line should be indented #{@indent} spaces, " \
36
+ "but was indented #{actual_indent.length} spaces")
37
+ true
38
38
  end
39
39
 
40
40
  # Deal with `else` statements
@@ -44,6 +44,7 @@ module SCSSLint
44
44
  end
45
45
 
46
46
  # Define node types that increase indentation level
47
+ alias_method :visit_atroot, :check_and_visit_children
47
48
  alias_method :visit_directive, :check_and_visit_children
48
49
  alias_method :visit_each, :check_and_visit_children
49
50
  alias_method :visit_for, :check_and_visit_children
@@ -7,11 +7,9 @@ module SCSSLint
7
7
  return unless node.type == :identifier
8
8
 
9
9
  non_string_values = remove_quoted_strings(node.value).split
10
-
11
10
  non_string_values.each do |value|
12
- if number = value[FRACTIONAL_DIGIT_REGEX, 1]
13
- check_number(node, number)
14
- end
11
+ next unless number = value[FRACTIONAL_DIGIT_REGEX, 1]
12
+ check_number(node, number)
15
13
  end
16
14
  end
17
15
 
@@ -41,12 +39,10 @@ module SCSSLint
41
39
  def check_number(node, original_number)
42
40
  style = config.fetch('style', 'exclude_zero')
43
41
  convention = CONVENTIONS[style]
42
+ return if convention[:validator].call(original_number)
44
43
 
45
- unless convention[:validator].call(original_number)
46
- corrected = convention[:converter].call(original_number)
47
-
48
- add_lint(node, convention[:explanation] % [original_number, corrected])
49
- end
44
+ corrected = convention[:converter].call(original_number)
45
+ add_lint(node, convention[:explanation] % [original_number, corrected])
50
46
  end
51
47
  end
52
48
  end
@@ -0,0 +1,59 @@
1
+ module SCSSLint
2
+ # Checks for rule sets that can be merged with other rule sets.
3
+ class Linter::MergeableSelector < Linter
4
+ include LinterRegistry
5
+
6
+ def check_node(node)
7
+ node.children.each_with_object([]) do |child_node, seen_nodes|
8
+ next unless child_node.is_a?(Sass::Tree::RuleNode)
9
+
10
+ mergeable_node = find_mergeable_node(child_node, seen_nodes)
11
+ seen_nodes << child_node
12
+ next unless mergeable_node
13
+
14
+ add_lint child_node.line,
15
+ "Merge rule `#{node_rule(child_node)}` with rule " \
16
+ "on line #{mergeable_node.line}"
17
+ end
18
+
19
+ yield # Continue linting children
20
+ end
21
+
22
+ alias_method :visit_root, :check_node
23
+ alias_method :visit_rule, :check_node
24
+
25
+ private
26
+
27
+ def find_mergeable_node(node, seen_nodes)
28
+ seen_nodes.find do |seen_node|
29
+ equal?(node, seen_node) ||
30
+ (config['force_nesting'] && nested?(node, seen_node))
31
+ end
32
+ end
33
+
34
+ def equal?(node1, node2)
35
+ node_rule(node1) == node_rule(node2)
36
+ end
37
+
38
+ def nested?(node1, node2)
39
+ return false unless single_rule?(node1) && single_rule?(node2)
40
+
41
+ rule1 = node_rule(node1)
42
+ rule2 = node_rule(node2)
43
+ subrule?(rule1, rule2) || subrule?(rule2, rule1)
44
+ end
45
+
46
+ def node_rule(node)
47
+ node.rule.join
48
+ end
49
+
50
+ def single_rule?(node)
51
+ node.parsed_rules.members.count == 1
52
+ end
53
+
54
+ def subrule?(rule1, rule2)
55
+ "#{rule1}".start_with?("#{rule2} ") ||
56
+ "#{rule1}".start_with?("#{rule2}.")
57
+ end
58
+ end
59
+ end
@@ -46,10 +46,10 @@ module SCSSLint
46
46
  ].to_set
47
47
 
48
48
  def check_name(node, node_type, node_text = node.name)
49
- if convention = violated_convention(node_text)
50
- add_lint(node, "Name of #{node_type} `#{node_text}` should be " \
51
- "written #{convention[:explanation]}")
52
- end
49
+ return unless violation = violated_convention(node_text)
50
+
51
+ add_lint(node, "Name of #{node_type} `#{node_text}` should be " \
52
+ "written #{violation[:explanation]}")
53
53
  end
54
54
 
55
55
  def check_placeholder(node)
@@ -7,11 +7,10 @@ module SCSSLint
7
7
  # The array returned by the parser is a bit awkward in that it splits on
8
8
  # every word boundary (so %placeholder becomes ['%', 'placeholder']).
9
9
  selector = node.selector.join
10
+ return if selector.start_with?('%')
10
11
 
11
- unless selector.start_with?('%')
12
- add_lint(node,
13
- 'Prefer using placeholder selectors (e.g. %some-placeholder) with @extend')
14
- end
12
+ add_lint(node, 'Prefer using placeholder selectors (e.g. ' \
13
+ '%some-placeholder) with @extend')
15
14
  end
16
15
  end
17
16
  end
@@ -21,10 +21,10 @@ module SCSSLint
21
21
  .sort { |a, b| compare_properties(a, b) }
22
22
 
23
23
  sorted_props.each_with_index do |prop, index|
24
- if prop != sortable_prop_info[index]
25
- add_lint(sortable_props[index], MESSAGE)
26
- break
27
- end
24
+ next unless prop != sortable_prop_info[index]
25
+
26
+ add_lint(sortable_props[index], MESSAGE)
27
+ break
28
28
  end
29
29
 
30
30
  yield # Continue linting children
@@ -3,7 +3,7 @@ module SCSSLint
3
3
  class Linter::PropertySpelling < Linter
4
4
  include LinterRegistry
5
5
 
6
- def visit_root(node)
6
+ def visit_root(_node)
7
7
  @extra_properties = config['extra_properties'].to_set
8
8
  yield # Continue linting children
9
9
  end
@@ -16,10 +16,10 @@ module SCSSLint
16
16
 
17
17
  # Ignore vendor-prefixed properties
18
18
  return if name.start_with?('-')
19
+ return if KNOWN_PROPERTIES.include?(name) ||
20
+ @extra_properties.include?(name)
19
21
 
20
- unless KNOWN_PROPERTIES.include?(name) || @extra_properties.include?(name)
21
- add_lint(node, "Unknown property #{name}")
22
- end
22
+ add_lint(node, "Unknown property #{name}")
23
23
  end
24
24
 
25
25
  private
@@ -3,7 +3,7 @@ module SCSSLint
3
3
  class Linter::SelectorDepth < Linter
4
4
  include LinterRegistry
5
5
 
6
- def visit_root(node)
6
+ def visit_root(_node)
7
7
  @max_depth = config['max_depth']
8
8
  @depth = 0
9
9
  yield # Continue
@@ -37,25 +37,23 @@ module SCSSLint
37
37
  # HACK: node_parent may not be initialized at this point, so we need to
38
38
  # set it ourselves
39
39
  value.node_parent = literal
40
+ return unless value.is_a?(Sass::Script::Value::String)
40
41
 
41
- if value.is_a?(Sass::Script::Value::String)
42
- check_script_string(prop, value)
43
- end
42
+ check_script_string(prop, value)
44
43
  end
45
44
 
46
- LIST_LITERAL_REGEX = %r{
45
+ LIST_LITERAL_REGEX = /
47
46
  \A
48
47
  (\S+\s+\S+(\s+\S+){0,2}) # Two to four values separated by spaces
49
48
  (\s+!\w+)? # Ignore `!important` priority overrides
50
49
  \z
51
- }x
50
+ /x
52
51
 
53
52
  def check_script_string(prop, script_string)
54
53
  return unless script_string.type == :identifier
54
+ return unless values = script_string.value.strip[LIST_LITERAL_REGEX, 1]
55
55
 
56
- if values = script_string.value.strip[LIST_LITERAL_REGEX, 1]
57
- check_shorthand(prop, script_string, values.split)
58
- end
56
+ check_shorthand(prop, script_string, values.split)
59
57
  end
60
58
 
61
59
  def check_shorthand(prop, node, values)
@@ -82,10 +80,10 @@ module SCSSLint
82
80
  end
83
81
 
84
82
  def can_condense_to_one_value(top, right, bottom, left)
85
- if top == right
86
- top == bottom && (bottom == left || left.nil?) ||
87
- bottom.nil? && left.nil?
88
- end
83
+ return unless top == right
84
+
85
+ top == bottom && (bottom == left || left.nil?) ||
86
+ bottom.nil? && left.nil?
89
87
  end
90
88
 
91
89
  def can_condense_to_two_values(top, right, bottom, left)
@@ -21,7 +21,7 @@ module SCSSLint
21
21
  # Sass::Script::Nodes, we need to condense it into a single string that we
22
22
  # can run a regex against.
23
23
  def condense_to_string(sequence_list)
24
- sequence_list.select { |item| item.is_a?(String) }.inject(:+)
24
+ sequence_list.select { |item| item.is_a?(String) }.inject(:+) || ''
25
25
  end
26
26
 
27
27
  # Removes extra spacing between lines in a comma-separated sequence due to
@@ -90,10 +90,9 @@ module SCSSLint
90
90
  spaces += 1
91
91
  offset += 1
92
92
  end
93
+ next if spaces == EXPECTED_SPACES_AFTER_COMMA
93
94
 
94
- if spaces != EXPECTED_SPACES_AFTER_COMMA
95
- add_lint arg, "Commas in #{arg_type} should be followed by a single space"
96
- end
95
+ add_lint arg, "Commas in #{arg_type} should be followed by a single space"
97
96
  end
98
97
  end
99
98
  end
@@ -5,9 +5,8 @@ module SCSSLint
5
5
  include LinterRegistry
6
6
 
7
7
  def visit_prop(node)
8
- if character_at(node.name_source_range.end_pos) != ':'
9
- add_lint node, 'Property name should be immediately followed by a colon'
10
- end
8
+ return unless character_at(node.name_source_range.end_pos) != ':'
9
+ add_lint node, 'Property name should be immediately followed by a colon'
11
10
  end
12
11
  end
13
12
  end
@@ -3,7 +3,7 @@ module SCSSLint
3
3
  class Linter::SpaceBetweenParens < Linter
4
4
  include LinterRegistry
5
5
 
6
- def visit_root(node)
6
+ def visit_root(_node)
7
7
  @spaces = config['spaces']
8
8
 
9
9
  engine.lines.each_with_index do |line, index|
@@ -15,22 +15,21 @@ module SCSSLint
15
15
  [ ]*\)
16
16
  )?
17
17
  /x) do |match|
18
- check(match[2], index, engine) if match[2]
18
+ check(match[2], index) if match[2]
19
19
  end
20
20
  end
21
21
  end
22
22
 
23
23
  private
24
24
 
25
- def check(str, index, engine)
25
+ def check(str, index)
26
26
  spaces = str.count ' '
27
+ return if spaces == @spaces
27
28
 
28
- if spaces != @spaces
29
- location = Location.new(index + 1)
30
- message = "Expected #{pluralize(@spaces, 'space')} " \
31
- "between parentheses instead of #{spaces}"
32
- @lints << Lint.new(engine.filename, location, message)
33
- end
29
+ location = Location.new(index + 1)
30
+ message = "Expected #{pluralize(@spaces, 'space')} " \
31
+ "between parentheses instead of #{spaces}"
32
+ add_lint(location, message)
34
33
  end
35
34
  end
36
35
  end
@@ -43,9 +43,8 @@ module SCSSLint
43
43
  }x
44
44
 
45
45
  def extract_string_without_quotes(source)
46
- if match = STRING_WITHOUT_QUOTES_REGEX.match(source)
47
- match[1]
48
- end
46
+ return unless match = STRING_WITHOUT_QUOTES_REGEX.match(source)
47
+ match[1]
49
48
  end
50
49
 
51
50
  def check_double_quotes(node, string)
@@ -16,22 +16,22 @@ module SCSSLint
16
16
 
17
17
  def visit_script_number(node)
18
18
  return unless match = REAL_NUMBER_REGEX.match(source_from_range(node.source_range))
19
+ return unless unnecessary_mantissa?(match[:mantissa])
19
20
 
20
- if unnecessary_mantissa?(match[:mantissa])
21
- add_lint(node, MESSAGE_FORMAT % [match[:number], match[:integer], match[:units]])
22
- end
21
+ add_lint(node, MESSAGE_FORMAT % [match[:number], match[:integer],
22
+ match[:units]])
23
23
  end
24
24
 
25
25
  private
26
26
 
27
- REAL_NUMBER_REGEX = %r{
27
+ REAL_NUMBER_REGEX = /
28
28
  \b(?<number>
29
29
  (?<integer>\d*)
30
30
  \.
31
31
  (?<mantissa>\d+)
32
32
  (?<units>\w*)
33
33
  )\b
34
- }ix
34
+ /ix
35
35
 
36
36
  MESSAGE_FORMAT = '`%s` should be written without the mantissa as `%s%s`'
37
37
 
@@ -18,9 +18,8 @@ module SCSSLint
18
18
  private
19
19
 
20
20
  def check(node, string)
21
- if string =~ /^\s*url\(\s*[^"']/
22
- add_lint(node, 'URLs should be enclosed in quotes')
23
- end
21
+ return unless string =~ /^\s*url\(\s*[^"']/
22
+ add_lint(node, 'URLs should be enclosed in quotes')
24
23
  end
25
24
  end
26
25
  end
@@ -7,31 +7,33 @@ module SCSSLint
7
7
  return unless node.type == :identifier
8
8
 
9
9
  node.value.scan(ZERO_UNIT_REGEX) do |match|
10
+ next unless zero_with_length_units?(match.first)
10
11
  add_lint(node, MESSAGE_FORMAT % match.first)
11
12
  end
12
13
  end
13
14
 
14
15
  def visit_script_number(node)
15
16
  length = source_from_range(node.source_range)[ZERO_UNIT_REGEX, 1]
17
+ return unless zero_with_length_units?(length)
16
18
 
17
- if node.value == 0 && zero_with_units?(length)
18
- add_lint(node, MESSAGE_FORMAT % length)
19
- end
19
+ add_lint(node, MESSAGE_FORMAT % length)
20
20
  end
21
21
 
22
22
  private
23
23
 
24
- ZERO_UNIT_REGEX = %r{
24
+ ZERO_UNIT_REGEX = /
25
25
  \b
26
- (?<!\.|\#) # Ignore zeroes following `#` or `.` (colors / decimals)
27
- (0[a-z]+) # Zero followed by letters (indicating some sort of unit)
26
+ (?<!\.|\#) # Ignore zeroes following `#` (colors) or `.` (decimals)
27
+ (0[a-z]+) # Zero followed by letters indicating some sort of unit
28
28
  \b
29
- }ix
29
+ /ix
30
30
 
31
31
  MESSAGE_FORMAT = '`%s` should be written without units as `0`'
32
32
 
33
- def zero_with_units?(string)
34
- string =~ /^0[a-z]+/
33
+ LENGTH_UNITS = %w[em ex ch rem vw vh vmin vmax cm mm in pt pc px].to_set
34
+
35
+ def zero_with_length_units?(string)
36
+ string =~ /^0([a-z]+)/ && LENGTH_UNITS.include?(Regexp.last_match(1))
35
37
  end
36
38
  end
37
39
  end
@@ -3,6 +3,10 @@ module SCSSLint
3
3
  class Reporter
4
4
  attr_reader :lints
5
5
 
6
+ def self.descendants
7
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
8
+ end
9
+
6
10
  def initialize(lints)
7
11
  @lints = lints
8
12
  end
@@ -0,0 +1,26 @@
1
+ module SCSSLint
2
+ # Returns a YAML configuration where all linters are disabled which
3
+ # caused a lint.
4
+ class Reporter::ConfigReporter < Reporter
5
+ def report_lints
6
+ { 'linters' => disabled_linters }.to_yaml unless lints.empty?
7
+ end
8
+
9
+ private
10
+
11
+ def disabled_linters
12
+ linters.each_with_object({}) do |linter, m|
13
+ m[linter] = { 'enabled' => false }
14
+ end
15
+ end
16
+
17
+ def linters
18
+ lints.map { |lint| linter_name(lint.linter) }.compact.uniq.sort
19
+ end
20
+
21
+ def linter_name(linter)
22
+ return unless linter
23
+ linter.class.to_s.split('::').last
24
+ end
25
+ end
26
+ end
@@ -2,13 +2,13 @@ module SCSSLint
2
2
  # Reports a single line per lint.
3
3
  class Reporter::DefaultReporter < Reporter
4
4
  def report_lints
5
- if lints.any?
6
- lints.map do |lint|
7
- type = lint.error? ? '[E]'.color(:red) : '[W]'.color(:yellow)
8
- "#{lint.filename.color(:cyan)}:" << "#{lint.location.line}".color(:magenta) <<
9
- " #{type} #{lint.description}"
10
- end.join("\n") + "\n"
11
- end
5
+ return unless lints.any?
6
+
7
+ lints.map do |lint|
8
+ type = lint.error? ? '[E]'.color(:red) : '[W]'.color(:yellow)
9
+ "#{lint.filename.color(:cyan)}:" << "#{lint.location.line}".color(:magenta) <<
10
+ " #{type} #{lint.description}"
11
+ end.join("\n") + "\n"
12
12
  end
13
13
  end
14
14
  end
@@ -9,10 +9,10 @@ module SCSSLint
9
9
  output << "<file name=#{filename.encode(xml: :attr)}>"
10
10
 
11
11
  file_lints.each do |lint|
12
- output << "<issue line=\"#{lint.location.line}\" " <<
13
- "column=\"#{lint.location.column}\" " <<
14
- "length=\"#{lint.location.length}\" " <<
15
- "severity=\"#{lint.severity}\" " <<
12
+ output << "<issue line=\"#{lint.location.line}\" " \
13
+ "column=\"#{lint.location.column}\" " \
14
+ "length=\"#{lint.location.length}\" " \
15
+ "severity=\"#{lint.severity}\" " \
16
16
  "reason=#{lint.description.encode(xml: :attr)} />"
17
17
  end
18
18
 
@@ -49,9 +49,9 @@ module SCSSLint
49
49
  end
50
50
  end
51
51
  rescue Sass::SyntaxError => ex
52
- @lints << Lint.new(ex.sass_filename, Location.new(ex.sass_line), ex.to_s, :error)
52
+ @lints << Lint.new(nil, ex.sass_filename, Location.new(ex.sass_line), ex.to_s, :error)
53
53
  rescue FileEncodingError => ex
54
- @lints << Lint.new(file, Location.new, ex.to_s, :error)
54
+ @lints << Lint.new(nil, file, Location.new, ex.to_s, :error)
55
55
  end
56
56
  end
57
57
  end
@@ -15,7 +15,7 @@ module Sass::Tree
15
15
  # Sass::Script::Variable nodes with no line numbers. This adds the line
16
16
  # numbers back in so lint reporting works for those nodes.
17
17
  def add_line_numbers_to_args(arg_list)
18
- arg_list.each do |variable, default_expr|
18
+ arg_list.each do |variable, _default_expr|
19
19
  add_line_number(variable)
20
20
  end
21
21
  end
@@ -20,17 +20,6 @@ module SCSSLint
20
20
  .split
21
21
  end
22
22
 
23
- def shortest_hex_form(hex)
24
- (can_be_condensed?(hex) ? (hex[0..1] + hex[3] + hex[5]) : hex).downcase
25
- end
26
-
27
- def can_be_condensed?(hex)
28
- hex.length == 7 &&
29
- hex[1] == hex[2] &&
30
- hex[3] == hex[4] &&
31
- hex[5] == hex[6]
32
- end
33
-
34
23
  # Takes a string like `hello "world" 'how are' you` and turns it into:
35
24
  # `hello you`.
36
25
  # This is useful for scanning for keywords in shorthand properties or lists
@@ -1,4 +1,4 @@
1
1
  # Defines the gem version.
2
2
  module SCSSLint
3
- VERSION = '0.23.1'
3
+ VERSION = '0.24.0'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scss-lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.1
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Causes Engineering
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-04-22 00:00:00.000000000 Z
12
+ date: 2014-05-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rainbow
@@ -71,16 +71,16 @@ dependencies:
71
71
  name: rubocop
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
- - - '>='
74
+ - - '='
75
75
  - !ruby/object:Gem::Version
76
- version: '0'
76
+ version: 0.21.0
77
77
  type: :development
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
- - - '>='
81
+ - - '='
82
82
  - !ruby/object:Gem::Version
83
- version: '0'
83
+ version: 0.21.0
84
84
  description: Configurable tool for writing clean and consistent SCSS
85
85
  email:
86
86
  - eng@causes.com
@@ -99,6 +99,7 @@ files:
99
99
  - lib/scss_lint/reporter/files_reporter.rb
100
100
  - lib/scss_lint/reporter/xml_reporter.rb
101
101
  - lib/scss_lint/reporter/default_reporter.rb
102
+ - lib/scss_lint/reporter/config_reporter.rb
102
103
  - lib/scss_lint/runner.rb
103
104
  - lib/scss_lint/reporter.rb
104
105
  - lib/scss_lint/lint.rb
@@ -112,7 +113,9 @@ files:
112
113
  - lib/scss_lint/linter/space_after_comma.rb
113
114
  - lib/scss_lint/linter/space_before_brace.rb
114
115
  - lib/scss_lint/linter/capitalization_in_selector.rb
116
+ - lib/scss_lint/linter/hex_length.rb
115
117
  - lib/scss_lint/linter/indentation.rb
118
+ - lib/scss_lint/linter/mergeable_selector.rb
116
119
  - lib/scss_lint/linter/trailing_semicolon_after_property_value.rb
117
120
  - lib/scss_lint/linter/shorthand.rb
118
121
  - lib/scss_lint/linter/final_newline.rb
@@ -121,8 +124,8 @@ files:
121
124
  - lib/scss_lint/linter/declaration_order.rb
122
125
  - lib/scss_lint/linter/zero_unit.rb
123
126
  - lib/scss_lint/linter/placeholder_in_extend.rb
124
- - lib/scss_lint/linter/duplicate_root.rb
125
127
  - lib/scss_lint/linter/selector_depth.rb
128
+ - lib/scss_lint/linter/hex_validation.rb
126
129
  - lib/scss_lint/linter/compass/property_with_mixin.rb
127
130
  - lib/scss_lint/linter/space_after_property_name.rb
128
131
  - lib/scss_lint/linter/property_spelling.rb
@@ -134,7 +137,6 @@ files:
134
137
  - lib/scss_lint/linter/debug_statement.rb
135
138
  - lib/scss_lint/linter/duplicate_property.rb
136
139
  - lib/scss_lint/linter/property_sort_order.rb
137
- - lib/scss_lint/linter/hex_format.rb
138
140
  - lib/scss_lint/linter/color_keyword.rb
139
141
  - lib/scss_lint/linter/string_quotes.rb
140
142
  - lib/scss_lint/linter/leading_zero.rb
@@ -142,6 +144,7 @@ files:
142
144
  - lib/scss_lint/linter/comment.rb
143
145
  - lib/scss_lint/linter/empty_rule.rb
144
146
  - lib/scss_lint/linter/single_line_per_selector.rb
147
+ - lib/scss_lint/linter/hex_notation.rb
145
148
  - lib/scss_lint/linter/url_format.rb
146
149
  - lib/scss_lint/cli.rb
147
150
  - lib/scss_lint/engine.rb
@@ -1,34 +0,0 @@
1
- module SCSSLint
2
- # Checks for identical root selectors.
3
- class Linter::DuplicateRoot < Linter
4
- include LinterRegistry
5
-
6
- def visit_root(node)
7
- # Root rules are evaluated per document, so use new hashes for eash file
8
- @roots = {}
9
- yield # Continue linting children
10
- end
11
-
12
- def visit_rule(node)
13
- if @roots[node.rule]
14
- add_lint node.line,
15
- "Merge root rule `#{node.rule.join}` with identical " \
16
- "rule on line #{@roots[node.rule].line}"
17
- else
18
- @roots[node.rule] = node
19
- end
20
-
21
- # Don't yield so we only check one level deep
22
- end
23
-
24
- # Define stubs so we don't check rules nested in other constructs
25
- %w[
26
- directive
27
- media
28
- mixin
29
- mixindef
30
- ].each do |node_type|
31
- define_method("visit_#{node_type}") { |*args| }
32
- end
33
- end
34
- end
@@ -1,34 +0,0 @@
1
- module SCSSLint
2
- # Checks for hexadecimal colors that can be shortened.
3
- class Linter::HexFormat < Linter
4
- include LinterRegistry
5
-
6
- def visit_script_color(node)
7
- return unless color = source_from_range(node.source_range)[HEX_REGEX, 1]
8
-
9
- unless valid_hex_format?(color)
10
- add_hex_lint(node, color)
11
- end
12
- end
13
-
14
- def visit_script_string(node)
15
- return unless node.type == :identifier
16
-
17
- node.value.scan(HEX_REGEX) do |match|
18
- add_hex_lint(node, match.first) unless valid_hex_format?(match.first)
19
- end
20
- end
21
-
22
- private
23
-
24
- HEX_REGEX = /(#\h{3,6})/
25
-
26
- def add_hex_lint(node, hex)
27
- add_lint(node, "Color `#{hex}` should be written as `#{shortest_hex_form(hex)}`")
28
- end
29
-
30
- def valid_hex_format?(hex)
31
- hex == shortest_hex_form(hex)
32
- end
33
- end
34
- end