scss_lint 0.41.0 → 0.42.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +21 -0
  3. data/config/default.yml +1 -0
  4. data/lib/scss_lint/cli.rb +0 -4
  5. data/lib/scss_lint/config.rb +1 -1
  6. data/lib/scss_lint/engine.rb +8 -2
  7. data/lib/scss_lint/exceptions.rb +0 -4
  8. data/lib/scss_lint/file_finder.rb +1 -8
  9. data/lib/scss_lint/linter.rb +3 -3
  10. data/lib/scss_lint/linter/color_variable.rb +20 -0
  11. data/lib/scss_lint/linter/else_placement.rb +1 -0
  12. data/lib/scss_lint/linter/import_path.rb +2 -2
  13. data/lib/scss_lint/linter/name_format.rb +1 -1
  14. data/lib/scss_lint/linter/space_after_comma.rb +11 -2
  15. data/lib/scss_lint/linter/space_around_operator.rb +115 -59
  16. data/lib/scss_lint/linter/trailing_semicolon.rb +17 -3
  17. data/lib/scss_lint/linter/url_format.rb +3 -3
  18. data/lib/scss_lint/linter/variable_for_property.rb +8 -0
  19. data/lib/scss_lint/linter/vendor_prefix.rb +1 -1
  20. data/lib/scss_lint/rake_task.rb +6 -1
  21. data/lib/scss_lint/runner.rb +2 -2
  22. data/lib/scss_lint/sass/tree.rb +1 -1
  23. data/lib/scss_lint/selector_visitor.rb +4 -4
  24. data/lib/scss_lint/version.rb +1 -1
  25. data/spec/scss_lint/cli_spec.rb +0 -12
  26. data/spec/scss_lint/file_finder_spec.rb +2 -6
  27. data/spec/scss_lint/linter/color_variable_spec.rb +41 -1
  28. data/spec/scss_lint/linter/space_after_comma_spec.rb +891 -153
  29. data/spec/scss_lint/linter/space_around_operator_spec.rb +25 -0
  30. data/spec/scss_lint/linter/trailing_semicolon_spec.rb +50 -0
  31. data/spec/scss_lint/linter/variable_for_property_spec.rb +10 -0
  32. data/spec/scss_lint/rake_task_spec.rb +59 -10
  33. data/spec/scss_lint/reporter/clean_files_reporter_spec.rb +2 -2
  34. data/spec/scss_lint/reporter/default_reporter_spec.rb +3 -3
  35. data/spec/scss_lint/reporter/files_reporter_spec.rb +1 -1
  36. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 478b23880809f4fb5eeb95543b34534de05edf5a
4
- data.tar.gz: 97f5d37498b751b7bdae0dd23032acc92de56fdf
3
+ metadata.gz: 84412ffcfbcad02e1d135dae52e56c33c0f42c19
4
+ data.tar.gz: af6e1f97f4755ab0b69cf7b4162412571aa42704
5
5
  SHA512:
6
- metadata.gz: 4808204c5756753ab99d9485590e034c6711f5b4ce67024c09dcfd281f6fa4199eb12331dd4a272933428c5991093bb4e6d2b5cf77743886df83488acc35651d
7
- data.tar.gz: 951df4e1c90579ff4ebc55b61693bab102ba66761875781f6e0baf7d64232c0e95004f0de6eaacdc2db483a1c0d5d287aa79dd14d75d52220540f7cfbc129ee4
6
+ metadata.gz: 1fe04a0e8578d96cb59af5ea52bb4ca274d386153bbe9cb47541bb6f04905ff5e47062237bb7956dfcbdf860fde3c3814344321a62e1d3d3f21cb92a99939ebf
7
+ data.tar.gz: cefba0a8a5a0ab3b7c19e706efaaac38fbde69fabcd2ae813f4fc363ce2e42babd5fbca2261cf5db9c964a05d35dc2a262d756db45e88c5ced577a5b76b8a4c5
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2014-2015 Brigade
2
+ http://www.brigade.com/
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/config/default.yml CHANGED
@@ -164,6 +164,7 @@ linters:
164
164
 
165
165
  SpaceAfterComma:
166
166
  enabled: true
167
+ style: one_space # or 'no_space', or 'at_least_one_space'
167
168
 
168
169
  SpaceAfterPropertyColon:
169
170
  enabled: true
data/lib/scss_lint/cli.rb CHANGED
@@ -19,7 +19,6 @@ module SCSSLint
19
19
  software: 70, # Internal software error
20
20
  config: 78, # Configuration error
21
21
  no_files: 80, # No files matched by specified glob patterns
22
- files_filtered: 81, # All matched files were filtered by exclusions
23
22
  plugin: 82, # Plugin loading error
24
23
  }
25
24
 
