Skip to content

Instantly share code, notes, and snippets.

@egonSchiele
Created July 7, 2012 04:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save egonSchiele/3064593 to your computer and use it in GitHub Desktop.
Save egonSchiele/3064593 to your computer and use it in GitHub Desktop.
Find undefined symbols in a ruby script
require 'rubygems'
require 'ruby2ruby'
require 'ruby_parser'
require 'set'
require 'trollop'
require 'contracts'
include Contracts
class Foo < SexpProcessor
include Contracts
@@assigns = Set.new
def initialize
super
self.auto_shift_type = true
end
Contract Sexp => Set
def self.find_symbols(_node)
to_check = [_node]
found = Set.new
to_check.each do |node|
node.each do |part|
if part.class == Sexp
to_check << part
elsif part.class == Symbol
if part == :colon2
to_check << node[1]
@@assigns.add node[2]
else
found.add part
# this is how we have to check classes
if part == :class
find_deep(node, :const).each { |x| @@assigns.add x[1] }
end
end
end
end
end
found
end
Contract Sexp, Symbol => ArrayOf[Sexp]
def self.find_deep(_node, node_name)
to_check = [_node]
nodes_found = []
to_check.each do |node|
node.each do |part|
if part.class == Sexp
to_check << part
if part.node_type == node_name
nodes_found << part
end
end
end
end
nodes_found
end
Contract Sexp => nil
def self.process_assigns(_sexp)
# adds to lasgns an array [variable name, variable's class]
sexp = _sexp.to_a
name = sexp[1]
assn = sexp[2]
real_assn = nil
return unless assn
case assn[0]
when :call
# instance of a class
if assn[2] == :new
real_assn = assn[1][1]
end
end
@@assigns.add name
end
def self.parsetree_symbols
Set.new([:lit, :arglist, :defn, :or, :str, :dstr, :if, :scope, :call, :ivar, :return, :hash, :colon2, :block, :iter, :args])
end
def self.methods
Set.new(Kernel.methods) # default methods on every object
end
Contract Sexp => { :symbols => Set, :defined => Set }
def self.process(sexp)
# function definitions
find_deep(sexp, :defn).each {|x| process_calls(x) }
klasses = sexp.find_nodes(:class)
# instance methods in classes
klasses.each do |klass|
defns = find_deep(klass, :defn)
defns.each { |x| process_calls(x) }
end
# assignments
find_deep(sexp, :lasgn).each { |x| process_assigns(x) }
# function calls
process_calls(sexp)
symbols = find_symbols(sexp) - methods - parsetree_symbols
# undefined = symbols - @@assigns
{ :symbols => symbols, :defined => @@assigns }
end
Contract Sexp => nil
def self.process_calls(sexp)
@@assigns.add sexp[1]
find_deep(sexp, :call).each do |call|
@@assigns += find_symbols(call.find_nodes(:arglist).first)
end
nil
end
end
Contract String => nil
def log x
puts x if OPTS[:verbose]
end
Contract None => Set
def erector_symbols
full_tags = [
'a', 'abbr', 'acronym', 'address', 'article', 'aside', 'audio',
'b', 'bdo', 'big', 'blockquote', 'body', 'button',
'canvas', 'caption', 'center', 'cite', 'code', 'colgroup', 'command',
'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt',
'em',
'fieldset', 'figure', 'footer', 'form', 'frameset',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'html', 'i',
'iframe', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li',
'map', 'mark', 'meter',
'nav', 'noframes', 'noscript',
'object', 'ol', 'optgroup', 'option',
'p', 'pre', 'progress',
'q', 'ruby', 'rt', 'rp', 's',
'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strike',
'strong', 'style', 'sub', 'sup',
'table', 'tbody', 'td', 'textarea', 'tfoot',
'th', 'thead', 'time', 'title', 'tr', 'tt',
'u', 'ul',
'var', 'video'
]
empty_tags = ['area', 'base', 'br', 'col', 'embed', 'frame',
'hr', 'img', 'input', 'link', 'meta', 'param']
erector = ['needs', 'text']
Set.new(full_tags.map(&:to_sym) + empty_tags.map(&:to_sym) + erector.map(&:to_sym))
end
Contract None => Set
def rails_symbols
Set.new [
:text_field_tag,
:submit_tag,
:form_tag,
:link_to_remote,
]
end
OPTS = Trollop::options do
banner <<-EOS
Find undefined symbols in a file or files.
Usage: #{$0} [files]
EOS
opt :verbose, "Print log info"
opt :skip_large_files, "Skip large files", :default => true
opt :size_limit, "Threshold for skip-large-files", :default => 2000
opt :first_only, "If true, only lists symbols that are used in the first file and are undefined.", :default => true
opt :erector, "Use if you're using this with erector...it will ignore erector tags", :default => false
opt :rails, "Use if you're using this with rails...it will ignore rails tags", :default => false
end
Trollop::die("no files specified") if ARGV.empty?
parser = RubyParser.new
first_symbols = nil
all_symbols = Set.new
defined_symbols = Set.new
ARGV.each_with_index do |file, i|
log "checking #{file}"
data = File.open(file, "r").readlines
next if data.size > OPTS[:size_limit] && OPTS[:skip_large_files]
parsed = parser.process(data.join("\n"))
results = Foo.process(parsed)
all_symbols += results[:symbols]
if i == 0
first_symbols = results[:symbols]
end
defined_symbols += results[:defined]
end
final_symbols = OPTS[:first_only] ? first_symbols : all_symbols
if OPTS[:erector]
defined_symbols += erector_symbols
end
if OPTS[:rails]
defined_symbols += rails_symbols
end
undefined_symbols = (final_symbols - defined_symbols).to_a
if undefined_symbols.empty?
puts "No undefined symbols."
else
puts "All undefined symbols:"
undefined_symbols.each do |symbol|
p symbol
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment