seattlerb/flog

View on GitHub
lib/flog_cli.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require "rubygems"
require "optparse"
require "forwardable"

require "path_expander"
require "flog"

##
# This is the CLI interface for Flog, responsible for processing
# options, finding files, loading plugins, and reporting results.

class FlogCLI
  extend Forwardable

  def_delegators :@flog, :average, :calculate, :each_by_score, :option
  def_delegators :@flog, :method_locations, :method_scores, :reset, :scores
  def_delegators :@flog, :threshold, :total_score, :no_method, :calculate_total_scores
  def_delegators :@flog, :max_method

  ##
  # This kicks off the whole thing.

  def self.run args = ARGV, extra = {}
    load_plugins

    expander = PathExpander.new args, "**/*.{rb,rake}"
    files = expander.process

    options = parse_options args, extra

    abort "no files or stdin (-) to process, aborting." if
      files.empty? and args.empty?

    flogger = new options
    flogger.flog(*files)
    flogger.report
  end

  ##
  # Loads all flog plugins. Files must be named "flog/*.rb".

  def self.load_plugins
    # TODO: I think I want to do this more like hoe's plugin system. Generalize?
    loaded, found = {}, {}

    Gem.find_files("flog/*.rb").reverse.each do |path|
      found[File.basename(path, ".rb").intern] = path
    end

    found.each do |name, plugin|
      next if loaded[name]
      begin
        warn "loading #{plugin}" # if $DEBUG
        loaded[name] = load plugin
      rescue LoadError => e
        warn "error loading #{plugin.inspect}: #{e.message}. skipping..."
      end
    end

    self.plugins.merge loaded

    names = Flog.constants.map {|s| s.to_s}.reject {|n| n =~ /^[A-Z_]+$/}

    names.each do |name|
      # next unless Hoe.plugins.include? name.downcase.intern
      mod = Flog.const_get(name)
      next if Class === mod
      warn "extend #{mod}" if $DEBUG
      # self.extend mod
    end
  end

  ##
  # Parse options in +args+ (defaults to ARGV).

  def self.parse_options args = ARGV, extra_options = {}
    option = {
      :quiet    => false,
      :continue => false,
      :parser   => RubyParser,
    }.merge extra_options

    OptionParser.new do |opts|
      opts.separator "Standard options:"

      opts.on("-a", "--all", "Display all flog results, not top 60%.") do
        option[:all] = true
      end

      opts.on("-b", "--blame", "Include blame information for methods.") do
        option[:blame] = true
      end

      opts.on("-c", "--continue", "Continue despite syntax errors.") do
        option[:continue] = true
      end

      opts.on("-d", "--details", "Show method details.") do
        option[:details] = true
      end

      opts.on("-g", "--group", "Group and sort by class.") do
        option[:group] = true
      end

      opts.on("-h", "--help", "Show this message.") do
        puts opts
        exit
      end

      opts.on("-I dir1,dir2,dir3", Array, "Add to LOAD_PATH.") do |dirs|
        dirs.each do |dir|
          $: << dir
        end
      end

      opts.on("-m", "--methods-only", "Skip code outside of methods.") do
        option[:methods] = true
      end

      opts.on("-q", "--quiet", "Don't show parse errors.") do
        option[:quiet] = true
      end

      opts.on("-e", "--extended", "Put file:line on a separate line (for rubymine & friends).") do
        option[:extended] = true
      end

      opts.on("-s", "--score", "Display total score only.") do
        option[:score] = true
      end

      opts.on("-tN", "--threshold=N", Integer, "Set the report cutoff threshold (def: 60%).") do |n|
        option[:threshold] = n / 100.0
      end

      opts.on("-v", "--verbose", "Display progress during processing.") do
        option[:verbose] = true
      end

      next if self.plugins.empty?
      opts.separator "Plugin options:"

      extra = self.method_scores.grep(/parse_options/) - %w(parse_options)

      extra.sort.each do |msg|
        self.send msg, opts, option
      end

    end.parse! Array(args)

    option
  end

  ##
  # The known plugins for Flog. See Flog.load_plugins.

  def self.plugins
    @plugins ||= {}
  end

  ##
  # Flog the given files. Deals with "-", syntax errors, and
  # traversing subdirectories intelligently. Use PathExpander to
  # process dirs into files.

  def flog(*files)
    files << "-" if files.empty?
    @flog.flog(*files)
  end

  ##
  # Creates a new Flog instance with +options+.

  def initialize options = {}
    @flog = Flog.new options
  end

  ##
  # Output the report up to a given max or report everything, if nil.

  def output_details io, max = nil
    io.puts

    each_by_score max do |class_method, score, call_list|
      self.print_score io, class_method, score

      if option[:details] then
        call_list.sort_by { |k,v| -v }.each do |call, count|
          io.puts "  %6.1f:   %s" % [count, call]
        end
        io.puts
      end
    end
  end

  ##
  # Output the report, grouped by class/module, up to a given max or
  # report everything, if nil.

  def output_details_grouped io, threshold = nil
    calculate

    scores.sort_by { |_, n| -n }.each do |klass, total|
      io.puts

      io.puts "%8.1f: %s" % [total, "#{klass} total"]

      method_scores[klass].each do |name, score|
        self.print_score io, name, score
      end
    end
  end

  ##
  # Print out one formatted score.

  def print_score io, name, score
    location = method_locations[name]
    if location then
      sep = " "
      sep = "%-11s" % "\n" if option[:extended]
      io.puts "%8.1f: %-32s%s%s" % [score, name, sep, location]
    else
      io.puts "%8.1f: %s" % [score, name]
    end
  end

  ##
  # Report results to #io, STDOUT by default.

  def report(io = $stdout)
    io.puts "%8.1f: %s" % [total_score, "flog total"]
    io.puts "%8.1f: %s" % [average, "flog/method average"]

    return if option[:score]

    if option[:group] then
      output_details_grouped io, threshold
    else
      output_details io, threshold
    end
  ensure
    self.reset
  end
end