Skip to content

Instantly share code, notes, and snippets.

@Hanmac
Forked from Quintus/ruby-class-mixin-graph.rb
Last active August 29, 2015 14:01
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 Hanmac/060ed42045c76081860d to your computer and use it in GitHub Desktop.
Save Hanmac/060ed42045c76081860d to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Marvin Gülker
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
require "optparse"
require "ostruct"
require "open3"
class Module
# Return only the modules that are directly included by
# this class. Modules included by modules included by
# this class are excluded.
def directly_included_modules
upper_ancestors = module_ancestors.reduce([]){|ary, mod| ary.concat(mod.module_ancestors)}
module_ancestors - upper_ancestors
end
# Return only the modules that are either directly included
# by this class or that are included by those modules. Modules
# included by superclasses are excluded.
def module_ancestors
ancestors.drop(1).take_while{|anc| !anc.kind_of?(Class)}
end
end
class Hash
def recursive_each(level = 0, &block)
each_pair do |k, v|
block.call(k, v, level)
v.recursive_each(level + 1, &block)
end
end
end
def check_start_with(klass, name_spaces)
name_spaces.any? {|n| klass.to_s.start_with?("#{n}::") or klass.directly_included_modules.any?{|k| k.to_s.start_with?("#{n}::") } }
end
options = OpenStruct.new
options.library = []
options.name_spaces = []
options.modules = true
options.module_shape = "box"
options.module_color = "blue"
options.encoding = "utf-8"
options.output = "-"
options.use = nil
options.verbose = false
opt_parser = OptionParser.new do |opts|
opts.banner = "Usage: example.rb [options]"
opts.separator ""
opts.separator "Specific options:"
# Mandatory argument.
opts.on("-r", "--require LIBRARY",
"Require the LIBRARY before executing your script") do |lib|
options.library << lib
end
opts.on("-n", "--namespace String",
"select the namespaces it should parse") do |lib|
options.name_spaces << lib
end
opts.on("-o", "--output path",
"output file where the stuff is written too (default is STDOUT)") do |o|
options.output = o
end
# Boolean switch.
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
options.verbose = v
end
opts.on("-u[=OPTIONAL]", "--use[=OPTIONAL]",
"program that is used to pipe the output too, default is dot") do |u|
options.use = u || "dot"
end
# No argument, shows at tail. This will print an options summary.
# Try it and see!
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
end
opt_parser.parse!(ARGV)
options.library.each do |l|
require l
end
tree = ObjectSpace.each_object(Class).with_object({}) do |klass, tree|
#check for namespaces
unless options.name_spaces.empty?
next unless check_start_with(klass, options.name_spaces)
end
# All the Errno:: classes make the graph extremely large,
# so just collect them under a single name.
if klass.to_s.start_with?("Errno::")
ary = ["Errno::*"]
else
ary = [klass.to_s]
end
while klass = klass.superclass
ary.unshift(klass.to_s)
end
hsh = tree
while k = ary.shift
hsh[k] ||= {}
hsh[k][ary.first] ||= {} unless ary.empty?
hsh = hsh[k]
end
end
def generate_modtree(mod, modtree = {})
modtree[mod] ||= {}
mod.directly_included_modules.each do |imod|
generate_modtree(imod, modtree[mod])
end
end
if options.modules
modtree = ObjectSpace.each_object(Class).with_object({}) do |klass, modtree|
#check for namespaces
unless options.name_spaces.empty?
next unless check_start_with(klass, options.name_spaces)
end
generate_modtree(klass, modtree)
end
end
def write_file(file, tree, modtree, options)
file.puts "digraph inheritance {"
# Draw the inheritance
tree.recursive_each do |k, v, level|
v.keys.each do |name|
file.puts("\"#{k}\" -> \"#{name}\";")
end
end
# Draw the includes
if options.modules
modtree.recursive_each do |mod, included_mods, level|
file.puts "\"#{mod.name}\" [shape=#{options.module_shape},color=#{options.module_color}];" unless mod.kind_of?(Class)
included_mods.keys.each do |included_mod|
file.puts "\"#{included_mod.name}\" -> \"#{mod.name}\" [color=#{options.module_color}];"
end
end
end
file.puts "}"
end
if options.use
# IO.popen([options.use, "-o" + options.output, "-T" + "svg"]) do |io|
# write_file(io, tree, modtree, options)
# io.close_write
# end
raise "not implmented yet"
else
if options.output == "-"
write_file(STDOUT, tree, modtree, options)
else
File.open(options.output, "w", :encoding => options.encoding ) do |file|
write_file(file, tree, modtree, options)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment