Class: RailsBestPractices::Analyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_best_practices/analyzer.rb

Overview

RailsBestPractices Analyzer helps you to analyze your rails code, according to best practices on rails-bestpractices. if it finds any violatioins to best practices, it will give you some readable suggestions.

The analysis process is partitioned into two parts,

  1. prepare process, it checks only model and mailer files, do some preparations, such as remember model names and associations.

  2. review process, it checks all files, according to configuration, it really check if codes violate the best practices, if so, remember the violations.

After analyzing, output the violations.

Constant Summary collapse

DEFAULT_CONFIG =
File.join(File.dirname(__FILE__), '..', '..', 'rails_best_practices.yml')
GITHUB_URL =
'https://github.com/'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, options = {}) ⇒ Analyzer

initialize

Parameters:

  • path (String)

    where to generate the configuration yaml file

  • options (Hash) (defaults to: {})


28
29
30
31
32
33
34
# File 'lib/rails_best_practices/analyzer.rb', line 28

def initialize(path, options = {})
  @path = File.expand_path(path || '.')

  @options = options
  @options['exclude'] ||= []
  @options['only'] ||= []
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



19
20
21
# File 'lib/rails_best_practices/analyzer.rb', line 19

def path
  @path
end

#runnerObject

Returns the value of attribute runner.



18
19
20
# File 'lib/rails_best_practices/analyzer.rb', line 18

def runner
  @runner
end

Instance Method Details

#analyzeObject

Analyze rails codes.

there are two steps to check rails codes,

  1. prepare process, check all model and mailer files.

  2. review process, check all files.

if there are violations to rails best practices, output them.

Parameters:

  • path (String)

    the directory of rails project

  • options (Hash)


52
53
54
55
56
57
58
59
# File 'lib/rails_best_practices/analyzer.rb', line 52

def analyze
  Core::Runner.base_path = @path
  Core::Runner.config_path = @options['config']
  @runner = Core::Runner.new

  analyze_source_codes
  analyze_vcs
end

#analyze_source_codesObject

analyze source codes.



329
330
331
332
333
# File 'lib/rails_best_practices/analyzer.rb', line 329

def analyze_source_codes
  @bar = ProgressBar.create(title: 'Source Code', total: parse_files.size * 4) if display_bar?
  %w[lexical prepare review inline_disable].each { |process| send(:process, process) }
  @bar.finish if display_bar?
end

#analyze_vcsObject

analyze version control system info.



336
337
338
339
# File 'lib/rails_best_practices/analyzer.rb', line 336

def analyze_vcs
  load_git_info if @options['with-git']
  load_hg_info if @options['with-hg']
end

#display_bar?Boolean

if disaply progress bar.

Returns:

  • (Boolean)


342
343
344
# File 'lib/rails_best_practices/analyzer.rb', line 342

def display_bar?
  !@options['debug'] && !@options['silent']
end

#error_typesObject

unique error types.



347
348
349
# File 'lib/rails_best_practices/analyzer.rb', line 347

def error_types
  errors.map(&:type).uniq
end

#errorsObject

delegate errors to runner



352
353
354
# File 'lib/rails_best_practices/analyzer.rb', line 352

def errors
  @runner.errors
end

#expand_dirs_to_files(*dirs) ⇒ Array

expand all files with extenstion rb, erb, haml, slim, builder and rxml under the dirs

Parameters:

  • dirs (Array)

    what directories to expand

Returns:

  • (Array)

    all files expanded



138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/rails_best_practices/analyzer.rb', line 138

def expand_dirs_to_files(*dirs)
  extensions = %w[rb erb rake rhtml haml slim builder rxml rabl]

  dirs.flatten.map do |entry|
    next unless File.exist? entry

    if File.directory? entry
      Dir[File.join(entry, '**', "*.{#{extensions.join(',')}}")]
    else
      entry
    end
  end.flatten
end

#file_accept(files, patterns) ⇒ Object

accept specific files.

Parameters:

  • files (Array)
  • patterns, (Regexp)

    files match any pattern will be accepted



182
183
184
# File 'lib/rails_best_practices/analyzer.rb', line 182

def file_accept(files, patterns)
  files.select { |file| patterns.any? { |pattern| file =~ pattern } }
end

#file_ignore(files, pattern) ⇒ Array

ignore specific files.

Parameters:

  • files (Array)
  • pattern (Regexp)

    files match the pattern will be ignored

Returns:

  • (Array)

    files that not match the pattern



