scss-lint 0.12.1 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1649ab811a945a99099b9d000b14059532b28365
4
- data.tar.gz: fae32ee4b1704070dd7a84f3a4b2a1e353d0c0ad
3
+ metadata.gz: 05986225943272819dc966c20d294f39f54bd7ef
4
+ data.tar.gz: c5fb97b91c46be45d9a4bb72573356639e1f5afb
5
5
  SHA512:
6
- metadata.gz: cbb264e07bd5c093f54bcd5dad3ff02e990441a734684bee27dbdf1404ea71e7b14d0234bf551ad48bc2ba050478b5fc9ee1feed028a55e01e1d9a08970b8d28
7
- data.tar.gz: 9334e0f4b9bedfcdcfb02edb653fbd0d3136986815647047ca19b1607f5f569398473feb7efead4d62d19efb942bfe0b161440a48d2b184f43c74c2b370a4bd2
6
+ metadata.gz: b38f342599f822cb5d26617986f264e5ef2ad00ddddc10c049a57369a191706e77b38b9a97b9735daaa424703d19b93829a33f0a09b2ebac208bec52a5874eba
7
+ data.tar.gz: 45ecd691b23e170805dd1e45f509fbff7891c0f32880c1eeb4ff9d4967d7b7733128d1a3dcad4f367ac44309eca8ab79884dda18fe0436654c95d985a4f639b0
@@ -0,0 +1,77 @@
1
+ # Default application configuration that all configurations inherit from.
2
+ linters:
3
+ BorderZero:
4
+ enabled: true
5
+
6
+ CapitalizationInSelector:
7
+ enabled: true
8
+
9
+ ColorKeyword:
10
+ enabled: true
11
+
12
+ Comment:
13
+ enabled: true
14
+
15
+ DebugStatement:
16
+ enabled: true
17
+
18
+ DeclarationOrder:
19
+ enabled: true
20
+
21
+ DeclaredName:
22
+ enabled: true
23
+
24
+ DuplicateProperty:
25
+ enabled: true
26
+
27
+ EmptyRule:
28
+ enabled: true
29
+
30
+ HexFormat:
31
+ enabled: true
32
+
33
+ IdWithExtraneousSelector:
34
+ enabled: true
35
+
36
+ Indentation:
37
+ enabled: true
38
+ width: 2
39
+
40
+ LeadingZero:
41
+ enabled: true
42
+
43
+ PlaceholderInExtend:
44
+ enabled: true
45
+
46
+ Shorthand:
47
+ enabled: true
48
+
49
+ SingleLinePerSelector:
50
+ enabled: true
51
+
52
+ SortedProperties:
53
+ enabled: true
54
+
55
+ SpaceAfterComma:
56
+ enabled: true
57
+
58
+ SpaceAfterPropertyColon:
59
+ enabled: true
60
+
61
+ SpaceAfterPropertyName:
62
+ enabled: true
63
+
64
+ SpaceBeforeBrace:
65
+ enabled: true
66
+
67
+ TrailingSemicolonAfterPropertyValue:
68
+ enabled: true
69
+
70
+ UsageName:
71
+ enabled: true
72
+
73
+ ZeroUnit:
74
+ enabled: true
75
+
76
+ Compass::*:
77
+ enabled: false
@@ -1,5 +1,6 @@
1
1
  require 'scss_lint/constants'
2
2
  require 'scss_lint/cli'
3
+ require 'scss_lint/config'
3
4
  require 'scss_lint/engine'
4
5
  require 'scss_lint/lint'
5
6
  require 'scss_lint/linter_registry'
@@ -5,33 +5,66 @@ module SCSSLint
5
5
  # Responsible for parsing command-line options and executing the appropriate
6
6
  # application logic based on the options specified.
7
7
  class CLI
8
- attr_accessor :options
8
+ attr_reader :config, :options
9
+
10
+ # Subset of semantic exit codes conforming to `sysexits` documentation.
11
+ EXIT_CODES = {
12
+ ok: 0,
13
+ usage: 64, # Command line usage error
14
+ data: 65, # User input was incorrect (i.e. contains lints)
15
+ no_input: 66, # Input file did not exist or was not readable
16
+ software: 70, # Internal software error
17
+ config: 78, # Configuration error
18
+ }
9
19
 
10
20
  def initialize(args = [])
11
21
  @args = args
12
22
  @options = {}
23
+ @config = Config.default
13
24
  end
14
25
 
15
26
  def parse_arguments
16
- parser = OptionParser.new do |opts|
27
+ begin
28
+ options_parser.parse!(@args)
29
+
30
+ # Take the rest of the arguments as files/directories
31
+ @options[:files] = @args
32
+ rescue OptionParser::InvalidOption => ex
33
+ print_help options_parser.help, ex
34
+ end
35
+
36
+ begin
37
+ setup_configuration
38
+ rescue NoSuchLinter => ex
39
+ puts ex.message
40
+ halt :config
41
+ end
42
+ end
43
+
44
+ def options_parser
45
+ @options_parser ||= OptionParser.new do |opts|
17
46
  opts.banner = "Usage: #{opts.program_name} [options] [scss-files]"
18
47
 
19
48
  opts.separator ''
20
49
  opts.separator 'Common options:'
21
50
 
51
+ opts.on('-c', '--config file', 'Specify configuration file', String) do |file|
52
+ @options[:config_file] = file
53
+ end
54
+
22
55
  opts.on('-e', '--exclude file,...', Array,
23
56
  'List of file names to exclude') do |files|
24
- options[:excluded_files] = files
57
+ @options[:excluded_files] = files
25
58
  end
26
59
 
27
60
  opts.on('-i', '--include-linter linter,...', Array,
28
61
  'Specify which linters you want to run') do |linters|
29
- options[:included_linters] = linters
62
+ @options[:included_linters] = linters
30
63
  end
31
64
 
32
65
  opts.on('-x', '--exclude-linter linter,...', Array,
33
66
  "Specify which linters you don't want to run") do |linters|
34
- options[:excluded_linters] = linters
67
+ @options[:excluded_linters] = linters
35
68
  end
36
69
 
37
70
  opts.on_tail('--show-linters', 'Shows available linters') do
@@ -47,42 +80,73 @@ module SCSSLint
47
80
  end
48
81
 
49
82
  opts.on('--xml', 'Output the results in XML format') do
50
- options[:reporter] = SCSSLint::Reporter::XMLReporter
83
+ @options[:reporter] = SCSSLint::Reporter::XMLReporter
51
84
  end
52
85
  end
53
-
54
- begin
55
- parser.parse!(@args)
56
-
57
- # Take the rest of the arguments as files/directories
58
- options[:files] = @args
59
- rescue OptionParser::InvalidOption => ex
60
- print_help parser.help, ex
61
- end
62
86
  end
63
87
 
64
88
  def run
65
- runner = Runner.new(options)
66
- runner.run(find_files)
89
+ runner = Runner.new(@config)
90
+ runner.run(files_to_lint)
67
91
  report_lints(runner.lints)
68
- halt(1) if runner.lints?
69
- rescue NoFilesError, NoSuchLinter, Errno::ENOENT => ex
92
+ halt :data if runner.lints.any?
93
+ rescue NoFilesError, Errno::ENOENT => ex
70
94
  puts ex.message
71
- halt(-1)
95
+ halt :no_input
96
+ rescue NoSuchLinter => ex
97
+ puts ex.message
98
+ halt :usage
72
99
  rescue => ex
73
100
  puts ex.message
74
101
  puts ex.backtrace
75
102
  puts 'Report this bug at '.yellow + BUG_REPORT_URL.cyan
76
- halt(-1)
103
+ halt :software
77
104
  end
78
105
 
79
106
  private
80
107
 
81
- def find_files
82
- excluded_files = options.fetch(:excluded_files, [])
108
+ def setup_configuration
109
+ if @options[:config_file]
110
+ @config = Config.load(@options[:config_file])
111
+ @config.preferred = true
112
+ end
83
113
 
84
- extract_files_from(options[:files]).reject do |file|
85
- excluded_files.include?(file)
114
+ merge_command_line_flags_with_config(@config)
115
+ end
116
+
117
+ def merge_command_line_flags_with_config(config)
118
+ if @options[:excluded_files]
119
+ @options[:excluded_files].each do |file|
120
+ config.exclude_file(file)
121
+ end
122
+ end
123
+
124
+ if @options[:included_linters]
125
+ config.disable_all_linters
126
+ LinterRegistry.extract_linters_from(@options[:included_linters]).each do |linter|
127
+ config.enable_linter(linter)
128
+ end
129
+ end
130
+
131
+ if @options[:excluded_linters]
132
+ LinterRegistry.extract_linters_from(@options[:excluded_linters]).each do |linter|
133
+ config.disable_linter(linter)
134
+ end
135
+ end
136
+
137
+ config
138
+ end
139
+
140
+ def files_to_lint
141
+ extract_files_from(@options[:files]).reject do |file|
142
+ config =
143
+ if !@config.preferred && (config_for_file = Config.for_file(file))
144
+ merge_command_line_flags_with_config(config_for_file.dup)
145
+ else
146
+ @config
147
+ end
148
+
149
+ config.excluded_file?(file)
86
150
  end
87
151
  end
88
152
 
@@ -105,7 +169,8 @@ module SCSSLint
105
169
 
106
170
  def report_lints(lints)
107
171
  sorted_lints = lints.sort_by { |l| [l.filename, l.line] }
108
- reporter = options.fetch(:reporter, Reporter::DefaultReporter).new(sorted_lints)
172
+ reporter = @options.fetch(:reporter, Reporter::DefaultReporter)
173
+ .new(sorted_lints)
109
174
  output = reporter.report_lints
110
175
  print output if output
111
176
  end
@@ -127,7 +192,7 @@ module SCSSLint
127
192
  def print_help(help_message, err = nil)
128
193
  puts err, '' if err
129
194
  puts help_message
130
- halt
195
+ halt(err ? :usage : :ok)
131
196
  end
132
197
 
133
198
  def print_version(program_name, version)
@@ -136,8 +201,8 @@ module SCSSLint
136
201
  end
137
202
 
138
203
  # Used for ease-of testing
139
- def halt(exit_status = 0)
140
- exit exit_status
204
+ def halt(exit_status = :ok)
205
+ exit(EXIT_CODES[exit_status])
141
206
  end
142
207
  end
143
208
  end
@@ -0,0 +1,223 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module SCSSLint
5
+ # Loads and manages application configuration.
6
+ class Config
7
+ FILE_NAME = '.scss-lint.yml'
8
+ DEFAULT_FILE = File.join(SCSS_LINT_HOME, 'config', 'default.yml')
9
+
10
+ attr_accessor :preferred # If this config should be preferred over others
11
+ attr_reader :options, :warnings
12
+
13
+ class << self
14
+ def default
15
+ load(DEFAULT_FILE, merge_with_default: false)
16
+ end
17
+
18
+ # Loads a configuration from a file, merging it with the default
19
+ # configuration.
20
+ def load(file, options = {})
21
+ config_options = load_options_hash_from_file(file)
22
+
23
+ if options.fetch(:merge_with_default, true)
24
+ config_options = smart_merge(default_options_hash, config_options)
25
+ end
26
+
27
+ Config.new(config_options)
28
+ end
29
+
30
+ # Loads the configuration for a given file.
31
+ def for_file(file_path)
32
+ directory = File.dirname(File.expand_path(file_path))
33
+ @dir_to_config ||= {}
34
+ @dir_to_config[directory] ||=
35
+ begin
36
+ config_file = possible_config_files(directory).find { |path| path.file? }
37
+ Config.load(config_file.to_s) if config_file
38
+ end
39
+ end
40
+
41
+ def linter_name(linter)
42
+ linter = linter.is_a?(Class) ? linter : linter.class
43
+ linter.name.split('::')[2..-1].join('::')
44
+ end
45
+
46
+ private
47
+
48
+ def possible_config_files(directory)
49
+ files = Pathname.new(directory)
50
+ .enum_for(:ascend)
51
+ .map { |path| path + FILE_NAME }
52
+ files << Pathname.new(FILE_NAME)
53
+ end
54
+
55
+ def default_options_hash
56
+ @default_options_hash ||= load_options_hash_from_file(DEFAULT_FILE)
57
+ end
58
+
59
+ # Recursively load config files, fetching files specified by `include`
60
+ # directives and merging the file's config with the files specified.
61
+ def load_options_hash_from_file(file)
62
+ file_contents = load_file_contents(file)
63
+
64
+ options =
65
+ if file_contents.strip.empty?
66
+ {}
67
+ else
68
+ YAML.load(file_contents).to_hash
69
+ end
70
+
71
+ if options['exclude']
72
+ # Ensure exclude is an array, since we allow user to specify a single
73
+ # string. We do this before merging with the config loaded via
74
+ # inherit_form since this allows us to merge the excludes from that,
75
+ # rather than overwriting them.
76
+ options['exclude'] = [options['exclude']].flatten
77
+ end
78
+
79
+ if options['inherit_from']
80
+ includes = [options.delete('inherit_from')].flatten.map do |include_file|
81
+ load_options_hash_from_file(path_relative_to_config(include_file, file))
82
+ end
83
+
84
+ merged_includes = includes[1..-1].inject(includes.first) do |merged, include_file|
85
+ smart_merge(merged, include_file)
86
+ end
87
+
88
+ options = smart_merge(merged_includes, options)
89
+ end
90
+
91
+ # Merge options from wildcard linters into individual linter configs
92
+ options.fetch('linters', {}).keys.each do |class_name|
93
+ next unless class_name.include?('*')
94
+
95
+ class_name_regex = /#{class_name.gsub('*', '[^:]+')}/
96
+
97
+ wildcard_options = options['linters'].delete(class_name)
98
+
99
+ LinterRegistry.linters.each do |linter_class|
100
+ name = linter_name(linter_class)
101
+
102
+ if name.match(class_name_regex)
103
+ old_options = options['linters'].fetch(name, {})
104
+ options['linters'][name] = smart_merge(old_options, wildcard_options)
105
+ end
106
+ end
107
+ end
108
+
109
+ # Ensure all excludes are absolute paths
110
+ if options['exclude']
111
+ excludes = [options['exclude']].flatten
112
+
113
+ options['exclude'] = excludes.map do |exclusion_glob|
114
+ if exclusion_glob.start_with?('/')
115
+ exclusion_glob
116
+ else
117
+ # Expand the path assuming it is relative to the config file itself
118
+ File.expand_path(exclusion_glob, File.expand_path(File.dirname(file)))
119
+ end
120
+ end
121
+ end
122
+
123
+ options
124
+ end
125
+
126
+ def path_relative_to_config(relative_include_path, base_config_path)
127
+ if relative_include_path.start_with?('/')
128
+ relative_include_path
129
+ else
130
+ File.join(File.dirname(base_config_path), relative_include_path)
131
+ end
132
+ end
133
+
134
+ # For easy stubbing in tests
135
+ def load_file_contents(file)
136
+ File.open(file, 'r').read
137
+ end
138
+
139
+ # Merge two hashes, concatenating lists and further merging nested hashes.
140
+ def smart_merge(parent, child)
141
+ parent.merge(child) do |key, old, new|
142
+ case old
143
+ when Array
144
+ old + new
145
+ when Hash
146
+ smart_merge(old, new)
147
+ else
148
+ new
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ def initialize(options)
155
+ @options = options
156
+ @warnings = []
157
+
158
+ validate_linters
159
+ end
160
+
161
+ def ==(other)
162
+ super || @options == other.options
163
+ end
164
+ alias :eql? :==
165
+
166
+ def enabled_linters
167
+ LinterRegistry.extract_linters_from(@options['linters'].keys).select do |linter|
168
+ linter_options(linter)['enabled']
169
+ end
170
+ end
171
+
172
+ def linter_enabled?(linter)
173
+ linter_options(linter)['enabled']
174
+ end
175
+
176
+ def enable_linter(linter)
177
+ linter_options(linter)['enabled'] = true
178
+ end
179
+
180
+ def disable_linter(linter)
181
+ linter_options(linter)['enabled'] = false
182
+ end
183
+
184
+ def disable_all_linters
185
+ @options['linters'].values.each do |linter_config|
186
+ linter_config['enabled'] = false
187
+ end
188
+ end
189
+
190
+ def linter_options(linter)
191
+ @options['linters'][self.class.linter_name(linter)]
192
+ end
193
+
194
+ def excluded_file?(file_path)
195
+ abs_path = File.expand_path(file_path)
196
+
197
+ @options.fetch('exclude', []).any? do |exclusion_glob|
198
+ File.fnmatch(exclusion_glob, abs_path)
199
+ end
200
+ end
201
+
202
+ def exclude_file(file_path)
203
+ abs_path = File.expand_path(file_path)
204
+
205
+ @options['exclude'] ||= []
206
+ @options['exclude'] << abs_path
207
+ end
208
+
209
+ private
210
+
211
+ def validate_linters
212
+ return unless linters = @options['linters']
213
+
214
+ linters.keys.each do |name|
215
+ begin
216
+ Linter.const_get(name)
217
+ rescue NameError
218
+ @warnings << "Linter #{name} does not exist; ignoring"
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -1,5 +1,7 @@
1
1
  # Global application constants.
