Class: Rubocop::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/rubocop/cli.rb

Overview

The CLI is a class responsible of handling all the command line interface logic.

Constant Summary collapse

DEFAULT_FORMATTER =
'progress'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeCLI

Returns a new instance of CLI.



18
19
20
21
22
23
# File 'lib/rubocop/cli.rb', line 18

def initialize
  @cops = Cop::Cop.all
  @errors = []
  @options = {}
  ConfigStore.prepare
end

Instance Attribute Details

#optionsObject

Returns the value of attribute options.



14
15
16
# File 'lib/rubocop/cli.rb', line 14

def options
  @options
end

#wants_to_quitObject Also known as: wants_to_quit?

If set true while running, RuboCop will abort processing and exit gracefully.



13
14
15
# File 'lib/rubocop/cli.rb', line 13

def wants_to_quit
  @wants_to_quit
end

Class Method Details

.parse(file) {|source_buffer| ... } ⇒ Object

Yields:

  • (source_buffer)


277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/rubocop/cli.rb', line 277

def self.parse(file)
  parser = Parser::CurrentRuby.new

  # On JRuby and Rubinius, there's a risk that we hang in
  # tokenize() if we don't set the all errors as fatal flag.
  parser.diagnostics.all_errors_are_fatal = RUBY_ENGINE != 'ruby'
  parser.diagnostics.ignore_warnings      = false

  diagnostics = []
  parser.diagnostics.consumer = lambda do |diagnostic|
    diagnostics << diagnostic
  end

  source_buffer = Parser::Source::Buffer.new(file, 1)
  yield source_buffer

  begin
    ast, comments, tokens = parser.tokenize(source_buffer)
  rescue Parser::SyntaxError # rubocop:disable HandleExceptions
    # All errors are in diagnostics. No need to handle exception.
  end

  if tokens
    tokens = tokens.map do |t|
      type, details = *t
      text, range = *details
      Rubocop::Cop::Token.new(range, type, text)
    end
  end

  syntax_offences = diagnostics.map do |d|
    Cop::Offence.new(d.level, d.location, "#{d.message}",
                     'Syntax')
  end

  source = source_buffer.source.split($RS)

  [ast, comments, tokens, source_buffer, source, syntax_offences]
end

Instance Method Details

#convert_deprecated_options!(args) ⇒ Object

rubocop:enable MethodLength



214
215
216
217
218
219
220
221
222
223
224
# File 'lib/rubocop/cli.rb', line 214

def convert_deprecated_options!(args)
  args.map! do |arg|
    case arg
    when '-e', '--emacs'
      deprecate("#{arg} option", '--format emacs', '1.0.0')
      %w(--format emacs)
    else
      arg
    end
  end.flatten!
end

#disabled_lines_in(source) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/rubocop/cli.rb', line 246

