scss-lint 0.23.1 → 0.24.0

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