scss_lint 0.40.1 → 0.41.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +20 -10
  3. data/data/property-sort-orders/recess.txt +12 -0
  4. data/data/property-sort-orders/smacss.txt +13 -0
  5. data/lib/scss_lint/cli.rb +4 -3
  6. data/lib/scss_lint/control_comment_processor.rb +34 -25
  7. data/lib/scss_lint/linter/border_zero.rb +1 -1
  8. data/lib/scss_lint/linter/disable_linter_reason.rb +39 -0
  9. data/lib/scss_lint/linter/mergeable_selector.rb +3 -1
  10. data/lib/scss_lint/linter/nesting_depth.rb +1 -0
  11. data/lib/scss_lint/linter/selector_depth.rb +1 -1
  12. data/lib/scss_lint/linter/single_line_per_property.rb +1 -1
  13. data/lib/scss_lint/linter/single_line_per_selector.rb +9 -1
  14. data/lib/scss_lint/linter/space_after_variable_name.rb +1 -1
  15. data/lib/scss_lint/linter/space_around_operator.rb +86 -0
  16. data/lib/scss_lint/linter/space_between_parens.rb +96 -20
  17. data/lib/scss_lint/linter/transition_all.rb +30 -0
  18. data/lib/scss_lint/linter/unnecessary_mantissa.rb +1 -0
  19. data/lib/scss_lint/reporter/clean_files_reporter.rb +10 -0
  20. data/lib/scss_lint/reporter.rb +5 -2
  21. data/lib/scss_lint/runner.rb +3 -2
  22. data/lib/scss_lint/sass/script.rb +19 -0
  23. data/lib/scss_lint/version.rb +1 -1
  24. data/spec/scss_lint/linter/bang_format_spec.rb +1 -1
  25. data/spec/scss_lint/linter/disable_linter_reason_spec.rb +63 -0
  26. data/spec/scss_lint/linter/nesting_depth_spec.rb +16 -0
  27. data/spec/scss_lint/linter/single_line_per_selector_spec.rb +23 -0
  28. data/spec/scss_lint/linter/space_after_variable_name_spec.rb +1 -1
  29. data/spec/scss_lint/linter/space_around_operator_spec.rb +240 -0
  30. data/spec/scss_lint/linter/space_between_parens_spec.rb +195 -1
  31. data/spec/scss_lint/linter/transition_all_spec.rb +81 -0
  32. data/spec/scss_lint/linter/unnecessary_mantissa_spec.rb +20 -0
  33. data/spec/scss_lint/linter_spec.rb +21 -0
  34. data/spec/scss_lint/report_lint_spec.rb +268 -0
  35. data/spec/scss_lint/reporter/clean_files_reporter_spec.rb +73 -0
  36. data/spec/scss_lint/reporter/config_reporter_spec.rb +1 -1
  37. data/spec/scss_lint/reporter/default_reporter_spec.rb +1 -1
  38. data/spec/scss_lint/reporter/files_reporter_spec.rb +3 -2
  39. data/spec/scss_lint/reporter/json_reporter_spec.rb +1 -1
  40. data/spec/spec_helper.rb +1 -1
  41. data/spec/support/matchers/report_lint.rb +11 -2
  42. metadata +20 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3d64ee14edbfefaee47bcf0b30500c2ee83231c0
4
- data.tar.gz: 513d15bc52ea0f5d854972d661053c6ccc79f855
3
+ metadata.gz: 478b23880809f4fb5eeb95543b34534de05edf5a
4
+ data.tar.gz: 97f5d37498b751b7bdae0dd23032acc92de56fdf
5
5
  SHA512:
