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,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