Parent

Class/Module Index [+]

Quicksearch

Flay

Constants

VERSION

Attributes

hashes[R]
identical[RW]
mass_threshold[RW]
masses[RW]
option[R]
total[RW]

Public Class Methods

default_options() click to toggle source
# File lib/flay.rb, line 14
def self.default_options
  {
    :diff    => false,
    :mass    => 16,
    :summary => false,
    :verbose => false,
  }
end
expand_dirs_to_files(*dirs) click to toggle source
# File lib/flay.rb, line 74
def self.expand_dirs_to_files *dirs
  extensions = ['rb'] + Flay.load_plugins

  dirs.flatten.map { |p|
    if File.directory? p then
      Dir[File.join(p, '**', "*.{#{extensions.join(',')}}")]
    else
      p
    end
  }.flatten
end
load_plugins() click to toggle source
# File lib/flay.rb, line 86
def self.load_plugins
  unless defined? @@plugins then
    plugins = Gem.find_files("flay_*.rb").reject { |p| p =~ /flay_task/ }

    plugins.each do |plugin|
      begin
        load plugin
      rescue LoadError => e
        warn "error loading #{plugin.inspect}: #{e.message}. skipping..."
      end
    end

    @@plugins = plugins.map { |f| File.basename(f, '.rb').sub(/^flay_/, '') }
  end
  @@plugins
rescue
  # ignore
end
new(option = nil) click to toggle source
# File lib/flay.rb, line 108
def initialize option = nil
  @option = option || Flay.default_options
  @hashes = Hash.new { |h,k| h[k] = [] }

  self.identical      = {}
  self.masses         = {}
  self.total          = 0
  self.mass_threshold = @option[:mass]

  require 'ruby2ruby' if @option[:diff]
end
parse_options() click to toggle source
# File lib/flay.rb, line 23
def self.parse_options
  options = self.default_options

  OptionParser.new do |opts|
    opts.banner  = 'flay [options] files_or_dirs'
    opts.version = Flay::VERSION

    opts.separator ""
    opts.separator "Specific options:"
    opts.separator ""

    opts.on('-h', '--help', 'Display this help.') do
      puts opts
      exit
    end

    opts.on('-f', '--fuzzy', "DEAD: fuzzy similarities.") do
      abort "--fuzzy is no longer supported. Sorry. It sucked."
    end

    opts.on('-m', '--mass MASS', Integer, "Sets mass threshold") do |m|
      options[:mass] = m.to_i
    end

    opts.on('-v', '--verbose', "Verbose. Show progress processing files.") do
      options[:verbose] = true
    end

    opts.on('-d', '--diff', "Diff Mode. Display N-Way diff for ruby.") do
      options[:diff] = true
    end

    opts.on('-s', '--summary', "Summarize. Show flay score per file only.") do
      options[:summary] = true
    end

    extensions = ['rb'] + Flay.load_plugins

    opts.separator ""
    opts.separator "Known extensions: #{extensions.join(', ')}"

    begin
      opts.parse!
    rescue => e
      abort "#{e}\n\n#{opts}"
    end
  end

  options
end

Public Instance Methods

analyze() click to toggle source
# File lib/flay.rb, line 153
def analyze
  self.prune

  self.hashes.each do |hash,nodes|
    identical[hash] = nodes[1..-1].all? { |n| n == nodes.first }
    masses[hash] = nodes.first.mass * nodes.size
    masses[hash] *= (nodes.size) if identical[hash]
    self.total += masses[hash]
  end
end
n_way_diff(*data) click to toggle source
# File lib/flay.rb, line 195
def n_way_diff *data
  data.each_with_index do |s, i|
    c = (AA + i).chr
    s.group = c
  end

  max = data.map { |s| s.scan(/^.*/).size }.max

  data.map! { |s| # FIX: this is tarded, but I'm out of brain
    c = s.group
    s = s.scan(/^.*/)
    s.push(*([""] * (max - s.size))) # pad
    s.each do |o|
      o.group = c
    end
    s
  }

  groups = data[0].zip(*data[1..-1])
  groups.map! { |lines|
    collapsed = lines.uniq
    if collapsed.size == 1 then
      "   #{lines.first}"
    else
      # TODO: make r2r have a canonical mode (doesn't make 1-liners)
      lines.reject { |l| l.empty? }.map { |l| "#{l.group}: #{l}" }
    end
  }
  groups.flatten.join("\n")
