class Flog

Constants

BRANCHING
OTHER_SCORES

Various non-call constructs

SCORES
THRESHOLD
VERSION

Attributes

calls[R]
class_stack[R]
mass[R]
method_locations[R]
method_stack[R]
multiplier[RW]
option[R]
sclass[R]

Public Class Methods

expand_dirs_to_files(*dirs) click to toggle source

REFACTOR: from flay

# File lib/flog.rb, line 121
def self.expand_dirs_to_files *dirs
  extensions = %w[rb rake]

  dirs.flatten.map { |p|
    if File.directory? p then
      Dir[File.join(p, '**', "*.{#{extensions.join(',')}}")]
    else
      p
    end
  }.flatten.sort
end
load_plugins() click to toggle source

TODO: I think I want to do this more like hoe's plugin system. Generalize?

# File lib/flog.rb, line 90
def self.load_plugins
  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 =~ %r^[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
new(option = {}) click to toggle source
# File lib/flog.rb, line 335
def initialize option = {}
  super()
  @option              = option
  @sclass              = nil
  @class_stack         = []
  @method_stack        = []
  @method_locations    = {}
  @mass                = {}
  @parser              = nil
  self.auto_shift_type = true
  self.reset
end
parse_options(args = ARGV) click to toggle source
# File lib/flog.rb, line 133
def self.parse_options args = ARGV
  option = {
    :quiet    => false,
    :continue => false,
    :parser   => RubyParser,
  }

  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("-s", "--score", "Display total score only.") do
      option[:score] = true
    end

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

    opts.on("--18", "Use a ruby 1.8 parser.") do
      option[:parser] = Ruby18Parser
    end

    opts.on("--19", "Use a ruby 1.9 parser.") do
      option[:parser] = Ruby19Parser
    end

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

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

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

  end.parse! Array(args)

  option
end
plugins() click to toggle source
# File lib/flog.rb, line 85
def self.plugins
  @plugins ||= {}
end

Public Instance Methods

add_to_score(name, score = OTHER_SCORES[name]) click to toggle source

Add a score to the tally. Score can be predetermined or looked up automatically. Uses multiplier for additional spankings. Spankings!

# File lib/flog.rb, line 217
def add_to_score name, score = OTHER_SCORES[name]
  @calls[signature][name] += score * @multiplier
end
average() click to toggle source

really?

# File lib/flog.rb, line 224
def average
  return 0 if calls.size == 0
  total / calls.size
end
dsl_name?(args) click to toggle source
# File lib/flog.rb, line 672
def dsl_name? args
  return false unless args and not args.empty?

  first_arg = args.first
  first_arg = first_arg[1] if first_arg[0] == :hash

  [:lit, :str].include? first_arg[0] and first_arg[1]
end
each_by_score(max = nil) { |class_method, score, call_list| ... } click to toggle source

Iterate over the calls sorted (descending) by score.

# File lib/flog.rb, line 232
def each_by_score max = nil
  my_totals = totals
  current   = 0

  calls.sort_by { |k,v| -my_totals[k] }.each do |class_method, call_list|
    score = my_totals[class_method]

    yield class_method, score, call_list

    current += score
    break if max and current >= max
  end
end
flog(*files_or_dirs) click to toggle source

Flog the given files or directories. Smart. Deals with "-", syntax errors, and traversing subdirectories intelligently.

# File lib/flog.rb, line 250
def flog(*files_or_dirs)
  files = Flog.expand_dirs_to_files(*files_or_dirs)

  files.each do |file|
    ruby = file == '-' ? $stdin.read : File.binread(file)

    flog_ruby ruby, file
  end
end
flog_ruby(ruby, file="-", timeout = 10) click to toggle source

Flog the given ruby source, optionally using file to provide paths for methods. Smart. Handles syntax errors and timeouts so you don't have to.

# File lib/flog.rb, line 265
def flog_ruby ruby, file="-", timeout = 10
  flog_ruby! ruby, file, timeout
rescue Timeout::Error
  warn "TIMEOUT parsing #{file}. Skipping."
rescue RubyParser::SyntaxError, Racc::ParseError => e
  q = option[:quiet]
  if e.inspect =~ %r<\%|%\>/ or ruby =~ %r<\%|%\>/ then
    return if q
    warn "#{e.inspect} at #{e.backtrace.first(5).join(', ')}"
    warn "\n...stupid lemmings and their bad erb templates... skipping"
  else
    warn "ERROR: parsing ruby file #{file}" unless q
    unless option[:continue] then
      warn "ERROR! Aborting. You may want to run with --continue."
      raise e
    end
    return if q
    warn "%s: %s at:\n  %s" % [e.class, e.message.strip,
                               e.backtrace.first(5).join("\n  ")]
  end
end
flog_ruby!(ruby, file="-", timeout = 10) click to toggle source

Flog the given ruby source, optionally using file to provide paths for methods. Does not handle timeouts or syntax errors. See flog_ruby.

# File lib/flog.rb, line 291
def flog_ruby! ruby, file="-", timeout = 10
  @parser = (option[:parser] || RubyParser).new

  warn "** flogging #{file}" if option[:verbose]

  ast = @parser.process ruby, file, timeout
  mass[file] = ast.mass
  process ast
end
in_klass(name) { || ... } click to toggle source

Adds name to the class stack, for the duration of the block

# File lib/flog.rb, line 304
def in_klass name
  if Sexp === name then
    name = case name.first
           when :colon2 then
             name = name.flatten
             name.delete :const
             name.delete :colon2
             name.join("::")
           when :colon3 then
             name.last.to_s
           else
             raise "unknown type #{name.inspect}"
           end
  end

  @class_stack.unshift name
  yield
  @class_stack.shift
end
in_method(name, file, line) { || ... } click to toggle source

Adds name to the method stack, for the duration of the block

# File lib/flog.rb, line 327
def in_method(name, file, line)
  method_name = Regexp === name ? name.inspect : name.to_s
  @method_stack.unshift method_name
  @method_locations[signature] = "#{file}:#{line}"
  yield
  @method_stack.shift
end
klass_name() click to toggle source

Returns the first class in the list, or @@no_class if there are none.

# File lib/flog.rb, line 352
def klass_name
  name = @class_stack.first

  if Sexp === name then
    raise "you shouldn't see me"
  elsif @class_stack.any?
    @class_stack.reverse.join("::").sub(%r\([^\)]+\)$/, '')
  else
    @@no_class
  end