6
- metadata.gz: 60cff3d31434509cb38dcee086f0f877f4346f5c9cb060d26897193e02b21e5c5109fe68dbc53f3bd99c4ca31ec09746a698ae5b8eed9ede9f57dd1dbf494777
7
- data.tar.gz: 386501b6cb355a6924e1e2f6c0741a68dd54a7245fd28f7b71dac6488c32e79322d435bb1f5d69a419b680bdc30aefca0bfe604b104f697054e5ae6cc81a98ec
6
+ metadata.gz: 4808204c5756753ab99d9485590e034c6711f5b4ce67024c09dcfd281f6fa4199eb12331dd4a272933428c5991093bb4e6d2b5cf77743886df83488acc35651d
7
+ data.tar.gz: 951df4e1c90579ff4ebc55b61693bab102ba66761875781f6e0baf7d64232c0e95004f0de6eaacdc2db483a1c0d5d287aa79dd14d75d52220540f7cfbc129ee4
data/config/default.yml CHANGED
@@ -36,6 +36,9 @@ linters:
36
36
  DeclarationOrder:
37
37
  enabled: true
38
38
 
39
+ DisableLinterReason:
40
+ enabled: false
41
+
39
42
  DuplicateProperty:
40
43
  enabled: true
41
44
 
@@ -111,6 +114,16 @@ linters:
111
114
  include_nested: false
112
115
  max_properties: 10
113
116
 
117
+ PropertySortOrder:
118
+ enabled: true
119
+ ignore_unspecified: false
120
+ min_properties: 2
121
+ separate_groups: false
122
+
123
+ PropertySpelling:
124
+ enabled: true
125
+ extra_properties: []
126
+
114
127
  PropertyUnits:
115
128
  enabled: true
116
129
  global: [
@@ -124,16 +137,6 @@ linters:
124
137
  '%'] # Other
125
138
  properties: {}
126
139
 
127
- PropertySortOrder:
128
- enabled: true
129
- ignore_unspecified: false
130
- min_properties: 2
131
- separate_groups: false
132
-
133
- PropertySpelling:
134
- enabled: true
135
- extra_properties: []
136
-
137
140
  QualifyingElement:
138
141
  enabled: true
139
142
  allow_element_with_attribute: false
@@ -172,6 +175,10 @@ linters:
172
175
  SpaceAfterVariableName:
173
176
  enabled: true
174
177
 
178
+ SpaceAroundOperator:
179
+ enabled: true
180
+ style: one_space # or 'no_space'
181
+
175
182
  SpaceBeforeBrace:
176
183
  enabled: true
177
184
  style: space # or 'new_line'
@@ -194,6 +201,9 @@ linters:
194
201
  TrailingZero:
195
202
  enabled: false
196
203
 
204
+ TransitionAll:
205
+ enabled: false
206
+
197
207
  UnnecessaryMantissa:
198
208
  enabled: true
199
209
 
@@ -8,6 +8,18 @@ bottom
8
8
  left
9
9
  z-index
10
10
  display
11
+ align-content
12
+ align-items
13
+ align-self
14
+ flex
15
+ flex-basis
16
+ flex-direction
17
+ flex-flow
18
+ flex-grow
19
+ flex-shrink
20
+ flex-wrap
21
+ justify-content
22
+ order
11
23
  float
12
24
  width
13
25
  height
@@ -10,6 +10,19 @@ right
10
10
  bottom
11
11
  left
12
12
 
13
+ flex
14
+ flex-basis
15
+ flex-direction
16
+ flex-flow
17
+ flex-grow
18
+ flex-shrink
19
+ flex-wrap
20
+ align-content
21
+ align-items
22
+ align-self
23
+ justify-content
24
+ order
25
+
13
26
  width
14
27
  min-width
15
28
  max-width
data/lib/scss_lint/cli.rb CHANGED
@@ -56,7 +56,7 @@ module SCSSLint
56
56
  def scan_for_lints(options, config)
57
57
  runner = Runner.new(config)
58
58
  runner.run(FileFinder.new(config).find(options[:files]))
59
- report_lints(options, runner.lints)
59
+ report_lints(options, runner.lints, runner.files)
60
60
 
61
61
  if runner.lints.any?(&:error?)
62
62
  halt :error
@@ -159,10 +159,11 @@ module SCSSLint
159
159
 
160
160
  # @param options [Hash]
161
161
  # @param lints [Array<Lint>]
162
- def report_lints(options, lints)
162
+ # @param files [Array<String>]
163
+ def report_lints(options, lints, files)
163
164
  sorted_lints = lints.sort_by { |l| [l.filename, l.location] }
