scss_lint 0.38.0

Sign up to get free protection for your applications and to get access to all the features.
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,49 @@
1
+ module SCSSLint
2
+ # Checks for unnecessary leading zeros in numeric values with decimal points.
3
+ class Linter::LeadingZero < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_script_string(node)
7
+ return unless node.type == :identifier
8
+
9
+ non_string_values = remove_quoted_strings(node.value).split
10
+ non_string_values.each do |value|
11
+ next unless number = value[NUMBER_WITH_LEADING_ZERO_REGEX, 1]
12
+ check_for_leading_zeros(node, number)
13
+ end
14
+ end
15
+
16
+ def visit_script_number(node)
17
+ return unless number =
18
+ source_from_range(node.source_range)[NUMBER_WITH_LEADING_ZERO_REGEX, 1]
19
+
20
+ check_for_leading_zeros(node, number)
21
+ end
22
+
23
+ private
24
+
25
+ NUMBER_WITH_LEADING_ZERO_REGEX = /^-?(0?\.\d+)/
26
+
27
+ CONVENTIONS = {
28
+ 'exclude_zero' => {
29
+ explanation: '`%s` should be written without a leading zero as `%s`',
30
+ validator: ->(original) { original =~ /^\.\d+$/ },
31
+ converter: ->(original) { original[1..-1] },
32
+ },
33
+ 'include_zero' => {
34
+ explanation: '`%s` should be written with a leading zero as `%s`',
35
+ validator: ->(original) { original =~ /^0\.\d+$/ },
36
+ converter: ->(original) { "0#{original}" }
37
+ },
38
+ }
39
+
40
+ def check_for_leading_zeros(node, original_number)
41
+ style = config.fetch('style', 'exclude_zero')
42
+ convention = CONVENTIONS[style]
43
+ return if convention[:validator].call(original_number)
44
+
45
+ corrected = convention[:converter].call(original_number)
46
+ add_lint(node, convention[:explanation] % [original_number, corrected])
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,60 @@
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
+ return unless node.parsed_rules
52
+ node.parsed_rules.members.count == 1
53
+ end
54
+
55
+ def subrule?(rule1, rule2)
56
+ "#{rule1}".start_with?("#{rule2} ") ||
57
+ "#{rule1}".start_with?("#{rule2}.")
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,117 @@
1
+ module SCSSLint
2
+ # Checks the format of declared names of functions, mixins, variables, and
3
+ # placeholders.
4
+ class Linter::NameFormat < Linter
5
+ include LinterRegistry
6
+
7
+ def visit_extend(node)
8
+ check_placeholder(node)
9
+ end
10
+
11
+ def visit_function(node)
12
+ check_name(node, 'function')
13
+ yield # Continue into content block of this function definition
14
+ end
15
+
16
+ def visit_mixin(node)
17
+ check_name(node, 'mixin') unless FUNCTION_WHITELIST.include?(node.name)
18
+ yield # Continue into content block of this mixin's block
19
+ end
20
+
21
+ def visit_mixindef(node)
22
+ check_name(node, 'mixin')
23
+ yield # Continue into content block of this mixin definition
24
+ end
25
+
26
+ def visit_script_funcall(node)
27
+ check_name(node, 'function') unless FUNCTION_WHITELIST.include?(node.name)
28
+ end
29
+
30
+ def visit_script_variable(node)
31
+ check_name(node, 'variable')
32
+ end
33
+
34
+ def visit_variable(node)
35
+ check_name(node, 'variable')
36
+ yield # Continue into expression tree for this variable definition
37
+ end
38
+
39
+ private
40
+
41
+ FUNCTION_WHITELIST = %w[
42
+ rotateX rotateY rotateZ
43
+ scaleX scaleY scaleZ
44
+ skewX skewY
45
+ translateX translateY translateZ
46
+ ].to_set
47
+
48
+ def check_name(node, node_type, node_text = node.name)
49
+ node_text = trim_underscore_prefix(node_text)
50
+ return unless violation = violated_convention(node_text, node_type)
51
+
52
+ add_lint(node,
53
+ "Name of #{node_type} `#{node_text}` #{violation[:explanation]}")
54
+ end
55
+
56
+ # Removes underscore prefix from name if leading underscores are allowed.
57
+ def trim_underscore_prefix(name)
58
+ if config['allow_leading_underscore']
59
+ # Remove if there is a single leading underscore
60
+ name = name.gsub(/^_(?!_)/, '')
61
+ end
62
+
63
+ name
64
+ end
65
+
66
+ def check_placeholder(node)
67
+ extract_string_selectors(node.selector).any? do |selector_str|
68
+ check_name(node, 'placeholder', selector_str.gsub('%', ''))
69
+ end
70
+ end
71
+
72
+ CONVENTIONS = {
73
+ 'camel_case' => {
74
+ explanation: 'should be written in camelCase format',
75
+ validator: ->(name) { name =~ /^[a-z][a-zA-Z0-9]*$/ },
76
+ },
77
+ 'snake_case' => {
78
+ explanation: 'should be written in snake_case',
79
+ validator: ->(name) { name !~ /[^_a-z0-9]/ },
80
+ },
81
+ 'hyphenated_lowercase' => {
82
+ explanation: 'should be written in all lowercase letters with hyphens ' \
83
+ 'instead of underscores',
84
+ validator: ->(name) { name !~ /[_A-Z]/ },
85
+ },
86
+ }
87
+
88
+ def violated_convention(name_string, type)
89
+ convention_name = convention_name(type)
90
+
91
+ existing_convention = CONVENTIONS[convention_name]
92
+
93
+ convention = (existing_convention || {
94
+ validator: ->(name) { name =~ /#{convention_name}/ }
95
+ }).merge(
96
+ explanation: convention_explanation(type), # Allow explanation to be customized
97
+ )
98
+
99
+ convention unless convention[:validator].call(name_string)
100
+ end
101
+
102
+ def convention_name(type)
103
+ config["#{type}_convention"] ||
104
+ config['convention'] ||
105
+ 'hyphenated_lowercase'
106
+ end
107
+
108
+ def convention_explanation(type)
109
+ existing_convention = CONVENTIONS[convention_name(type)]
110
+
111
+ config["#{type}_convention_explanation"] ||
112
+ config['convention_explanation'] ||
113
+ (existing_convention && existing_convention[:explanation]) ||
114
+ "should match regex /#{convention_name(type)}/"
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,24 @@
1
+ module SCSSLint
2
+ # Checks for rule sets nested deeper than a specified maximum depth.
3
+ class Linter::NestingDepth < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ @max_depth = config['max_depth']
8
+ @depth = 1
9
+ yield # Continue linting children
10
+ end
11
+
12
+ def visit_rule(node)
13
+ if @depth > @max_depth
14
+ add_lint(node, "Nesting should be no greater than #{@max_depth}, but was #{@depth}")
15
+ else
16
+ # Only continue if we didn't exceed the max depth already (this makes
17
+ # the lint less noisy)
18
+ @depth += 1
19
+ yield # Continue linting children
20
+ @depth -= 1
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module SCSSLint
2
+ # Checks that `@extend` is always used with a placeholder selector.
3
+ class Linter::PlaceholderInExtend < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_extend(node)
7
+ # Ignore if it cannot be statically determined that this selector is a
8
+ # placeholder since its prefix is dynamically generated
9
+ return if node.selector.first.is_a?(Sass::Script::Tree::Node)
10
+
11
+ # The array returned by the parser is a bit awkward in that it splits on
12
+ # every word boundary (so %placeholder becomes ['%', 'placeholder']).
13
+ selector = node.selector.join
14
+
15
+ # Ignore if this is a placeholder
16
+ return if selector.start_with?('%')
17
+
18
+ add_lint(node, 'Prefer using placeholder selectors (e.g. ' \
19
+ '%some-placeholder) with @extend')
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ module SCSSLint
2
+ # Checks that the number of properties in a rule set is under a defined limit.
3
+ class Linter::PropertyCount < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ @property_count = {} # Lookup table of counts for rule sets
8
+ @max = config['max_properties']
9
+ yield # Continue linting children
10
+ end
11
+
12
+ def visit_rule(node)
13
+ count = property_count(node)
14
+
15
+ if count > @max
16
+ add_lint node,
17
+ "Rule set contains (#{count}/#{@max}) properties" \
18
+ "#{' (including properties in nested rule sets)' if config['include_nested']}"
19
+
20
+ # Don't lint nested rule sets as we already have them in the count
21
+ return if config['include_nested']
22
+ end
23
+
24
+ yield # Lint nested rule sets
25
+ end
26
+
27
+ private
28
+
29
+ def property_count(rule_node)
30
+ @property_count[rule_node] ||=
31
+ begin
32
+ count = rule_node.children.count { |node| node.is_a?(Sass::Tree::PropNode) }
33
+
34
+ if config['include_nested']
35
+ count += rule_node.children.inject(0) do |sum, node|
36
+ node.is_a?(Sass::Tree::RuleNode) ? sum + property_count(node) : sum
37
+ end
38
+ end
39
+
40
+ count
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,198 @@
1
+ module SCSSLint
2
+ # Checks the declaration order of properties.
3
+ class Linter::PropertySortOrder < Linter # rubocop:disable ClassLength
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ @preferred_order = extract_preferred_order_from_config
8
+
9
+ if @preferred_order && config['separate_groups']
10
+ @group = assign_groups(@preferred_order)
11
+ end
12
+
13
+ yield # Continue linting children
14
+ end
15
+
16
+ def check_order(node)
17
+ sortable_props = node.children.select do |child|
18
+ child.is_a?(Sass::Tree::PropNode) && !ignore_property?(child)
19
+ end
20
+
21
+ if sortable_props.count >= config.fetch('min_properties', 2)
22
+ sortable_prop_info = sortable_props
23
+ .map do |child|
24
+ name = child.name.join
25
+ /^(?<vendor>-\w+(-osx)?-)?(?<property>.+)/ =~ name
26
+ { name: name, vendor: vendor, property: "#{@nested_under}#{property}", node: child }
27
+ end
28
+
29
+ check_sort_order(sortable_prop_info)
30
+ check_group_separation(sortable_prop_info) if @group
31
+ end
32
+
33
+ yield # Continue linting children
34
+ end
35
+
36
+ alias_method :visit_media, :check_order
37
+ alias_method :visit_mixin, :check_order
38
+ alias_method :visit_rule, :check_order
39
+ alias_method :visit_prop, :check_order
40
+
41
+ def visit_prop(node, &block)
42
+ # Handle nested properties by appending the parent property they are
43
+ # nested under to the name
44
+ @nested_under = "#{node.name.join}-"
45
+ check_order(node, &block)
46
+ @nested_under = nil
47
+ end
48
+
49
+ def visit_if(node, &block)
50
+ check_order(node, &block)
51
+ visit(node.else) if node.else
52
+ end
53
+
54
+ private
55
+
56
+ # When enforcing whether a blank line should separate "groups" of
57
+ # properties, we need to assign those properties to group numbers so we can
58
+ # quickly tell traversing from one property to the other that a blank line
59
+ # is required (since the group number would change).
60
+ def assign_groups(order)
61
+ group_number = 0
62
+ last_was_empty = false
63
+
64
+ order.each_with_object({}) do |property, group|
65
+ # A gap indicates the start of the next group
66
+ if property.nil? || property.strip.empty?
67
+ group_number += 1 unless last_was_empty # Treat multiple gaps as single gap
68
+ last_was_empty = true
69
+ next
70
+ end
71
+
72
+ last_was_empty = false
73
+
74
+ group[property] = group_number
75
+ end
76
+ end
77
+
78
+ def check_sort_order(sortable_prop_info)
79
+ sorted_props = sortable_prop_info
80
+ .sort { |a, b| compare_properties(a, b) }
81
+
82
+ sorted_props.each_with_index do |prop, index|
83
+ next unless prop != sortable_prop_info[index]
84
+
85
+ add_lint(sortable_prop_info[index][:node], lint_message(sorted_props))
86
+ break
87
+ end
88
+ end
89
+
90
+ def check_group_separation(sortable_prop_info) # rubocop:disable AbcSize
91
+ group_number = @group[sortable_prop_info.first[:property]]
92
+
93
+ sortable_prop_info[0..-2].zip(sortable_prop_info[1..-1]).each do |first, second|
94
+ next unless @group[second[:property]] != group_number
95
+
96
+ # We're now in the next group
97
+ group_number = @group[second[:property]]
98
+
99
+ # The group number has changed, so ensure this property is separated
100
+ # from the previous property by at least a line (could be a comment,
101
+ # we don't care, but at least one line that isn't another property).
102
+ next if first[:node].line < second[:node].line - 1
103
+
104
+ add_lint second[:node], "Property #{second[:name]} should be " \
105
+ 'separated from the previous group of ' \
106
+ "properties ending with #{first[:name]}"
107
+ end
108
+ end
109
+
110
+ # Compares two properties which can contain a vendor prefix. It allows for a
111
+ # sort order like:
112
+ #
113
+ # p {
114
+ # border: ...
115
+ # -moz-border-radius: ...
116
+ # -o-border-radius: ...
117
+ # -webkit-border-radius: ...
118
+ # border-radius: ...
119
+ # color: ...
120
+ # }
121
+ #
122
+ # ...where vendor-prefixed properties come before the standard property, and
123
+ # are ordered amongst themselves by vendor prefix.
124
+ def compare_properties(a, b)
125
+ if a[:property] == b[:property]
126
+ compare_by_vendor(a, b)
127
+ else
128
+ if @preferred_order
129
+ compare_by_order(a, b, @preferred_order)
130
+ else
131
+ a[:property] <=> b[:property]
132
+ end
133
+ end
134
+ end
135
+
136
+ def compare_by_vendor(a, b)
137
+ if a[:vendor] && b[:vendor]
138
+ a[:vendor] <=> b[:vendor]
139
+ elsif a[:vendor]
140
+ -1
141
+ elsif b[:vendor]
142
+ 1
143
+ else
144
+ 0
145
+ end
146
+ end
147
+
148
+ def compare_by_order(a, b, order)
149
+ (order.index(a[:property]) || Float::INFINITY) <=>
150
+ (order.index(b[:property]) || Float::INFINITY)
151
+ end
152
+
153
+ def extract_preferred_order_from_config
154
+ case config['order']
155
+ when nil
156
+ nil # No custom order specified
157
+ when Array
158
+ config['order']
159
+ when String
160
+ begin
161
+ file = File.open(File.join(SCSS_LINT_DATA,
162
+ 'property-sort-orders',
163
+ "#{config['order']}.txt"))
164
+ file.read.split("\n").reject { |line| line =~ /^(#|\s*$)/ }
165
+ rescue Errno::ENOENT
166
+ raise SCSSLint::Exceptions::LinterError,
167
+ "Preset property sort order '#{config['order']}' does not exist"
168
+ end
169
+ else
170
+ raise SCSSLint::Exceptions::LinterError,
171
+ 'Invalid property sort order specified -- must be the name of a '\
172
+ 'preset or an array of strings'
173
+ end
174
+ end
175
+
176
+ # Return whether to ignore a property in the sort order.
177
+ #
178
+ # This includes:
179
+ # - properties containing interpolation
180
+ # - properties not explicitly defined in the sort order (if ignore_unspecified is set)
181
+ def ignore_property?(prop_node)
182
+ return true if prop_node.name.any? { |part| !part.is_a?(String) }
183
+
184
+ config['ignore_unspecified'] &&
185
+ @preferred_order &&
186
+ !@preferred_order.include?(prop_node.name.join)
187
+ end
188
+
189
+ def preset_order?
190
+ config['order'].is_a?(String)
191
+ end
192
+
193
+ def lint_message(sortable_prop_info)
194
+ props = sortable_prop_info.map { |prop| prop[:name] }.join(', ')
195
+ "Properties should be ordered #{props}"
196
+ end
197
+ end
198
+ end