scss_lint 0.39.0 → 0.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +15 -0
  3. data/lib/scss_lint.rb +1 -0
  4. data/lib/scss_lint/cli.rb +10 -3
  5. data/lib/scss_lint/config.rb +54 -2
  6. data/lib/scss_lint/control_comment_processor.rb +15 -6
  7. data/lib/scss_lint/exceptions.rb +3 -0
  8. data/lib/scss_lint/linter/color_variable.rb +7 -0
  9. data/lib/scss_lint/linter/else_placement.rb +1 -0
  10. data/lib/scss_lint/linter/extend_directive.rb +11 -0
  11. data/lib/scss_lint/linter/final_newline.rb +1 -0
  12. data/lib/scss_lint/linter/name_format.rb +1 -12
  13. data/lib/scss_lint/linter/nesting_depth.rb +22 -1
  14. data/lib/scss_lint/linter/property_sort_order.rb +8 -0
  15. data/lib/scss_lint/linter/property_units.rb +21 -3
  16. data/lib/scss_lint/linter/space_after_variable_name.rb +18 -0
  17. data/lib/scss_lint/linter/space_between_parens.rb +1 -0
  18. data/lib/scss_lint/linter/trailing_whitespace.rb +15 -0
  19. data/lib/scss_lint/plugins.rb +33 -0
  20. data/lib/scss_lint/plugins/linter_dir.rb +24 -0
  21. data/lib/scss_lint/plugins/linter_gem.rb +51 -0
  22. data/lib/scss_lint/version.rb +1 -1
  23. data/spec/scss_lint/config_spec.rb +64 -0
  24. data/spec/scss_lint/fixtures/plugins/linter_plugin.rb +7 -0
  25. data/spec/scss_lint/linter/color_variable_spec.rb +12 -0
  26. data/spec/scss_lint/linter/else_placement_spec.rb +34 -0
  27. data/spec/scss_lint/linter/extend_directive_spec.rb +73 -0
  28. data/spec/scss_lint/linter/nesting_depth_spec.rb +72 -0
  29. data/spec/scss_lint/linter/property_sort_order_spec.rb +32 -0
  30. data/spec/scss_lint/linter/property_units_spec.rb +40 -0
  31. data/spec/scss_lint/linter/space_after_variable_name_spec.rb +13 -0
  32. data/spec/scss_lint/linter/trailing_whitespace_spec.rb +33 -0
  33. data/spec/scss_lint/linter_spec.rb +15 -2
  34. data/spec/scss_lint/plugins/linter_dir_spec.rb +21 -0
  35. data/spec/scss_lint/plugins/linter_gem_spec.rb +60 -0
  36. data/spec/scss_lint/plugins_spec.rb +53 -0
  37. data/spec/spec_helper.rb +10 -0
  38. metadata +26 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 246cc4b3c9ca9fe6b52f8ef4edd262694b701809
4
- data.tar.gz: 18a0b888b01b4ddc391a17719b3853a77075ff92
3
+ metadata.gz: 2875735c6ff15b0a0c04496ae19043825c289c95
4
+ data.tar.gz: abf49a6521c68efcedcf413a15208983bd116914
5
5
  SHA512:
6
- metadata.gz: 49174f90f307f91898c0c409b4e8e711f9b0d99088005f5e1e979a1663b9be89c953a2a2431497c06b224a48d3e9dd7113392ac891c6bba6e08f9b0c67a2263b
7
- data.tar.gz: 14098a3c8478fc0a65c222052054f3190a623a0bfc85fc4b107bb15b4cc5d802a1f95f341413a0dafe9f06d8fef74f6a9e845e1587c0ef1e317034c39dac2d15
6
+ metadata.gz: f14a4ee19fc7124cadc19e33fa9f3ee8232f9b34c27bb2824100d4400d998e590004b214ccefc3b37b97490f167aef0950dc1775467e84d5fe2fa63ad6f37798
7
+ data.tar.gz: 56abaad1303a536a32d57a6cfc3862144c05e5a4c6140e377745a7a801bbd9ba9bd43fdb397527532918aa3203579473dcd0336f0e387e15e223d7194eef8628
data/config/default.yml CHANGED
@@ -1,6 +1,11 @@
1
1
  # Default application configuration that all configurations inherit from.
