scss_lint 0.42.2 → 0.43.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 (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