scss_lint 0.40.1 → 0.41.0

Sign up to get free protection for your applications and to get access to all the features.
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