class RailsBestPractices::Analyzer

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.

Constants

DEFAULT_CONFIG
GITHUB_URL

Attributes

path[R]
runner[RW]

Public Class Methods

new(path, options = {}) click to toggle source

initialize

@param [String] path where to generate the configuration yaml file @param [Hash] options

# 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

Public Instance Methods

analyze() click to toggle source

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.

@param [String] path the directory of rails project @param [Hash] options

# 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_codes() click to toggle source

analyze source codes.

# 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_vcs() click to toggle source

analyze version control system info.

# 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?() click to toggle source

if disaply progress bar.

# File lib/rails_best_practices/analyzer.rb, line 342
def display_bar?
  !@options['debug'] && !@options['silent']
end
error_types() click to toggle source

unique error types.

# File lib/rails_best_practices/analyzer.rb, line 347
def error_types
  errors.map(&:type).uniq
end
errors() click to toggle source

delegate errors to runner

# File lib/rails_best_practices/analyzer.rb, line 352
def errors
  @runner.errors
end
expand_dirs_to_files(*dirs) click to toggle source

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

@param [Array] dirs what directories to expand @return [Array] all files expanded

# 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) click to toggle source

accept specific files.

@param [Array] files @param [Regexp] patterns, files match any pattern will be accepted

# 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) click to toggle source

ignore specific files.

@param [Array] files @param [Regexp] pattern files match the pattern will be ignored @return [Array] files that not match the pattern

# File lib/rails_best_practices/analyzer.rb, line 174
def file_ignore(files, pattern)
  files.reject { |file| file.index(pattern) }
end
file_sort(files) click to toggle source

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

models and mailers first as for prepare process.

@param [Array] files @return [Array] sorted files

# 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
generate() click to toggle source

generate configuration yaml file.

# 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_info() click to toggle source

load git commit and git username info.

# 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_info() click to toggle source

load hg commit and hg username info.

# 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
output() click to toggle source

Output the analyze result.

# 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_errors() click to toggle source

output errors with html format.

# 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_errors() click to toggle source

output errors with json format.

# 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_errors() click to toggle source

output errors on terminal.

# 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_errors() click to toggle source
# 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_errors() click to toggle source

output errors with yaml format.

# 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_files() click to toggle source

get all files for parsing.

@return [Array] all files for parsing

# 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) click to toggle source

plain output with color.

@param [String] message to output @param [String] color

# 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) click to toggle source

process lexical, prepare or reivew.

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

@param [String] process the process name, lexical, prepare or review.

# 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