end
process(*files) click to toggle source
# File lib/flay.rb, line 120
def process(*files) # TODO: rename from process - should act as SexpProcessor
  files.each do |file|
    warn "Processing #{file}" if option[:verbose]

    ext = File.extname(file).sub(/^\./, '')
    ext = "rb" if ext.nil? || ext.empty?
    msg = "process_#{ext}"

    unless respond_to? msg then
      warn "  Unknown file type: #{ext}, defaulting to ruby"
      msg = "process_rb"
    end

    begin
      sexp = begin
               send msg, file
             rescue => e
               warn "  #{e.message.strip}"
               warn "  skipping #{file}"
               nil
             end

      next unless sexp

      process_sexp sexp
    rescue SyntaxError => e
      warn "  skipping #{file}: #{e.message}"
    end
  end

  analyze
end
process_erb(file) click to toggle source
# File lib/flay_erb.rb, line 8
def process_erb file
  erb = File.read file

  src = ERB.new(erb).src
  RubyParser.new.process(src, file)
end
process_rb(file) click to toggle source
# File lib/flay.rb, line 164
def process_rb file
  RubyParser.new.process(File.read(file), file)
end
process_sexp(pt) click to toggle source
# File lib/flay.rb, line 168
def process_sexp pt
  pt.deep_each do |node|
    next unless node.any? { |sub| Sexp === sub }
    next if node.mass < self.mass_threshold

    self.hashes[node.structural_hash] << node
  end
end
prune() click to toggle source
# File lib/flay.rb, line 177
def prune
  # prune trees that aren't duped at all, or are too small
  self.hashes.delete_if { |_,nodes| nodes.size == 1 }

  # extract all subtree hashes from all nodes
  all_hashes = {}
  self.hashes.values.each do |nodes|
    nodes.each do |node|
      node.all_structural_subhashes.each do |h|
        all_hashes[h] = true
      end
    end
  end

  # nuke subtrees so we show the biggest matching tree possible
  self.hashes.delete_if { |h,_| all_hashes[h] }
end
report(prune = nil) click to toggle source
# File lib/flay.rb, line 240
def report prune = nil
  puts "Total score (lower is better) = #{self.total}"
  puts

  if option[:summary] then

    self.summary.sort_by { |_,v| -v }.each do |file, score|
      puts "%8.2f: %s" % [score, file]
    end

    return
  end

  count = 0
  masses.sort_by { |h,m| [-m, hashes[h].first.file] }.each do |hash, mass|
    nodes = hashes[hash]
    next unless nodes.first.first == prune if prune
    puts

    same = identical[hash]
    node = nodes.first
    n = nodes.size
    match, bonus = if same then
                     ["IDENTICAL", "*#{n}"]
                   else
                     ["Similar",   ""]
                   end

    count += 1
    puts "%d) %s code found in %p (mass%s = %d)" %
      [count, match, node.first, bonus, mass]

    nodes.each_with_index do |x, i|
      if option[:diff] then
        c = (AA + i).chr
        puts "  #{c}: #{x.file}:#{x.line}"
      else
        puts "  #{x.file}:#{x.line}"
      end
    end

    if option[:diff] then
      puts
      r2r = Ruby2Ruby.new
      puts n_way_diff(*nodes.map { |s| r2r.process(s.deep_clone) })
    end
  end
end
summary() click to toggle source
# File lib/flay.rb, line 226
def summary
  score = Hash.new 0

  masses.each do |hash, mass|
    sexps = hashes[hash]
    mass_per_file = mass.to_f / sexps.size
    sexps.each do |sexp|
      score[sexp.file] += mass_per_file
    end
  end

  score
end

[Validate]

Generated with the Darkfish Rdoc Generator 2.