scss_lint 0.39.0 → 0.40.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 (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