end
max_method() click to toggle source
# File lib/flog.rb, line 509
def max_method
  totals.max_by { |_, score| score }
end
max_score() click to toggle source
# File lib/flog.rb, line 505
def max_score
  max_method.last
end
method_name() click to toggle source

Returns the first method in the list, or "none" if there are none.

# File lib/flog.rb, line 368
def method_name
  m = @method_stack.first || @@no_method
  m = "##{m}" unless m =~ %r::/
  m
end
output_details(io, max = nil) click to toggle source

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

# File lib/flog.rb, line 377
def output_details io, max = nil
  io.puts

  each_by_score max do |class_method, score, call_list|
    return 0 if option[:methods] and class_method =~ %r##{@@no_method}/

    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
  # io.puts
end
output_details_grouped(io, max = nil) click to toggle source

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

# File lib/flog.rb, line 399
def output_details_grouped io, max = nil
  scores  = Hash.new 0
  methods = Hash.new { |h,k| h[k] = [] }

  each_by_score max do |class_method, score, call_list|
    klass = class_method.split(%r#|::/).first

    methods[klass] << [class_method, score]
    scores[klass]  += score
  end

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

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

    methods[klass].each do |name, score|
      self.print_score io, name, score
    end
  end
end
penalize_by(bonus) { || ... } click to toggle source

For the duration of the block the complexity factor is increased by bonus This allows the complexity of sub-expressions to be influenced by the expressions in which they are found. Yields 42 to the supplied block.

# File lib/flog.rb, line 427
def penalize_by bonus
  @multiplier += bonus
  yield
  @multiplier -= bonus
end
print_score(io, name, score) click to toggle source

Print out one formatted score.

process_alias(exp) click to toggle source

Process Methods:

# File lib/flog.rb, line 536
def process_alias(exp)
  process exp.shift
  process exp.shift
  add_to_score :alias
  s()
end
process_and(exp) click to toggle source
# File lib/flog.rb, line 543
def process_and(exp)
  add_to_score :branch
  penalize_by 0.1 do
    process exp.shift # lhs
    process exp.shift # rhs
  end
  s()
end
Also aliased as: process_or
process_attrasgn(exp) click to toggle source
# File lib/flog.rb, line 553
def process_attrasgn(exp)
  add_to_score :assignment
  process exp.shift # lhs
  exp.shift # name
  process_until_empty exp # rhs
  s()
end
process_block(exp) click to toggle source
# File lib/flog.rb, line 561
def process_block(exp)
  penalize_by 0.1 do
    process_until_empty exp
  end
  s()
end
process_block_pass(exp) click to toggle source
# File lib/flog.rb, line 568
def process_block_pass(exp)
  arg = exp.shift

  add_to_score :block_pass

  case arg.first
  when :lvar, :dvar, :ivar, :cvar, :self, :const, :colon2, :nil then
    # do nothing
  when :lit, :call then
    add_to_score :to_proc_normal
  when :lasgn then # blah(&l = proc { ... })
    add_to_score :to_proc_lasgn
  when :iter, :dsym, :dstr, *BRANCHING then
    add_to_score :to_proc_icky!
  else
    raise({:block_pass_even_ickier! => arg}.inspect)
  end

  process arg

  s()
end
process_call(exp) click to toggle source
# File lib/flog.rb, line 591
def process_call(exp)
  penalize_by 0.2 do
    process exp.shift # recv
  end

  name = exp.shift

  penalize_by 0.2 do
    process_until_empty exp
  end

  add_to_score name, SCORES[name]

  s()
end
process_case(exp) click to toggle source
# File lib/flog.rb, line 607
def process_case(exp)
  add_to_score :branch
  process exp.shift # recv
  penalize_by 0.1 do
    process_until_empty exp
  end
  s()
end
process_class(exp) click to toggle source
# File lib/flog.rb, line 616
def process_class(exp)
  in_klass exp.shift do
    penalize_by 1.0 do
      process exp.shift # superclass expression
    end
    process_until_empty exp
  end
  s()
end
process_dasgn_curr(exp) click to toggle source
# File lib/flog.rb, line 626
def process_dasgn_curr(exp) # FIX: remove
  add_to_score :assignment
  exp.shift # name
  process exp.shift # assigment, if any
  s()
end
Also aliased as: process_iasgn, process_lasgn
process_defn(exp) click to toggle source
# File lib/flog.rb, line 635
def process_defn(exp)
  name = @sclass ? "::#{exp.shift}" : exp.shift
  in_method name, exp.file, exp.line do
    process_until_empty exp
  end
  s()
end
process_defs(exp) click to toggle source
# File lib/flog.rb, line 643
def process_defs(exp)
  process exp.shift # recv
  in_method "::#{exp.shift}", exp.file, exp.line do
    process_until_empty exp
  end
  s()
end
process_else(exp) click to toggle source

TODO: it's not clear to me whether this can be generated at all.

# File lib/flog.rb, line 652
def process_else(exp)
  add_to_score :branch
  penalize_by 0.1 do
    process_until_empty exp
  end
  s()
end
Also aliased as: process_rescue, process_when
process_iasgn(exp) click to toggle source
Alias for: process_dasgn_curr
process_if(exp) click to toggle source
# File lib/flog.rb, line 662
def process_if(exp)
  add_to_score :branch
  process exp.shift # cond
  penalize_by 0.1 do
    process exp.shift # true
    process exp.shift # false
  end
  s()
end
process_iter(exp) click to toggle source
# File lib/flog.rb, line 681
def process_iter(exp)
  context = (self.context - [:class, :module, :scope])
  context = context.uniq.sort_by { |s| s.to_s }

  exp.delete 0 # { || ... } has 0 in arg slot

  if context == [:block, :iter] or context == [:iter] then
    recv = exp.first

    # DSL w/ names. eg task :name do ... end
    #   looks like s(:call, nil, :task, s(:lit, :name))
    #           or s(:call, nil, :task, s(:str, "name"))
    #           or s(:call, nil, :task, s(:hash, s(:lit, :name) ...))

    t, r, m, *a = recv

    if t == :call and r == nil and submsg = dsl_name?(a) then
      m = "#{m}(#{submsg})" if m and [String, Symbol].include?(submsg.class)
      in_klass m do                             # :task/namespace
        in_method submsg, exp.file, exp.line do # :name
          process_until_empty exp
        end
      end
      return s()
    end
  end

  add_to_score :branch

  process exp.shift # no penalty for LHS

  penalize_by 0.1 do
    process_until_empty exp
  end

  s()
end
process_lasgn(exp) click to toggle source
Alias for: process_dasgn_curr
process_lit(exp) click to toggle source
# File lib/flog.rb, line 719
def process_lit(exp)
  value = exp.shift
  case value
  when 0, -1 then
    # ignore those because they're used as array indicies instead of
    # first/last
  when Integer then
    add_to_score :lit_fixnum
  when Float, Symbol, Regexp, Range then
    # do nothing
  else
    raise value.inspect
  end
  s()
end
process_masgn(exp) click to toggle source
# File lib/flog.rb, line 735
def process_masgn(exp)
  add_to_score :assignment

  exp.map! { |s| Sexp === s ? s : s(:lasgn, s) }

  process_until_empty exp
  s()
end
process_module(exp) click to toggle source
# File lib/flog.rb, line 744
def process_module(exp)
  in_klass exp.shift do
    process_until_empty exp
  end
  s()
end
process_or(exp) click to toggle source
Alias for: process_and
process_rescue(exp) click to toggle source
Alias for: process_else
process_sclass(exp) click to toggle source
# File lib/flog.rb, line 751
def process_sclass(exp)
  @sclass = true
  penalize_by 0.5 do
    process exp.shift # recv
    process_until_empty exp
  end
  @sclass = nil

  add_to_score :sclass
  s()
end
process_super(exp) click to toggle source
# File lib/flog.rb, line 763
def process_super(exp)
  add_to_score :super
  process_until_empty exp
  s()
end
process_until(exp) click to toggle source
Alias for: process_while
process_until_empty(exp) click to toggle source

Process each element of exp in turn.

# File lib/flog.rb, line 448
def process_until_empty exp
  process exp.shift until exp.empty?
end
process_when(exp) click to toggle source
Alias for: process_else
process_while(exp) click to toggle source
# File lib/flog.rb, line 769
def process_while(exp)
  add_to_score :branch
  penalize_by 0.1 do
    process exp.shift # cond
    process exp.shift # body
  end
  exp.shift # pre/post
  s()
end
Also aliased as: process_until
process_yield(exp) click to toggle source
# File lib/flog.rb, line 780
def process_yield(exp)
  add_to_score :yield
  process_until_empty exp
  s()
end
report(io = $stdout) click to toggle source

Report results to io, STDOUT by default.

# File lib/flog.rb, line 455
def report(io = $stdout)
  io.puts "%8.1f: %s" % [total, "flog total"]
  io.puts "%8.1f: %s" % [average, "flog/method average"]

  return if option[:score]

  max = option[:all] ? nil : total * THRESHOLD
  if option[:group] then
    output_details_grouped io, max
  else
    output_details io, max
  end
ensure
  self.reset
end
reset() click to toggle source

Reset score data

# File lib/flog.rb, line 474
def reset
  @totals     = @total_score = nil
  @multiplier = 1.0
  @calls      = Hash.new { |h,k| h[k] = Hash.new 0 }
end
score_method(tally) click to toggle source

Compute the distance formula for a given tally

# File lib/flog.rb, line 483
def score_method(tally)
  a, b, c = 0, 0, 0
  tally.each do |cat, score|
    case cat
    when :assignment then a += score
    when :branch     then b += score
    else                  c += score
    end
  end
  Math.sqrt(a*a + b*b + c*c)
end
signature() click to toggle source
# File lib/flog.rb, line 495
def signature
  "#{klass_name}#{method_name}"
end
total() click to toggle source
# File lib/flog.rb, line 499
def total # FIX: I hate this indirectness
  totals unless @total_score # calculates total_score as well

  @total_score
end
totals() click to toggle source

Return the total score and populates @totals.

# File lib/flog.rb, line 516
def totals
  unless @totals then
    @total_score = 0
    @totals = Hash.new(0)

    calls.each do |meth, tally|
      next if option[:methods] and meth =~ %r##{@@no_method}$/
      score = score_method(tally)

      @totals[meth] = score
      @total_score += score
    end
  end

  @totals
end