174
175
176
# File 'lib/rails_best_practices/analyzer.rb', line 174

def file_ignore(files, pattern)
  files.reject { |file| file.index(pattern) }
end

#file_sort(files) ⇒ Array

sort files, models first, mailers, helpers, and then sort other files by characters.

models and mailers first as for prepare process.

Parameters:

  • files (Array)

Returns:

  • (Array)

    sorted files



158
159
160
161
162
163
164
165
166
167
# File 'lib/rails_best_practices/analyzer.rb', line 158

def file_sort(files)
  models = files.find_all { |file| file =~ Core::Check::MODEL_FILES }
  mailers = files.find_all { |file| file =~ Core::Check::MAILER_FILES }
  helpers = files.find_all { |file| file =~ Core::Check::HELPER_FILES }
  others =
    files.find_all do |file|
      file !~ Core::Check::MAILER_FILES && file !~ Core::Check::MODEL_FILES && file !~ Core::Check::HELPER_FILES
    end
  models + mailers + helpers + others
end

#generateObject

generate configuration yaml file.



37
38
39
# File 'lib/rails_best_practices/analyzer.rb', line 37

def generate
  FileUtils.cp DEFAULT_CONFIG, File.join(@path, 'config/rails_best_practices.yml')
end

#load_git_infoObject

load git commit and git username info.



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/rails_best_practices/analyzer.rb', line 216

def load_git_info
  git_progressbar = ProgressBar.create(title: 'Git Info', total: errors.size) if display_bar?
  start = @runner.class.base_path =~ %r{/$} ? @runner.class.base_path.size : @runner.class.base_path.size + 1
  errors.each do |error|
    info_command = "cd #{@runner.class.base_path}"
    info_command += " && git blame -L #{error.line_number.split(',').first},+1 #{error.filename[start..-1]}"
    git_info = system(info_command)
    unless git_info == ''
      git_commit, git_username = git_info.split(/\d{4}-\d{2}-\d{2}/).first.split('(')
      error.git_commit = git_commit.split(' ').first.strip
      error.git_username = git_username.strip
    end
    git_progressbar.increment if display_bar?
  end
  git_progressbar.finish if display_bar?
end

#load_hg_infoObject

load hg commit and hg username info.



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/rails_best_practices/analyzer.rb', line 198

def load_hg_info
  hg_progressbar = ProgressBar.create(title: 'Hg Info', total: errors.size) if display_bar?
  errors.each do |error|
    info_command = "cd #{@runner.class.base_path}"
    info_command += " && hg blame -lvcu #{error.filename[@runner.class.base_path.size..-1].gsub(%r{^/}, '')}"
    info_command += " | sed -n /:#{error.line_number.split(',').first}:/p"
    hg_info = system(info_command)
    unless hg_info == ''
      hg_commit_username = hg_info.split(':')[0].strip
      error.hg_username = hg_commit_username.split(/\ /)[0..-2].join(' ')
      error.hg_commit = hg_commit_username.split(/\ /)[-1]
    end
    hg_progressbar.increment if display_bar?
  end
  hg_progressbar.finish if display_bar?
end

#outputObject

Output the analyze result.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/rails_best_practices/analyzer.rb', line 62

def output
  case @options['format']
  when 'html'
    @options['output-file'] ||= 'rails_best_practices_output.html'
    output_html_errors
  when 'json'
    @options['output-file'] ||= 'rails_best_practices_output.json'
    output_json_errors
  when 'yaml'
    @options['output-file'] ||= 'rails_best_practices_output.yaml'
    output_yaml_errors
  when 'xml'
    @options['output-file'] ||= 'rails_best_practices_output.xml'
    output_xml_errors
  else
    output_terminal_errors
  end
end

#output_html_errorsObject

output errors with html format.



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
# File 'lib/rails_best_practices/analyzer.rb', line 234

def output_html_errors
  require 'erubis'
  template =
    @options['template'] ?
      File.read(File.expand_path(@options['template'])) :
      File.read(File.join(File.dirname(__FILE__), '..', '..', 'assets', 'result.html.erb'))

  if @options['with-github']
    last_commit_id = @options['last-commit-id'] || `cd #{@runner.class.base_path} && git rev-parse HEAD`.chomp
    unless @options['github-name'].start_with?('https')
      @options['github-name'] = GITHUB_URL + @options['github-name']
    end
  end
  File.open(@options['output-file'], 'w+') do |file|
    eruby = Erubis::Eruby.new(template)
    file.puts eruby.evaluate(
                errors: errors,
                error_types: error_types,
                atom: @options['with-atom'],
                textmate: @options['with-textmate'],
                vscode: @options['with-vscode'],
                sublime: @options['with-sublime'],
                mvim: @options['with-mvim'],
                github: @options['with-github'],
                github_name: @options['github-name'],
                last_commit_id: last_commit_id,
                git: @options['with-git'],
                hg: @options['with-hg']
              )
  end