2
2
 
3
3
  scss_files: "**/*.scss"
4
+ plugin_directories: ['.scss-linters']
5
+
6
+ # List of gem names to load custom linters from (make sure they are already
7
+ # installed)
8
+ plugin_gems: []
4
9
 
5
10
  linters:
6
11
  BangFormat:
@@ -45,6 +50,9 @@ linters:
45
50
  EmptyRule:
46
51
  enabled: true
47
52
 
53
+ ExtendDirective:
54
+ enabled: false
55
+
48
56
  FinalNewline:
49
57
  enabled: true
50
58
  present: true
@@ -93,6 +101,7 @@ linters:
93
101
  NestingDepth:
94
102
  enabled: true
95
103
  max_depth: 3
104
+ ignore_parent_selectors: false
96
105
 
97
106
  PlaceholderInExtend:
98
107
  enabled: true
@@ -160,6 +169,9 @@ linters:
160
169
  SpaceAfterPropertyName:
161
170
  enabled: true
162
171
 
172
+ SpaceAfterVariableName:
173
+ enabled: true
174
+
163
175
  SpaceBeforeBrace:
164
176
  enabled: true
165
177
  style: space # or 'new_line'
@@ -176,6 +188,9 @@ linters:
176
188
  TrailingSemicolon:
177
189
  enabled: true
178
190
 
191
+ TrailingWhitespace:
192
+ enabled: true
193
+
179
194
  TrailingZero:
180
195
  enabled: false
181
196
 
data/lib/scss_lint.rb CHANGED
@@ -11,6 +11,7 @@ require 'scss_lint/selector_visitor'
11
11
  require 'scss_lint/control_comment_processor'
12
12
  require 'scss_lint/version'
13
13
  require 'scss_lint/utils'
14
+ require 'scss_lint/plugins'
14
15
 
15
16
  # Load Sass classes and then monkey patch them
16
17
  require 'sass'
data/lib/scss_lint/cli.rb CHANGED
@@ -20,6 +20,7 @@ module SCSSLint
20
20
  config: 78, # Configuration error
21
21
  no_files: 80, # No files matched by specified glob patterns
22
22
  files_filtered: 81, # All matched files were filtered by exclusions
23
+ plugin: 82, # Plugin loading error
23
24
  }
24
25
 
25
26
  def run(args)
@@ -39,13 +40,16 @@ module SCSSLint
39
40
  print_help(options)
40
41
  elsif options[:version]
41
42
  print_version
42
- elsif options[:show_linters]
43
- print_linters
44
43
  elsif options[:show_formatters]
45
44
  print_formatters
46
45
  else
47
46
  config = setup_configuration(options)
48
- scan_for_lints(options, config)
47
+
48
+ if options[:show_linters]
49
+ print_linters
50
+ else
51
+ scan_for_lints(options, config)
52
+ end
49
53
  end
50
54
  end
51
55
 
@@ -81,6 +85,9 @@ module SCSSLint
81
85
  when SCSSLint::Exceptions::NoFilesError
82
86
  puts exception.message
83
87
  halt :no_files
88
+ when SCSSLint::Exceptions::PluginGemLoadError
89
+ puts exception.message
90
+ halt :plugin
84
91
  when Errno::ENOENT
85
92
  puts exception.message
86
93
  halt :no_input
@@ -18,11 +18,18 @@ module SCSSLint
18
18
  def load(file, options = {})
19
19
  config_options = load_options_hash_from_file(file)
20
20
 
21
+ config = new(config_options)
22
+
23
+ # Need to call this before merging with the default configuration so
24
+ # that plugins can override the default configuration while still being
25
+ # overridden by the repo's configuration.
26
+ config.load_plugins
27
+
21
28
  if options.fetch(:merge_with_default, true)
