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