end

#output_json_errorsObject

output errors with json format.



305
306
307
308
309
310
311
312
313
314
# File 'lib/rails_best_practices/analyzer.rb', line 305

def output_json_errors
  errors_as_hashes =
    errors.map do |err|
      { filename: err.filename, line_number: err.line_number, message: err.message }
    end

  File.open(@options['output-file'], 'w+') do |file|
    file.write JSON.dump(errors_as_hashes)
  end
end

#output_terminal_errorsObject

output errors on terminal.



187
188
189
190
191
192
193
194
195
# File 'lib/rails_best_practices/analyzer.rb', line 187

def output_terminal_errors
  errors.each { |error| plain_output(error.to_s, 'red') }
  plain_output("\nPlease go to https://rails-bestpractices.com to see more useful Rails Best Practices.", 'green')
  if errors.empty?
    plain_output("\nNo warning found. Cool!", 'green')
  else
    plain_output("\nFound #{errors.size} warnings.", 'red')
  end
end

#output_xml_errorsObject



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/rails_best_practices/analyzer.rb', line 266

def output_xml_errors
  require 'rexml/document'

  document =
    REXML::Document.new.tap do |d|
      d << REXML::XMLDecl.new
    end

  checkstyle = REXML::Element.new('checkstyle', document)

  errors.group_by(&:filename).each do |file, group|
    REXML::Element.new('file', checkstyle).tap do |f|
      f.attributes['name'] = file
      group.each do |error|
        REXML::Element.new('error', f).tap do |e|
          e.attributes['line'] = error.line_number
          e.attributes['column'] = 0
          e.attributes['severity'] = 'error'
          e.attributes['message'] = error.message
          e.attributes['source'] = 'com.puppycrawl.tools.checkstyle.' + error.type
        end
      end
    end
  end

  formatter = REXML::Formatters::Default.new
  File.open(@options['output-file'], 'w+') do |result|
    formatter.write(document, result)
  end
end

#output_yaml_errorsObject

output errors with yaml format.



298
299
300
301
302
# File 'lib/rails_best_practices/analyzer.rb', line 298

def output_yaml_errors
  File.open(@options['output-file'], 'w+') do |file|
    file.write YAML.dump(errors)
  end
end

#parse_filesArray

get all files for parsing.

Returns:

  • (Array)

    all files for parsing



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
# File 'lib/rails_best_practices/analyzer.rb', line 106

def parse_files
  @parse_files ||=
    begin
      files = expand_dirs_to_files(@path)
      files = file_sort(files)

      if @options['only'].present?
        files = file_accept(files, @options['only'])
      end

      # By default, tmp, vender, spec, test, features are ignored.
      %w[vendor spec test features tmp].each do |dir|
        files = file_ignore(files, File.join(@path, dir)) unless @options[dir]
      end

      # Exclude files based on exclude regexes if the option is set.
      @options['exclude'].each do |pattern|
        files = file_ignore(files, pattern)
      end

      %w[Capfile Gemfile Gemfile.lock].each do |file|
        files.unshift File.join(@path, file)
      end

      files.compact
    end
end

#plain_output(message, color) ⇒ Object

plain output with color.

Parameters:

  • message (String)

    to output

  • color (String)


320
321
322
323
324
325
326
# File 'lib/rails_best_practices/analyzer.rb', line 320

def plain_output(message, color)
  if @options['without-color']
    puts message
  else
    puts Colorize.send(color, message)
  end
end

#process(process) ⇒ Object

process lexical, prepare or reivew.

get all files for the process, analyze each file, and increment progress bar unless debug.

Parameters:

  • process (String)

    the process name, lexical, prepare or review.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/rails_best_practices/analyzer.rb', line 87

def process(process)
  parse_files.each do |file|
    begin
      puts file if @options['debug']
      @runner.send(process, file, File.read(file))
    rescue StandardError
      if @options['debug']
        warning = "#{file} looks like it's not a valid Ruby file.  Skipping..."
        plain_output(warning, 'red')
      end
    end
    @bar.increment if display_bar?
  end
  @runner.send("after_#{process}")
end