def disabled_lines_in(source)
  disabled_lines = Hash.new([])
  disabled_section = {}
  regexp = '# rubocop : (%s)\b ((?:\w+,? )+)'.gsub(' ', '\s*')
  section_regexp = '^\s*' + sprintf(regexp, '(?:dis|en)able')
  single_line_regexp = '\S.*' + sprintf(regexp, 'disable')

  source.each_with_index do |line, ix|
    each_mentioned_cop(/#{section_regexp}/, line) do |cop_name, kind|
      disabled_section[cop_name] = (kind == 'disable')
    end
    disabled_section.keys.each do |cop_name|
      disabled_lines[cop_name] += [ix + 1] if disabled_section[cop_name]
    end

    each_mentioned_cop(/#{single_line_regexp}/, line) do |cop_name, kind|
      disabled_lines[cop_name] += [ix + 1] if kind == 'disable'
    end
  end
  disabled_lines
end

#display_error_summary(errors) ⇒ Object



235
236
237
238
239
240
241
242
243
244
# File 'lib/rubocop/cli.rb', line 235

def display_error_summary(errors)
  return if errors.empty?
  plural = errors.count > 1 ? 's' : ''
  puts "\n#{errors.count} error#{plural} occurred:".color(:red)
  errors.each { |error| puts error }
  puts 'Errors are usually caused by RuboCop bugs.'
  puts 'Please, report your problems to RuboCop\'s issue tracker.'
  puts 'Mention the following information in the issue report:'
  puts Rubocop::Version.version(true)
end

#each_mentioned_cop(regexp, line) ⇒ Object



268
269
270
271
272
273
274
275
# File 'lib/rubocop/cli.rb', line 268

def each_mentioned_cop(regexp, line)
  match = line.match(regexp)
  if match
    kind, cops = match.captures
    cops = Cop::Cop.all.map(&:cop_name).join(',') if cops.include?('all')
    cops.split(/,\s*/).each { |cop_name| yield cop_name, kind }
  end
end

#handle_error(e, message) ⇒ Object



132
133
134
135
136
137
138
139
140
# File 'lib/rubocop/cli.rb', line 132

def handle_error(e, message)
  @errors << message
  warn message
  if @options[:debug]
    puts e.message, e.backtrace
  else
    warn 'To see the complete backtrace run rubocop -d.'
  end
end

#inspect_file(file) ⇒ Object



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
# File 'lib/rubocop/cli.rb', line 79

def inspect_file(file)
  begin
    ast, comments, tokens, source_buffer, source, syntax_offences =
      CLI.parse(file) { |sb| sb.read }

  rescue Encoding::UndefinedConversionError, ArgumentError => e
    handle_error(e, "An error occurred while parsing #{file}.".color(:red))
    return []
  end

  # If we got any syntax errors, return only the syntax offences.
  # Parser may return nil for AST even though there are no syntax errors.
  # e.g. sources which contain only comments
  return syntax_offences unless syntax_offences.empty?

  config = ConfigStore.for(file)
  disabled_lines = disabled_lines_in(source)

  set_config_for_all_cops(config)

  @cops.reduce([]) do |offences, cop_class|
    cop_name = cop_class.cop_name
    if config.cop_enabled?(cop_name)
      cop = setup_cop(cop_class, disabled_lines)
      if !@options[:only] || @options[:only] == cop_name
        begin
          cop.inspect(source_buffer, source, tokens, ast, comments)
        rescue => e
          handle_error(e,
                       "An error occurred while #{cop.name}".color(:red) +
                       " cop was inspecting #{file}.".color(:red))
        end
      end
      offences.concat(cop.offences)
    end
    offences
  end.sort
end

#parse_options(args) ⇒ Object

rubocop:disable MethodLength



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
# File 'lib/rubocop/cli.rb', line 143

def parse_options(args)
  convert_deprecated_options!(args)

  OptionParser.new do |opts|
    opts.banner = 'Usage: rubocop [options] [file1, file2, ...]'

    opts.on('-d', '--debug', 'Display debug info.') do |d|
      @options[:debug] = d
    end
    opts.on('-L', '--list-cops', 'List class name of cops.') do |c|
      @options[:list_cops] = c
    end
    opts.on('-c', '--config FILE', 'Specify configuration file.') do |f|
      @options[:config] = f
      ConfigStore.set_options_config(@options[:config])
    end
    opts.on('--only COP', 'Run just one cop.') do |s|
      @options[:only] = s
      validate_only_option
    end
    opts.on('-f', '--format FORMATTER',
            'Choose an output formatter. This option',
            'can be specified multiple times to enable',
            'multiple formatters at the same time.',
            '  [p]rogress (default)',
            '  [s]imple',
            '  [c]lang',
            '  [e]macs',
            '  [j]son',
            '  custom formatter class name') do |key|
      @options[:formatters] ||= []
      @options[:formatters] << [key]
    end
    opts.on('-o', '--out FILE',
            'Write output to a file instead of STDOUT.',
            'This option applies to the previously',
            'specified --format, or the default format',
            'if no format is specified.') do |path|
      @options[:formatters] ||= [[DEFAULT_FORMATTER]]
      @options[:formatters].last << path
    end
    opts.on('-r', '--require FILE', 'Require Ruby file.') do |f|
      require f
    end
    opts.on('-R', '--rails', 'Run extra Rails cops.') do |r|
      @options[:rails] = r
    end
    opts.on('-l', '--lint', 'Run only lint cops.') do |l|
      @options[:lint] = l
    end
    opts.on('-a', '--auto-correct', 'Auto-correct offences.') do |a|
      @options[:autocorrect] = a
    end
    opts.on('-s', '--silent', 'Silence summary.') do |s|
      @options[:silent] = s
    end
    opts.on('-n', '--no-color', 'Disable color output.') do |s|
      Sickill::Rainbow.enabled = false
    end
    opts.on('-v', '--version', 'Display version.') do
      puts Rubocop::Version.version(false)
      exit(0)
    end
    opts.on('-V', '--verbose-version', 'Display verbose version.') do
      puts Rubocop::Version.version(true)
      exit(0)
    end
  end.parse!(args)
end

#ruby_files(root = Dir.pwd) ⇒ Array

Finds all Ruby source files under the current or other supplied directory. A Ruby source file is defined as a file with the .rb extension or a file with no extension that has a ruby shebang line as its first line. It is possible to specify includes and excludes using the config file, so you can include other Ruby files like Rakefiles and gemspecs.

Parameters:

  • root (defaults to: Dir.pwd)

    Root directory under which to search for ruby source files

Returns:

  • (Array)

    Array of filenames



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/rubocop/cli.rb', line 347

def ruby_files(root = Dir.pwd)
  files = Dir["#{root}/**/*"].select { |file| FileTest.file?(file) }

  rb = []

  rb += files.select { |file| File.extname(file) == '.rb' }
  rb += files.select do |file|
    if File.extname(file) == '' && !excluded_file?(file)
      begin
        File.open(file) { |f| f.readline } =~ /#!.*ruby/
      rescue EOFError, ArgumentError => e
        log_error(e, "Unprocessable file #{file.inspect}: ")
        false
      end
    end
  end

  rb += files.select do |file|
    config = ConfigStore.for(file)
    config.file_to_include?(file)
  end

  rb.reject { |file| excluded_file?(file) }.uniq
end

#run(args = ARGV) ⇒ Fixnum

Entry point for the application logic. Here we do the command line arguments processing and inspect the target files

Returns:

  • (Fixnum)

    UNIX exit code



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
# File 'lib/rubocop/cli.rb', line 29

def run(args = ARGV)
  trap_interrupt

  begin
    parse_options(args)
  rescue => e
    $stderr.puts e.message
    return 1
  end

  # filter out Rails cops unless requested
  @cops.reject!(&:rails?) unless @options[:rails]

  # filter out style cops when --lint is passed
  @cops.select!(&:lint?) if @options[:lint]

  target_files = target_files(args)
  target_files.each(&:freeze).freeze
  inspected_files = []
  any_failed = false

  formatter_set.started(target_files)

  target_files.each do |file|
    break if wants_to_quit?

    puts "Scanning #{file}" if @options[:debug]
    formatter_set.file_started(file, {})

    offences = inspect_file(file)

    any_failed = true unless offences.empty?
    inspected_files << file
    formatter_set.file_finished(file, offences.freeze)
  end

  formatter_set.finished(inspected_files.freeze)
  formatter_set.close_output_files

  display_error_summary(@errors) unless @options[:silent]

  !any_failed && !wants_to_quit ? 0 : 1
end

#set_config_for_all_cops(config) ⇒ Object



118
119
120
121
122
# File 'lib/rubocop/cli.rb', line 118

def set_config_for_all_cops(config)
  @cops.each do |cop_class|
    cop_class.config = config.for_cop(cop_class.cop_name)
  end
end

#setup_cop(cop_class, disabled_lines = nil) ⇒ Object



124
125
126
127
128
129
130
# File 'lib/rubocop/cli.rb', line 124

def setup_cop(cop_class, disabled_lines = nil)
  cop = cop_class.new
  cop.debug = @options[:debug] || @options[:list_cops]
  cop.autocorrect = @options[:autocorrect]
  cop.disabled_lines = disabled_lines[cop_class.cop_name] if disabled_lines
  cop
end

#target_files(args) ⇒ Array

Generate a list of target files by expanding globing patterns (if any). If args is empty recursively finds all Ruby source files under the current directory

Returns:

  • (Array)

    array of filenames



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/rubocop/cli.rb', line 321

def target_files(args)
  return ruby_files if args.empty?

  files = []

  args.each do |target|
    if File.directory?(target)
      files += ruby_files(target.chomp(File::SEPARATOR))
    elsif target =~ /\*/
      files += Dir[target]
    else
      files << target
    end
  end

  files.map { |f| File.expand_path(f) }.uniq
end

#trap_interruptObject



226
227
228
229
230
231
232
233
# File 'lib/rubocop/cli.rb', line 226

def trap_interrupt
  Signal.trap('INT') do
    exit!(1) if wants_to_quit?
    self.wants_to_quit = true
    $stderr.puts
    $stderr.puts 'Exiting... Interrupt again to exit immediately.'
  end
end

#validate_only_optionObject



73
74
75
76
77
# File 'lib/rubocop/cli.rb', line 73

def validate_only_option
  if @cops.none? { |c| c.cop_name == @options[:only] }
    fail ArgumentError, "Unrecognized cop name: #{@options[:only]}."
  end
end