164
165
  options.fetch(:reporters).each do |reporter, output|
165
- results = reporter.new(sorted_lints).report_lints
166
+ results = reporter.new(sorted_lints, files).report_lints
166
167
  io = (output == :stdout ? $stdout : File.new(output, 'w+'))
167
168
  io.print results if results
168
169
  end
@@ -25,11 +25,11 @@ module SCSSLint
25
25
  linters = command[:linters]
26
26
  return unless linters.include?('all') || linters.include?(@linter.name)
27
27
 
28
- process_command(command[:action], node)
28
+ process_command(command, node)
29
29
 
30
30
  # Is the control comment the only thing on this line?
31
31
  return if node.is_a?(Sass::Tree::RuleNode) ||
32
- %r{^\s*(//|/\*)}.match(@linter.engine.lines[node.line - 1])
32
+ %r{^\s*(//|/\*)}.match(@linter.engine.lines[command[:line] - 1])
33
33
 
34
34
  # Otherwise, pop since we only want comment to apply to the single line
35
35
  pop_control_comment_stack(node)
@@ -39,7 +39,7 @@ module SCSSLint
39
39
  #
40
40
  # @param node [Sass::Tree::Node]
41
41
  def after_node_visit(node)
42
- while @disable_stack.any? && @disable_stack.last.node_parent == node
42
+ while @disable_stack.any? && @disable_stack.last[:node].node_parent == node
43
43
  pop_control_comment_stack(node)
44
44
  end
45
45
  end
@@ -47,41 +47,50 @@ module SCSSLint
47
47
  private
48
48
 
49
49
  def extract_command(node)
50
- comment =
51
- case node
52
- when Sass::Tree::CommentNode
53
- node.value.first
54
- when Sass::Tree::RuleNode
55
- node.rule.select { |chunk| chunk.is_a?(String) }.join
50
+ return unless comment = retrieve_comment_text(node)
51
+
52
+ comment.split(/(?<=\n)/).each_with_index do |comment_line, line_no|
53
+ if match = %r{
54
+ (/|\*|^ \*)\s* # Comment start marker
55
+ scss-lint:
56
+ (?<action>disable|enable)\s+
57
+ (?<linters>.*?)
58
+ \s*(?:\*/|\n) # Comment end marker or end of line
59
+ }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
+ }
56
65
  end
66
+ end
67
+
68
+ false
69
+ end
57
70
 
58
- return unless match = %r{
59
- (/|\*)\s* # Comment start marker
60
- scss-lint:
61
- (?<action>disable|enable)\s+
62
- (?<linters>.*?)
63
- \s*(?:\*/|\n) # Comment end marker or end of line
64
- }x.match(comment)
65
-
66
- {
67
- action: match[:action],
68
- linters: match[:linters].split(/\s*,\s*|\s+/),
69
- }
71
+ 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
70
78
  end
71
79
 
72
80
  def process_command(command, node)
73
- case command
81
+ case command[:action]
74
82
  when 'disable'
75
- @disable_stack << node
83
+ @disable_stack << { node: node, line: command[:line] }
76
84
  when 'enable'
77
85
  pop_control_comment_stack(node)
78
86
  end
79
87
  end
80
88
 
81
89
  def pop_control_comment_stack(node)
82
- return unless comment_node = @disable_stack.pop
90
+ return unless command = @disable_stack.pop
83
91
 
84
- start_line = comment_node.line
92
+ comment_node = command[:node]
93
+ start_line = command[:line]
85
94
  if comment_node.class.node_name == :rule
86
95
  end_line = start_line
87
96
  elsif node.class.node_name == :root
@@ -32,7 +32,7 @@ module SCSSLint
32
32
  return unless %w[0 none].include?(border)
33
33
  return if @preference[0] == border
34
34
 
35
- add_lint(node, "`border: #{@preference[0]} is preferred over " \
35
+ add_lint(node, "`border: #{@preference[0]}` is preferred over " \
36
36
  "`border: #{@preference[1]}`")
37
37
  end
38
38
  end
@@ -0,0 +1,39 @@
1
+ module SCSSLint
2
+ # Checks for "reason" comments above linter-disabling comments.
3
+ class Linter::DisableLinterReason < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_comment(node)
7
+ # No lint if the first line of the comment is not a command (because then
8
+ # either this comment has no commands, or the first line serves as a the
9
+ # reason for a command on a later line).
10
+ return unless comment_lines(node).first.match(COMMAND_REGEX)
11
+
12
+ # Maybe the previous node is the "reason" comment.
13
+ prev = previous_node(node)
14
+
15
+ if prev && prev.is_a?(Sass::Tree::CommentNode)
16
+ # No lint if the last line of the previous comment is not a command.
17
+ return unless comment_lines(prev).last.match(COMMAND_REGEX)
18
+ end
19
+
20
+ add_lint(node,
21
+ 'scss-lint:disable control comments should be preceded by a ' \
22
+ 'comment explaining why the linters need to be disabled.')
23
+ end
24
+
25
+ private
26
+
27
+ COMMAND_REGEX = %r{
28
+ (/|\*)\s* # Comment start marker
29
+ scss-lint:
30
+ (?<action>disable)\s+
31
+ (?<linters>.*?)
32
+ \s*(?:\*/|\n) # Comment end marker or end of line
33
+ }x
34
+
35
+ def comment_lines(node)
36
+ node.value.join.split("\n")
37
+ end
38
+ end
39
+ end
@@ -11,8 +11,10 @@ module SCSSLint
11
11
  seen_nodes << child_node
12
12
  next unless mergeable_node
13
13
 
14
+ rule_text = node_rule(child_node).gsub(/(\r?\n)+/, ' ')
15
+
14
16
  add_lint child_node.line,
15
- "Merge rule `#{node_rule(child_node)}` with rule " \
17
+ "Merge rule `#{rule_text}` with rule " \
16
18
  "on line #{mergeable_node.line}"
17
19
  end
18
20
 
@@ -30,6 +30,7 @@ module SCSSLint
30
30
 
31
31
  def ignore_selectors?(node)
32
32
  return unless config['ignore_parent_selectors']
33
+ return unless node.parsed_rules
33
34
 
34
35
  simple_selectors(node.parsed_rules).all? do |selector|
35
36
  IGNORED_SELECTORS.include?(selector.class)
@@ -47,7 +47,7 @@ module SCSSLint
47
47
  # combinator, as these "combine" simple sequences such that they do not
48
48
  # increase depth.
49
49
  depth = simple_sequences.size -
50
- separators.count { |item| item == '~' || item == '+' }
50
+ separators.count { |item| item == '~' || item == '+' }
51
51
 
52
52
  if parent_selectors > 0
53
53
  # If parent selectors are present, add the current depth for each
@@ -3,7 +3,7 @@ module SCSSLint
3
3
  class Linter::SingleLinePerProperty < Linter
4
4
  include LinterRegistry
5
5
 
6
- def visit_rule(node) # rubocop:disable CyclomaticComplexity
6
+ def visit_rule(node)
7
7
  single_line = single_line_rule_set?(node)
8
8
  return if single_line && config['allow_single_line_rule_sets']
9
9
 
@@ -3,7 +3,7 @@ module SCSSLint
3
3
  class Linter::SingleLinePerSelector < Linter
4
4
  include LinterRegistry
5
5
 
6
- MESSAGE = 'Each selector in a comma sequence should be on its own line'
6
+ MESSAGE = 'Each selector in a comma sequence should be on its own single line'
7
7
 
8
8
  def visit_comma_sequence(node)
9
9
  return unless node.members.count > 1
@@ -15,6 +15,14 @@ module SCSSLint
15
15
  end
16
16
  end
17
17
 
18
+ def visit_sequence(node)
19
+ node.members[1..-1].each_with_index do |item, index|
20
+ next unless item == "\n"
21
+
22
+ add_lint(node.line + index, MESSAGE)
23
+ end
24
+ end
25
+
18
26
  private
19
27
 
20
28
  def check_comma_on_own_line(node)
@@ -1,7 +1,7 @@
1
1
  module SCSSLint
2
2
  # Checks for spaces following the name of a variable and before the colon
3
3
  # separating the variables's name from its value.
4
- class SpaceAfterVariableName < Linter
4
+ class Linter::SpaceAfterVariableName < Linter
5
5
  include LinterRegistry
6
6
 
7
7
  def visit_variable(node)
@@ -0,0 +1,86 @@
1
+ module SCSSLint
2
+ # Checks for space around operators on values.
3
+ class Linter::SpaceAroundOperator < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_script_operation(node) # rubocop:disable Metrics/AbcSize
7
+ source = normalize_source(source_from_range(node.source_range))
8
+ left_range = node.operand1.source_range
9
+ right_range = node.operand2.source_range
10
+
11
+ # We need to #chop at the end because an operation's operand1 _always_
12
+ # includes one character past the actual operand (which is either a
13
+ # whitespace character, or the first character of the operation).
14
+ left_source = normalize_source(source_from_range(left_range))
15
+ right_source = normalize_source(source_from_range(right_range))
16
+ operator_source = source_between(left_range, right_range)
17
+ left_source, operator_source = adjust_left_boundary(left_source, operator_source)
18
+
19
+ match = operator_source.match(/
20
+ (?<left_space>\s*)
21
+ (?<operator>\S+)
22
+ (?<right_space>\s*)
23
+ /x)
24
+
25
+ if config['style'] == 'one_space'
26
+ if match[:left_space] != ' ' || match[:right_space] != ' '
27
+ add_lint(node, SPACE_MSG % [source, left_source, match[:operator], right_source])
28
+ end
29
+ elsif match[:left_space] != '' || match[:right_space] != ''
30
+ add_lint(node, NO_SPACE_MSG % [source, left_source, match[:operator], right_source])
31
+ end
32
+
33
+ yield
34
+ end
35
+
36
+ private
37
+
38
+ SPACE_MSG = '`%s` should be written with a single space on each side of ' \
39
+ 'the operator: `%s %s %s`'
40
+ NO_SPACE_MSG = '`%s` should be written without spaces around the ' \
41
+ 'operator: `%s%s%s`'
42
+
43
+ def source_between(range1, range2)
44
+ # We don't want to add 1 to range1.end_pos.offset for the same reason as
45
+ # the #chop comment above.
46
+ between_start = Sass::Source::Position.new(
47
+ range1.end_pos.line,
48
+ range1.end_pos.offset,
49
+ )
50
+ between_end = Sass::Source::Position.new(
51
+ range2.start_pos.line,
52
+ range2.start_pos.offset - 1,
53
+ )
54
+
55
+ source_from_range(Sass::Source::Range.new(between_start,
56
+ between_end,
57
+ range1.file,
58
+ range1.importer))
59
+ end
60
+
61
+ # Removes trailing parentheses and compacts newlines into a single space
62
+ def normalize_source(source)
63
+ source.chop.gsub(/\s*\n\s*/, ' ')
64
+ end
65
+
66
+ def adjust_left_boundary(left, operator)
67
+ # If the left operand is wrapped in parentheses, any right parens end up
68
+ # in the operator source. Here, we move them into the left operand
69
+ # source, which is awkward in any messaging, but it works.
70
+ if match = operator.match(/^(\s*\))+/)
71
+ left += match[0]
72
+ operator = operator[match.end(0)..-1]
73
+ end
74
+
75
+ # If the left operand is a nested operation, Sass includes any whitespace
76
+ # before the (outer) operator in the left operator's source_range's
77
+ # end_pos, which is not the case with simple, non-operation operands.
78
+ if match = left.match(/\s+$/)
79
+ left = left[0..match.begin(0)]
80
+ operator = match[0] + operator
81
+ end
82
+
83
+ [left, operator]
84
+ end
85
+ end
86
+ end
@@ -1,36 +1,112 @@
1
1
  module SCSSLint
2
+ # rubocop:disable Metrics/AbcSize
3
+
2
4
  # Checks for the presence of spaces between parentheses.
3
5
  class Linter::SpaceBetweenParens < Linter
4
6
  include LinterRegistry
5
7
 
6
- def visit_root(_node)
8
+ def check_node(node)
9
+ check(node, source_from_range(node.source_range))
10
+ yield
11
+ end
12
+
13
+ alias_method :visit_atroot, :check_node
14
+ alias_method :visit_cssimport, :check_node
15
+ alias_method :visit_function, :check_node
16
+ alias_method :visit_media, :check_node
17
+ alias_method :visit_mixindef, :check_node
18
+ alias_method :visit_mixin, :check_node
19
+ alias_method :visit_script_funcall, :check_node
20
+
21
+ def feel_for_parens_and_check_node(node)
22
+ source = feel_for_enclosing_parens(node)
23
+ check(node, source)
24
+ yield
25
+ end
26
+
27
+ alias_method :visit_script_listliteral, :feel_for_parens_and_check_node
28
+ alias_method :visit_script_mapliteral, :feel_for_parens_and_check_node
29
+ alias_method :visit_script_operation, :feel_for_parens_and_check_node
30
+ alias_method :visit_script_string, :feel_for_parens_and_check_node
31
+
32
+ private
33
+
34
+ TRAILING_WHITESPACE = /\s*$/
35
+
36
+ def check(node, source) # rubocop:disable Metrics/MethodLength
7
37
  @spaces = config['spaces']
38
+ source = trim_right_paren(source)
39
+ return if source.count('(') != source.count(')')
40
+ source.scan(/
41
+ \(
42
+ (?<left>\s*)
43
+ (?<contents>.*)
44
+ (?<right>\s*)
45
+ \)
46
+ /x) do |left, contents, right|
47
+ right = contents.match(TRAILING_WHITESPACE)[0] + right
48
+ contents.gsub(TRAILING_WHITESPACE, '')
49
+
50
+ # We don't lint on multiline parenthetical source.
51
+ break if (left + contents + right).include? "\n"
8
52
 
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]
53
+ if contents.empty?
54
+ # If we're looking at empty parens (like `()`, `( )`, `( )`, etc.),
55
+ # only report a possible lint on the left side.
56
+ right = ' ' * @spaces
57
+ end
58
+
59
+ if left != ' ' * @spaces
60
+ message = "#{expected_spaces} after `(` instead of `#{left}`"
61
+ add_lint(node, message)
62
+ end
63
+
64
+ if right != ' ' * @spaces
65
+ message = "#{expected_spaces} before `)` instead of `#{right}`"
66
+ add_lint(node, message)
19
67
  end
20
68
  end
21
- yield
22
69
  end
23
70
 
24
- private
71
+ # An expression enclosed in parens will include or not include each paren, depending
72
+ # on whitespace. Here we feel out for enclosing parens, and return them as the new
73
+ # source for the node.
74
+ def feel_for_enclosing_parens(node) # rubocop:disable Metrics/CyclomaticComplexity
75
+ range = node.source_range
76
+ original_source = source_from_range(range)
77
+ left_offset = -1
78
+ right_offset = 0
79
+
80
+ if original_source[-1] != ')'
81
+ right_offset += 1 while character_at(range.end_pos, right_offset) =~ /\s/
25
82
 
26
- def check(str, index)
27
- spaces = str.count ' '
28
- return if spaces == @spaces
83
+ return original_source if character_at(range.end_pos, right_offset) != ')'
84
+ end
85
+
86
+ # At this point, we know that we're wrapped on the right by a ')'.
87
+ # Are we wrapped on the left by a '('?
88
+ left_offset -= 1 while character_at(range.start_pos, left_offset) =~ /\s/
89
+ return original_source if character_at(range.start_pos, left_offset) != '('
90
+
91
+ # At this point, we know we're wrapped on both sides by parens. However,
92
+ # those parens may be part of a parent function call. We don't care about
93
+ # such parens. This depends on whether the preceding character is part of
94
+ # a function name.
95
+ return original_source if character_at(range.start_pos, left_offset - 1) =~ /[A-Za-z0-9_]/
96
+
97
+ range.start_pos.offset += left_offset
98
+ range.end_pos.offset += right_offset
99
+ source_from_range(range)
100
+ end
101
+
102
+ # An unrelated right paren will sneak into the source of a node if there is no
103
+ # whitespace between the node and the right paren.
104
+ def trim_right_paren(source)
105
+ (source.count(')') == source.count('(') + 1) ? source[0..-2] : source
106
+ end
29
107
 
30
- location = Location.new(index + 1)
31
- message = "Expected #{pluralize(@spaces, 'space')} " \
32
- "between parentheses instead of #{spaces}"
33
- add_lint(location, message)
108
+ def expected_spaces
109
+ "Expected #{pluralize(@spaces, 'space')}"
34
110
  end
35
111
  end
36
112
  end
@@ -0,0 +1,30 @@
1
+ module SCSSLint
2
+ # Checks for explicitly transitioned properties instead of transition all.
3
+ class Linter::TransitionAll < Linter
4
+ include LinterRegistry
5
+
6
+ TRANSITION_PROPERTIES = %w[
7
+ transition
8
+ transition-property
9
+ ]
10
+
11
+ def visit_prop(node)
12
+ property = node.name.first.to_s
13
+ return unless TRANSITION_PROPERTIES.include?(property)
14
+
15
+ check_transition(node, property, node.value.to_sass)
16
+ end
17
+
18
+ private
19
+
20
+ def check_transition(node, property, value)
21
+ return unless offset = value =~ /\ball\b/
22
+
23
+ pos = node.value_source_range.start_pos.after(value[0, offset])
24
+
25
+ add_lint(Location.new(pos.line, pos.offset, 3),
26
+ "#{property} should contain explicit properties " \
27
+ 'instead of using the keyword all')
28
+ end
29
+ end
30
+ end
@@ -6,6 +6,7 @@ module SCSSLint
6
6
 
7
7
  def visit_script_string(node)
8
8
  return unless node.type == :identifier
9
+ return if node.value =~ /^'|"/
9
10
 
10
11
  node.value.scan(REAL_NUMBER_REGEX) do |number, integer, mantissa, units|
11
12
  if unnecessary_mantissa?(mantissa)
@@ -0,0 +1,10 @@
1
+ module SCSSLint
2
+ # Reports a single line for each clean file (having zero lints).
3
+ class Reporter::CleanFilesReporter < Reporter
4
+ def report_lints
5
+ dirty_files = lints.map(&:filename).uniq
6
+ clean_files = files - dirty_files
7
+ clean_files.sort.join("\n") + "\n" if clean_files.any?
8
+ end
9
+ end
10
+ end
@@ -1,14 +1,17 @@
1
1
  module SCSSLint
2
2
  # Responsible for displaying lints to the user in some format.
3
3
  class Reporter
4
- attr_reader :lints
4
+ attr_reader :lints, :files
5
5
 
6
6
  def self.descendants
7
7
  ObjectSpace.each_object(Class).select { |klass| klass < self }
8
8
  end
9
9
 
10
- def initialize(lints)
10
+ # @param lints [List<Lint>] a list of Lints sorted by file and line number
11
+ # @param files [List<String>] a list of the files that were linted
12
+ def initialize(lints, files)
11
13
  @lints = lints
14
+ @files = files
12
15
  end
13
16
 
14
17
  def report_lints
@@ -2,7 +2,7 @@ module SCSSLint
2
2
  # Finds and aggregates all lints found by running the registered linters
3
3
  # against a set of SCSS files.
4
4
  class Runner
5
- attr_reader :lints
5
+ attr_reader :lints, :files
6
6
 
7
7
  # @param config [Config]
8
8
  def initialize(config)
@@ -13,7 +13,8 @@ module SCSSLint
13
13
 
14
14
  # @param files [Array]
15
15
  def run(files)
16
- files.each do |file|
16
+ @files = files
17
+ @files.each do |file|
17
18
  find_lints(file)
18
19
  end
19
20
  end