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 misspelled properties.
3
+ class Linter::PropertySpelling < Linter
4
+ include LinterRegistry
5
+
6
+ KNOWN_PROPERTIES = File.open(File.join(SCSS_LINT_DATA, 'properties.txt'))
7
+ .read
8
+ .split
9
+ .to_set
10
+
11
+ def visit_root(_node)
12
+ @extra_properties = config['extra_properties'].to_set
13
+ yield # Continue linting children
14
+ end
15
+
16
+ def visit_prop(node)
17
+ # Ignore properties with interpolation
18
+ return if node.name.count > 1 || !node.name.first.is_a?(String)
19
+
20
+ nested_properties = node.children.select { |child| child.is_a?(Sass::Tree::PropNode) }
21
+ if nested_properties.any?
22
+ # Treat nested properties specially, as they are a concatenation of the
23
+ # parent with child property
24
+ nested_properties.each do |nested_prop|
25
+ check_property(nested_prop, node.name.join)
26
+ end
27
+ else
28
+ check_property(node)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def check_property(node, prefix = nil) # rubocop:disable CyclomaticComplexity
35
+ # Ignore properties with interpolation
36
+ return if node.name.count > 1 || !node.name.first.is_a?(String)
37
+
38
+ name = prefix ? "#{prefix}-" : ''
39
+ name += node.name.join
40
+
41
+ # Ignore vendor-prefixed properties
42
+ return if name.start_with?('-')
43
+ return if KNOWN_PROPERTIES.include?(name) ||
44
+ @extra_properties.include?(name)
45
+
46
+ add_lint(node, "Unknown property #{name}")
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,59 @@
1
+ module SCSSLint
2
+ # Check for allowed units
3
+ class Linter::PropertyUnits < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ @globally_allowed_units = config['global'].to_set
8
+ @allowed_units_for_property = config['properties']
9
+
10
+ yield # Continue linting children
11
+ end
12
+
13
+ def visit_prop(node)
14
+ property = node.name.join
15
+
16
+ # Handle nested properties by ensuring the full name is extracted
17
+ if @nested_under
18
+ property = "#{@nested_under}-#{property}"
19
+ end
20
+
21
+ if node.value.respond_to?(:value) &&
22
+ units = node.value.value.to_s[/(?:^|\s)(?:\d+|\d*\.?\d+)([a-z%]+)/i, 1]
23
+ check_units(node, property, units)
24
+ end
25
+
26
+ @nested_under = property
27
+ yield # Continue linting nested properties
28
+ @nested_under = nil
29
+ end
30
+
31
+ private
32
+
33
+ # Checks if a property value's units are allowed.
34
+ #
35
+ # @param node [Sass::Tree::Node]
36
+ # @param property [String]
37
+ # @param units [String]
38
+ def check_units(node, property, units)
39
+ allowed_units = allowed_units_for_property(property)
40
+ return if allowed_units.include?(units)
41
+
42
+ add_lint(node,
43
+ "#{units} units not allowed on `#{property}`; must be one of " \
44
+ "(#{allowed_units.to_a.sort.join(', ')})")
45
+ end
46
+
47
+ # Return the list of allowed units for a property.
48
+ #
49
+ # @param property [String]
50
+ # @return Array<String>
51
+ def allowed_units_for_property(property)
52
+ if @allowed_units_for_property.key?(property)
53
+ @allowed_units_for_property[property]
54
+ else
55
+ @globally_allowed_units
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,42 @@
1
+ module SCSSLint
2
+ # Checks for element selectors qualifying id, classe, or attribute selectors.
3
+ class Linter::QualifyingElement < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_simple_sequence(seq)
7
+ return unless seq_contains_sel_class?(seq, Sass::Selector::Element)
8
+ check_id(seq) unless config['allow_element_with_id']
9
+ check_class(seq) unless config['allow_element_with_class']
10
+ check_attribute(seq) unless config['allow_element_with_attribute']
11
+ end
12
+
13
+ private
14
+
15
+ # Checks if a simple sequence contains a
16
+ # simple selector of a certain class.
17
+ #
18
+ # @param seq [Sass::Selector::SimpleSequence]
19
+ # @param selector_class [Sass::Selector::Simple]
20
+ # @returns [Boolean]
21
+ def seq_contains_sel_class?(seq, selector_class)
22
+ seq.members.any? do |simple|
23
+ simple.is_a?(selector_class)
24
+ end
25
+ end
26
+
27
+ def check_id(seq)
28
+ return unless seq_contains_sel_class?(seq, Sass::Selector::Id)
29
+ add_lint(seq.line, 'Avoid qualifying id selectors with an element.')
30
+ end
31
+
32
+ def check_class(seq)
33
+ return unless seq_contains_sel_class?(seq, Sass::Selector::Class)
34
+ add_lint(seq.line, 'Avoid qualifying class selectors with an element.')
35
+ end
36
+
37
+ def check_attribute(seq)
38
+ return unless seq_contains_sel_class?(seq, Sass::Selector::Attribute)
39
+ add_lint(seq.line, 'Avoid qualifying attribute selectors with an element.')
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,64 @@
1
+ module SCSSLint
2
+ # Checks for selectors with large depths of applicability.
3
+ class Linter::SelectorDepth < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ @max_depth = config['max_depth']
8
+ @depth = 0
9
+ yield # Continue
10
+ end
11
+
12
+ def visit_rule(node)
13
+ old_depth = @depth
14
+ @depth = max_sequence_depth(node.parsed_rules, @depth)
15
+
16
+ if @depth > @max_depth
17
+ add_lint(node.parsed_rules || node,
18
+ 'Selector should have depth of applicability no greater ' \
19
+ "than #{@max_depth}, but was #{@depth}")
20
+ end
21
+
22
+ yield # Continue linting children
23
+ @depth = old_depth
24
+ end
25
+
26
+ private
27
+
28
+ # Find the maximum depth of all sequences in a comma sequence.
29
+ def max_sequence_depth(comma_sequence, current_depth)
30
+ # Sequence contains interpolation; assume a depth of 1
31
+ return current_depth + 1 unless comma_sequence
32
+
33
+ comma_sequence.members.map { |sequence| sequence_depth(sequence, current_depth) }.max
34
+ end
35
+
36
+ def sequence_depth(sequence, current_depth)
37
+ separators, simple_sequences = sequence.members.partition do |item|
38
+ item.is_a?(String)
39
+ end
40
+
41
+ parent_selectors = simple_sequences.count do |item|
42
+ next if item.is_a?(Array) # @keyframe percentages end up as Arrays
43
+ item.rest.any? { |i| i.is_a?(Sass::Selector::Parent) }
44
+ end
45
+
46
+ # Take the number of simple sequences and subtract one for each sibling
47
+ # combinator, as these "combine" simple sequences such that they do not
48
+ # increase depth.
49
+ depth = simple_sequences.size -
50
+ separators.count { |item| item == '~' || item == '+' }
51
+
52
+ if parent_selectors > 0
53
+ # If parent selectors are present, add the current depth for each
54
+ # additional parent selector.
55
+ depth += parent_selectors * (current_depth - 1)
56
+ else
57
+ # Otherwise this just descends from the containing selector
58
+ depth += current_depth
59
+ end
60
+
61
+ depth
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,102 @@
1
+ module SCSSLint
2
+ # Checks that selector names use a specified convention
3
+ class Linter::SelectorFormat < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ @ignored_names = Array(config['ignored_names']).to_set
8
+ @ignored_types = Array(config['ignored_types']).to_set
9
+ yield
10
+ end
11
+
12
+ def visit_attribute(attribute)
13
+ check(attribute, 'attribute') unless @ignored_types.include?('attribute')
14
+ end
15
+
16
+ def visit_class(klass)
17
+ check(klass, 'class') unless @ignored_types.include?('class')
18
+ end
19
+
20
+ def visit_element(element)
21
+ check(element, 'element') unless @ignored_types.include?('element')
22
+ end
23
+
24
+ def visit_id(id)
25
+ check(id, 'id') unless @ignored_types.include?('id')
26
+ end
27
+
28
+ def visit_placeholder(placeholder)
29
+ check(placeholder, 'placeholder') unless @ignored_types.include?('placeholder')
30
+ end
31
+
32
+ private
33
+
34
+ def check(node, type)
35
+ name = node.name
36
+
37
+ return if @ignored_names.include?(name)
38
+ return unless violation = violated_convention(name, type)
39
+
40
+ add_lint(node, "Selector `#{name}` #{violation[:explanation]}")
41
+ end
42
+
43
+ CONVENTIONS = {
44
+ 'hyphenated_lowercase' => {
45
+ explanation: 'should be written in lowercase with hyphens',
46
+ validator: ->(name) { name !~ /[^\-a-z0-9]/ },
47
+ },
48
+ 'snake_case' => {
49
+ explanation: 'should be written in lowercase with underscores',
50
+ validator: ->(name) { name !~ /[^_a-z0-9]/ },
51
+ },
52
+ 'camel_case' => {
53
+ explanation: 'should be written in camelCase format',
54
+ validator: ->(name) { name =~ /^[a-z][a-zA-Z0-9]*$/ },
55
+ },
56
+ 'hyphenated_BEM' => {
57
+ explanation: 'should be written in hyphenated BEM (Block Element Modifier) format',
58
+ validator: ->(name) { name !~ /[A-Z]|-{3}|_{3}|[^_]_[^_]/ },
59
+ },
60
+ 'strict_BEM' => {
61
+ explanation: 'should be written in BEM (Block Element Modifier) format',
62
+ validator: lambda do |name|
63
+ name =~ /
64
+ ^[a-z]([-]?[a-z0-9]+)*
65
+ (__[a-z0-9]([-]?[a-z0-9]+)*)?
66
+ ((_[a-z0-9]([-]?[a-z0-9]+)*){2})?$
67
+ /x
68
+ end,
69
+ },
70
+ }
71
+
72
+ # Checks the given name and returns the violated convention if it failed.
73
+ def violated_convention(name_string, type)
74
+ convention_name = convention_name(type)
75
+
76
+ existing_convention = CONVENTIONS[convention_name]
77
+
78
+ convention = (existing_convention || {
79
+ validator: ->(name) { name =~ /#{convention_name}/ }
80
+ }).merge(
81
+ explanation: convention_explanation(type), # Allow explanation to be customized
82
+ )
83
+
84
+ convention unless convention[:validator].call(name_string)
85
+ end
86
+
87
+ def convention_name(type)
88
+ config["#{type}_convention"] ||
89
+ config['convention'] ||
90
+ 'hyphenated_lowercase'
91
+ end
92
+
93
+ def convention_explanation(type)
94
+ existing_convention = CONVENTIONS[convention_name(type)]
95
+
96
+ config["#{type}_convention_explanation"] ||
97
+ config['convention_explanation'] ||
98
+ (existing_convention && existing_convention[:explanation]) ||
99
+ "should match regex /#{convention_name(type)}/"
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,139 @@
1
+ # rubocop:disable Metrics/CyclomaticComplexity
2
+ module SCSSLint
3
+ # Checks for the use of the shortest form for properties that can be written
4
+ # in shorthand.
5
+ class Linter::Shorthand < Linter
6
+ include LinterRegistry
7
+
8
+ # @param node [Sass::Tree::Node]
9
+ def visit_prop(node)
10
+ property_name = node.name.join
11
+ return unless SHORTHANDABLE_PROPERTIES.include?(property_name)
12
+
13
+ case node.value
14
+ when Sass::Script::Tree::Literal
15
+ check_script_literal(property_name, node.value)
16
+ when Sass::Script::Tree::ListLiteral
17
+ check_script_list(property_name, node.value)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ SHORTHANDABLE_PROPERTIES = %w[
24
+ border-color
25
+ border-radius
26
+ border-style
27
+ border-width
28
+ margin
29
+ padding
30
+ ]
31
+
32
+ # @param prop [String]
33
+ # @param list [Sass::Script::Tree::ListLiteral]
34
+ def check_script_list(prop, list)
35
+ check_shorthand(prop, list, list.children.map(&:to_sass))
36
+ end
37
+
38
+ # @param prop [String]
39
+ # @param literal [Sass::Script::Tree::Literal]
40
+ def check_script_literal(prop, literal)
41
+ value = literal.value
42
+
43
+ # HACK: node_parent may not be initialized at this point, so we need to
44
+ # set it ourselves
45
+ value.node_parent = literal
46
+ return unless value.is_a?(Sass::Script::Value::String)
47
+
48
+ check_script_string(prop, value)
49
+ end
50
+
51
+ LIST_LITERAL_REGEX = /
52
+ \A
53
+ (\S+\s+\S+(\s+\S+){0,2}) # Two to four values separated by spaces
54
+ (\s+!\w+)? # Ignore `!important` priority overrides
55
+ \z
56
+ /x
57
+
58
+ # @param prop [String]
59
+ # @param script_string [Sass::Script::Value::String]
60
+ def check_script_string(prop, script_string)
61
+ return unless script_string.type == :identifier
62
+ return unless values = script_string.value.strip[LIST_LITERAL_REGEX, 1]
63
+
64
+ check_shorthand(prop, script_string, values.split)
65
+ end
66
+
67
+ # @param prop [String]
68
+ # @param node [Sass::Script::Value::String]
69
+ # @param values [Array<String>]
70
+ def check_shorthand(prop, node, values)
71
+ return unless (2..4).member?(values.count)
72
+
73
+ shortest_form = condensed_shorthand(*values)
74
+ return if values == shortest_form
75
+
76
+ add_lint(node, "Shorthand form for property `#{prop}` should be " \
77
+ "written more concisely as `#{shortest_form.join(' ')}` " \
78
+ "instead of `#{values.join(' ')}`")
79
+ end
80
+
81
+ # @param top [String]
82
+ # @param right [String]
83
+ # @param bottom [String]
84
+ # @param left [String]
85
+ # @return [Array]
86
+ def condensed_shorthand(top, right, bottom = nil, left = nil)
87
+ if condense_to_one_value?(top, right, bottom, left)
88
+ [top]
89
+ elsif condense_to_two_values?(top, right, bottom, left)
90
+ [top, right]
91
+ elsif condense_to_three_values?(top, right, bottom, left)
92
+ [top, right, bottom]
93
+ else
94
+ [top, right, bottom, left].compact
95
+ end
96
+ end
97
+
98
+ # @param top [String]
99
+ # @param right [String]
100
+ # @param bottom [String]
101
+ # @param left [String]
102
+ # @return [Boolean]
103
+ def condense_to_one_value?(top, right, bottom, left)
104
+ return unless allowed?(1)
105
+ return unless top == right
106
+
107
+ top == bottom && (bottom == left || left.nil?) ||
108
+ bottom.nil? && left.nil?
109
+ end
110
+
111
+ # @param top [String]
112
+ # @param right [String]
113
+ # @param bottom [String]
114
+ # @param left [String]
115
+ # @return [Boolean]
116
+ def condense_to_two_values?(top, right, bottom, left)
117
+ return unless allowed?(2)
118
+
119
+ top == bottom && right == left ||
120
+ top == bottom && left.nil? && top != right
121
+ end
122
+
123
+ # @param right [String]
124
+ # @param left [String]
125
+ # @return [Boolean]
126
+ def condense_to_three_values?(_, right, __, left)
127
+ return unless allowed?(3)
128
+
129
+ right == left
130
+ end
131
+
132
+ # @param size [Number]
133
+ # @return [Boolean]
134
+ def allowed?(size)
135
+ return false unless config['allowed_shorthands']
136
+ config['allowed_shorthands'].map(&:to_i).include?(size)
137
+ end
138
+ end
139
+ end