-
Notifications
You must be signed in to change notification settings - Fork 465
/
cli.rb
269 lines (234 loc) · 7.67 KB
/
cli.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
require 'scss_lint/options'
module SCSSLint
# Responsible for parsing command-line options and executing the appropriate
# application logic based on the options specified.
class CLI
attr_reader :config, :options
# Subset of semantic exit codes conforming to `sysexits` documentation.
EXIT_CODES = {
ok: 0,
warning: 1, # One or more warnings (but no errors) were reported
error: 2, # One or more errors were reported
usage: 64, # Command line usage error
no_input: 66, # Input file did not exist or was not readable
unavailable: 69, # Required library is unavailable
software: 70, # Internal software error
config: 78, # Configuration error
no_files: 80, # No files matched by specified glob patterns
plugin: 82, # Plugin loading error
preprocessor: 84, # Preprocessor error
}.freeze
# Create a CLI that outputs to the specified logger.
#
# @param logger [SCSSLint::Logger]
def initialize(logger)
@log = logger
end
def run(args)
options = SCSSLint::Options.new.parse(args)
act_on_options(options)
rescue StandardError => e
handle_runtime_exception(e, options)
end
private
attr_reader :log
def act_on_options(options)
log.color_enabled = options.fetch(:color, log.tty?)
load_required_paths(options)
load_reporters(options)
if options[:help]
print_help(options)
elsif options[:version]
print_version
elsif options[:show_formatters]
print_formatters
else
config = setup_configuration(options)
if options[:show_linters]
print_linters
else
scan_for_lints(options, config)
end
end
end
def scan_for_lints(options, config)
runner = Runner.new(config)
files = files_to_lint(options, config)
runner.run(files)
report_lints(options, runner.lints, files)
if runner.lints.any?(&:error?)
halt :error
elsif runner.lints.any?
halt :warning
else
halt :ok
end
end
def files_to_lint(options, config)
if options[:stdin_file_path]
[{ file: $stdin, path: options[:stdin_file_path] }]
else
patterns = Array(options[:files]).any? ? Array(options[:files]) : config.scss_files
FileFinder.new(config).find(patterns).map do |file_path|
{ path: file_path }
end
end
end
def handle_runtime_exception(exception, options) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
case exception
when SCSSLint::Exceptions::InvalidCLIOption
log.error exception.message
log.info 'Run `scss-lint --help` for usage documentation'
halt :usage
when SCSSLint::Exceptions::InvalidConfiguration
log.error exception.message
halt :config
when SCSSLint::Exceptions::RequiredLibraryMissingError
log.error exception.message
halt :unavailable
when SCSSLint::Exceptions::NoFilesError
log.error exception.message
halt :no_files
when SCSSLint::Exceptions::PluginGemLoadError
log.error exception.message
halt :plugin
when Errno::ENOENT
log.error exception.message
halt :no_input
when NoSuchLinter
log.error exception.message
halt :usage
when SCSSLint::Exceptions::PreprocessorError
log.error exception.message
halt :preprocessor
else
config_file = relevant_configuration_file(options) if options
log.bold_error exception.message
log.error exception.backtrace.join("\n")
log.warning 'Report this bug at ', false
log.info BUG_REPORT_URL
log.newline
log.success 'To help fix this issue, please include:'
log.log '- The above stack trace'
log.log '- SCSS-Lint version: ', false
log.info SCSSLint::VERSION
log.log '- Sass version: ', false
log.info Gem.loaded_specs['sass'].version.to_s
log.log '- Ruby version: ', false
log.info RUBY_VERSION
if config_file
log.log '- Contents of ', false
log.info File.expand_path(config_file)
end
halt :software
end
end
def setup_configuration(options)
config_file = relevant_configuration_file(options)
config = config_file ? Config.load(config_file) : Config.default
merge_options_with_config(options, config)
end
# Return the path of the configuration file that should be loaded.
#
# @param options [Hash]
# @return [String]
def relevant_configuration_file(options)
if options[:config_file]
options[:config_file]
elsif File.exist?(Config::FILE_NAME)
Config::FILE_NAME
elsif File.exist?(Config.user_file)
Config.user_file
end
end
# @param options [Hash]
# @param config [Config]
# @return [Config]
def merge_options_with_config(options, config)
if options[:excluded_files]
options[:excluded_files].each do |file|
config.exclude_file(file)
end
end
if options[:included_linters]
config.disable_all_linters
LinterRegistry.extract_linters_from(options[:included_linters]).each do |linter|
config.enable_linter(linter)
end
end
if options[:excluded_linters]
LinterRegistry.extract_linters_from(options[:excluded_linters]).each do |linter|
config.disable_linter(linter)
end
end
config
end
# @param options [Hash]
# @param lints [Array<Lint>]
# @param files [Array<Hash>]
def report_lints(options, lints, files)
sorted_lints = lints.sort_by { |l| [l.filename, l.location] }
options.fetch(:reporters).each do |reporter, output|
results = reporter.new(sorted_lints, files, log).report_lints
next unless results
if output == :stdout
log.log results
else
File.new(output, 'w+').print results
end
end
end
def load_required_paths(options)
Array(options[:required_paths]).each do |path|
require path
end
rescue LoadError => e
raise SCSSLint::Exceptions::RequiredLibraryMissingError,
"Required library not found: #{e.message}"
end
def load_reporters(options)
options[:reporters].map! do |reporter_name, output_file|
begin
reporter = SCSSLint::Reporter.const_get("#{reporter_name}Reporter")
rescue NameError
raise SCSSLint::Exceptions::InvalidCLIOption,
"Invalid output format specified: #{reporter_name}"
end
[reporter, output_file]
end
end
def print_formatters
log.log 'Installed formatters:'
reporter_names = SCSSLint::Reporter.descendants.map do |reporter|
reporter.name.split('::').last.split('Reporter').first
end
reporter_names.sort.each do |reporter_name|
log.log " - #{reporter_name}"
end
halt
end
def print_linters
log.log 'Installed linters:'
linter_names = LinterRegistry.linters.map(&:simple_name)
linter_names.sort.each do |linter_name|
log.log " - #{linter_name}"
end
halt
end
# @param options [Hash]
def print_help(options)
log.log options[:help]
halt :ok
end
# @param options [Hash]
def print_version
log.log "scss-lint #{SCSSLint::VERSION}"
halt :ok
end
# Used for ease-of testing
# @param exit_status [Symbol]
def halt(exit_status = :ok)
EXIT_CODES[exit_status]
end
end
end