@@ -79,9 +78,6 @@ module SCSSLint
79
78
  when SCSSLint::Exceptions::RequiredLibraryMissingError
80
79
  puts exception.message
81
80
  halt :unavailable
82
- when SCSSLint::Exceptions::AllFilesFilteredError
83
- puts exception.message
84
- halt :files_filtered
85
81
  when SCSSLint::Exceptions::NoFilesError
86
82
  puts exception.message
87
83
  halt :no_files
@@ -156,7 +156,7 @@ module SCSSLint
156
156
  else
157
157
  path = File.join(File.dirname(base_config_path), relative_include_path)
158
158
  # Remove double backslashes appearing in Windows paths.
159
- path.gsub(%r{^//}, File::SEPARATOR)
159
+ path.sub(%r{^//}, File::SEPARATOR)
160
160
  end
161
161
  end
162
162
 
@@ -8,7 +8,7 @@ module SCSSLint
8
8
  class Engine
9
9
  ENGINE_OPTIONS = { cache: false, syntax: :scss }
10
10
 
11
- attr_reader :contents, :filename, :lines, :tree
11
+ attr_reader :contents, :filename, :lines, :tree, :any_control_commands
12
12
 
13
13
  # Creates a parsed representation of an SCSS document from the given string
14
14
  # or file.
@@ -27,7 +27,8 @@ module SCSSLint
27
27
  # Need `to_a` for Ruby 1.9.3.
28
28
  @lines = @contents.force_encoding('UTF-8').lines.to_a
29
29
  @tree = @engine.to_tree
30
- rescue Encoding::UndefinedConversionError, Sass::SyntaxError => error
30
+ find_any_control_commands
31
+ rescue Encoding::UndefinedConversionError, Sass::SyntaxError, ArgumentError => error
31
32
  if error.is_a?(Encoding::UndefinedConversionError) ||
32
33
  error.message.match(/invalid.*(byte sequence|character)/i)
33
34
  raise FileEncodingError,
@@ -52,5 +53,10 @@ module SCSSLint
52
53
  @engine = Sass::Engine.new(scss, ENGINE_OPTIONS)
53
54
  @contents = scss
54
55
  end
56
+
57
+ def find_any_control_commands
58
+ @any_control_commands =
59
+ @lines.any? { |line| line['scss-lint:disable'] || line['scss-line:enable'] }
60
+ end
55
61
  end
56
62
  end
@@ -1,8 +1,4 @@
1
1
  module SCSSLint::Exceptions
2
- # Raised when all files matched by the specified glob patterns were filtered
3
- # by exclude patterns.
4
- class AllFilesFilteredError < StandardError; end
5
-
6
2
  # Raised when an invalid flag is given via the command line.
7
3
  class InvalidCLIOption < StandardError; end
8
4
 
@@ -28,14 +28,7 @@ module SCSSLint
28
28
  "No SCSS files matched by the patterns: #{patterns.join(' ')}"
29
29
  end
30
30
 
31
- filtered_files = matched_files.reject { |file| @config.excluded_file?(file) }
32
- if filtered_files.empty?
33
- raise SCSSLint::Exceptions::AllFilesFilteredError,
34
- "All files matched by the patterns [#{patterns.join(', ')}] " \
35
- "were excluded by the patterns: [#{@config.exclude_patterns.join(', ')}]"
36
- end
37
-
38
- filtered_files
31
+ matched_files.reject { |file| @config.excluded_file?(file) }
39
32
  end
40
33
 
41
34
  private
@@ -68,7 +68,7 @@ module SCSSLint
68
68
  actual_line = source_position.line - 1
69
69
  actual_offset = source_position.offset + offset - 1
70
70
 
71
- engine.lines[actual_line][actual_offset]
71
+ engine.lines.size > actual_line && engine.lines[actual_line][actual_offset]
72
72
  end
73
73
 
74
74
  # Extracts the original source code given a range.
@@ -127,9 +127,9 @@ module SCSSLint
127
127
  visit_selector(node.parsed_rules)
128
128
  end
129
129
 
130
- @comment_processor.before_node_visit(node)
130
+ @comment_processor.before_node_visit(node) if @engine.any_control_commands
131
131
  super
132
- @comment_processor.after_node_visit(node)
132
+ @comment_processor.after_node_visit(node) if @engine.any_control_commands
133
133
  end
134
134
 
135
135
  # Redefine so we can set the `node_parent` of each node
@@ -3,6 +3,8 @@ module SCSSLint
3
3
  class Linter::ColorVariable < Linter
4
4
  include LinterRegistry
5
5
 
6
+ COLOR_FUNCTIONS = %w[rgb rgba hsl hsla]
7
+
6
8
  def visit_script_color(node)
7
9
  return if in_variable_declaration?(node) ||
8
10
  in_map_declaration?(node) ||
@@ -30,6 +32,14 @@ module SCSSLint
30
32
  # comments, so it's easiest to just ignore them.
31
33
  end
32
34
 
35
+ def visit_script_funcall(node)
36
+ if color_function?(node) && all_arguments_are_literals?(node)
37
+ record_lint node, node.to_sass
38
+ else
39
+ yield
40
+ end
41
+ end
42
+
33
43
  private
34
44
 
35
45
  def record_lint(node, color)
@@ -63,5 +73,15 @@ module SCSSLint
63
73
  def in_map_declaration?(node)
64
74
  node_ancestor(node, 2).is_a?(Sass::Script::Tree::MapLiteral)
65
75
  end
76
+
77
+ def all_arguments_are_literals?(node)
78
+ node.args.all? do |arg|
79
+ arg.is_a?(Sass::Script::Tree::Literal)
80
+ end
81
+ end
82
+
83
+ def color_function?(node)
84
+ COLOR_FUNCTIONS.include?(node.name)
85
+ end
66
86
  end
67
87
  end
@@ -7,6 +7,7 @@ module SCSSLint
7
7
  def visit_if(node)
8
8
  visit_else(node, node.else) if node.else
9
9
  yield # Lint nested @if statements
10
+ visit(node.else) if node.else
10
11
  end
11
12
 
12
13
  def visit_else(if_node, else_node)
@@ -43,13 +43,13 @@ module SCSSLint
43
43
  fixed_basename = orig_basename
44
44
 
45
45
  if config['leading_underscore']
46
- fixed_basename = '_' + fixed_basename unless fixed_basename.match(/^_/)
46
+ fixed_basename = '_' + fixed_basename unless fixed_basename.start_with?('_')
47
47
  else
48
48
  fixed_basename = fixed_basename.sub(/^_/, '')
49
49
  end
50
50
 
51
51
  if config['filename_extension']
52
- fixed_basename += '.scss' unless fixed_basename.match(/\.scss$/)
52
+ fixed_basename += '.scss' unless fixed_basename.end_with?('.scss')
53
53
  else
54
54
  fixed_basename = fixed_basename.sub(/\.scss$/, '')
55
55
  end
@@ -52,7 +52,7 @@ module SCSSLint
52
52
  def trim_underscore_prefix(name)
53
53
  if config['allow_leading_underscore']
54
54
  # Remove if there is a single leading underscore
55
- name = name.gsub(/^_(?!_)/, '')
55
+ name = name.sub(/^_(?!_)/, '')
56
56
  end
57
57
 
58
58
  name
@@ -59,7 +59,16 @@ module SCSSLint
59
59
  end
60
60
  end
61
61
 
62
- EXPECTED_SPACES_AFTER_COMMA = 1
62
+ def valid_spaces_after_comma?(spaces)
63
+ case config['style']
64
+ when 'one_space'
65
+ spaces == 1
66
+ when 'no_space'
67
+ spaces == 0
68
+ when 'at_least_one_space'
69
+ spaces >= 1
70
+ end
71
+ end
63
72
 
64
73
  # Check the comma after each argument in a list for a space following it,
65
74
  # reporting a lint using the given [arg_type].
@@ -79,7 +88,7 @@ module SCSSLint
79
88
  offset += 1
80
89
  end
81
90
  next if char == "\n" || # Ignore trailing spaces
82
- spaces == EXPECTED_SPACES_AFTER_COMMA
91
+ valid_spaces_after_comma?(spaces)
83
92
 
84
93
  add_lint arg, "Commas in #{arg_type} should be followed by a single space"
85
94
  end
@@ -3,20 +3,32 @@ module SCSSLint
3
3
  class Linter::SpaceAroundOperator < Linter
4
4
  include LinterRegistry
5
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(/
6
+ def visit_script_operation(node)
7
+ operation_sources = OperationSources.new(node, self)
8
+ operation_sources.adjust_sources
9
+
10
+ # When an operation is found interpolated within something not a String
11
+ # (only selectors?), the source ranges are offset by two (probably not
12
+ # accounting for the `#{`. Slide everything to the left by 2, and maybe
13
+ # things will look sane this time.
14
+ unless operation_sources.operator_source =~ Sass::Script::Lexer::REGULAR_EXPRESSIONS[:op]
15
+ operation_sources.adjust_for_interpolation
16
+ operation_sources.adjust_sources
17
+ end
18
+
19
+ check(node, operation_sources)
20
+
21
+ yield
22
+ end
23
+
24
+ def source_fm_range(range)
25
+ source_from_range(range)
26
+ end
27
+
28
+ private
29
+
30
+ def check(node, operation_sources)
31
+ match = operation_sources.operator_source.match(/
20
32
  (?<left_space>\s*)
21
33
  (?<operator>\S+)
22
34
  (?<right_space>\s*)
@@ -24,63 +36,107 @@ module SCSSLint
24
36
 
25
37
  if config['style'] == 'one_space'
26
38
  if match[:left_space] != ' ' || match[:right_space] != ' '
27
- add_lint(node, SPACE_MSG % [source, left_source, match[:operator], right_source])
39
+ add_lint(node, operation_sources.space_msg(match[:operator]))
28
40
  end
29
41
  elsif match[:left_space] != '' || match[:right_space] != ''
30
- add_lint(node, NO_SPACE_MSG % [source, left_source, match[:operator], right_source])
42
+ add_lint(node, operation_sources.no_space_msg(match[:operator]))
31
43
  end
32
-
33
- yield
34
44
  end
35
45
 
36
- private
46
+ # A helper class for storing and adjusting the sources of the different
47
+ # components of an Operation node.
48
+ class OperationSources
49
+ attr_reader :operator_source
37
50
 
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
51
+ def initialize(node, linter)
52
+ @node = node
53
+ @linter = linter
54
+ @source = normalize_source(@linter.source_fm_range(@node.source_range))
55
+ @left_range = @node.operand1.source_range
56
+ @right_range = @node.operand2.source_range
57
+ end
60
58
 
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
59
+ def adjust_sources
60
+ # We need to #chop at the end because an operation's operand1 _always_
61
+ # includes one character past the actual operand (which is either a
62
+ # whitespace character, or the first character of the operation).
63
+ @left_source = normalize_source(@linter.source_fm_range(@left_range))
64
+ @right_source = normalize_source(@linter.source_fm_range(@right_range))
65
+ @operator_source = calculate_operator_source
66
+ adjust_left_boundary
67
+ end
65
68
 
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]
69
+ def adjust_for_interpolation
70
+ @source = normalize_source(
71
+ @linter.source_fm_range(slide_to_the_left(@node.source_range)))
72
+ @left_range = slide_to_the_left(@node.operand1.source_range)
73
+ @right_range = slide_to_the_left(@node.operand2.source_range)
73
74
  end
74
75
 
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
76
+ def space_msg(operator)
77
+ SPACE_MSG % [@source, @left_source, operator, @right_source]
81
78
  end
82
79
 
83
- [left, operator]
80
+ def no_space_msg(operator)
81
+ NO_SPACE_MSG % [@source, @left_source, operator, @right_source]
82
+ end
83
+
84
+ private
85
+
86
+ SPACE_MSG = '`%s` should be written with a single space on each side of ' \
87
+ 'the operator: `%s %s %s`'
88
+
89
+ NO_SPACE_MSG = '`%s` should be written without spaces around the ' \
90
+ 'operator: `%s%s%s`'
91
+
92
+ def calculate_operator_source
93
+ # We don't want to add 1 to range1.end_pos.offset for the same reason as
94
+ # the #chop comment above.
95
+ between_start = Sass::Source::Position.new(
96
+ @left_range.end_pos.line,
97
+ @left_range.end_pos.offset,
98
+ )
99
+ between_end = Sass::Source::Position.new(
100
+ @right_range.start_pos.line,
101
+ @right_range.start_pos.offset - 1,
102
+ )
103
+
104
+ @linter.source_fm_range(Sass::Source::Range.new(between_start,
105
+ between_end,
106
+ @left_range.file,
107
+ @left_range.importer))
108
+ end
109
+
110
+ def adjust_left_boundary
111
+ # If the left operand is wrapped in parentheses, any right parens end up
112
+ # in the operator source. Here, we move them into the left operand
113
+ # source, which is awkward in any messaging, but it works.
114
+ if match = @operator_source.match(/^(\s*\))+/)
115
+ @left_source += match[0]
116
+ @operator_source = @operator_source[match.end(0)..-1]
117
+ end
118
+
119
+ # If the left operand is a nested operation, Sass includes any whitespace
120
+ # before the (outer) operator in the left operator's source_range's
121
+ # end_pos, which is not the case with simple, non-operation operands.
122
+ if match = @left_source.match(/\s+$/)
123
+ @left_source = @left_source[0..match.begin(0)]
124
+ @operator_source = match[0] + @operator_source
125
+ end
126
+
127
+ [@left_source, @operator_source]
128
+ end
129
+
130
+ # Removes trailing parentheses and compacts newlines into a single space
131
+ def normalize_source(source)
132
+ source.chop.gsub(/\s*\n\s*/, ' ')
133
+ end
134
+
135
+ def slide_to_the_left(range)
136
+ start_pos = Sass::Source::Position.new(range.start_pos.line, range.start_pos.offset - 2)
137
+ end_pos = Sass::Source::Position.new(range.end_pos.line, range.end_pos.offset - 2)
138
+ Sass::Source::Range.new(start_pos, end_pos, range.file, range.importer)
139
+ end
84
140
  end
85
141
  end
86
142
  end