22
- config_options = smart_merge(default_options_hash, config_options)
29
+ config = default.extend(config)
23
30
  end
24
31
 
25
- Config.new(config_options)
32
+ config
26
33
  end
27
34
 
28
35
  # Returns the location of the user-wide scss-lint configuration.
@@ -178,6 +185,32 @@ module SCSSLint
178
185
  validate_linters
179
186
  end
180
187
 
188
+ def [](key)
189
+ @options[key]
190
+ end
191
+
192
+ # Compares this configuration with another.
193
+ #
194
+ # @param other [SCSSLint::Config]
195
+ # @return [true,false]
196
+ def ==(other)
197
+ super || @options == other.options
198
+ end
199
+ alias_method :eql?, :==
200
+
201
+ # Extend this {Config} with another configuration.
202
+ #
203
+ # @return [SCSSLint::Config]
204
+ def extend(config)
205
+ @options = self.class.send(:smart_merge, @options, config.options)
206
+ @warnings += config.warnings
207
+ self
208
+ end
209
+
210
+ def load_plugins
211
+ load_plugins_and_merge_config.tap { ensure_plugins_have_default_options }
212
+ end
213
+
181
214
  def enabled_linters
182
215
  LinterRegistry.extract_linters_from(@options['linters'].keys).select do |linter|
183
216
  linter_options(linter)['enabled']
@@ -255,5 +288,24 @@ module SCSSLint
255
288
  end
256
289
  end
257
290
  end
291
+
292
+ def load_plugins_and_merge_config
293
+ SCSSLint::Plugins.new(self).load.each do |plugin|
294
+ # Have the plugin options be overrideable by the local configuration
295
+ @options = self.class.send(:smart_merge, plugin.config.options, @options)
296
+ end
297
+ end
298
+
299
+ def ensure_plugins_have_default_options
300
+ LinterRegistry.linters.each do |linter|
301
+ if linter_options(linter).nil?
302
+ @options['linters'].merge!(default_plugin_options(linter))
303
+ end
304
+ end
305
+ end
306
+
307
+ def default_plugin_options(linter)
308
+ { self.class.linter_name(linter) => { 'enabled' => true } }
309
+ end
258
310
  end
259
311
  end
@@ -82,10 +82,21 @@ module SCSSLint
82
82
  return unless comment_node = @disable_stack.pop
83
83
 
84
84
  start_line = comment_node.line
85
+ if comment_node.class.node_name == :rule
86
+ end_line = start_line
87
+ elsif node.class.node_name == :root
88
+ end_line = @linter.engine.lines.length
89
+ else
90
+ end_line = end_line(node)
91
+ end
92
+
93
+ @disabled_lines.merge(start_line..end_line)
94
+ end
85
95
 
86
- # Find the deepest child that has a line number to which a lint might
87
- # apply (if it is a control comment enable node, it will be the line of
88
- # the comment itself).
96
+ # Find the deepest child that has a line number to which a lint might
97
+ # apply (if it is a control comment enable node, it will be the line of
98
+ # the comment itself).
99
+ def end_line(node)
89
100
  child = node
90
101
  prev_child = node
91
102
  until [nil, prev_child].include?(child = last_child(child))
@@ -94,9 +105,7 @@ module SCSSLint
94
105
 
95
106
  # Fall back to prev_child if last_child() returned nil (i.e. node had no
96
107
  # children with line numbers)
97
- end_line = (child || prev_child).line
98
-
99
- @disabled_lines.merge(start_line..end_line)
108
+ (child || prev_child).line
100
109
  end
101
110
 
102
111
  # Gets the child of the node that resides on the lowest line in the file.
@@ -18,4 +18,7 @@ module SCSSLint::Exceptions
18
18
 
19
19
  # Raised when a required library (specified via command line) does not exist.
20
20
  class RequiredLibraryMissingError < StandardError; end
21
+
22
+ # Raised when a linter gem plugin is required but not installed.
23
+ class PluginGemLoadError < StandardError; end
21
24
  end