2
2
  module SCSSLint
3
+ SCSS_LINT_HOME = File.realpath(File.join(File.dirname(__FILE__), '..', '..'))
4
+
3
5
  REPO_URL = 'https://github.com/causes/scss-lint'
4
6
  BUG_REPORT_URL = "#{REPO_URL}/issues"
5
7
  end
@@ -1,15 +1,17 @@
1
1
  module SCSSLint
2
+ # Defines common functionality available to all linters.
2
3
  class Linter < Sass::Tree::Visitors::Base
3
4
  include SelectorVisitor
4
5
  include Utils
5
6
 
6
- attr_reader :engine, :lints
7
+ attr_reader :config, :engine, :lints
7
8
 
8
9
  def initialize
9
10
  @lints = []
10
11
  end
11
12
 
12
- def run(engine)
13
+ def run(engine, config)
14
+ @config = config
13
15
  @engine = engine
14
16
  visit(engine.tree)
15
17
  end
@@ -4,6 +4,7 @@ module SCSSLint
4
4
  include LinterRegistry
5
5
 
6
6
  def visit_root(node)
7
+ @indent_width = config['width']
7
8
  @indent = 0
8
9
  yield
9
10
  end
@@ -14,9 +15,9 @@ module SCSSLint
14
15
  # indentation problems as that would likely make the lint too noisy.
