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,52 @@
|
|
|
1
|
+
module SCSSLint
|
|
2
|
+
# Checks spacing of ! declarations, like !important and !default
|
|
3
|
+
class Linter::BangFormat < Linter
|
|
4
|
+
include LinterRegistry
|
|
5
|
+
|
|
6
|
+
STOPPING_CHARACTERS = ['!', "'", '"', nil]
|
|
7
|
+
|
|
8
|
+
def visit_prop(node)
|
|
9
|
+
return unless source_from_range(node.source_range).include?('!')
|
|
10
|
+
return unless check_spacing(node)
|
|
11
|
+
|
|
12
|
+
before_qualifier = config['space_before_bang'] ? '' : 'not '
|
|
13
|
+
after_qualifier = config['space_after_bang'] ? '' : 'not '
|
|
14
|
+
|
|
15
|
+
add_lint(node, "! should #{before_qualifier}be preceeded by a space, " \
|
|
16
|
+
"and should #{after_qualifier}be followed by a space")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Start from the back and move towards the front so that any !important or
|
|
22
|
+
# !default !'s will be found *before* quotation marks. Then we can
|
|
23
|
+
# stop at quotation marks to protect against linting !'s within strings
|
|
24
|
+
# (e.g. `content`)
|
|
25
|
+
def find_bang_offset(range)
|
|
26
|
+
offset = 0
|
|
27
|
+
offset -= 1 until STOPPING_CHARACTERS.include?(character_at(range.end_pos, offset))
|
|
28
|
+
offset
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def is_before_wrong?(range, offset)
|
|
32
|
+
before_expected = config['space_before_bang'] ? / / : /[^ ]/
|
|
33
|
+
before_actual = character_at(range.end_pos, offset - 1)
|
|
34
|
+
(before_actual =~ before_expected).nil?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def is_after_wrong?(range, offset)
|
|
38
|
+
after_expected = config['space_after_bang'] ? / / : /[^ ]/
|
|
39
|
+
after_actual = character_at(range.end_pos, offset + 1)
|
|
40
|
+
(after_actual =~ after_expected).nil?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def check_spacing(node)
|
|
44
|
+
range = node.value_source_range
|
|
45
|
+
offset = find_bang_offset(range)
|
|
46
|
+
|
|
47
|
+
return if character_at(range.end_pos, offset) != '!'
|
|
48
|
+
|
|
49
|
+
is_before_wrong?(range, offset) || is_after_wrong?(range, offset)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module SCSSLint
|
|
2
|
+
# Enforce a particular value for empty borders.
|
|
3
|
+
class Linter::BorderZero < Linter
|
|
4
|
+
include LinterRegistry
|
|
5
|
+
|
|
6
|
+
CONVENTION_TO_PREFERENCE = {
|
|
7
|
+
'zero' => %w[0 none],
|
|
8
|
+
'none' => %w[none 0],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
BORDER_PROPERTIES = %w[
|
|
12
|
+
border
|
|
13
|
+
border-top
|
|
14
|
+
border-right
|
|
15
|
+
border-bottom
|
|
16
|
+
border-left
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
def visit_root(_node)
|
|
20
|
+
@preference = CONVENTION_TO_PREFERENCE[config['convention']]
|
|
21
|
+
yield # Continue linting children
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit_prop(node)
|
|
25
|
+
return unless BORDER_PROPERTIES.include?(node.name.first.to_s)
|
|
26
|
+
check_border(node, node.value.to_sass.strip)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def check_border(node, border)
|
|
32
|
+
return unless %w[0 none].include?(border)
|
|
33
|
+
return if @preference[0] == border
|
|
34
|
+
|
|
35
|
+
add_lint(node, "`border: #{@preference[0]} is preferred over " \
|
|
36
|
+
"`border: #{@preference[1]}`")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module SCSSLint
|
|
2
|
+
# Checks for uses of a color keyword instead of the preferred hexadecimal
|
|
3
|
+
# form.
|
|
4
|
+
class Linter::ColorKeyword < Linter
|
|
5
|
+
include LinterRegistry
|
|
6
|
+
|
|
7
|
+
def visit_script_color(node)
|
|
8
|
+
word = source_from_range(node.source_range)[/([a-z]+)/i, 1]
|
|
9
|
+
add_color_lint(node, word) if color_keyword?(word)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def visit_script_string(node)
|
|
13
|
+
return unless node.type == :identifier
|
|
14
|
+
|
|
15
|
+
remove_quoted_strings(node.value).scan(/(^|\s)([a-z]+)(?=\s|$)/i) do |_, word|
|
|
16
|
+
add_color_lint(node, word) if color_keyword?(word)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def add_color_lint(node, original)
|
|
23
|
+
hex_form = Sass::Script::Value::Color.new(color_keyword_to_code(original)).tap do |color|
|
|
24
|
+
color.options = {} # `inspect` requires options to be set
|
|
25
|
+
end.inspect
|
|
26
|
+
|
|
27
|
+
add_lint(node,
|
|
28
|
+
"Color `#{original}` should be written in hexadecimal form " \
|
|
29
|
+
"as `#{hex_form}`")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module SCSSLint
|
|
2
|
+
# Ensures color literals are used only in variable declarations.
|
|
3
|
+
class Linter::ColorVariable < Linter
|
|
4
|
+
include LinterRegistry
|
|
5
|
+
|
|
6
|
+
def visit_script_color(node)
|
|
7
|
+
return if in_variable_declaration?(node) ||
|
|
8
|
+
in_map_declaration?(node) ||
|
|
9
|
+
in_rgba_function_call?(node)
|
|
10
|
+
|
|
11
|
+
# Source range sometimes includes closing parenthesis, so extract it
|
|
12
|
+
color = source_from_range(node.source_range)[/(#?[a-z0-9]+)/i, 1]
|
|
13
|
+
|
|
14
|
+
record_lint(node, color) if color?(color)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def visit_script_string(node)
|
|
18
|
+
return if literal_string?(node)
|
|
19
|
+
|
|
20
|
+
remove_quoted_strings(node.value)
|
|
21
|
+
.scan(/(^|\s)(#[a-f0-9]+|[a-z]+)(?=\s|$)/i)
|
|
22
|
+
.select { |_, word| color?(word) }
|
|
23
|
+
.each { |_, color| record_lint(node, color) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def record_lint(node, color)
|
|
29
|
+
add_lint node, "Color literals like `#{color}` should only be used in " \
|
|
30
|
+
'variable declarations; they should be referred to via ' \
|
|
31
|
+
'variable everywhere else.'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def literal_string?(script_string)
|
|
35
|
+
return unless script_string.respond_to?(:source_range) &&
|
|
36
|
+
source_range = script_string.source_range
|
|
37
|
+
|
|
38
|
+
# If original source starts with a quote character, it's a string, not a
|
|
39
|
+
# color
|
|
40
|
+
%w[' "].include?(source_from_range(source_range)[0])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def in_variable_declaration?(node)
|
|
44
|
+
parent = node.node_parent
|
|
45
|
+
parent.is_a?(Sass::Script::Tree::Literal) &&
|
|
46
|
+
parent.node_parent.is_a?(Sass::Tree::VariableNode)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def in_rgba_function_call?(node)
|
|
50
|
+
grandparent = node_ancestor(node, 2)
|
|
51
|
+
|
|
52
|
+
grandparent.is_a?(Sass::Script::Tree::Funcall) &&
|
|
53
|
+
grandparent.name == 'rgba'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def in_map_declaration?(node)
|
|
57
|
+
node_ancestor(node, 2).is_a?(Sass::Script::Tree::MapLiteral)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module SCSSLint
|
|
2
|
+
# Checks for uses of renderable comments (/* ... */)
|
|
3
|
+
class Linter::Comment < Linter
|
|
4
|
+
include LinterRegistry
|
|
5
|
+
|
|
6
|
+
def visit_comment(node)
|
|
7
|
+
add_lint(node, 'Use `//` comments everywhere') unless node.invisible? || allowed?(node)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# @param node [CommentNode]
|
|
13
|
+
# @return [Boolean]
|
|
14
|
+
def allowed?(node)
|
|
15
|
+
return false unless config['allowed']
|
|
16
|
+
re = Regexp.new(config['allowed'])
|
|
17
|
+
|
|
18
|
+
node.value.join.match(re)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module SCSSLint
|
|
2
|
+
# Checks for uses of properties where a Compass mixin would be preferred.
|
|
3
|
+
class Linter::Compass::PropertyWithMixin < Linter::Compass
|
|
4
|
+
include LinterRegistry
|
|
5
|
+
|
|
6
|
+
def visit_prop(node)
|
|
7
|
+
check_for_properties_with_mixins(node)
|
|
8
|
+
check_for_inline_block(node)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Set of properties where the Compass mixin version is preferred
|
|
14
|
+
PROPERTIES_WITH_MIXINS = %w[
|
|
15
|
+
background-clip
|
|
16
|
+
background-origin
|
|
17
|
+
border-radius
|
|
18
|
+
box-shadow
|
|
19
|
+
box-sizing
|
|
20
|
+
opacity
|
|
21
|
+
text-shadow
|
|
22
|
+
transform
|
|
23
|
+
].to_set
|
|
24
|
+
|
|
25
|
+
def check_for_properties_with_mixins(node)
|
|
26
|
+
prop_name = node.name.join
|
|
27
|
+
return unless PROPERTIES_WITH_MIXINS.include?(prop_name) &&
|
|
28
|
+
!ignore_compass_mixin?(prop_name)
|
|
29
|
+
|
|
30
|
+
add_lint node, "Use the Compass `#{prop_name}` mixin instead of the property"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check_for_inline_block(node)
|
|
34
|
+
prop_name = node.name.join
|
|
35
|
+
return unless prop_name == 'display' &&
|
|
36
|
+
node.value.to_sass == 'inline-block' &&
|
|
37
|
+
!ignore_compass_mixin?('inline-block')
|
|
38
|
+
|
|
39
|
+
add_lint node,
|
|
40
|
+
'Use the Compass `inline-block` mixin instead of `display: inline-block`'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ignore_compass_mixin?(prop_name)
|
|
44
|
+
config.fetch('ignore', []).include?(prop_name)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module SCSSLint
|
|
2
|
+
# Checks the order of nested items within a rule set.
|
|
3
|
+
class Linter::DeclarationOrder < Linter
|
|
4
|
+
include LinterRegistry
|
|
5
|
+
|
|
6
|
+
def check_order(node)
|
|
7
|
+
check_node(node)
|
|
8
|
+
yield # Continue linting children
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
alias_method :visit_rule, :check_order
|
|
12
|
+
alias_method :visit_mixin, :check_order
|
|
13
|
+
alias_method :visit_media, :check_order
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
MESSAGE =
|
|
18
|
+
'Rule sets should be ordered as follows: '\
|
|
19
|
+
'`@extends`, `@includes` without `@content`, ' \
|
|
20
|
+
'properties, `@includes` with `@content`, ' \
|
|
21
|
+
'nested rule sets'
|
|
22
|
+
|
|
23
|
+
MIXIN_WITH_CONTENT = 'mixin_with_content'
|
|
24
|
+
|
|
25
|
+
DECLARATION_ORDER = [
|
|
26
|
+
Sass::Tree::ExtendNode,
|
|
27
|
+
Sass::Tree::MixinNode,
|
|
28
|
+
Sass::Tree::PropNode,
|
|
29
|
+
MIXIN_WITH_CONTENT,
|
|
30
|
+
Sass::Tree::RuleNode,
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def important_node?(node)
|
|
34
|
+
DECLARATION_ORDER.include?(node.class)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def check_node(node)
|
|
38
|
+
children = node.children.each_with_index
|
|
39
|
+
.select { |n, _| important_node?(n) }
|
|
40
|
+
.map { |n, i| [n, node_declaration_type(n), i] }
|
|
41
|
+
|
|
42
|
+
sorted_children = children.sort do |(_, a_type, i), (_, b_type, j)|
|
|
43
|
+
[DECLARATION_ORDER.index(a_type), i] <=> [DECLARATION_ORDER.index(b_type), j]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
check_children_order(sorted_children, children)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Find the child that is out of place
|
|
50
|
+
def check_children_order(sorted_children, children)
|
|
51
|
+
sorted_children.each_with_index do |sorted_item, index|
|
|
52
|
+
next if sorted_item == children[index]
|
|
53
|
+
|
|
54
|
+
add_lint(sorted_item.first.line,
|
|
55
|
+
"Expected item on line #{sorted_item.first.line} to appear " \
|
|
56
|
+
"before line #{children[index].first.line}. #{MESSAGE}")
|
|
57
|
+
break
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def node_declaration_type(node)
|
|
62
|
+
# If the node has no children, return the class.
|
|
63
|
+
return node.class unless node.has_children
|
|
64
|
+
|
|
65
|
+
# If the node is a mixin with children, indicate that;
|
|
66
|
+
# otherwise, just return the class.
|
|
67
|
+
return node.class unless node.is_a?(Sass::Tree::MixinNode)
|
|
68
|
+
MIXIN_WITH_CONTENT
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module SCSSLint
|
|
2
|
+
# Checks for a property declared twice in a rule set.
|
|
3
|
+
class Linter::DuplicateProperty < Linter
|
|
4
|
+
include LinterRegistry
|
|
5
|
+
|
|
6
|
+
def check_properties(node)
|
|
7
|
+
static_properties(node).each_with_object({}) do |prop, prop_names|
|
|
8
|
+
prop_key = property_key(prop)
|
|
9
|
+
|
|
10
|
+
if existing_prop = prop_names[prop_key]
|
|
11
|
+
add_lint(prop, "Property `#{existing_prop.name.join}` already "\
|
|
12
|
+
"defined on line #{existing_prop.line}")
|
|
13
|
+
else
|
|
14
|
+
prop_names[prop_key] = prop
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
yield # Continue linting children
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
alias_method :visit_rule, :check_properties
|
|
22
|
+
alias_method :visit_mixindef, :check_properties
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def static_properties(node)
|
|
27
|
+
node.children
|
|
28
|
+
.select { |child| child.is_a?(Sass::Tree::PropNode) }
|
|
29
|
+
.reject { |prop| prop.name.any? { |item| item.is_a?(Sass::Script::Node) } }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns a key identifying the bucket this property and value correspond to
|
|
33
|
+
# for purposes of uniqueness.
|
|
34
|
+
def property_key(prop)
|
|
35
|
+
prop_key = prop.name.join
|
|
36
|
+
prop_value = property_value(prop)
|
|
37
|
+
|
|
38
|
+
# Differentiate between values for different vendor prefixes
|
|
39
|
+
prop_value.to_s.scan(/^(-[^-]+-.+)/) do |vendor_keyword|
|
|
40
|
+
prop_key << vendor_keyword.first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
prop_key
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def property_value(prop)
|
|
47
|
+
case prop.value
|
|
48
|
+
when Sass::Script::Funcall
|
|
49
|
+
prop.value.name
|
|
50
|
+
when Sass::Script::String
|
|
51
|
+
when Sass::Script::Tree::Literal
|
|
52
|
+
prop.value.value
|
|
53
|
+
else
|
|
54
|
+
prop.value.to_s
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module SCSSLint
|
|
2
|
+
# Checks where `@else` and `@else if` directives are placed with respect to
|
|
3
|
+
# the previous curly brace.
|
|
4
|
+
class Linter::ElsePlacement < Linter
|
|
5
|
+
include LinterRegistry
|
|
6
|
+
|
|
7
|
+
def visit_if(node)
|
|
8
|
+
visit_else(node, node.else) if node.else
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def visit_else(if_node, else_node)
|
|
12
|
+
# Check each @else branch if there are multiple `@else if`s
|
|
13
|
+
visit_else(else_node, else_node.else) if else_node.else
|
|
14
|
+
|
|
15
|
+
# Skip @else statements on the same line as the previous @if, since we
|
|
16
|
+
# don't care about placement in that case
|
|
17
|
+
return if if_node.line == else_node.line
|
|
18
|
+
|
|
19
|
+
spaces = 0
|
|
20
|
+
while (char = character_at(else_node.source_range.start_pos, - (spaces + 1)))
|
|
21
|
+
if char == '}'
|
|
22
|
+
curly_on_same_line = true
|
|
23
|
+
break
|
|
24
|
+
end
|
|
25
|
+
spaces += 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
check_placement(else_node, curly_on_same_line)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def check_placement(else_node, curly_on_same_line)
|
|
34
|
+
if same_line_preferred?
|
|
35
|
+
unless curly_on_same_line
|
|
36
|
+
add_lint(else_node,
|
|
37
|
+
'@else should be placed on same line as previous curly brace')
|
|
38
|
+
end
|
|
39
|
+
elsif curly_on_same_line
|
|
40
|
+
add_lint(else_node, '@else should be placed on its own line')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def same_line_preferred?
|
|
45
|
+
config['style'] == 'same_line'
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|