@@ -23,6 +23,13 @@ module SCSSLint
23
23
  .each { |_, color| record_lint(node, color) }
24
24
  end
25
25
 
26
+ def visit_comment(_node)
27
+ # Don't lint children. Sass multiline comments (/*...*/) are actually
28
+ # rendered in code and thus allow variable interpolation. Unfortunately,
29
+ # the Sass parser returns bad source ranges for interpolation in these
30
+ # comments, so it's easiest to just ignore them.
31
+ end
32
+
26
33
  private
27
34
 
28
35
  def record_lint(node, color)
@@ -6,6 +6,7 @@ module SCSSLint
6
6
 
7
7
  def visit_if(node)
8
8
  visit_else(node, node.else) if node.else
9
+ yield # Lint nested @if statements
9
10
  end
10
11
 
11
12
  def visit_else(if_node, else_node)
@@ -0,0 +1,11 @@
1
+ module SCSSLint
2
+ # Checks that `@extend` is never used.
3
+ class Linter::ExtendDirective < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_extend(node)
7
+ add_lint(node, 'Do not use the @extend directive (@include a @mixin ' \
8
+ 'instead)')
9
+ end
10
+ end
11
+ end
@@ -15,6 +15,7 @@ module SCSSLint
15
15
  add_lint(engine.lines.count,
16
16
  'Files should not end with a trailing newline') if ends_with_newline
17
17
  end
18
+ yield
18
19
  end
19
20
  end
20
21
  end
@@ -1,13 +1,8 @@
1
1
  module SCSSLint
2
- # Checks the format of declared names of functions, mixins, variables, and
3
- # placeholders.
2
+ # Checks the format of declared names of functions, mixins, and variables.
4
3
  class Linter::NameFormat < Linter
5
4
  include LinterRegistry
6
5
 
7
- def visit_extend(node)
8
- check_placeholder(node)
9
- end
10
-
11
6
  def visit_function(node)
12
7
  check_name(node, 'function')
13
8
  yield # Continue into content block of this function definition
@@ -63,12 +58,6 @@ module SCSSLint
63
58
  name
64
59
  end
65
60
 
