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,59 @@
1
+ module SCSSLint
2
+ # Checks that all properties in a rule set are on their own distinct lines.
3
+ class Linter::SingleLinePerProperty < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_rule(node) # rubocop:disable CyclomaticComplexity
7
+ single_line = single_line_rule_set?(node)
8
+ return if single_line && config['allow_single_line_rule_sets']
9
+
10
+ properties = node.children.select { |child| child.is_a?(Sass::Tree::PropNode) }
11
+ return unless properties.any?
12
+
13
+ # Special case: if single line rule sets aren't allowed, we want to report
14
+ # when the first property isn't on a separate line from the selector
15
+ if single_line && !config['allow_single_line_rule_sets']
16
+ add_lint(properties.first,
17
+ "Property '#{properties.first.name.join}' should be placed " \
18
+ 'on separate line from selector')
19
+ end
20
+
21
+ check_adjacent_properties(properties)
22
+ end
23
+
24
+ private
25
+
26
+ # Return whether this rule set occupies a single line.
27
+ #
28
+ # Note that this allows:
29
+ # a,
30
+ # b,
31
+ # i { margin: 0; padding: 0; }
32
+ #
33
+ # and:
34
+ #
35
+ # p { margin: 0; padding: 0; }
36
+ #
37
+ # In other words, the line of the opening curly brace is the line that the
38
+ # rule set is considered to occupy.
39
+ def single_line_rule_set?(rule)
40
+ rule.children.all? { |child| child.line == rule.source_range.end_pos.line }
41
+ end
42
+
43
+ def first_property_not_on_own_line?(rule, properties)
44
+ properties.any? && properties.first.line == rule.line
45
+ end
46
+
47
+ # Compare each property against the next property to see if they are on
48
+ # the same line.
49
+ #
50
+ # @param properties [Array<Sass::Tree::PropNode>]
51
+ def check_adjacent_properties(properties)
52
+ properties[0..-2].zip(properties[1..-1]).each do |first, second|
53
+ next unless first.line == second.line
54
+
55
+ add_lint(second, "Property '#{second.name.join}' should be placed on own line")
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ module SCSSLint
2
+ # Checks that selector sequences are split over multiple lines by comma.
3
+ class Linter::SingleLinePerSelector < Linter
4
+ include LinterRegistry
5
+
6
+ MESSAGE = 'Each selector in a comma sequence should be on its own line'
7
+
8
+ def visit_comma_sequence(node)
9
+ return unless node.members.count > 1
10
+
11
+ check_comma_on_own_line(node)
12
+
13
+ node.members[1..-1].each_with_index do |sequence, index|
14
+ check_sequence_commas(node, sequence, index)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def check_comma_on_own_line(node)
21
+ return unless node.members[0].members[1] == "\n"
22
+ add_lint(node, MESSAGE)
23
+ end
24
+
25
+ def check_sequence_commas(node, sequence, index)
26
+ if sequence.members[0] != "\n"
27
+ # Next sequence doesn't reside on its own line
28
+ add_lint(node.line + index, MESSAGE)
29
+ elsif sequence.members[1] == "\n"
30
+ # Comma is on its own line
31
+ add_lint(node.line + index, MESSAGE)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,110 @@
1
+ module SCSSLint
2
+ # Checks for spaces after commas in argument lists.
3
+ class Linter::SpaceAfterComma < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_mixindef(node)
7
+ check_definition(node, :mixin)
8
+ yield
9
+ end
10
+
11
+ def visit_mixin(node)
12
+ check_invocation(node, :mixin)
13
+ yield
14
+ end
15
+
16
+ def visit_function(node)
17
+ check_definition(node, :function)
18
+ yield
19
+ end
20
+
21
+ def visit_script_funcall(node)
22
+ check_invocation(node, :function)
23
+ yield
24
+ end
25
+
26
+ def visit_script_listliteral(node)
27
+ check_commas_after_args(node.elements, 'lists') if node.separator == :comma
28
+ yield
29
+ end
30
+
31
+ private
32
+
33
+ # Check parameters of a function/mixin definition
34
+ def check_definition(node, type)
35
+ # Use the default value's source range if one is defined, since that will
36
+ # be the item with the comma after it
37
+ args = node.args.map { |name, default_value| default_value || name }
38
+ args << node.splat if node.splat
39
+
40
+ check_commas_after_args(args, "#{type} parameters")
41
+ end
42
+
43
+ # Check arguments passed to a function/mixin invocation
44
+ def check_invocation(node, type)
45
+ args = sort_args_by_position(node.args,
46
+ node.splat,
47
+ node.keywords.values,
48
+ node.kwarg_splat)
49
+
50
+ check_commas_after_args(args, "#{type} arguments")
51
+ end
52
+
53
+ # Since keyword arguments are not guaranteed to be in order, use the source
54
+ # range to order arguments so we check them in the order they were declared.
55
+ def sort_args_by_position(*args)
56
+ args.flatten.compact.sort_by do |arg|
57
+ pos = arg.source_range.end_pos
58
+ [pos.line, pos.offset]
59
+ end
60
+ end
61
+
62
+ EXPECTED_SPACES_AFTER_COMMA = 1
63
+
64
+ # Check the comma after each argument in a list for a space following it,
65
+ # reporting a lint using the given [arg_type].
66
+ def check_commas_after_args(args, arg_type)
67
+ # For each arg except the last, check the character following the comma
68
+ args[0..-2].each do |arg|
69
+ # Sometimes the line we're looking at doesn't even contain a comma!
70
+ next unless engine.lines[arg.line - 1].include?(',')
71
+
72
+ offset = find_comma_offset(arg)
73
+
74
+ # Check for space or newline after comma (we allow arguments to be split
75
+ # up over multiple lines).
76
+ spaces = 0
77
+ while (char = character_at(arg.source_range.end_pos, offset + 1)) == ' '
78
+ spaces += 1
79
+ offset += 1
80
+ end
81
+ next if char == "\n" || # Ignore trailing spaces
82
+ spaces == EXPECTED_SPACES_AFTER_COMMA
83
+
84
+ add_lint arg, "Commas in #{arg_type} should be followed by a single space"
85
+ end
86
+ end
87
+
88
+ # Find the comma following this argument.
89
+ #
90
+ # The Sass parser is unpredictable in where it marks the end of the
91
+ # source range. Thus we need to start at the indicated range, and check
92
+ # left and right of that range, gradually moving further outward until
93
+ # we find the comma.
94
+ def find_comma_offset(arg)
95
+ offset = 0
96
+
97
+ if character_at(arg.source_range.end_pos, offset) != ','
98
+ loop do
99
+ offset += 1
100
+ break if character_at(arg.source_range.end_pos, offset) == ','
101
+ offset = -offset
102
+ break if character_at(arg.source_range.end_pos, offset) == ','
103
+ offset = -offset
104
+ end
105
+ end
106
+
107
+ offset
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,92 @@
1
+ module SCSSLint
2
+ # Checks for spaces following the colon that separates a property's name from
3
+ # its value.
4
+ class Linter::SpaceAfterPropertyColon < Linter
5
+ include LinterRegistry
6
+
7
+ def visit_rule(node)
8
+ if config['style'] == 'aligned'
9
+ check_properties_alignment(node)
10
+ end
11
+
12
+ yield # Continue linting children
13
+ end
14
+
15
+ def visit_prop(node)
16
+ whitespace = whitespace_after_colon(node)
17
+
18
+ case config['style']
19
+ when 'no_space'
20
+ check_for_no_spaces(node, whitespace)
21
+ when 'one_space'
22
+ check_for_one_space(node, whitespace)
23
+ when 'at_least_one_space'
24
+ check_for_at_least_one_space(node, whitespace)
25
+ when 'one_space_or_newline'
26
+ check_for_one_space_or_newline(node, whitespace)
27
+ end
28
+
29
+ yield # Continue linting children
30
+ end
31
+
32
+ private
33
+
34
+ def check_for_no_spaces(node, whitespace)
35
+ return if whitespace == []
36
+ add_lint(node, 'Colon after property should not be followed by any spaces')
37
+ end
38
+
39
+ def check_for_one_space(node, whitespace)
40
+ return if whitespace == [' ']
41
+ add_lint(node, 'Colon after property should be followed by one space')
42
+ end
43
+
44
+ def check_for_at_least_one_space(node, whitespace)
45
+ return if whitespace.uniq == [' ']
46
+ add_lint(node, 'Colon after property should be followed by at least one space')
47
+ end
48
+
49
+ def check_for_one_space_or_newline(node, whitespace)
50
+ return if whitespace == [' '] || whitespace == ["\n"]
51
+ return if whitespace[0] == "\n" && whitespace[1..-1].uniq == [' ']
52
+ add_lint(node, 'Colon after property should be followed by one space or a newline')
53
+ end
54
+
55
+ def check_properties_alignment(rule_node)
56
+ properties = rule_node.children.select { |node| node.is_a?(Sass::Tree::PropNode) }
57
+
58
+ properties.each_slice(2) do |prop1, prop2|
59
+ next unless prop2
60
+ next unless value_offset(prop1) != value_offset(prop2)
61
+ add_lint(prop1, 'Property values should be aligned')
62
+ break
63
+ end
64
+ end
65
+
66
+ # Offset of value for property
67
+ def value_offset(prop)
68
+ src_range = prop.name_source_range
69
+ src_range.start_pos.offset +
70
+ (src_range.end_pos.offset - src_range.start_pos.offset) +
71
+ whitespace_after_colon(prop).take_while { |w| w == ' ' }.size
72
+ end
73
+
74
+ def whitespace_after_colon(node)
75
+ whitespace = []
76
+ offset = 1
77
+
78
+ # Find the colon after the property name
79
+ while character_at(node.name_source_range.start_pos, offset - 1) != ':'
80
+ offset += 1
81
+ end
82
+
83
+ # Count spaces after the colon
84
+ while [' ', "\t", "\n"].include? character_at(node.name_source_range.start_pos, offset)
85
+ whitespace << character_at(node.name_source_range.start_pos, offset)
86
+ offset += 1
87
+ end
88
+
89
+ whitespace
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,27 @@
1
+ module SCSSLint
2
+ # Checks for spaces following the name of a property and before the colon
3
+ # separating the property's name from its value.
4
+ class Linter::SpaceAfterPropertyName < Linter
5
+ include LinterRegistry
6
+
7
+ def visit_prop(node)
8
+ offset = property_name_colon_offset(node)
9
+ return unless character_at(node.name_source_range.start_pos, offset - 1) == ' '
10
+ add_lint node, 'Property name should be immediately followed by a colon'
11
+ end
12
+
13
+ private
14
+
15
+ # Deals with a weird Sass bug where the name_source_range of a PropNode does
16
+ # not start at the beginning of the property name.
17
+ def property_name_colon_offset(node)
18
+ offset = 0
19
+
20
+ while character_at(node.name_source_range.start_pos, offset) != ':'
21
+ offset += 1
22
+ end
23
+
24
+ offset
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,72 @@
1
+ module SCSSLint
2
+ # Checks for the presence of a single space before an opening brace.
3
+ class Linter::SpaceBeforeBrace < Linter
4
+ include LinterRegistry
5
+
6
+ def check_node(node)
7
+ source = source_from_range(node.source_range).strip
8
+
9
+ # Only lint `@include`s which have curly braces
10
+ if source[-1] == '{'
11
+ check_for_space(node, source)
12
+ end
13
+
14
+ yield
15
+ end
16
+
17
+ alias_method :visit_function, :check_node
18
+ alias_method :visit_each, :check_node
19
+ alias_method :visit_for, :check_node
20
+ alias_method :visit_function, :check_node
21
+ alias_method :visit_mixindef, :check_node
22
+ alias_method :visit_mixin, :check_node
23
+ alias_method :visit_rule, :check_node
24
+ alias_method :visit_while, :check_node
25
+
26
+ private
27
+
28
+ def check_for_space(node, string)
29
+ line = node.source_range.end_pos.line
30
+
31
+ if config['allow_single_line_padding'] && node_on_single_line?(node)
32
+ return unless string[-2] != ' '
33
+ add_lint(line, 'Opening curly brace in a single line rule set '\
34
+ '`{` should be preceded by at least one space')
35
+ else
36
+ return unless chars_before_incorrect(string)
37
+ style_message = (config['style'] == 'new_line') ? 'a new line' : 'one space'
38
+ add_lint(line, 'Opening curly brace `{` should be ' \
39
+ "preceded by #{style_message}")
40
+ end
41
+ end
42
+
43
+ # Check if the characters before the end of the string
44
+ # are not what they should be
45
+ def chars_before_incorrect(string)
46
+ if config['style'] != 'new_line'
47
+ return !single_space_before(string)
48
+ end
49
+ !newline_before_nonwhitespace(string)
50
+ end
51
+
52
+ # Check if there is one space and only one
53
+ # space before the end of the string
54
+ def single_space_before(string)
55
+ return false if string[-2] != ' '
56
+ return false if string[-3] == ' '
57
+ true
58
+ end
59
+
60
+ # Check if, starting from the end of a string
61
+ # and moving backwards, towards the beginning,
62
+ # we find a new line before any non-whitespace characters
63
+ def newline_before_nonwhitespace(string)
64
+ offset = -2
65
+ while /\S/.match(string[offset]).nil?
66
+ return true if string[offset] == "\n"
67
+ offset -= 1
68
+ end
69
+ false
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,35 @@
1
+ module SCSSLint
2
+ # Checks for the presence of spaces between parentheses.
3
+ class Linter::SpaceBetweenParens < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ @spaces = config['spaces']
8
+
9
+ engine.lines.each_with_index do |line, index|
10
+ line.gsub(%r{((//|/\*).*$)}, '').scan(/
11
+ (^(\t|\s)*\))? # Capture leading spaces and tabs followed by a `)`
12
+ (
13
+ \([ ]*(?!$) # Find `( ` as long as its not EOL )
14
+ |
15
+ [ ]*\)
16
+ )?
17
+ /x) do |match|
18
+ check(match[2], index) if match[2]
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def check(str, index)
26
+ spaces = str.count ' '
27
+ return if spaces == @spaces
28
+
29
+ location = Location.new(index + 1)
30
+ message = "Expected #{pluralize(@spaces, 'space')} " \
31
+ "between parentheses instead of #{spaces}"
32
+ add_lint(location, message)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,94 @@
1
+ module SCSSLint
2
+ # Checks the type of quotes used in string literals.
3
+ class Linter::StringQuotes < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_comment(_node)
7
+ # Sass allows you to write Sass Script in non-silent comments (/* ... */).
8
+ # Unfortunately, it doesn't report correct source ranges for these script
9
+ # nodes.
10
+ # It's unlikely that a developer wanted to lint the script they wrote in a
11
+ # comment, so just ignore this case entirely and stop traversing the
12
+ # children of comment nodes.
13
+ end
14
+
15
+ def visit_script_stringinterpolation(node)
16
+ # We can't statically determine what the resultant string looks like when
17
+ # string interpolation is used, e.g. "one #{$var} three" could be a very
18
+ # different string depending on $var = `'" + "'` or $var = `two`.
19
+ #
20
+ # Thus we manually skip the substrings in the string interpolation and
21
+ # visit the expressions in the interpolation itself.
22
+ node.children.reject { |child| child.is_a?(Sass::Script::Tree::Literal) }
23
+ .each { |child| visit(child) }
24
+ end
25
+
26
+ def visit_script_string(node)
27
+ check_quotes(node, source_from_range(node.source_range))
28
+ end
29
+
30
+ def visit_import(node)
31
+ # `@import` source range conveniently includes only the quoted string
32
+ check_quotes(node, source_from_range(node.source_range))
33
+ end
34
+
35
+ def visit_charset(node)
36
+ # `@charset` source range includes entire declaration, so exclude that prefix
37
+ source = source_from_range(node.source_range)[('@charset'.length)..-1]
38
+
39
+ check_quotes(node, source)
40
+ end
41
+
42
+ private
43
+
44
+ def check_quotes(node, source)
45
+ source = source.strip
46
+ string = extract_string_without_quotes(source)
47
+ return unless string
48
+
49
+ case source[0]
50
+ when '"'
51
+ check_double_quotes(node, string)
52
+ when "'"
53
+ check_single_quotes(node, string)
54
+ end
55
+ end
56
+
57
+ STRING_WITHOUT_QUOTES_REGEX = %r{
58
+ \A
59
+ ["'](.*)["'] # Extract text between quotes
60
+ \s*\)?\s*;?\s* # Sometimes the Sass parser includes a trailing ) or ;
61
+ (//.*)? # Exclude any trailing comments that might have snuck in
62
+ \z
63
+ }x
64
+
65
+ def extract_string_without_quotes(source)
66
+ return unless match = STRING_WITHOUT_QUOTES_REGEX.match(source)
67
+ match[1]
68
+ end
69
+
70
+ def check_double_quotes(node, string)
71
+ if config['style'] == 'single_quotes'
72
+ add_lint(node, 'Prefer single quoted strings') if string !~ /'/
73
+ else
74
+ if string =~ /(?<! \\) \\"/x && string !~ /'/
75
+ add_lint(node, 'Use single-quoted strings when writing double ' \
76
+ 'quotes to avoid having to escape the double quotes')
77
+ end
78
+ end
79
+ end
80
+
81
+ def check_single_quotes(node, string)
82
+ if config['style'] == 'single_quotes'
83
+ if string =~ /(?<! \\) \\'/x && string !~ /"/
84
+ add_lint(node, 'Use double-quoted strings when writing single ' \
85
+ 'quotes to avoid having to escape the single quotes')
86
+ elsif string =~ /(?<! \\) \\"/x
87
+ add_lint(node, "Don't escape double quotes in single-quoted strings")
88
+ end
89
+ else
90
+ add_lint(node, 'Prefer double-quoted strings') if string !~ /"/
91
+ end
92
+ end
93
+ end
94
+ end