class Debride
A static code analyzer that points out possible dead methods.
Constants
- PROJECT
- RAILS_DSL_METHODS
Rails’ macro-style methods that setup method calls to happen during a rails app’s execution.
- RAILS_MACRO_METHODS
Rails’ macro-style methods that define methods dynamically.
- RAILS_VALIDATION_METHODS
Rails’ macro-style methods that count as method calls if their options include
:if
or:unless
.
Attributes
called[RW]
A set of called method names.
known[RW]
A collection of know methods, mapping method name to implementing classes.
option[RW]
Command-line options.
Public Class Methods
file_extensions()
click to toggle source
# File lib/debride.rb, line 42 def self.file_extensions %w[rb rake jbuilder] + load_plugins end
load_plugins(proj = PROJECT)
click to toggle source
# File lib/debride.rb, line 18 def self.load_plugins proj = PROJECT unless defined? @@plugins then @@plugins = [] task_re = /#{PROJECT}_task/o plugins = Gem.find_files("#{PROJECT}_*.rb").reject { |p| p =~ task_re } plugins.each do |plugin| plugin_name = File.basename(plugin, ".rb").sub(/^#{PROJECT}_/o, "") next if @@plugins.include? plugin_name begin load plugin @@plugins << plugin_name rescue RuntimeError, LoadError => e warn "error loading #{plugin.inspect}: #{e.message}. skipping..." end end end @@plugins rescue [] end
new(options = {})
click to toggle source
Create a new Debride
instance w/ options
Calls superclass method
# File lib/debride.rb, line 207 def initialize options = {} self.option = { :whitelist => [] }.merge options self.known = Hash.new { |h,k| h[k] = Set.new } self.called = Set.new super() end
parse_options(args)
click to toggle source
Parse command line options and return a hash of parsed option values.
# File lib/debride.rb, line 111 def self.parse_options args options = { :whitelist => %i[ extended included inherited method_added method_missing prepended ], :exclude => [], :format => :text, } op = OptionParser.new do |opts| opts.banner = "debride [options] files_or_dirs" opts.version = Debride::VERSION opts.separator "" opts.separator "Specific options:" opts.separator "" opts.on("-h", "--help", "Display this help.") do puts opts exit end opts.on("-e", "--exclude FILE1,FILE2,ETC", Array, "Exclude files or directories in comma-separated list.") do |list| options[:exclude].concat list end opts.on("-w", "--whitelist FILE", String, "Whitelist these messages.") do |s| options[:whitelist] = File.read(s).split(/\n+/) rescue [] end opts.on("-f", "--focus PATH", String, "Only report against this path") do |s| unless File.exist? s then abort "ERROR: --focus path #{s} doesn't exist." end s = "#{s.chomp "/"}/*" if File.directory?(s) options[:focus] = s end opts.on("-r", "--rails", "Add some rails call conversions.") do options[:rails] = true end opts.on("-m", "--minimum N", Integer, "Don't show hits less than N locs.") do |n| options[:minimum] = n end opts.on("-v", "--verbose", "Verbose. Show progress processing files.") do options[:verbose] = true end opts.on "--json" do options[:format] = :json end opts.on "--yaml" do options[:format] = :yaml end end op.parse! args abort op.to_s if args.empty? options rescue OptionParser::InvalidOption => e warn op.to_s warn "" warn e.message exit 1 end
run(args)
click to toggle source
Top level runner for bin/debride.
# File lib/debride.rb, line 49 def self.run args opt = parse_options args debride = Debride.new opt extensions = self.file_extensions glob = "**/*.{#{extensions.join(",")}}" expander = PathExpander.new(args, glob) files = expander.process excl = debride.option[:exclude] excl.map! { |fd| File.directory?(fd) ? "#{fd}/**/*" : fd } if excl files = expander.filter_files files, StringIO.new(excl.join "\n") if excl debride.run(files) debride end
Public Instance Methods
inspect()
click to toggle source
# File lib/debride.rb, line 596 def inspect "Debride[current=%s]" % [signature] end
missing()
click to toggle source
Calculate the difference between known methods and called methods.
# File lib/debride.rb, line 469 def missing whitelist_regexps = [] option[:whitelist].each do |s| if s =~ /^\/.+?\/$/ then whitelist_regexps << Regexp.new(s[1..-2]) else called << s.to_sym end end not_called = known.keys - called.to_a whitelist_regexp = Regexp.union whitelist_regexps not_called.reject! { |s| whitelist_regexp =~ s.to_s } by_class = Hash.new { |h,k| h[k] = [] } not_called.each do |meth| known[meth].each do |klass| by_class[klass] << meth end end by_class.each do |klass, meths| by_class[klass] = meths.sort_by(&:to_s) end by_class.sort_by { |k,v| k } end
missing_locations()
click to toggle source
# File lib/debride.rb, line 501 def missing_locations focus = option[:focus] missing.map { |klass, meths| bad = meths.map { |meth| location = method_locations["#{klass}##{meth}"] || method_locations["#{klass}::#{meth}"] if focus then path = location[/(.+):\d+/, 1] next unless File.fnmatch(focus, path) end [meth, location] }.compact [klass, bad] } .to_h .reject { |k,v| v.empty? } end
name_to_string(exp)
click to toggle source
# File lib/debride.rb, line 408 def name_to_string exp case exp.sexp_type when :const, :lit, :str then exp.last.to_s when :colon2 then _, lhs, rhs = exp "#{name_to_string lhs}::#{rhs}" when :colon3 then _, rhs = exp "::#{rhs}" when :self then # wtf? "self" else raise "Not handled: #{exp.inspect}" end end
process_alias(exp)
click to toggle source
# File lib/debride.rb, line 364 def process_alias exp _, (_, lhs), (_, rhs) = exp record_method lhs, exp.file, exp.line called << rhs exp end
process_attrasgn(sexp)
click to toggle source
# File lib/debride.rb, line 222 def process_attrasgn(sexp) _, _, method_name, * = sexp method_name = method_name.last if Sexp === method_name called << method_name process_until_empty sexp sexp end
process_op_asgn2(sexp)
click to toggle source
handle &&=, ||=, etc
# File lib/debride.rb, line 231 def process_op_asgn2(sexp) _, _, method_name, * = sexp called << method_name process_until_empty sexp sexp end
process_rb(path_or_io)
click to toggle source
# File lib/debride.rb, line 88 def process_rb path_or_io warn "Processing ruby: #{path_or_io}" if option[:verbose] case path_or_io when String then path, file = path_or_io, File.binread(path_or_io) when IO, StringIO then path, file = "(io)", path_or_io.read else raise "Unhandled type: #{path_or_io.class}:#{path_or_io.inspect}" end RubyParser.new.process(file, path, option[:timeout]) rescue Racc::ParseError, RegexpError => e warn "Parse Error parsing #{path}. Skipping." warn " #{e.message}" rescue Timeout::Error warn "TIMEOUT parsing #{path}. Skipping." end
record_method(name, file, line)
click to toggle source
# File lib/debride.rb, line 238 def record_method name, file, line signature = "#{klass_name}##{name}" method_locations[signature] = "#{file}:#{line}" known[name] << klass_name end
report(io = $stdout)
click to toggle source
Print out a report of suspects.
# File lib/debride.rb, line 528 def report io = $stdout focus = option[:focus] type = option[:format] || :text send "report_#{type}", io, focus, missing_locations end
report_json(io, focus, missing)
click to toggle source
# File lib/debride.rb, line 572 def report_json io, focus, missing require "json" data = { :missing => missing } data[:focus] = focus if focus JSON.dump data, io end
report_text(io, focus, missing)
click to toggle source
# File lib/debride.rb, line 535 def report_text io, focus, missing if focus then io.puts "Focusing on #{focus}" io.puts end io.puts "These methods MIGHT not be called:" total = 0 missing.each do |klass, meths| bad = meths.map { |(meth, location)| loc = if location then l0, l1 = location.split(/:/).last.scan(/\d+/).flatten.map(&:to_i) l1 ||= l0 l1 - l0 + 1 else 1 end next if option[:minimum] && loc < option[:minimum] total += loc " %-35s %s (%d)" % [meth, location, loc] }.compact next if bad.empty? io.puts io.puts klass io.puts bad.join "\n" end io.puts io.puts "Total suspect LOC: %d" % [total] end
report_yaml(io, focus, missing)
click to toggle source
# File lib/debride.rb, line 584 def report_yaml io, focus, missing require "yaml" data = { :missing => missing } data[:focus] = focus if focus YAML.dump data, io end
run(*files)
click to toggle source
# File lib/debride.rb, line 67 def run(*files) files.flatten.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" if option[:verbose] msg = "process_rb" end begin process send(msg, file) rescue RuntimeError, SyntaxError => e warn " skipping #{file}: #{e.message}" end end end