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,85 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Reports the lack of empty lines between block defintions.
|
3
|
+
class Linter::EmptyLineBetweenBlocks < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_function(node)
|
7
|
+
check(node, '@function')
|
8
|
+
yield
|
9
|
+
end
|
10
|
+
|
11
|
+
def visit_mixin(node)
|
12
|
+
# Ignore @includes which don't have any block content
|
13
|
+
check(node, '@include') if node.children
|
14
|
+
.any? { |child| child.is_a?(Sass::Tree::Node) }
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
|
18
|
+
def visit_mixindef(node)
|
19
|
+
check(node, '@mixin')
|
20
|
+
yield
|
21
|
+
end
|
22
|
+
|
23
|
+
def visit_rule(node)
|
24
|
+
check(node, 'Rule')
|
25
|
+
yield
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
MESSAGE_FORMAT = '%s declaration should be %s by an empty line'
|
31
|
+
|
32
|
+
def check(node, type)
|
33
|
+
return if config['ignore_single_line_blocks'] && node_on_single_line?(node)
|
34
|
+
check_preceding_node(node, type)
|
35
|
+
check_following_node(node, type)
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_following_node(node, type)
|
39
|
+
return unless (following_node = next_node(node)) &&
|
40
|
+
(next_start_line = following_node.line)
|
41
|
+
|
42
|
+
# Special case: ignore comments immediately after a closing brace
|
43
|
+
line = engine.lines[next_start_line - 1].strip
|
44
|
+
return if following_node.is_a?(Sass::Tree::CommentNode) &&
|
45
|
+
line =~ %r{\s*\}?\s*/(/|\*)}
|
46
|
+
|
47
|
+
# Otherwise check if line before the next node's starting line is blank
|
48
|
+
line = engine.lines[next_start_line - 2].strip
|
49
|
+
return if line.empty?
|
50
|
+
|
51
|
+
add_lint(next_start_line - 1, MESSAGE_FORMAT % [type, 'followed'])
|
52
|
+
end
|
53
|
+
|
54
|
+
# In cases where the previous node is not a block declaration, we won't
|
55
|
+
# have run any checks against it, so we need to check here if the previous
|
56
|
+
# line is an empty line
|
57
|
+
def check_preceding_node(node, type)
|
58
|
+
case prev_node(node)
|
59
|
+
when
|
60
|
+
nil,
|
61
|
+
Sass::Tree::FunctionNode,
|
62
|
+
Sass::Tree::MixinNode,
|
63
|
+
Sass::Tree::MixinDefNode,
|
64
|
+
Sass::Tree::RuleNode,
|
65
|
+
Sass::Tree::CommentNode
|
66
|
+
# Ignore
|
67
|
+
else
|
68
|
+
unless engine.lines[node.line - 2].strip.empty?
|
69
|
+
add_lint(node.line, MESSAGE_FORMAT % [type, 'preceded'])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def next_node(node)
|
75
|
+
return unless siblings = node_siblings(node)
|
76
|
+
siblings[siblings.index(node) + 1] if siblings.count > 1
|
77
|
+
end
|
78
|
+
|
79
|
+
def prev_node(node)
|
80
|
+
return unless siblings = node_siblings(node)
|
81
|
+
index = siblings.index(node)
|
82
|
+
siblings[index - 1] if index > 0 && siblings.count > 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks for final newlines at the end of a file.
|
3
|
+
class Linter::FinalNewline < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_root(_node)
|
7
|
+
return if engine.lines.empty?
|
8
|
+
|
9
|
+
ends_with_newline = engine.lines[-1][-1] == "\n"
|
10
|
+
|
11
|
+
if config['present']
|
12
|
+
add_lint(engine.lines.count,
|
13
|
+
'Files should end with a trailing newline') unless ends_with_newline
|
14
|
+
else
|
15
|
+
add_lint(engine.lines.count,
|
16
|
+
'Files should not end with a trailing newline') if ends_with_newline
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks that hexadecimal colors are written in the desired number of
|
3
|
+
# characters.
|
4
|
+
class Linter::HexLength < Linter
|
5
|
+
include LinterRegistry
|
6
|
+
|
7
|
+
HEX_REGEX = /(#(\h{3}|\h{6}))(?!\h)/
|
8
|
+
|
9
|
+
def visit_script_color(node)
|
10
|
+
return unless hex = source_from_range(node.source_range)[HEX_REGEX, 1]
|
11
|
+
check_hex(hex, node)
|
12
|
+
end
|
13
|
+
|
14
|
+
def visit_script_string(node)
|
15
|
+
return unless node.type == :identifier
|
16
|
+
|
17
|
+
node.value.scan(HEX_REGEX) do |match|
|
18
|
+
check_hex(match.first, node)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def check_hex(hex, node)
|
25
|
+
return if expected(hex) == hex
|
26
|
+
|
27
|
+
add_lint(node, "Color `#{hex}` should be written as `#{expected(hex)}`")
|
28
|
+
end
|
29
|
+
|
30
|
+
def expected(hex)
|
31
|
+
return short_hex_form(hex) if can_be_shorter?(hex) && short_style?
|
32
|
+
return long_hex_form(hex) if hex.length == 4 && !short_style?
|
33
|
+
|
34
|
+
hex
|
35
|
+
end
|
36
|
+
|
37
|
+
def can_be_shorter?(hex)
|
38
|
+
hex.length == 7 &&
|
39
|
+
hex[1] == hex[2] &&
|
40
|
+
hex[3] == hex[4] &&
|
41
|
+
hex[5] == hex[6]
|
42
|
+
end
|
43
|
+
|
44
|
+
def short_hex_form(hex)
|
45
|
+
[hex[0..1], hex[3], hex[5]].join
|
46
|
+
end
|
47
|
+
|
48
|
+
def long_hex_form(hex)
|
49
|
+
[hex[0..1], hex[1], hex[2], hex[2], hex[3], hex[3]].join
|
50
|
+
end
|
51
|
+
|
52
|
+
def short_style?
|
53
|
+
config['style'] == 'short'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks if hexadecimal colors are written lowercase / uppercase.
|
3
|
+
class Linter::HexNotation < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
HEX_REGEX = /(#(\h{3}|\h{6}))(?!\h)/
|
7
|
+
|
8
|
+
def visit_script_color(node)
|
9
|
+
return unless hex = source_from_range(node.source_range)[HEX_REGEX, 1]
|
10
|
+
check_hex(hex, node)
|
11
|
+
end
|
12
|
+
|
13
|
+
def visit_script_string(node)
|
14
|
+
return unless node.type == :identifier
|
15
|
+
|
16
|
+
node.value.scan(HEX_REGEX) do |match|
|
17
|
+
check_hex(match.first, node)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def check_hex(hex, node)
|
24
|
+
return if expected(hex) == hex
|
25
|
+
|
26
|
+
add_lint(node, "Color `#{hex}` should be written as `#{expected(hex)}`")
|
27
|
+
end
|
28
|
+
|
29
|
+
def expected(color)
|
30
|
+
return color.downcase if lowercase_style?
|
31
|
+
color.upcase
|
32
|
+
end
|
33
|
+
|
34
|
+
def lowercase_style?
|
35
|
+
config['style'] == 'lowercase'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks for invalid hexadecimal colors.
|
3
|
+
class Linter::HexValidation < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_script_string(node)
|
7
|
+
return unless node.type == :identifier
|
8
|
+
|
9
|
+
node.value.scan(/(#\h+)/) do |match|
|
10
|
+
check_hex(match.first, node)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
HEX_REGEX = /(#(\h{3}|\h{6}|\h{8}))(?!\h)/
|
17
|
+
|
18
|
+
def check_hex(hex, node)
|
19
|
+
return if HEX_REGEX.match(hex)
|
20
|
+
add_lint(node, "Colors must have either three or six digits: `#{hex}`")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks formatting of the basenames of @imported partials
|
3
|
+
class Linter::ImportPath < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_import(node)
|
7
|
+
# Ignore CSS imports
|
8
|
+
return if File.extname(node.imported_filename) == '.css'
|
9
|
+
basename = File.basename(node.imported_filename)
|
10
|
+
return if underscore_ok?(basename) && extension_ok?(basename)
|
11
|
+
add_lint(node, compose_message(node.imported_filename))
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Checks if the presence or absence of a leading underscore
|
17
|
+
# on a string is ok, given config option.
|
18
|
+
#
|
19
|
+
# @param str [String] the string to check
|
20
|
+
# @return [Boolean]
|
21
|
+
def underscore_ok?(str)
|
22
|
+
underscore_exists = str.start_with?('_')
|
23
|
+
config['leading_underscore'] ? underscore_exists : !underscore_exists
|
24
|
+
end
|
25
|
+
|
26
|
+
# Checks if the presence or absence of an `scss` filename
|
27
|
+
# extension on a string is ok, given config option.
|
28
|
+
#
|
29
|
+
# @param str [String] the string to check
|
30
|
+
# @return [Boolean]
|
31
|
+
def extension_ok?(str)
|
32
|
+
extension_exists = str.end_with?('.scss')
|
33
|
+
config['filename_extension'] ? extension_exists : !extension_exists
|
34
|
+
end
|
35
|
+
|
36
|
+
# Composes a helpful lint message based on the original filename
|
37
|
+
# and the config options.
|
38
|
+
#
|
39
|
+
# @param orig_filename [String] the original filename
|
40
|
+
# @return [String] the helpful lint message
|
41
|
+
def compose_message(orig_filename)
|
42
|
+
orig_basename = File.basename(orig_filename)
|
43
|
+
fixed_basename = orig_basename
|
44
|
+
|
45
|
+
if config['leading_underscore']
|
46
|
+
fixed_basename = '_' + fixed_basename unless fixed_basename.match(/^_/)
|
47
|
+
else
|
48
|
+
fixed_basename = fixed_basename.sub(/^_/, '')
|
49
|
+
end
|
50
|
+
|
51
|
+
if config['filename_extension']
|
52
|
+
fixed_basename += '.scss' unless fixed_basename.match(/\.scss$/)
|
53
|
+
else
|
54
|
+
fixed_basename = fixed_basename.sub(/\.scss$/, '')
|
55
|
+
end
|
56
|
+
|
57
|
+
fixed_filename = orig_filename.sub(/(.*)#{Regexp.quote(orig_basename)}/,
|
58
|
+
"\\1#{fixed_basename}")
|
59
|
+
"Imported partial `#{orig_filename}` should be written as `#{fixed_filename}`"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Reports the use of !important in properties.
|
3
|
+
class Linter::ImportantRule < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_prop(node)
|
7
|
+
return unless source_from_range(node.source_range).include?('!important')
|
8
|
+
|
9
|
+
add_lint(node, '!important should not be used')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module SCSSLint
|
2
|
+
# Checks for consistent indentation of nested declarations and rule sets.
|
3
|
+
class Linter::Indentation < Linter # rubocop:disable ClassLength
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_root(_node)
|
7
|
+
@indent_width = config['width'].to_i
|
8
|
+
@indent_character = config['character'] || 'space'
|
9
|
+
@indent = 0
|
10
|
+
yield
|
11
|
+
end
|
12
|
+
|
13
|
+
def check_and_visit_children(node)
|
14
|
+
# Don't continue checking children as the moment a parent's indentation is
|
15
|
+
# off it's likely the children will be as will. We don't display the child
|
16
|
+
# indentation problems as that would likely make the lint too noisy.
|
17
|
+
return if check_indentation(node)
|
18
|
+
|
19
|
+
@indent += @indent_width
|
20
|
+
yield
|
21
|
+
@indent -= @indent_width
|
22
|
+
end
|
23
|
+
|
24
|
+
def check_indentation(node)
|
25
|
+
return unless node.line
|
26
|
+
|
27
|
+
# Ignore the case where the node is on the same line as its previous
|
28
|
+
# sibling or its parent, as indentation isn't possible
|
29
|
+
return if nodes_on_same_line?(previous_node(node), node)
|
30
|
+
|
31
|
+
if @indent_character == 'tab'
|
32
|
+
other_character = ' '
|
33
|
+
other_character_name = 'space'
|
34
|
+
else
|
35
|
+
other_character = "\t"
|
36
|
+
other_character_name = 'tab'
|
37
|
+
end
|
38
|
+
|
39
|
+
check_indent_width(node, other_character, @indent_character, other_character_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def check_indent_width(node, other_character, character_name, other_character_name)
|
43
|
+
actual_indent = node_indent(node)
|
44
|
+
|
45
|
+
if actual_indent.include?(other_character)
|
46
|
+
add_lint(node.line,
|
47
|
+
"Line should be indented with #{character_name}s, " \
|
48
|
+
"not #{other_character_name}s")
|
49
|
+
return true
|
50
|
+
end
|
51
|
+
|
52
|
+
if config['allow_non_nested_indentation']
|
53
|
+
check_arbitrary_indent(node, actual_indent.length, character_name)
|
54
|
+
else
|
55
|
+
check_regular_indent(node, actual_indent.length, character_name)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Deal with `else` statements
|
60
|
+
def visit_if(node, &block)
|
61
|
+
check_and_visit_children(node, &block)
|
62
|
+
visit(node.else) if node.else
|
63
|
+
end
|
64
|
+
|
65
|
+
# Need to define this explicitly since @at-root directives can contain
|
66
|
+
# inline selectors which produces the same parse tree as if the selector was
|
67
|
+
# nested within it. For example:
|
68
|
+
#
|
69
|
+
# @at-root {
|
70
|
+
# .something {
|
71
|
+
# ...
|
72
|
+
# }
|
73
|
+
# }
|
74
|
+
#
|
75
|
+
# ...and...
|
76
|
+
#
|
77
|
+
# @at-root .something {
|
78
|
+
# ...
|
79
|
+
# }
|
80
|
+
#
|
81
|
+
# ...produce the same parse tree, but result in different indentation
|
82
|
+
# levels.
|
83
|
+
def visit_atroot(node, &block)
|
84
|
+
if at_root_contains_inline_selector?(node)
|
85
|
+
return if check_indentation(node)
|
86
|
+
yield
|
87
|
+
else
|
88
|
+
check_and_visit_children(node, &block)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Define node types that increase indentation level
|
93
|
+
alias_method :visit_directive, :check_and_visit_children
|
94
|
+
alias_method :visit_each, :check_and_visit_children
|
95
|
+
alias_method :visit_for, :check_and_visit_children
|
96
|
+
alias_method :visit_function, :check_and_visit_children
|
97
|
+
alias_method :visit_media, :check_and_visit_children
|
98
|
+
alias_method :visit_mixin, :check_and_visit_children
|
99
|
+
alias_method :visit_mixindef, :check_and_visit_children
|
100
|
+
alias_method :visit_prop, :check_and_visit_children
|
101
|
+
alias_method :visit_rule, :check_and_visit_children
|
102
|
+
alias_method :visit_supports, :check_and_visit_children
|
103
|
+
alias_method :visit_while, :check_and_visit_children
|
104
|
+
|
105
|
+
# Define node types to check indentation of (notice comments are left out)
|
106
|
+
alias_method :visit_charset, :check_indentation
|
107
|
+
alias_method :visit_content, :check_indentation
|
108
|
+
alias_method :visit_cssimport, :check_indentation
|
109
|
+
alias_method :visit_extend, :check_indentation
|
110
|
+
alias_method :visit_import, :check_indentation
|
111
|
+
alias_method :visit_return, :check_indentation
|
112
|
+
alias_method :visit_variable, :check_indentation
|
113
|
+
alias_method :visit_warn, :check_indentation
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def nodes_on_same_line?(node1, node2)
|
118
|
+
return unless node1
|
119
|
+
|
120
|
+
node1.line == node2.line ||
|
121
|
+
(node1.source_range && node1.source_range.end_pos.line == node2.line)
|
122
|
+
end
|
123
|
+
|
124
|
+
def at_root_contains_inline_selector?(node)
|
125
|
+
return unless node.children.any?
|
126
|
+
return unless first_child_source = node.children.first.source_range
|
127
|
+
|
128
|
+
same_position?(node.source_range.end_pos, first_child_source.start_pos)
|
129
|
+
end
|
130
|
+
|
131
|
+
def check_regular_indent(node, actual_indent, character_name)
|
132
|
+
return if actual_indent == @indent
|
133
|
+
|
134
|
+
add_lint(node.line,
|
135
|
+
"Line should be indented #{@indent} #{character_name}s, " \
|
136
|
+
"but was indented #{actual_indent} #{character_name}s")
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
def check_arbitrary_indent(node, actual_indent, character_name) # rubocop:disable CyclomaticComplexity, MethodLength, LineLength
|
141
|
+
# Allow rulesets to be indented any amount when the indent is zero, as
|
142
|
+
# long as it's a multiple of the indent width
|
143
|
+
if ruleset_under_root_node?(node)
|
144
|
+
unless actual_indent % @indent_width == 0
|
145
|
+
add_lint(node.line,
|
146
|
+
"Line must be indented a multiple of #{@indent_width} " \
|
147
|
+
"#{character_name}s, but was indented #{actual_indent} #{character_name}s")
|
148
|
+
return true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
if @indent == 0
|
153
|
+
unless node.is_a?(Sass::Tree::RuleNode) || actual_indent == 0
|
154
|
+
add_lint(node.line,
|
155
|
+
"Line should be indented 0 #{character_name}s, " \
|
156
|
+
"but was indented #{actual_indent} #{character_name}s")
|
157
|
+
return true
|
158
|
+
end
|
159
|
+
elsif !one_shift_greater_than_parent?(node, actual_indent)
|
160
|
+
parent_indent = node_indent(node.node_parent).length
|
161
|
+
expected_indent = parent_indent + @indent_width
|
162
|
+
|
163
|
+
add_lint(node.line,
|
164
|
+
"Line should be indented #{expected_indent} #{character_name}s, " \
|
165
|
+
"but was indented #{actual_indent} #{character_name}s")
|
166
|
+
return true
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns whether node is a ruleset not nested within any other ruleset.
|
171
|
+
#
|
172
|
+
# @param node [Sass::Tree::Node]
|
173
|
+
# @return [true,false]
|
174
|
+
def ruleset_under_root_node?(node)
|
175
|
+
@indent == 0 && node.is_a?(Sass::Tree::RuleNode)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns whether node is indented exactly one indent width greater than its
|
179
|
+
# parent.
|
180
|
+
#
|
181
|
+
# @param node [Sass::Tree::Node]
|
182
|
+
# @return [true,false]
|
183
|
+
def one_shift_greater_than_parent?(node, actual_indent)
|
184
|
+
parent_indent = node_indent(node.node_parent).length
|
185
|
+
expected_indent = parent_indent + @indent_width
|
186
|
+
expected_indent == actual_indent
|
187
|
+
end
|
188
|
+
|
189
|
+
# Return indentation of a node.
|
190
|
+
#
|
191
|
+
# @param node [Sass::Tree::Node]
|
192
|
+
# @return [Integer]
|
193
|
+
def node_indent(node)
|
194
|
+
engine.lines[node.line - 1][/^(\s*)/, 1]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|