scss_lint 0.38.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/scss-lint +6 -0
- data/config/default.yml +205 -0
- data/data/prefixed-identifiers/base.txt +107 -0
- data/data/prefixed-identifiers/bourbon.txt +71 -0
- data/data/properties.txt +477 -0
- data/data/property-sort-orders/concentric.txt +134 -0
- data/data/property-sort-orders/recess.txt +149 -0
- data/data/property-sort-orders/smacss.txt +137 -0
- data/lib/scss_lint.rb +31 -0
- data/lib/scss_lint/cli.rb +215 -0
- data/lib/scss_lint/config.rb +251 -0
- data/lib/scss_lint/constants.rb +8 -0
- data/lib/scss_lint/control_comment_processor.rb +126 -0
- data/lib/scss_lint/engine.rb +56 -0
- data/lib/scss_lint/exceptions.rb +21 -0
- data/lib/scss_lint/file_finder.rb +68 -0
- data/lib/scss_lint/lint.rb +24 -0
- data/lib/scss_lint/linter.rb +161 -0
- data/lib/scss_lint/linter/bang_format.rb +52 -0
- data/lib/scss_lint/linter/border_zero.rb +39 -0
- data/lib/scss_lint/linter/color_keyword.rb +32 -0
- data/lib/scss_lint/linter/color_variable.rb +60 -0
- data/lib/scss_lint/linter/comment.rb +21 -0
- data/lib/scss_lint/linter/compass.rb +7 -0
- data/lib/scss_lint/linter/compass/property_with_mixin.rb +47 -0
- data/lib/scss_lint/linter/debug_statement.rb +10 -0
- data/lib/scss_lint/linter/declaration_order.rb +71 -0
- data/lib/scss_lint/linter/duplicate_property.rb +58 -0
- data/lib/scss_lint/linter/else_placement.rb +48 -0
- data/lib/scss_lint/linter/empty_line_between_blocks.rb +85 -0
- data/lib/scss_lint/linter/empty_rule.rb +11 -0
- data/lib/scss_lint/linter/final_newline.rb +20 -0
- data/lib/scss_lint/linter/hex_length.rb +56 -0
- data/lib/scss_lint/linter/hex_notation.rb +38 -0
- data/lib/scss_lint/linter/hex_validation.rb +23 -0
- data/lib/scss_lint/linter/id_selector.rb +10 -0
- data/lib/scss_lint/linter/import_path.rb +62 -0
- data/lib/scss_lint/linter/important_rule.rb +12 -0
- data/lib/scss_lint/linter/indentation.rb +197 -0
- data/lib/scss_lint/linter/leading_zero.rb +49 -0
- data/lib/scss_lint/linter/mergeable_selector.rb +60 -0
- data/lib/scss_lint/linter/name_format.rb +117 -0
- data/lib/scss_lint/linter/nesting_depth.rb +24 -0
- data/lib/scss_lint/linter/placeholder_in_extend.rb +22 -0
- data/lib/scss_lint/linter/property_count.rb +44 -0
- data/lib/scss_lint/linter/property_sort_order.rb +198 -0
- data/lib/scss_lint/linter/property_spelling.rb +49 -0
- data/lib/scss_lint/linter/property_units.rb +59 -0
- data/lib/scss_lint/linter/qualifying_element.rb +42 -0
- data/lib/scss_lint/linter/selector_depth.rb +64 -0
- data/lib/scss_lint/linter/selector_format.rb +102 -0
- data/lib/scss_lint/linter/shorthand.rb +139 -0
- data/lib/scss_lint/linter/single_line_per_property.rb +59 -0
- data/lib/scss_lint/linter/single_line_per_selector.rb +35 -0
- data/lib/scss_lint/linter/space_after_comma.rb +110 -0
- data/lib/scss_lint/linter/space_after_property_colon.rb +92 -0
- data/lib/scss_lint/linter/space_after_property_name.rb +27 -0
- data/lib/scss_lint/linter/space_before_brace.rb +72 -0
- data/lib/scss_lint/linter/space_between_parens.rb +35 -0
- data/lib/scss_lint/linter/string_quotes.rb +94 -0
- data/lib/scss_lint/linter/trailing_semicolon.rb +67 -0
- data/lib/scss_lint/linter/trailing_zero.rb +41 -0
- data/lib/scss_lint/linter/unnecessary_mantissa.rb +42 -0
- data/lib/scss_lint/linter/unnecessary_parent_reference.rb +49 -0
- data/lib/scss_lint/linter/url_format.rb +56 -0
- data/lib/scss_lint/linter/url_quotes.rb +27 -0
- data/lib/scss_lint/linter/variable_for_property.rb +30 -0
- data/lib/scss_lint/linter/vendor_prefix.rb +64 -0
- data/lib/scss_lint/linter/zero_unit.rb +39 -0
- data/lib/scss_lint/linter_registry.rb +26 -0
- data/lib/scss_lint/location.rb +38 -0
- data/lib/scss_lint/options.rb +109 -0
- data/lib/scss_lint/rake_task.rb +106 -0
- data/lib/scss_lint/reporter.rb +18 -0
- data/lib/scss_lint/reporter/config_reporter.rb +26 -0
- data/lib/scss_lint/reporter/default_reporter.rb +27 -0
- data/lib/scss_lint/reporter/files_reporter.rb +8 -0
- data/lib/scss_lint/reporter/json_reporter.rb +30 -0
- data/lib/scss_lint/reporter/xml_reporter.rb +33 -0
- data/lib/scss_lint/runner.rb +51 -0
- data/lib/scss_lint/sass/script.rb +78 -0
- data/lib/scss_lint/sass/tree.rb +168 -0
- data/lib/scss_lint/selector_visitor.rb +34 -0
- data/lib/scss_lint/utils.rb +112 -0
- data/lib/scss_lint/version.rb +4 -0
- data/spec/scss_lint/cli_spec.rb +177 -0
- data/spec/scss_lint/config_spec.rb +253 -0
- data/spec/scss_lint/engine_spec.rb +24 -0
- data/spec/scss_lint/file_finder_spec.rb +134 -0
- data/spec/scss_lint/linter/bang_format_spec.rb +121 -0
- data/spec/scss_lint/linter/border_zero_spec.rb +118 -0
- data/spec/scss_lint/linter/color_keyword_spec.rb +83 -0
- data/spec/scss_lint/linter/color_variable_spec.rb +155 -0
- data/spec/scss_lint/linter/comment_spec.rb +79 -0
- data/spec/scss_lint/linter/compass/property_with_mixin_spec.rb +55 -0
- data/spec/scss_lint/linter/debug_statement_spec.rb +21 -0
- data/spec/scss_lint/linter/declaration_order_spec.rb +575 -0
- data/spec/scss_lint/linter/duplicate_property_spec.rb +189 -0
- data/spec/scss_lint/linter/else_placement_spec.rb +106 -0
- data/spec/scss_lint/linter/empty_line_between_blocks_spec.rb +276 -0
- data/spec/scss_lint/linter/empty_rule_spec.rb +27 -0
- data/spec/scss_lint/linter/final_newline_spec.rb +49 -0
- data/spec/scss_lint/linter/hex_length_spec.rb +104 -0
- data/spec/scss_lint/linter/hex_notation_spec.rb +104 -0
- data/spec/scss_lint/linter/hex_validation_spec.rb +40 -0
- data/spec/scss_lint/linter/id_selector_spec.rb +62 -0
- data/spec/scss_lint/linter/import_path_spec.rb +300 -0
- data/spec/scss_lint/linter/important_rule_spec.rb +43 -0
- data/spec/scss_lint/linter/indentation_spec.rb +347 -0
- data/spec/scss_lint/linter/leading_zero_spec.rb +233 -0
- data/spec/scss_lint/linter/mergeable_selector_spec.rb +283 -0
- data/spec/scss_lint/linter/name_format_spec.rb +282 -0
- data/spec/scss_lint/linter/nesting_depth_spec.rb +114 -0
- data/spec/scss_lint/linter/placeholder_in_extend_spec.rb +63 -0
- data/spec/scss_lint/linter/property_count_spec.rb +104 -0
- data/spec/scss_lint/linter/property_sort_order_spec.rb +482 -0
- data/spec/scss_lint/linter/property_spelling_spec.rb +84 -0
- data/spec/scss_lint/linter/property_units_spec.rb +229 -0
- data/spec/scss_lint/linter/qualifying_element_spec.rb +125 -0
- data/spec/scss_lint/linter/selector_depth_spec.rb +159 -0
- data/spec/scss_lint/linter/selector_format_spec.rb +632 -0
- data/spec/scss_lint/linter/shorthand_spec.rb +198 -0
- data/spec/scss_lint/linter/single_line_per_property_spec.rb +73 -0
- data/spec/scss_lint/linter/single_line_per_selector_spec.rb +130 -0
- data/spec/scss_lint/linter/space_after_comma_spec.rb +332 -0
- data/spec/scss_lint/linter/space_after_property_colon_spec.rb +373 -0
- data/spec/scss_lint/linter/space_after_property_name_spec.rb +37 -0
- data/spec/scss_lint/linter/space_before_brace_spec.rb +829 -0
- data/spec/scss_lint/linter/space_between_parens_spec.rb +263 -0
- data/spec/scss_lint/linter/string_quotes_spec.rb +335 -0
- data/spec/scss_lint/linter/trailing_semicolon_spec.rb +304 -0
- data/spec/scss_lint/linter/trailing_zero_spec.rb +176 -0
- data/spec/scss_lint/linter/unnecessary_mantissa_spec.rb +67 -0
- data/spec/scss_lint/linter/unnecessary_parent_reference_spec.rb +98 -0
- data/spec/scss_lint/linter/url_format_spec.rb +55 -0
- data/spec/scss_lint/linter/url_quotes_spec.rb +73 -0
- data/spec/scss_lint/linter/variable_for_property_spec.rb +145 -0
- data/spec/scss_lint/linter/vendor_prefix_spec.rb +371 -0
- data/spec/scss_lint/linter/zero_unit_spec.rb +113 -0
- data/spec/scss_lint/linter_registry_spec.rb +50 -0
- data/spec/scss_lint/linter_spec.rb +292 -0
- data/spec/scss_lint/location_spec.rb +42 -0
- data/spec/scss_lint/options_spec.rb +34 -0
- data/spec/scss_lint/rake_task_spec.rb +43 -0
- data/spec/scss_lint/reporter/config_reporter_spec.rb +42 -0
- data/spec/scss_lint/reporter/default_reporter_spec.rb +73 -0
- data/spec/scss_lint/reporter/files_reporter_spec.rb +38 -0
- data/spec/scss_lint/reporter/json_reporter_spec.rb +96 -0
- data/spec/scss_lint/reporter/xml_reporter_spec.rb +103 -0
- data/spec/scss_lint/reporter_spec.rb +11 -0
- data/spec/scss_lint/runner_spec.rb +123 -0
- data/spec/scss_lint/selector_visitor_spec.rb +264 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/isolated_environment.rb +25 -0
- data/spec/support/matchers/report_lint.rb +48 -0
- 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
|