scss_lint 0.38.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/bin/scss-lint +6 -0
  3. data/config/default.yml +205 -0
  4. data/data/prefixed-identifiers/base.txt +107 -0
  5. data/data/prefixed-identifiers/bourbon.txt +71 -0
  6. data/data/properties.txt +477 -0
  7. data/data/property-sort-orders/concentric.txt +134 -0
  8. data/data/property-sort-orders/recess.txt +149 -0
  9. data/data/property-sort-orders/smacss.txt +137 -0
  10. data/lib/scss_lint.rb +31 -0
  11. data/lib/scss_lint/cli.rb +215 -0
  12. data/lib/scss_lint/config.rb +251 -0
  13. data/lib/scss_lint/constants.rb +8 -0
  14. data/lib/scss_lint/control_comment_processor.rb +126 -0
  15. data/lib/scss_lint/engine.rb +56 -0
  16. data/lib/scss_lint/exceptions.rb +21 -0
  17. data/lib/scss_lint/file_finder.rb +68 -0
  18. data/lib/scss_lint/lint.rb +24 -0
  19. data/lib/scss_lint/linter.rb +161 -0
  20. data/lib/scss_lint/linter/bang_format.rb +52 -0
  21. data/lib/scss_lint/linter/border_zero.rb +39 -0
  22. data/lib/scss_lint/linter/color_keyword.rb +32 -0
  23. data/lib/scss_lint/linter/color_variable.rb +60 -0
  24. data/lib/scss_lint/linter/comment.rb +21 -0
  25. data/lib/scss_lint/linter/compass.rb +7 -0
  26. data/lib/scss_lint/linter/compass/property_with_mixin.rb +47 -0
  27. data/lib/scss_lint/linter/debug_statement.rb +10 -0
  28. data/lib/scss_lint/linter/declaration_order.rb +71 -0
  29. data/lib/scss_lint/linter/duplicate_property.rb +58 -0
  30. data/lib/scss_lint/linter/else_placement.rb +48 -0
  31. data/lib/scss_lint/linter/empty_line_between_blocks.rb +85 -0
  32. data/lib/scss_lint/linter/empty_rule.rb +11 -0
  33. data/lib/scss_lint/linter/final_newline.rb +20 -0
  34. data/lib/scss_lint/linter/hex_length.rb +56 -0
  35. data/lib/scss_lint/linter/hex_notation.rb +38 -0
  36. data/lib/scss_lint/linter/hex_validation.rb +23 -0
  37. data/lib/scss_lint/linter/id_selector.rb +10 -0
  38. data/lib/scss_lint/linter/import_path.rb +62 -0
  39. data/lib/scss_lint/linter/important_rule.rb +12 -0
  40. data/lib/scss_lint/linter/indentation.rb +197 -0
  41. data/lib/scss_lint/linter/leading_zero.rb +49 -0
  42. data/lib/scss_lint/linter/mergeable_selector.rb +60 -0
  43. data/lib/scss_lint/linter/name_format.rb +117 -0
  44. data/lib/scss_lint/linter/nesting_depth.rb +24 -0
  45. data/lib/scss_lint/linter/placeholder_in_extend.rb +22 -0
  46. data/lib/scss_lint/linter/property_count.rb +44 -0
  47. data/lib/scss_lint/linter/property_sort_order.rb +198 -0
  48. data/lib/scss_lint/linter/property_spelling.rb +49 -0
  49. data/lib/scss_lint/linter/property_units.rb +59 -0
  50. data/lib/scss_lint/linter/qualifying_element.rb +42 -0
  51. data/lib/scss_lint/linter/selector_depth.rb +64 -0
  52. data/lib/scss_lint/linter/selector_format.rb +102 -0
  53. data/lib/scss_lint/linter/shorthand.rb +139 -0
  54. data/lib/scss_lint/linter/single_line_per_property.rb +59 -0
  55. data/lib/scss_lint/linter/single_line_per_selector.rb +35 -0
  56. data/lib/scss_lint/linter/space_after_comma.rb +110 -0
  57. data/lib/scss_lint/linter/space_after_property_colon.rb +92 -0
  58. data/lib/scss_lint/linter/space_after_property_name.rb +27 -0
  59. data/lib/scss_lint/linter/space_before_brace.rb +72 -0
  60. data/lib/scss_lint/linter/space_between_parens.rb +35 -0
  61. data/lib/scss_lint/linter/string_quotes.rb +94 -0
  62. data/lib/scss_lint/linter/trailing_semicolon.rb +67 -0
  63. data/lib/scss_lint/linter/trailing_zero.rb +41 -0
  64. data/lib/scss_lint/linter/unnecessary_mantissa.rb +42 -0
  65. data/lib/scss_lint/linter/unnecessary_parent_reference.rb +49 -0
  66. data/lib/scss_lint/linter/url_format.rb +56 -0
  67. data/lib/scss_lint/linter/url_quotes.rb +27 -0
  68. data/lib/scss_lint/linter/variable_for_property.rb +30 -0
  69. data/lib/scss_lint/linter/vendor_prefix.rb +64 -0
  70. data/lib/scss_lint/linter/zero_unit.rb +39 -0
  71. data/lib/scss_lint/linter_registry.rb +26 -0
  72. data/lib/scss_lint/location.rb +38 -0
  73. data/lib/scss_lint/options.rb +109 -0
  74. data/lib/scss_lint/rake_task.rb +106 -0
  75. data/lib/scss_lint/reporter.rb +18 -0
  76. data/lib/scss_lint/reporter/config_reporter.rb +26 -0
  77. data/lib/scss_lint/reporter/default_reporter.rb +27 -0
  78. data/lib/scss_lint/reporter/files_reporter.rb +8 -0
  79. data/lib/scss_lint/reporter/json_reporter.rb +30 -0
  80. data/lib/scss_lint/reporter/xml_reporter.rb +33 -0
  81. data/lib/scss_lint/runner.rb +51 -0
  82. data/lib/scss_lint/sass/script.rb +78 -0
  83. data/lib/scss_lint/sass/tree.rb +168 -0
  84. data/lib/scss_lint/selector_visitor.rb +34 -0
  85. data/lib/scss_lint/utils.rb +112 -0
  86. data/lib/scss_lint/version.rb +4 -0
  87. data/spec/scss_lint/cli_spec.rb +177 -0
  88. data/spec/scss_lint/config_spec.rb +253 -0
  89. data/spec/scss_lint/engine_spec.rb +24 -0
  90. data/spec/scss_lint/file_finder_spec.rb +134 -0
  91. data/spec/scss_lint/linter/bang_format_spec.rb +121 -0
  92. data/spec/scss_lint/linter/border_zero_spec.rb +118 -0
  93. data/spec/scss_lint/linter/color_keyword_spec.rb +83 -0
  94. data/spec/scss_lint/linter/color_variable_spec.rb +155 -0
  95. data/spec/scss_lint/linter/comment_spec.rb +79 -0
  96. data/spec/scss_lint/linter/compass/property_with_mixin_spec.rb +55 -0
  97. data/spec/scss_lint/linter/debug_statement_spec.rb +21 -0
  98. data/spec/scss_lint/linter/declaration_order_spec.rb +575 -0
  99. data/spec/scss_lint/linter/duplicate_property_spec.rb +189 -0
  100. data/spec/scss_lint/linter/else_placement_spec.rb +106 -0
  101. data/spec/scss_lint/linter/empty_line_between_blocks_spec.rb +276 -0
  102. data/spec/scss_lint/linter/empty_rule_spec.rb +27 -0
  103. data/spec/scss_lint/linter/final_newline_spec.rb +49 -0
  104. data/spec/scss_lint/linter/hex_length_spec.rb +104 -0
  105. data/spec/scss_lint/linter/hex_notation_spec.rb +104 -0
  106. data/spec/scss_lint/linter/hex_validation_spec.rb +40 -0
  107. data/spec/scss_lint/linter/id_selector_spec.rb +62 -0
  108. data/spec/scss_lint/linter/import_path_spec.rb +300 -0
  109. data/spec/scss_lint/linter/important_rule_spec.rb +43 -0
  110. data/spec/scss_lint/linter/indentation_spec.rb +347 -0
  111. data/spec/scss_lint/linter/leading_zero_spec.rb +233 -0
  112. data/spec/scss_lint/linter/mergeable_selector_spec.rb +283 -0
  113. data/spec/scss_lint/linter/name_format_spec.rb +282 -0
  114. data/spec/scss_lint/linter/nesting_depth_spec.rb +114 -0
  115. data/spec/scss_lint/linter/placeholder_in_extend_spec.rb +63 -0
  116. data/spec/scss_lint/linter/property_count_spec.rb +104 -0
  117. data/spec/scss_lint/linter/property_sort_order_spec.rb +482 -0
  118. data/spec/scss_lint/linter/property_spelling_spec.rb +84 -0
  119. data/spec/scss_lint/linter/property_units_spec.rb +229 -0
  120. data/spec/scss_lint/linter/qualifying_element_spec.rb +125 -0
  121. data/spec/scss_lint/linter/selector_depth_spec.rb +159 -0
  122. data/spec/scss_lint/linter/selector_format_spec.rb +632 -0
  123. data/spec/scss_lint/linter/shorthand_spec.rb +198 -0
  124. data/spec/scss_lint/linter/single_line_per_property_spec.rb +73 -0
  125. data/spec/scss_lint/linter/single_line_per_selector_spec.rb +130 -0
  126. data/spec/scss_lint/linter/space_after_comma_spec.rb +332 -0
  127. data/spec/scss_lint/linter/space_after_property_colon_spec.rb +373 -0
  128. data/spec/scss_lint/linter/space_after_property_name_spec.rb +37 -0
  129. data/spec/scss_lint/linter/space_before_brace_spec.rb +829 -0
  130. data/spec/scss_lint/linter/space_between_parens_spec.rb +263 -0
  131. data/spec/scss_lint/linter/string_quotes_spec.rb +335 -0
  132. data/spec/scss_lint/linter/trailing_semicolon_spec.rb +304 -0
  133. data/spec/scss_lint/linter/trailing_zero_spec.rb +176 -0
  134. data/spec/scss_lint/linter/unnecessary_mantissa_spec.rb +67 -0
  135. data/spec/scss_lint/linter/unnecessary_parent_reference_spec.rb +98 -0
  136. data/spec/scss_lint/linter/url_format_spec.rb +55 -0
  137. data/spec/scss_lint/linter/url_quotes_spec.rb +73 -0
  138. data/spec/scss_lint/linter/variable_for_property_spec.rb +145 -0
  139. data/spec/scss_lint/linter/vendor_prefix_spec.rb +371 -0
  140. data/spec/scss_lint/linter/zero_unit_spec.rb +113 -0
  141. data/spec/scss_lint/linter_registry_spec.rb +50 -0
  142. data/spec/scss_lint/linter_spec.rb +292 -0
  143. data/spec/scss_lint/location_spec.rb +42 -0
  144. data/spec/scss_lint/options_spec.rb +34 -0
  145. data/spec/scss_lint/rake_task_spec.rb +43 -0
  146. data/spec/scss_lint/reporter/config_reporter_spec.rb +42 -0
  147. data/spec/scss_lint/reporter/default_reporter_spec.rb +73 -0
  148. data/spec/scss_lint/reporter/files_reporter_spec.rb +38 -0
  149. data/spec/scss_lint/reporter/json_reporter_spec.rb +96 -0
  150. data/spec/scss_lint/reporter/xml_reporter_spec.rb +103 -0
  151. data/spec/scss_lint/reporter_spec.rb +11 -0
  152. data/spec/scss_lint/runner_spec.rb +123 -0
  153. data/spec/scss_lint/selector_visitor_spec.rb +264 -0
  154. data/spec/spec_helper.rb +34 -0
  155. data/spec/support/isolated_environment.rb +25 -0
  156. data/spec/support/matchers/report_lint.rb +48 -0
  157. 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,11 @@
1
+ module SCSSLint
2
+ # Checks for rules with no content.
3
+ class Linter::EmptyRule < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_rule(node)
7
+ add_lint(node, 'Empty rule') if node.children.empty?
8
+ yield # Continue linting children
9
+ end
10
+ end
11
+ 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,10 @@
1
+ module SCSSLint
2
+ # Checks for the use of an ID selector.
3
+ class Linter::IdSelector < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_id(id)
7
+ add_lint(id, 'Avoid using id selectors')
8
+ end
9
+ end
10
+ 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