15
16
  return if check_indentation(node)
16
17
 
17
- @indent += INDENT_WIDTH
18
+ @indent += @indent_width
18
19
  yield
19
- @indent -= INDENT_WIDTH
20
+ @indent -= @indent_width
20
21
  end
21
22
 
22
23
  def check_indentation(node)
@@ -64,9 +65,5 @@ module SCSSLint
64
65
  alias :visit_return :check_indentation
65
66
  alias :visit_variable :check_indentation
66
67
  alias :visit_warn :check_indentation
67
-
68
- private
69
-
70
- INDENT_WIDTH = 2
71
68
  end
72
69
  end
@@ -1,52 +1,42 @@
1
1
  module SCSSLint
2
2
  class LinterError < StandardError; end
3
3
  class NoFilesError < StandardError; end
4
- class NoLintersError < StandardError; end
5
4
 
6
5
  # Finds and aggregates all lints found by running the registered linters
7
6
  # against a set of SCSS files.
8
7
  class Runner
9
- attr_reader :linters, :lints
8
+ attr_reader :lints
10
9
 
11
- def initialize(options = {})
10
+ def initialize(config)
11
+ @config = config
12
12
  @lints = []
13
-
14
- included_linters = LinterRegistry.
15
- extract_linters_from(options.fetch(:included_linters, []))
16
-
17
- included_linters = LinterRegistry.linters if included_linters.empty?
18
-
19
- excluded_linters = LinterRegistry.
20
- extract_linters_from(options.fetch(:excluded_linters, []))
21
-
22
- @linters = (included_linters - excluded_linters).map(&:new)
13
+ @linters = LinterRegistry.linters.map(&:new)
23
14
  end
24
15
 
25
- def run(files = [])
16
+ def run(files)
26
17
  raise NoFilesError, 'No SCSS files specified' if files.empty?
27
- raise NoLintersError, 'No linters specified' if linters.empty?
28
18
 
29
19
  files.each do |file|
30
20
  find_lints(file)
31
21
  end
32
22
 
33
- linters.each do |linter|
23
+ @linters.each do |linter|
34
24
  @lints += linter.lints
35
25
  end
36
26
  end
37
27
 
38
- def lints?
39
- lints.any?
40
- end
41
-
42
28
  private
43
29
 
44
30
  def find_lints(file)
45
31
  engine = Engine.new(file)
32
+ config = @config.preferred ? @config : Config.for_file(file)
33
+ config ||= @config
34
+
35
+ @linters.each do |linter|
36
+ next unless config.linter_enabled?(linter)
46
37
 
47
- linters.each do |linter|
48
38
  begin
49
- linter.run(engine)
39
+ linter.run(engine, config.linter_options(linter))
50
40
  rescue => error
51
41
  raise LinterError,
52
42
  "#{linter.class} raised unexpected error linting file #{file}: " <<
@@ -1,3 +1,3 @@
1
1
  module SCSSLint
2
- VERSION = '0.12.1'
2
+ VERSION = '0.13.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scss-lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Causes Engineering
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-11-08 00:00:00.000000000 Z
12
+ date: 2013-12-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: colorize
@@ -76,6 +76,7 @@ executables:
76
76
  extensions: []
77
77
  extra_rdoc_files: []
78
78
  files:
79
+ - config/default.yml
79
80
  - lib/scss_lint/version.rb
80
81
  - lib/scss_lint/constants.rb
81
82
  - lib/scss_lint/utils.rb
@@ -88,6 +89,7 @@ files:
88
89
  - lib/scss_lint/selector_visitor.rb
89
90
  - lib/scss_lint/sass/tree.rb
90
91
  - lib/scss_lint/sass/script.rb
92
+ - lib/scss_lint/config.rb
91
93
  - lib/scss_lint/linter/compass.rb
92
94
  - lib/scss_lint/linter/space_after_comma.rb
93
95
  - lib/scss_lint/linter/space_before_brace.rb