scss_lint 0.42.2 → 0.43.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +9 -1
  3. data/data/properties.txt +1 -1
  4. data/lib/scss_lint/cli.rb +11 -5
  5. data/lib/scss_lint/config.rb +1 -2
  6. data/lib/scss_lint/control_comment_processor.rb +33 -28
  7. data/lib/scss_lint/engine.rb +10 -7
  8. data/lib/scss_lint/linter.rb +56 -12
  9. data/lib/scss_lint/linter/chained_classes.rb +21 -0
  10. data/lib/scss_lint/linter/color_variable.rb +0 -7
  11. data/lib/scss_lint/linter/comment.rb +15 -1
  12. data/lib/scss_lint/linter/empty_line_between_blocks.rb +10 -0
  13. data/lib/scss_lint/linter/indentation.rb +47 -53
  14. data/lib/scss_lint/linter/mergeable_selector.rb +29 -0
  15. data/lib/scss_lint/linter/property_spelling.rb +18 -6
  16. data/lib/scss_lint/linter/pseudo_element.rb +18 -0
  17. data/lib/scss_lint/linter/single_line_per_selector.rb +23 -7
  18. data/lib/scss_lint/linter/space_after_property_colon.rb +5 -6
  19. data/lib/scss_lint/linter/space_after_property_name.rb +1 -7
  20. data/lib/scss_lint/linter/space_after_variable_name.rb +1 -1
  21. data/lib/scss_lint/linter/space_around_operator.rb +23 -7
  22. data/lib/scss_lint/linter/string_quotes.rb +2 -9
  23. data/lib/scss_lint/linter/trailing_semicolon.rb +10 -0
  24. data/lib/scss_lint/options.rb +5 -0
  25. data/lib/scss_lint/runner.rb +10 -8
  26. data/lib/scss_lint/selector_visitor.rb +11 -3
  27. data/lib/scss_lint/version.rb +1 -1
  28. data/spec/scss_lint/cli_spec.rb +14 -0
  29. data/spec/scss_lint/linter/chained_classes_spec.rb +45 -0
  30. data/spec/scss_lint/linter/comment_spec.rb +16 -0
  31. data/spec/scss_lint/linter/empty_line_between_blocks_spec.rb +62 -0
  32. data/spec/scss_lint/linter/indentation_spec.rb +1 -9
  33. data/spec/scss_lint/linter/leading_zero_spec.rb +12 -0
  34. data/spec/scss_lint/linter/mergeable_selector_spec.rb +59 -0
  35. data/spec/scss_lint/linter/property_spelling_spec.rb +28 -0
  36. data/spec/scss_lint/linter/pseudo_element_spec.rb +71 -0
  37. data/spec/scss_lint/linter/single_line_per_selector_spec.rb +28 -1
  38. data/spec/scss_lint/linter/space_after_variable_name_spec.rb +12 -0
  39. data/spec/scss_lint/linter/space_around_operator_spec.rb +51 -0
  40. data/spec/scss_lint/linter/trailing_semicolon_spec.rb +28 -0
  41. data/spec/scss_lint/linter_spec.rb +17 -0
  42. data/spec/scss_lint/runner_spec.rb +2 -2
  43. metadata +9 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5bb29e6e4cef4d98cce3e61248d11f2dde2315a1
4
- data.tar.gz: 52c5cdfb7895f10c883188633a75c32b3e77267a
3
+ metadata.gz: 26102783a3fa8f2e4ade260a8a483aa2d2a315f6
4
+ data.tar.gz: ce8673ca36772ff3505bb6ce3eec4861f78ebed4
5
5
  SHA512:
6
- metadata.gz: 5e68b7e8d5afa4970b42da268f4fb2085c2ee81ab5f4eb0d020a67b97af286ec629ccad33429d542723bf0dd851e9c8ae31a93c464d203609c0b0c4c26db7c5b
7
- data.tar.gz: 5cc833524095ed48bd5a01e3aebd2a4919ec4c79149b7d0f7c006552bdeb5d6fd42512109fdb6994ee78b50461cb144c3a7283703726d99b71a5cca2d4b691f7
6
+ metadata.gz: d5558f7bbe06908b1901d949215856aad7c0318a4fb10305ff89e3dbea00b08a73c1dcd5edece21898b37cf331585e37c02429ec66337a3a5a887365fa151a18
7
+ data.tar.gz: 08b6257a9482c94ee656debfc1b141a0c557dccdb970bec4df3588f6ec69dab8888e9a282ab8f74a580f783c81844136f2c729c034a32703dd756873a2a4d0a7
@@ -21,6 +21,9 @@ linters:
21
21
  enabled: true
