scss-lint 0.12.1 → 0.13.0

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