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