scss_lint 0.38.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 (157) hide show
  1. checksums.yaml +7 -0
  2. data/bin/scss-lint +6 -0
  3. data/config/default.yml +205 -0
  4. data/data/prefixed-identifiers/base.txt +107 -0
  5. data/data/prefixed-identifiers/bourbon.txt +71 -0
  6. data/data/properties.txt +477 -0
  7. data/data/property-sort-orders/concentric.txt +134 -0
  8. data/data/property-sort-orders/recess.txt +149 -0
  9. data/data/property-sort-orders/smacss.txt +137 -0
  10. data/lib/scss_lint.rb +31 -0
  11. data/lib/scss_lint/cli.rb +215 -0
  12. data/lib/scss_lint/config.rb +251 -0
  13. data/lib/scss_lint/constants.rb +8 -0
  14. data/lib/scss_lint/control_comment_processor.rb +126 -0
  15. data/lib/scss_lint/engine.rb +56 -0
  16. data/lib/scss_lint/exceptions.rb +21 -0
  17. data/lib/scss_lint/file_finder.rb +68 -0
  18. data/lib/scss_lint/lint.rb +24 -0
  19. data/lib/scss_lint/linter.rb +161 -0
  20. data/lib/scss_lint/linter/bang_format.rb +52 -0
  21. data/lib/scss_lint/linter/border_zero.rb +39 -0
  22. data/lib/scss_lint/linter/color_keyword.rb +32 -0
  23. data/lib/scss_lint/linter/color_variable.rb +60 -0
  24. data/lib/scss_lint/linter/comment.rb +21 -0
  25. data/lib/scss_lint/linter/compass.rb +7 -0
  26. data/lib/scss_lint/linter/compass/property_with_mixin.rb +47 -0
  27. data/lib/scss_lint/linter/debug_statement.rb +10 -0
  28. data/lib/scss_lint/linter/declaration_order.rb +71 -0
  29. data/lib/scss_lint/linter/duplicate_property.rb +58 -0
  30. data/lib/scss_lint/linter/else_placement.rb +48 -0
  31. data/lib/scss_lint/linter/empty_line_between_blocks.rb +85 -0
  32. data/lib/scss_lint/linter/empty_rule.rb +11 -0
  33. data/lib/scss_lint/linter/final_newline.rb +20 -0
  34. data/lib/scss_lint/linter/hex_length.rb +56 -0
  35. data/lib/scss_lint/linter/hex_notation.rb +38 -0
  36. data/lib/scss_lint/linter/hex_validation.rb +23 -0
  37. data/lib/scss_lint/linter/id_selector.rb +10 -0
  38. data/lib/scss_lint/linter/import_path.rb +62 -0
  39. data/lib/scss_lint/linter/important_rule.rb +12 -0
  40. data/lib/scss_lint/linter/indentation.rb +197 -0
  41. data/lib/scss_lint/linter/leading_zero.rb +49 -0
  42. data/lib/scss_lint/linter/mergeable_selector.rb +60 -0
  43. data/lib/scss_lint/linter/name_format.rb +117 -0
  44. data/lib/scss_lint/linter/nesting_depth.rb +24 -0
  45. data/lib/scss_lint/linter/placeholder_in_extend.rb +22 -0
  46. data/lib/scss_lint/linter/property_count.rb +44 -0
  47. data/lib/scss_lint/linter/property_sort_order.rb +198 -0
  48. data/lib/scss_lint/linter/property_spelling.rb +49 -0
  49. data/lib/scss_lint/linter/property_units.rb +59 -0
  50. data/lib/scss_lint/linter/qualifying_element.rb +42 -0
  51. data/lib/scss_lint/linter/selector_depth.rb +64 -0
  52. data/lib/scss_lint/linter/selector_format.rb +102 -0
  53. data/lib/scss_lint/linter/shorthand.rb +139 -0
  54. data/lib/scss_lint/linter/single_line_per_property.rb +59 -0
  55. data/lib/scss_lint/linter/single_line_per_selector.rb +35 -0
  56. data/lib/scss_lint/linter/space_after_comma.rb +110 -0
  57. data/lib/scss_lint/linter/space_after_property_colon.rb +92 -0
  58. data/lib/scss_lint/linter/space_after_property_name.rb +27 -0
  59. data/lib/scss_lint/linter/space_before_brace.rb +72 -0
  60. data/lib/scss_lint/linter/space_between_parens.rb +35 -0
  61. data/lib/scss_lint/linter/string_quotes.rb +94 -0
  62. data/lib/scss_lint/linter/trailing_semicolon.rb +67 -0
  63. data/lib/scss_lint/linter/trailing_zero.rb +41 -0
  64. data/lib/scss_lint/linter/unnecessary_mantissa.rb +42 -0
  65. data/lib/scss_lint/linter/unnecessary_parent_reference.rb +49 -0
  66. data/lib/scss_lint/linter/url_format.rb +56 -0
  67. data/lib/scss_lint/linter/url_quotes.rb +27 -0
  68. data/lib/scss_lint/linter/variable_for_property.rb +30 -0
  69. data/lib/scss_lint/linter/vendor_prefix.rb +64 -0
  70. data/lib/scss_lint/linter/zero_unit.rb +39 -0
  71. data/lib/scss_lint/linter_registry.rb +26 -0
  72. data/lib/scss_lint/location.rb +38 -0
  73. data/lib/scss_lint/options.rb +109 -0
  74. data/lib/scss_lint/rake_task.rb +106 -0
  75. data/lib/scss_lint/reporter.rb +18 -0
  76. data/lib/scss_lint/reporter/config_reporter.rb +26 -0
  77. data/lib/scss_lint/reporter/default_reporter.rb +27 -0
  78. data/lib/scss_lint/reporter/files_reporter.rb +8 -0
  79. data/lib/scss_lint/reporter/json_reporter.rb +30 -0
  80. data/lib/scss_lint/reporter/xml_reporter.rb +33 -0
  81. data/lib/scss_lint/runner.rb +51 -0
  82. data/lib/scss_lint/sass/script.rb +78 -0
  83. data/lib/scss_lint/sass/tree.rb +168 -0
  84. data/lib/scss_lint/selector_visitor.rb +34 -0
  85. data/lib/scss_lint/utils.rb +112 -0
  86. data/lib/scss_lint/version.rb +4 -0
  87. data/spec/scss_lint/cli_spec.rb +177 -0
  88. data/spec/scss_lint/config_spec.rb +253 -0
  89. data/spec/scss_lint/engine_spec.rb +24 -0
  90. data/spec/scss_lint/file_finder_spec.rb +134 -0
  91. data/spec/scss_lint/linter/bang_format_spec.rb +121 -0
  92. data/spec/scss_lint/linter/border_zero_spec.rb +118 -0
  93. data/spec/scss_lint/linter/color_keyword_spec.rb +83 -0
  94. data/spec/scss_lint/linter/color_variable_spec.rb +155 -0
  95. data/spec/scss_lint/linter/comment_spec.rb +79 -0
  96. data/spec/scss_lint/linter/compass/property_with_mixin_spec.rb +55 -0
  97. data/spec/scss_lint/linter/debug_statement_spec.rb +21 -0
  98. data/spec/scss_lint/linter/declaration_order_spec.rb +575 -0
  99. data/spec/scss_lint/linter/duplicate_property_spec.rb +189 -0
  100. data/spec/scss_lint/linter/else_placement_spec.rb +106 -0
  101. data/spec/scss_lint/linter/empty_line_between_blocks_spec.rb +276 -0
  102. data/spec/scss_lint/linter/empty_rule_spec.rb +27 -0
  103. data/spec/scss_lint/linter/final_newline_spec.rb +49 -0
  104. data/spec/scss_lint/linter/hex_length_spec.rb +104 -0
  105. data/spec/scss_lint/linter/hex_notation_spec.rb +104 -0
  106. data/spec/scss_lint/linter/hex_validation_spec.rb +40 -0
  107. data/spec/scss_lint/linter/id_selector_spec.rb +62 -0
  108. data/spec/scss_lint/linter/import_path_spec.rb +300 -0
  109. data/spec/scss_lint/linter/important_rule_spec.rb +43 -0
  110. data/spec/scss_lint/linter/indentation_spec.rb +347 -0
  111. data/spec/scss_lint/linter/leading_zero_spec.rb +233 -0
  112. data/spec/scss_lint/linter/mergeable_selector_spec.rb +283 -0
  113. data/spec/scss_lint/linter/name_format_spec.rb +282 -0
  114. data/spec/scss_lint/linter/nesting_depth_spec.rb +114 -0
  115. data/spec/scss_lint/linter/placeholder_in_extend_spec.rb +63 -0
  116. data/spec/scss_lint/linter/property_count_spec.rb +104 -0
  117. data/spec/scss_lint/linter/property_sort_order_spec.rb +482 -0
  118. data/spec/scss_lint/linter/property_spelling_spec.rb +84 -0
  119. data/spec/scss_lint/linter/property_units_spec.rb +229 -0
  120. data/spec/scss_lint/linter/qualifying_element_spec.rb +125 -0
  121. data/spec/scss_lint/linter/selector_depth_spec.rb +159 -0
  122. data/spec/scss_lint/linter/selector_format_spec.rb +632 -0
  123. data/spec/scss_lint/linter/shorthand_spec.rb +198 -0
  124. data/spec/scss_lint/linter/single_line_per_property_spec.rb +73 -0
  125. data/spec/scss_lint/linter/single_line_per_selector_spec.rb +130 -0
  126. data/spec/scss_lint/linter/space_after_comma_spec.rb +332 -0
  127. data/spec/scss_lint/linter/space_after_property_colon_spec.rb +373 -0
  128. data/spec/scss_lint/linter/space_after_property_name_spec.rb +37 -0
  129. data/spec/scss_lint/linter/space_before_brace_spec.rb +829 -0
  130. data/spec/scss_lint/linter/space_between_parens_spec.rb +263 -0
  131. data/spec/scss_lint/linter/string_quotes_spec.rb +335 -0
  132. data/spec/scss_lint/linter/trailing_semicolon_spec.rb +304 -0
  133. data/spec/scss_lint/linter/trailing_zero_spec.rb +176 -0
  134. data/spec/scss_lint/linter/unnecessary_mantissa_spec.rb +67 -0
  135. data/spec/scss_lint/linter/unnecessary_parent_reference_spec.rb +98 -0
  136. data/spec/scss_lint/linter/url_format_spec.rb +55 -0
  137. data/spec/scss_lint/linter/url_quotes_spec.rb +73 -0
  138. data/spec/scss_lint/linter/variable_for_property_spec.rb +145 -0
  139. data/spec/scss_lint/linter/vendor_prefix_spec.rb +371 -0
  140. data/spec/scss_lint/linter/zero_unit_spec.rb +113 -0
  141. data/spec/scss_lint/linter_registry_spec.rb +50 -0
  142. data/spec/scss_lint/linter_spec.rb +292 -0
  143. data/spec/scss_lint/location_spec.rb +42 -0
  144. data/spec/scss_lint/options_spec.rb +34 -0
  145. data/spec/scss_lint/rake_task_spec.rb +43 -0
  146. data/spec/scss_lint/reporter/config_reporter_spec.rb +42 -0
  147. data/spec/scss_lint/reporter/default_reporter_spec.rb +73 -0
  148. data/spec/scss_lint/reporter/files_reporter_spec.rb +38 -0
  149. data/spec/scss_lint/reporter/json_reporter_spec.rb +96 -0
  150. data/spec/scss_lint/reporter/xml_reporter_spec.rb +103 -0
  151. data/spec/scss_lint/reporter_spec.rb +11 -0
  152. data/spec/scss_lint/runner_spec.rb +123 -0
  153. data/spec/scss_lint/selector_visitor_spec.rb +264 -0
  154. data/spec/spec_helper.rb +34 -0
  155. data/spec/support/isolated_environment.rb +25 -0
  156. data/spec/support/matchers/report_lint.rb +48 -0
  157. metadata +328 -0
