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