22
22
  convention: zero # or `none`
23
23
 
24
+ ChainedClasses:
25
+ enabled: false
26
+
24
27
  ColorKeyword:
25
28
  enabled: true
26
29
 
@@ -29,6 +32,7 @@ linters:
29
32
 
30
33
  Comment:
31
34
  enabled: true
35
+ style: silent
32
36
 
33
37
  DebugStatement:
34
38
  enabled: true
@@ -123,6 +127,7 @@ linters:
123
127
  PropertySpelling:
124
128
  enabled: true
125
129
  extra_properties: []
130
+ disabled_properties: []
126
131
 
127
132
  PropertyUnits:
128
133
  enabled: true
@@ -137,6 +142,9 @@ linters:
137
142
  '%'] # Other
138
143
  properties: {}
139
144
 
145
+ PseudoElement:
146
+ enabled: true
147
+
140
148
  QualifyingElement:
141
149
  enabled: true
142
150
  allow_element_with_attribute: false
@@ -178,7 +186,7 @@ linters:
178
186
 
179
187
  SpaceAroundOperator:
180
188
  enabled: true
181
- style: one_space # or 'no_space'
189
+ style: one_space # or 'at_least_one_space', or 'no_space'
182
190
 
183
191
  SpaceBeforeBrace:
184
192
  enabled: true
@@ -421,8 +421,8 @@ text-overline-style
421
421
  text-overline-width
422
422
  text-rendering
423
423
  text-security
424
- text-size-adjust
425
424
  text-shadow
425
+ text-size-adjust
426
426
  text-stroke
427
427
  text-stroke-color
428
428
  text-stroke-width
@@ -54,8 +54,16 @@ module SCSSLint
54
54
 
55
55
  def scan_for_lints(options, config)
56
56
  runner = Runner.new(config)
57
- runner.run(FileFinder.new(config).find(options[:files]))
58
- report_lints(options, runner.lints, runner.files)
57
+ files =
58
+ if options[:stdin_file_path]
59
+ [{ file: STDIN, path: options[:stdin_file_path] }]
60
+ else
61
+ FileFinder.new(config).find(options[:files]).map do |file_path|
62
+ { path: file_path }
63
+ end
64
+ end
65
+ runner.run(files)
66
+ report_lints(options, runner.lints, files)
59
67
 
60
68
  if runner.lints.any?(&:error?)
61
69
  halt :error
@@ -203,9 +211,7 @@ module SCSSLint
203
211
  def print_linters
204
212
  puts 'Installed linters:'
205
213
 
206
- linter_names = LinterRegistry.linters.map do |linter|
207
- linter.name.split('::').last
208
- end
214
+ linter_names = LinterRegistry.linters.map(&:simple_name)
209
215
 
210
216
  linter_names.sort.each do |linter_name|
211
217
  puts " - #{linter_name}"
@@ -41,8 +41,7 @@ module SCSSLint
41
41
  end
42
42
 
43
43
  def linter_name(linter)
44
- linter = linter.is_a?(Class) ? linter : linter.class
45
- linter.name.split('::')[2..-1].join('::')
44
+ (linter.is_a?(Class) ? linter : linter.class).simple_name
46
45
  end
47
46
 
48
47
  private
@@ -20,19 +20,21 @@ module SCSSLint
20
20
  #
21
21
  # @param node [Sass::Tree::Node]
22
22
  def before_node_visit(node)
23
- return unless command = extract_command(node)
23
+ return unless (commands = Array(extract_commands(node))).any?
24
24
 
25
- linters = command[:linters]
26
- return unless linters.include?('all') || linters.include?(@linter.name)
25
+ commands.each do |command|
26
+ linters = command[:linters]
27
+ next unless linters.include?('all') || linters.include?(@linter.name)
27
28
 
28
- process_command(command, node)
29
+ process_command(command, node)
29
30
 
