class Flay

Public Class Methods

default_options() click to toggle source

Returns the default options.

# File lib/flay.rb, line 36
def self.default_options
  {
    :diff    => false,
    :mass    => 16,
    :summary => false,
    :verbose => false,
    :number  => true,
    :timeout => 10,
    :liberal => false,
    :fuzzy   => false,
    :only    => nil,
    :filters => [],
  }
end
load_plugins() click to toggle source

Loads all flay plugins. Files must be named “flay_*.rb”.

# File lib/flay.rb, line 135
def self.load_plugins
  unless defined? @@plugins then
    @@plugins = []

    plugins = Gem.find_files("flay_*.rb").reject { |p| p =~ /flay_task/ }

    plugins.each do |plugin|
      plugin_name = File.basename(plugin, ".rb").sub(/^flay_/, "")
      next if @@plugins.include? plugin_name
      begin
        load plugin
        @@plugins << plugin_name
      rescue LoadError => e
        warn "error loading #{plugin.inspect}: #{e.message}. skipping..."
      end
    end
  end
  @@plugins
rescue => e
  warn "Error loading plugins: #{e}" if option[:verbose]
end
new(option = {}) click to toggle source

Create a new instance of Flay with +option+s.

# File lib/flay.rb, line 165
def initialize option = {}
  @option = Flay.default_options.merge option
  @hashes = Hash.new { |h,k| h[k] = [] }

  self.identical      = {}
  self.masses         = {}
  self.total          = 0
  self.mass_threshold = @option[:mass]
end
parse_options(args = ARGV) click to toggle source

Process options in args, defaulting to ARGV.

# File lib/flay.rb, line 54
def self.parse_options args = ARGV
  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 [DIFF]", Integer,
            "Detect fuzzy (copy & paste) duplication (default 1).") do |n|
      options[:fuzzy] = n || 1
    end

    opts.on("-l", "--liberal", "Use a more liberal detection method.") do
      options[:liberal] = true
    end

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

    opts.on("-#", "Don't number output (helps with diffs)") do |m|
      options[:number] = false
    end

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

    opts.on("-o", "--only NODE", String, "Only show matches on NODE type.") do |s|
      options[:only] = s.to_sym
    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

    opts.on("-t", "--timeout TIME", Integer,
            "Set the timeout. (default = #{options[:timeout]})") do |t|
      options[:timeout] = t.to_i
    end

    extensions = ["rb"] + Flay.load_plugins

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

    extensions.each do |meth|
      msg = "options_#{meth}"
      send msg, opts, options if self.respond_to?(msg)
    end

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

  options
end
run(args = ARGV) click to toggle source
# File lib/flay.rb, line 21
def self.run args = ARGV
  extensions = ["rb"] + Flay.load_plugins
  glob = "**/*.{#{extensions.join ","}}"

  expander = PathExpander.new args, glob
  files = expander.filter_files expander.process, DEFAULT_IGNORE

  flay = Flay.new Flay.parse_options args
  flay.process(*files.sort)
  flay
end

Public Instance Methods

analyze(filter = nil) click to toggle source

Prune, find identical nodes, and update masses.

# File lib/flay.rb, line 212
def analyze filter = nil
  self.prune

  self.hashes.each do |hash,nodes|
    identical[hash] = nodes[1..-1].all? { |n| n == nodes.first }
  end

  update_masses

  sorted = masses.sort_by { |h,m|
    exp = hashes[h].first
    [-m,
     exp.file,
     exp.line,
     exp.sexp_type.to_s]
  }

  sorted.map { |hash, mass|
    nodes = hashes[hash]

    next unless nodes.first.first == filter if filter

    same  = identical[hash]
    node  = nodes.first
    n     = nodes.size
    bonus = "*#{n}" if same

    locs = nodes.sort_by { |x| [x.file, x.line] }.each_with_index.map { |x, i|
      extra = :fuzzy if x.modified?
      Location[x.file, x.line, extra]
    }

    Item[hash, node.sexp_type, bonus, mass, locs]
  }.compact
end
filter_sexp(exp) click to toggle source

Before processing, filter any sexp’s that match against filters specified in option[:filters]. This changes the sexp itself.

# File lib/flay.rb, line 279
def filter_sexp exp
  exp.delete_if { |sexp|
    if Sexp === sexp then
      del = option[:filters].any? { |pattern| pattern.satisfy? sexp }
      del or (filter_sexp(sexp); false)
    end
  }
end
process(*files) click to toggle source

Process any number of files.

# File lib/flay.rb, line 178
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
end
process_erb(file) click to toggle source

Process erb and parse the result. Returns the sexp of the parsed ruby.

# File lib/flay_erb.rb, line 10
def process_erb file
  erb = File.read file

  ruby = Erubi.new(erb).src

  begin
    RubyParser.new.process(ruby, file)
  rescue => e
    warn ruby if option[:verbose]
    raise e
  end
end
process_rb(file) click to toggle source

Parse a ruby file and return the sexp.

– TODO: change the system and rename this to parse_rb.

# File lib/flay.rb, line 267
def process_rb file
  begin
    RubyParser.new.process(File.binread(file), file, option[:timeout])
  rescue Timeout::Error
    warn "TIMEOUT parsing #{file}. Skipping."
  end
end
process_sexp(pt) click to toggle source

Process a sexp pt.

# File lib/flay.rb, line 291
def process_sexp pt
  filter_sexp(pt).deep_each do |node|
    next :skip if node.none? { |sub| Sexp === sub }
    next :skip if node.mass < self.mass_threshold

    self.hashes[node.structural_hash] << node

    process_fuzzy node, option[:fuzzy] if option[:fuzzy]
  end
end
update_masses() click to toggle source

Reset total and recalculate the masses for all nodes in hashes.

# File lib/flay.rb, line 251
def update_masses
  self.total = 0
  masses.clear
  self.hashes.each do |hash, nodes|
    masses[hash] = nodes.first.mass * nodes.size
    masses[hash] *= (nodes.size) if identical[hash]
    self.total += masses[hash]
  end
end