scss_lint 0.41.0 → 0.42.0

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