30
- # Is the control comment the only thing on this line?
31
- return if node.is_a?(Sass::Tree::RuleNode) ||
32
- %r{^\s*(//|/\*)}.match(@linter.engine.lines[command[:line] - 1])
31
+ # Is the control comment the only thing on this line?
32
+ next if node.is_a?(Sass::Tree::RuleNode) ||
33
+ %r{^\s*(//|/\*)}.match(@linter.engine.lines[command[:line] - 1])
33
34
 
34
- # Otherwise, pop since we only want comment to apply to the single line
35
- pop_control_comment_stack(node)
35
+ # Otherwise, pop since we only want comment to apply to the single line
36
+ pop_control_comment_stack(node)
37
+ end
36
38
  end
37
39
 
38
40
  # Executed after a node has been visited.
@@ -46,35 +48,38 @@ module SCSSLint
46
48
 
47
49
  private
48
50
 
49
- def extract_command(node)
51
+ def extract_commands(node)
50
52
  return unless comment = retrieve_comment_text(node)
51
53
 
52
- comment.split(/(?<=\n)/).each_with_index do |comment_line, line_no|
53
- if match = %r{
54
- (/|\*|^ \*)\s* # Comment start marker
55
- scss-lint:
54
+ commands = []
55
+ comment.split("\n").each_with_index do |comment_line, line_no|
56
+ next unless match = %r{
57
+ //\s*scss-lint:
56
58
  (?<action>disable|enable)\s+
57
59
  (?<linters>.*?)
58
- \s*(?:\*/|\n) # Comment end marker or end of line
60
+ \s*($|\*\/) # End of line
59
61
  }x.match(comment_line)
60
- return {
61
- action: match[:action],
62
- linters: match[:linters].split(/\s*,\s*|\s+/),
63
- line: node.line + line_no
64
- }
65
- end
62
+
63
+ commands << {
64
+ action: match[:action],
65
+ linters: match[:linters].split(/\s*,\s*|\s+/),
66
+ line: node.line + line_no
67
+ }
66
68
  end
67
69
 
68
- false
70
+ commands
69
71
  end
70
72
 
71
73
  def retrieve_comment_text(node)
72
- case node
73
- when Sass::Tree::CommentNode
74
- node.value.first
75
- when Sass::Tree::RuleNode
76
- node.rule.select { |chunk| chunk.is_a?(String) }.join
77
- end
74
+ text_with_markers =
75
+ case node
76
+ when Sass::Tree::CommentNode
77
+ node.value.first
78
+ when Sass::Tree::RuleNode
79
+ node.rule.select { |chunk| chunk.is_a?(String) }.join
80
+ end
81
+
82
+ text_with_markers.gsub(%r{\A/\*}, '//').gsub(/\n \*/, "\n//") if text_with_markers
78
83
  end
79
84
 
80
85
  def process_command(command, node)
@@ -17,8 +17,8 @@ module SCSSLint
17
17
  # @option options [String] :file The file to load
18
18
  # @option options [String] :code The code to parse
19
19
  def initialize(options = {})
20
- if options[:file]
21
- build_from_file(options[:file])
20
+ if options[:path]
21
+ build_from_file(options)
22
22
  elsif options[:code]
23
23
  build_from_string(options[:code])
24
24
  end
@@ -41,11 +41,14 @@ module SCSSLint
41
41
 
42
42
  private
43
43
 
44
- # @param path [String]
45
- def build_from_file(path)
46
- @filename = path
47
- @engine = Sass::Engine.for_file(path, ENGINE_OPTIONS)
48
- @contents = File.open(path, 'r').read
44
+ # @param options [Hash]
45
+ # @option file [IO] if provided, us this as the file object
46
+ # @option path [String] path of file, loading from this if `file` object not
47
+ # given
48
+ def build_from_file(options)
49
+ @filename = options[:path]
50
+ @contents = options[:file] ? file.read : File.read(@filename)
51
+ @engine = Sass::Engine.new(@contents, ENGINE_OPTIONS.merge(filename: @filename))
49
52
  end
50
53
 
51
54
  # @param scss [String]
@@ -1,9 +1,25 @@
1
1
  module SCSSLint
2
2
  # Defines common functionality available to all linters.
3
- class Linter < Sass::Tree::Visitors::Base
3
+ class Linter < Sass::Tree::Visitors::Base # rubocop:disable ClassLength
4
4
  include SelectorVisitor
5
5
  include Utils
6
6
 
7
+ class << self
8
+ attr_accessor :simple_name
9
+
10
+ # When defining a Linter class, define its simple name as well. This
11
+ # assumes that the module hierarchy of every linter starts with
12
+ # `SCSSLint::Linter::`, and removes this part of the class name.
13
+ #
14
+ # `SCSSLint::Linter::Foo.simple_name` #=> "Foo"
15
+ # `SCSSLint::Linter::Compass::Bar.simple_name` #=> "Compass::Bar"
16
+ def inherited(linter)
17
+ name_parts = linter.name.split('::')
18
+ name = name_parts.length < 3 ? '' : name_parts[2..-1].join('::')
19
+ linter.simple_name = name
20
+ end
21
+ end
22
+
7
23
  attr_reader :config, :engine, :lints
8
24
 
9
25
  # Create a linter.
@@ -29,7 +45,7 @@ module SCSSLint
29
45
  # Return the human-friendly name of this linter as specified in the
30
46
  # configuration file and in lint descriptions.
31
47
  def name
32
- self.class.name.split('::')[2..-1].join('::')
48
+ self.class.simple_name
33
49
  end
34
50
 
35
51
  protected
@@ -61,16 +77,6 @@ module SCSSLint
61
77
  Location.new(range.start_pos.line, range.start_pos.offset, length)
62
78
  end
63
79
 
64
- # @param source_position [Sass::Source::Position]
65
- # @param offset [Integer]
66
- # @return [String] the character at the given [Sass::Source::Position]
67
- def character_at(source_position, offset = 0)
68
- actual_line = source_position.line - 1
69
- actual_offset = source_position.offset + offset - 1
70
-
71
- engine.lines.size > actual_line && engine.lines[actual_line][actual_offset]
72
- end
73
-
74
80
  # Extracts the original source code given a range.
75
81
  #
76
82
  # @param source_range [Sass::Source::Range]
@@ -145,6 +151,16 @@ module SCSSLint
145
151
 
146
152
  private
147
153
 
154
+ def visit_comment(_node)
155
+ # Don't lint children of comments by default, as the Sass parser contains
156
+ # many bugs related to the source ranges reported within code in /*...*/
157
+ # comments.
158
+ #
159
+ # Instead of defining this empty method on every linter, we assume every
160
+ # linter ignores comments by default. Individual linters can override at
161
+ # their discretion.
162
+ end
163
+
148
164
  def extract_location(node_or_line_or_location)
149
165
  if node_or_line_or_location.is_a?(Location)
150
166
  node_or_line_or_location
@@ -157,5 +173,33 @@ module SCSSLint
157
173
  Location.new(node_or_line_or_location)
158
174
  end
159
175
  end
176
+
177
+ # @param source_position [Sass::Source::Position]
178
+ # @param offset [Integer]
179
+ # @return [String] the character at the given [Sass::Source::Position]
180
+ def character_at(source_position, offset = 0)
181
+ actual_line = source_position.line - 1
182
+ actual_offset = source_position.offset + offset - 1
183
+
184
+ engine.lines.size > actual_line && engine.lines[actual_line][actual_offset]
185
+ end
186
+
187
+ # Starting at source_position (plus offset), search for pattern and return
188
+ # the offset from the source_position.
189
+ #
190
+ # @param source_position [Sass::Source::Position]
191
+ # @param pattern [String, RegExp] the pattern to search for
192
+ # @param offset [Integer]
193
+ # @return [Integer] the offset at which [pattern] was found.
194
+ def offset_to(source_position, pattern, offset = 0)
195
+ actual_line = source_position.line - 1
196
+ actual_offset = source_position.offset + offset - 1
197
+
198
+ return nil if actual_line >= engine.lines.size
199
+
200
+ actual_index = engine.lines[actual_line].index(pattern, actual_offset)
201
+
202
+ actual_index && actual_index + 1 - source_position.offset
203
+ end
160
204
  end
161
205
  end
@@ -0,0 +1,21 @@
1
+ module SCSSLint
2
+ # Checks for uses of chained classes (e.g. .foo.bar).
3
+ class Linter::ChainedClasses < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_sequence(sequence)
7
+ sequence.members.each do |simple_sequence|
8
+ next unless chained_class?(simple_sequence)
9
+ add_lint(simple_sequence.line,
10
+ 'Prefer using a distinct class over chained classes ' \
11
+ '(e.g. .new-class over .foo.bar')
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def chained_class?(simple_sequence)
18
+ simple_sequence.members.count { |member| member.is_a?(Sass::Selector::Class) } >= 2
19
+ end
20
+ end
21
+ end
@@ -25,13 +25,6 @@ module SCSSLint
25
25
  .each { |_, color| record_lint(node, color) }
26
26
  end
27
27
 
28
- def visit_comment(_node)
29
- # Don't lint children. Sass multiline comments (/*...*/) are actually
30
- # rendered in code and thus allow variable interpolation. Unfortunately,
31
- # the Sass parser returns bad source ranges for interpolation in these
32
- # comments, so it's easiest to just ignore them.
33
- end
34
-
35
28
  def visit_script_funcall(node)
36
29
  if literal_color_function?(node)
37
30
  record_lint node, node.to_sass
@@ -4,11 +4,25 @@ module SCSSLint
4
4
  include LinterRegistry
5
5
 
6
6
  def visit_comment(node)
7
- add_lint(node, 'Use `//` comments everywhere') unless node.invisible? || allowed?(node)
7
+ add_lint(node, 'Use `//` comments everywhere') unless valid_comment?(node)
8
8
  end
9
9
 
10
10
  private
11
11
 
12
+ def valid_comment?(node)
13
+ allowed_type =
14
+ if config.fetch('style', 'silent') == 'silent'
15
+ node.invisible?
16
+ else
17
+ !node.invisible?
18
+ end
19
+ return true if allowed_type
20
+
21
+ # Otherwise check if comment contains content that excludes it (i.e. a
22
+ # copyright notice for loud comments)
23
+ allowed?(node)
24
+ end
25
+
12
26
  # @param node [CommentNode]
13
27
  # @return [Boolean]
14
28
  def allowed?(node)
@@ -3,11 +3,21 @@ module SCSSLint
3
3
  class Linter::EmptyLineBetweenBlocks < Linter
4
4
  include LinterRegistry
5
5
 
6
+ def visit_atroot(node)
7
+ check(node, '@at-root')
8
+ yield
9
+ end
10
+
6
11
  def visit_function(node)
7
12
  check(node, '@function')
8
13
  yield
9
14
  end
10
15
 
16
+ def visit_media(node)
17
+ check(node, '@media')
18
+ yield
19
+ end
20
+
11
21
  def visit_mixin(node)
12
22
  # Ignore @includes which don't have any block content
13
23
  check(node, '@include') if node.children
@@ -6,7 +6,16 @@ module SCSSLint
6
6
  def visit_root(_node)
7
7
  @indent_width = config['width'].to_i
8
8
  @indent_character = config['character'] || 'space'
9
+ if @indent_character == 'tab'
10
+ @other_character = ' '
11
+ @other_character_name = 'space'
12
+ else
13
+ @other_character = "\t"
14
+ @other_character_name = 'tab'
15
+ end
16
+ @allow_non_nested_indentation = config['allow_non_nested_indentation']
9
17
  @indent = 0
18
+ @indentations = {}
10
19
  yield
11
20
  end
12
21
 
@@ -28,31 +37,23 @@ module SCSSLint
28
37
  # sibling or its parent, as indentation isn't possible
29
38
  return if nodes_on_same_line?(previous_node(node), node)
30
39
 
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
+ check_indent_width(node)
40
41
  end
41
42
 
42
- def check_indent_width(node, other_character, character_name, other_character_name)
43
+ def check_indent_width(node)
43
44
  actual_indent = node_indent(node)
44
45
 
45
- if actual_indent.include?(other_character)
46
+ if actual_indent.include?(@other_character)
46
47
  add_lint(node.line,
47
- "Line should be indented with #{character_name}s, " \
48
- "not #{other_character_name}s")
48
+ "Line should be indented with #{@indent_character}s, " \
49
+ "not #{@other_character_name}s")
49
50
  return true
50
51
  end
51
52
 
52
- if config['allow_non_nested_indentation']
53
- check_arbitrary_indent(node, actual_indent.length, character_name)
53
+ if @allow_non_nested_indentation
54
+ check_arbitrary_indent(node, actual_indent.length)
54
55
  else
55
- check_regular_indent(node, actual_indent.length, character_name)
56
+ check_regular_indent(node, actual_indent.length)
56
57
  end
57
58
  end
58
59
 
@@ -61,7 +62,7 @@ module SCSSLint
61
62
  def visit_if(node)
62
63
  check_indentation(node)
63
64
 
64
- if config['allow_non_nested_indentation']
65
+ if @allow_non_nested_indentation
65
66
  yield # Continue linting else statement
66
67
  else
67
68
  visit(node.else) if node.else
@@ -139,51 +140,39 @@ module SCSSLint
139
140
  same_position?(node.source_range.end_pos, first_child_source.start_pos)
140
141
  end
141
142
 
142
- def check_regular_indent(node, actual_indent, character_name)
143
+ def check_regular_indent(node, actual_indent)
143
144
  return if actual_indent == @indent
144
145
 
145
- add_lint(node.line,
146
- "Line should be indented #{@indent} #{character_name}s, " \
147
- "but was indented #{actual_indent} #{character_name}s")
146
+ add_lint(node.line, lint_message(@indent, actual_indent))
148
147
  true
149
148
  end
150
149
 
151
- def check_arbitrary_indent(node, actual_indent, character_name) # rubocop:disable CyclomaticComplexity, MethodLength, LineLength
152
- # Allow rulesets to be indented any amount when the indent is zero, as
153
- # long as it's a multiple of the indent width
154
- if ruleset_under_root_node?(node)
155
- unless actual_indent % @indent_width == 0
156
- add_lint(node.line,
157
- "Line must be indented a multiple of #{@indent_width} " \
158
- "#{character_name}s, but was indented #{actual_indent} #{character_name}s")
159
- return true
160
- end
161
- end
150
+ def check_arbitrary_indent(node, actual_indent)
151
+ return if check_root_ruleset_indent(node, actual_indent)
162
152
 
163
- if @indent == 0
164
- unless node.is_a?(Sass::Tree::RuleNode) || actual_indent == 0
165
- add_lint(node.line,
166
- "Line should be indented 0 #{character_name}s, " \
167
- "but was indented #{actual_indent} #{character_name}s")
153
+ # Allow any root-level node (i.e. one that would normally have an indent
154
+ # of zero) to have an arbitrary amount of indent
155
+ return if @indent == 0
156
+
157
+ return if one_shift_greater_than_parent?(node, actual_indent)
158
+ parent_indent = node_indent(node_indent_parent(node)).length
159
+ expected_indent = parent_indent + @indent_width
160
+ add_lint(node.line, lint_message(expected_indent, actual_indent))
161
+ true
162
+ end
163
+
164
+ # Allow rulesets to be indented any amount when the indent is zero, as long
165
+ # as it's a multiple of the indent width
166
+ def check_root_ruleset_indent(node, actual_indent)
167
+ # Whether node is a ruleset not nested within any other ruleset.
168
+ if @indent == 0 && node.is_a?(Sass::Tree::RuleNode)
169
+ unless actual_indent % @indent_width == 0
170
+ add_lint(node.line, lint_message("a multiple of #{@indent_width}", actual_indent))
168
171
  return true
169
172
  end
170
- elsif !one_shift_greater_than_parent?(node, actual_indent)
171
- parent_indent = node_indent(node_indent_parent(node)).length
172
- expected_indent = parent_indent + @indent_width
173
-
174
- add_lint(node.line,
175
- "Line should be indented #{expected_indent} #{character_name}s, " \
176
- "but was indented #{actual_indent} #{character_name}s")
177
- return true
178
173
  end
179
- end
180
174
 
181
- # Returns whether node is a ruleset not nested within any other ruleset.
182
- #
183
- # @param node [Sass::Tree::Node]
184
- # @return [true,false]
185
- def ruleset_under_root_node?(node)
186
- @indent == 0 && node.is_a?(Sass::Tree::RuleNode)
175
+ false
187
176
  end
188
177
 
189
178
  # Returns whether node is indented exactly one indent width greater than its
@@ -202,7 +191,7 @@ module SCSSLint
202
191
  # @param node [Sass::Tree::Node]
203
192
  # @return [Integer]
204
193
  def node_indent(node)
205
- engine.lines[node.line - 1][/^(\s*)/, 1]
194
+ @indentations[node] ||= engine.lines[node.line - 1][/^(\s*)/]
206
195
  end
207
196
 
208
197
  def node_indent_parent(node)
@@ -215,5 +204,10 @@ module SCSSLint
215
204
 
216
205
  node.node_parent
217
206
  end
207
+
208
+ def lint_message(expected, actual)
209
+ "Line should be indented #{expected} #{@indent_character}s, but was " \
210
+ "indented #{actual} #{@indent_character}s"
211
+ end
218
212
  end
219
213
  end