66
- def check_placeholder(node)
67
- extract_string_selectors(node.selector).any? do |selector_str|
68
- check_name(node, 'placeholder', selector_str.gsub('%', ''))
69
- end
70
- end
71
-
72
61
  CONVENTIONS = {
73
62
  'camel_case' => {
74
63
  explanation: 'should be written in camelCase format',
@@ -3,6 +3,8 @@ module SCSSLint
3
3
  class Linter::NestingDepth < Linter
4
4
  include LinterRegistry
5
5
 
6
+ IGNORED_SELECTORS = [Sass::Selector::Parent, Sass::Selector::Pseudo]
7
+
6
8
  def visit_root(_node)
7
9
  @max_depth = config['max_depth']
8
10
  @depth = 1
@@ -10,8 +12,11 @@ module SCSSLint
10
12
  end
11
13
 
12
14
  def visit_rule(node)
15
+ return yield if ignore_selectors?(node)
16
+
13
17
  if @depth > @max_depth
14
- add_lint(node, "Nesting should be no greater than #{@max_depth}, but was #{@depth}")
18
+ add_lint node, "Nesting should be no greater than #{@max_depth}, " \
19
+ "but was #{@depth}"
15
20
  else
16
21
  # Only continue if we didn't exceed the max depth already (this makes
17
22
  # the lint less noisy)
@@ -20,5 +25,21 @@ module SCSSLint
20
25
  @depth -= 1
21
26
  end
22
27
  end
28
+
29
+ private
30
+
31
+ def ignore_selectors?(node)
32
+ return unless config['ignore_parent_selectors']
33
+
34
+ simple_selectors(node.parsed_rules).all? do |selector|
35
+ IGNORED_SELECTORS.include?(selector.class)
36
+ end
37
+ end
38
+
39
+ def simple_selectors(node)
40
+ node.members.flat_map(&:members).reject do |simple_sequence|
41
+ simple_sequence.is_a?(String)
42
+ end.flat_map(&:members)
43
+ end
23
44
  end
24
45
  end
@@ -80,6 +80,10 @@ module SCSSLint
80
80
  .sort { |a, b| compare_properties(a, b) }
81
81
 
82
82
  sorted_props.each_with_index do |prop, index|
83
+ # Once we reach the portion of the list with unspecified properties, we
84
+ # can stop checking since we don't care about order after that point
85
+ break unless specified_property?(prop[:property])
86
+
83
87
  next unless prop != sortable_prop_info[index]
84
88
 
85
89
  add_lint(sortable_prop_info[index][:node], lint_message(sorted_props))
@@ -186,6 +190,10 @@ module SCSSLint
186
190
  !@preferred_order.include?(prop_node.name.join)
187
191
  end
188
192
 
193
+ def specified_property?(prop_name)
194
+ !@preferred_order || @preferred_order.include?(prop_name)
195
+ end
196
+
189
197
  def preset_order?
190
198
  config['order'].is_a?(String)
191
199
  end
@@ -3,6 +3,20 @@ module SCSSLint
3
3
  class Linter::PropertyUnits < Linter
4
4
  include LinterRegistry
5
5
 
6
+ NUMBER_WITH_UNITS_REGEX = /
7
+ (?:
8
+ (["']).+?\1 # [0: quote mark] quoted string, e.g. "hi there"
9
+ | # or
10
+ (?:^|\s) # beginning of value or whitespace
11
+ (?:
12
+ \d+ # any number of digits, e.g. 123
13
+ | # or
14
+ \d*\.?\d+ # any number of digits with decimal, e.g. 1.23 or .123
15
+ )
16
+ ([a-z%]+) # [1: units] letters or percent sign, e.g. px or %
17
+ )
18
+ /ix
19
+
6
20
  def visit_root(_node)
7
21
  @globally_allowed_units = config['global'].to_set
8
22
  @allowed_units_for_property = config['properties']
@@ -18,9 +32,13 @@ module SCSSLint
18
32
  property = "#{@nested_under}-#{property}"
19
33
  end
20
34
 
21
- if node.value.respond_to?(:value) &&
22
- units = node.value.value.to_s[/(?:^|\s)(?:\d+|\d*\.?\d+)([a-z%]+)/i, 1]
23
- check_units(node, property, units)
35
+ if node.value.respond_to?(:value)
36
+ node.value.value.to_s.scan(NUMBER_WITH_UNITS_REGEX).each do |matches|
37
+ is_quoted_value = !matches[0].nil?
38
+ next if is_quoted_value
39
+ units = matches[1]
40
+ check_units(node, property, units)
41
+ end
24
42
  end
25
43
 
26
44
  @nested_under = property
@@ -0,0 +1,18 @@
1
+ module SCSSLint
2
+ # Checks for spaces following the name of a variable and before the colon
3
+ # separating the variables's name from its value.
4
+ class SpaceAfterVariableName < Linter
5
+ include LinterRegistry
6
+
7
+ def visit_variable(node)
8
+ return unless spaces_before_colon?(node)
9
+ add_lint(node, 'Variable names should be followed immediately by a colon')
10
+ end
11
+
12
+ private
13
+
14
+ def spaces_before_colon?(node)
15
+ source_from_range(node.source_range) =~ /\s+:/
16
+ end
17
+ end
18
+ end
@@ -18,6 +18,7 @@ module SCSSLint
18
18
  check(match[2], index) if match[2]
19
19
  end
20
20
  end
21
+ yield
21
22
  end
22
23
 
23
24
  private
@@ -0,0 +1,15 @@
1
+ module SCSSLint
2
+ # Checks for trailing whitespace on a line.
3
+ class Linter::TrailingWhitespace < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_root(_node)
7
+ engine.lines.each_with_index do |line, index|
8
+ next unless line =~ /[ \t]+$/
9
+
10
+ add_lint(index + 1, 'Line contains trailing whitespace')
11
+ end
12
+ yield
13
+ end
14
+ end
15
+ end