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