@@ -0,0 +1,52 @@
1
+ module SCSSLint
2
+ # Checks spacing of ! declarations, like !important and !default
3
+ class Linter::BangFormat < Linter
4
+ include LinterRegistry
5
+
6
+ STOPPING_CHARACTERS = ['!', "'", '"', nil]
7
+
8
+ def visit_prop(node)
9
+ return unless source_from_range(node.source_range).include?('!')
10
+ return unless check_spacing(node)
11
+
12
+ before_qualifier = config['space_before_bang'] ? '' : 'not '
13
+ after_qualifier = config['space_after_bang'] ? '' : 'not '
14
+
15
+ add_lint(node, "! should #{before_qualifier}be preceeded by a space, " \
16
+ "and should #{after_qualifier}be followed by a space")
17
+ end
18
+
19
+ private
20
+
21
+ # Start from the back and move towards the front so that any !important or
22
+ # !default !'s will be found *before* quotation marks. Then we can
23
+ # stop at quotation marks to protect against linting !'s within strings
24
+ # (e.g. `content`)
25
+ def find_bang_offset(range)
26
+ offset = 0
27
+ offset -= 1 until STOPPING_CHARACTERS.include?(character_at(range.end_pos, offset))
28
+ offset
29
+ end
30
+
31
+ def is_before_wrong?(range, offset)
32
+ before_expected = config['space_before_bang'] ? / / : /[^ ]/
33
+ before_actual = character_at(range.end_pos, offset - 1)
34
+ (before_actual =~ before_expected).nil?
35
+ end
36
+
37
+ def is_after_wrong?(range, offset)
38
+ after_expected = config['space_after_bang'] ? / / : /[^ ]/
39
+ after_actual = character_at(range.end_pos, offset + 1)
40
+ (after_actual =~ after_expected).nil?
41
+ end
42
+
43
+ def check_spacing(node)
44
+ range = node.value_source_range
45
+ offset = find_bang_offset(range)
46
+
47
+ return if character_at(range.end_pos, offset) != '!'
48
+
49
+ is_before_wrong?(range, offset) || is_after_wrong?(range, offset)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,39 @@
1
+ module SCSSLint
2
+ # Enforce a particular value for empty borders.
3
+ class Linter::BorderZero < Linter
4
+ include LinterRegistry
5
+
6
+ CONVENTION_TO_PREFERENCE = {
7
+ 'zero' => %w[0 none],
8
+ 'none' => %w[none 0],
9
+ }
10
+
11
+ BORDER_PROPERTIES = %w[
12
+ border
13
+ border-top
14
+ border-right
15
+ border-bottom
16
+ border-left
17
+ ]
18
+
19
+ def visit_root(_node)
20
+ @preference = CONVENTION_TO_PREFERENCE[config['convention']]
21
+ yield # Continue linting children
22
+ end
23
+
24
+ def visit_prop(node)
25
+ return unless BORDER_PROPERTIES.include?(node.name.first.to_s)
26
+ check_border(node, node.value.to_sass.strip)
27
+ end
28
+
29
+ private
30
+
31
+ def check_border(node, border)
32
+ return unless %w[0 none].include?(border)
33
+ return if @preference[0] == border
34
+
35
+ add_lint(node, "`border: #{@preference[0]} is preferred over " \
36
+ "`border: #{@preference[1]}`")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ module SCSSLint
2
+ # Checks for uses of a color keyword instead of the preferred hexadecimal
3
+ # form.
4
+ class Linter::ColorKeyword < Linter
5
+ include LinterRegistry
6
+
7
+ def visit_script_color(node)
8
+ word = source_from_range(node.source_range)[/([a-z]+)/i, 1]
9
+ add_color_lint(node, word) if color_keyword?(word)
10
+ end
11
+
12
+ def visit_script_string(node)
13
+ return unless node.type == :identifier
14
+
15
+ remove_quoted_strings(node.value).scan(/(^|\s)([a-z]+)(?=\s|$)/i) do |_, word|
16
+ add_color_lint(node, word) if color_keyword?(word)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def add_color_lint(node, original)
23
+ hex_form = Sass::Script::Value::Color.new(color_keyword_to_code(original)).tap do |color|
24
+ color.options = {} # `inspect` requires options to be set
25
+ end.inspect
26
+
27
+ add_lint(node,
28
+ "Color `#{original}` should be written in hexadecimal form " \
29
+ "as `#{hex_form}`")
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ module SCSSLint
2
+ # Ensures color literals are used only in variable declarations.
3
+ class Linter::ColorVariable < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_script_color(node)
7
+ return if in_variable_declaration?(node) ||
8
+ in_map_declaration?(node) ||
9
+ in_rgba_function_call?(node)
10
+
11
+ # Source range sometimes includes closing parenthesis, so extract it
12
+ color = source_from_range(node.source_range)[/(#?[a-z0-9]+)/i, 1]
13
+
14
+ record_lint(node, color) if color?(color)
15
+ end
16
+
17
+ def visit_script_string(node)
18
+ return if literal_string?(node)
19
+
20
+ remove_quoted_strings(node.value)
21
+ .scan(/(^|\s)(#[a-f0-9]+|[a-z]+)(?=\s|$)/i)
22
+ .select { |_, word| color?(word) }
23
+ .each { |_, color| record_lint(node, color) }
24
+ end
25
+
26
+ private
27
+
28
+ def record_lint(node, color)
29
+ add_lint node, "Color literals like `#{color}` should only be used in " \
30
+ 'variable declarations; they should be referred to via ' \
31
+ 'variable everywhere else.'
32
+ end
33
+
34
+ def literal_string?(script_string)
35
+ return unless script_string.respond_to?(:source_range) &&
36
+ source_range = script_string.source_range
37
+
38
+ # If original source starts with a quote character, it's a string, not a
39
+ # color
40
+ %w[' "].include?(source_from_range(source_range)[0])
41
+ end
42
+
43
+ def in_variable_declaration?(node)
44
+ parent = node.node_parent
45
+ parent.is_a?(Sass::Script::Tree::Literal) &&
46
+ parent.node_parent.is_a?(Sass::Tree::VariableNode)
47
+ end
48
+
49
+ def in_rgba_function_call?(node)
50
+ grandparent = node_ancestor(node, 2)
51
+
52
+ grandparent.is_a?(Sass::Script::Tree::Funcall) &&
53
+ grandparent.name == 'rgba'
54
+ end
55
+
56
+ def in_map_declaration?(node)
57
+ node_ancestor(node, 2).is_a?(Sass::Script::Tree::MapLiteral)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,21 @@
1
+ module SCSSLint
2
+ # Checks for uses of renderable comments (/* ... */)
3
+ class Linter::Comment < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_comment(node)
7
+ add_lint(node, 'Use `//` comments everywhere') unless node.invisible? || allowed?(node)
8
+ end
9
+
10
+ private
11
+
12
+ # @param node [CommentNode]
13
+ # @return [Boolean]
14
+ def allowed?(node)
15
+ return false unless config['allowed']
16
+ re = Regexp.new(config['allowed'])
17
+
18
+ node.value.join.match(re)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module SCSSLint
2
+ # Superclass for linters that apply to codebases using the Compass framework.
3
+ #
4
+ # Any shared code/constants amongst Compass linters should be stored here.
5
+ class Linter::Compass < Linter
6
+ end
7
+ end
@@ -0,0 +1,47 @@
1
+ module SCSSLint
2
+ # Checks for uses of properties where a Compass mixin would be preferred.
3
+ class Linter::Compass::PropertyWithMixin < Linter::Compass
4
+ include LinterRegistry
5
+
6
+ def visit_prop(node)
7
+ check_for_properties_with_mixins(node)
8
+ check_for_inline_block(node)
9
+ end
10
+
11
+ private
12
+
13
+ # Set of properties where the Compass mixin version is preferred
14
+ PROPERTIES_WITH_MIXINS = %w[
15
+ background-clip
16
+ background-origin
17
+ border-radius
18
+ box-shadow
19
+ box-sizing
20
+ opacity
21
+ text-shadow
22
+ transform
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
+ !ignore_compass_mixin?(prop_name)
29
+
30
+ add_lint node, "Use the Compass `#{prop_name}` mixin instead of the property"
31
+ end
32
+
33
+ def check_for_inline_block(node)
34
+ prop_name = node.name.join
35
+ return unless prop_name == 'display' &&
36
+ node.value.to_sass == 'inline-block' &&
37
+ !ignore_compass_mixin?('inline-block')
38
+
39
+ add_lint node,
40
+ 'Use the Compass `inline-block` mixin instead of `display: inline-block`'
41
+ end
42
+
43
+ def ignore_compass_mixin?(prop_name)
44
+ config.fetch('ignore', []).include?(prop_name)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ module SCSSLint
2
+ # Checks for leftover `@debug` statements.
3
+ class Linter::DebugStatement < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_debug(node)
7
+ add_lint(node, 'Remove @debug line')
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,71 @@
1
+ module SCSSLint
2
+ # Checks the order of nested items within a rule set.
3
+ class Linter::DeclarationOrder < Linter
4
+ include LinterRegistry
5
+
6
+ def check_order(node)
7
+ check_node(node)
8
+ yield # Continue linting children
9
+ end
10
+
11
+ alias_method :visit_rule, :check_order
12
+ alias_method :visit_mixin, :check_order
13
+ alias_method :visit_media, :check_order
14
+
15
+ private
16
+
17
+ MESSAGE =
18
+ 'Rule sets should be ordered as follows: '\
19
+ '`@extends`, `@includes` without `@content`, ' \
20
+ 'properties, `@includes` with `@content`, ' \
21
+ 'nested rule sets'
22
+
23
+ MIXIN_WITH_CONTENT = 'mixin_with_content'
24
+
25
+ DECLARATION_ORDER = [
26
+ Sass::Tree::ExtendNode,
27
+ Sass::Tree::MixinNode,
28
+ Sass::Tree::PropNode,
29
+ MIXIN_WITH_CONTENT,
30
+ Sass::Tree::RuleNode,
31
+ ]
32
+
33
+ def important_node?(node)
34
+ DECLARATION_ORDER.include?(node.class)
35
+ end
36
+
37
+ def check_node(node)
38
+ children = node.children.each_with_index
39
+ .select { |n, _| important_node?(n) }
40
+ .map { |n, i| [n, node_declaration_type(n), i] }
41
+
42
+ sorted_children = children.sort do |(_, a_type, i), (_, b_type, j)|
43
+ [DECLARATION_ORDER.index(a_type), i] <=> [DECLARATION_ORDER.index(b_type), j]
44
+ end
45
+
46
+ check_children_order(sorted_children, children)
47
+ end
48
+
49
+ # Find the child that is out of place
50
+ def check_children_order(sorted_children, children)
51
+ sorted_children.each_with_index do |sorted_item, index|
52
+ next if sorted_item == children[index]
53
+
54
+ add_lint(sorted_item.first.line,
55
+ "Expected item on line #{sorted_item.first.line} to appear " \
56
+ "before line #{children[index].first.line}. #{MESSAGE}")
57
+ break
58
+ end
59
+ end
60
+
61
+ def node_declaration_type(node)
62
+ # If the node has no children, return the class.
63
+ return node.class unless node.has_children
64
+
65
+ # If the node is a mixin with children, indicate that;
66
+ # otherwise, just return the class.
67
+ return node.class unless node.is_a?(Sass::Tree::MixinNode)
68
+ MIXIN_WITH_CONTENT
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,58 @@
1
+ module SCSSLint
2
+ # Checks for a property declared twice in a rule set.
3
+ class Linter::DuplicateProperty < Linter
4
+ include LinterRegistry
5
+
6
+ def check_properties(node)
7
+ static_properties(node).each_with_object({}) do |prop, prop_names|
8
+ prop_key = property_key(prop)
9
+
10
+ if existing_prop = prop_names[prop_key]
11
+ add_lint(prop, "Property `#{existing_prop.name.join}` already "\
12
+ "defined on line #{existing_prop.line}")
13
+ else
14
+ prop_names[prop_key] = prop
15
+ end
16
+ end
17
+
18
+ yield # Continue linting children
19
+ end
20
+
21
+ alias_method :visit_rule, :check_properties
22
+ alias_method :visit_mixindef, :check_properties
23
+
24
+ private
25
+
26
+ def static_properties(node)
27
+ node.children
28
+ .select { |child| child.is_a?(Sass::Tree::PropNode) }
29
+ .reject { |prop| prop.name.any? { |item| item.is_a?(Sass::Script::Node) } }
30
+ end
31
+
32
+ # Returns a key identifying the bucket this property and value correspond to
33
+ # for purposes of uniqueness.
34
+ def property_key(prop)
35
+ prop_key = prop.name.join
36
+ prop_value = property_value(prop)
37
+
38
+ # Differentiate between values for different vendor prefixes
39
+ prop_value.to_s.scan(/^(-[^-]+-.+)/) do |vendor_keyword|
40
+ prop_key << vendor_keyword.first
41
+ end
42
+
43
+ prop_key
44
+ end
45
+
46
+ def property_value(prop)
47
+ case prop.value
48
+ when Sass::Script::Funcall
49
+ prop.value.name
50
+ when Sass::Script::String
51
+ when Sass::Script::Tree::Literal
52
+ prop.value.value
53
+ else
54
+ prop.value.to_s
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,48 @@
1
+ module SCSSLint
2
+ # Checks where `@else` and `@else if` directives are placed with respect to
3
+ # the previous curly brace.
4
+ class Linter::ElsePlacement < Linter
5
+ include LinterRegistry
6
+
7
+ def visit_if(node)
8
+ visit_else(node, node.else) if node.else
9
+ end
10
+
11
+ def visit_else(if_node, else_node)
12
+ # Check each @else branch if there are multiple `@else if`s
13
+ visit_else(else_node, else_node.else) if else_node.else
14
+
15
+ # Skip @else statements on the same line as the previous @if, since we
16
+ # don't care about placement in that case
17
+ return if if_node.line == else_node.line
18
+
19
+ spaces = 0
20
+ while (char = character_at(else_node.source_range.start_pos, - (spaces + 1)))
21
+ if char == '}'
22
+ curly_on_same_line = true
23
+ break
24
+ end
25
+ spaces += 1
26
+ end
27
+
28
+ check_placement(else_node, curly_on_same_line)
29
+ end
30
+
31
+ private
32
+
33
+ def check_placement(else_node, curly_on_same_line)
34
+ if same_line_preferred?
35
+ unless curly_on_same_line
36
+ add_lint(else_node,
37
+ '@else should be placed on same line as previous curly brace')
38
+ end
39
+ elsif curly_on_same_line
40
+ add_lint(else_node, '@else should be placed on its own line')
41
+ end
42
+ end
43
+
44
+ def same_line_preferred?
45
+ config['style'] == 'same_line'
46
+ end
47
+ end
48
+ end