Skip to content

Instantly share code, notes, and snippets.

@snipsnipsnip
Last active September 28, 2018 11:55
Show Gist options
  • Save snipsnipsnip/4008092 to your computer and use it in GitHub Desktop.
Save snipsnipsnip/4008092 to your computer and use it in GitHub Desktop.
dotdep.rb prints source code dependency in dot format
# frozen_string_literal: true
require 'optparse'
require 'set'
class Dep
def self.main(argv=ARGV)
dep = new
froms = []
tos = []
OptionParser.new do |o|
configure_parser o, dep, froms, tos
o.parse!(argv)
if froms.size != tos.size
warn o.help
abort "specify filter in form of: -s foo -g bar"
end
if argv.empty?
abort o.help
end
end
dep.source_code_filters = froms.zip(tos)
dep.run(argv)
end
def self.configure_parser(o, dep, froms, tos)
o.banner += " source-file.."
o.on('-i REGEXP', '--ignore', 'ignore files') do |arg|
dep.ignore_file_matcher and warn "warning: overwriting ignore expression; you should use |"
dep.ignore_file_matcher = Regexp.new(arg)
end
o.on('-s REGEXP', '--gsub-from', 'filter source code like s///g (from part)') do |arg|
froms << Regexp.new(arg)
end
o.on('-g TO', '--gsub-to', 'filter source code like s///g (to part)') do |arg|
tos << arg
end
o.on('-c', '--case-sensitive', "make module name case sensitive (default: #{dep.case_sensitive})") do |a|
dep.case_sensitive = a
end
o.on('-l', '--cluster', TrueClass, "clustering by directory structure (default: #{dep.cluster})") do |a|
dep.cluster = a
end
end
attr_accessor :ignore_file_matcher
attr_accessor :source_code_filters
attr_accessor :case_sensitive
attr_accessor :cluster
def initialize
@io = STDOUT
@ignore_file_matcher = nil
@source_code_filters = []
@case_sensitive = false
@cluster = false
end
def run(globs)
scanner = TreeScanner.new(@source_code_filters, @case_sensitive)
printer = GraphPrinter.new(@io)
sources = list(globs, @ignore_file_matcher)
graph = scanner.scan(sources)
clusters = calc_cluster(graph)
if clusters && clusters.size >= 2
printer.print_cluster(graph, clusters)
else
printer.print_flat(graph)
end
end
private
def list(globs, ignore)
globs.flat_map {|g| Dir[g].reject {|x| ignore === x } }
end
def calc_cluster(dep)
dep.group_by {|k,v| v.cluster } if @cluster
end
Node = Struct.new(:label, :files, :links, :cluster)
class TreeScanner
def initialize(source_code_filters, case_sensitive)
@source_code_filters = source_code_filters
@case_sensitive = case_sensitive
end
def scan(sources)
gsubs = @source_code_filters
case_sensitive = @case_sensitive
labels = sources.map {|x| calc_label x }
nodenames = sources.map {|x| calc_nodename x }
patterns = sources.map {|x| make_pattern x }.sort_by {|n| -n.size }
pattern = Regexp.new("(?:#{patterns.join('|')})\\b", !case_sensitive)
tree = {}
sources.zip(nodenames, labels) do |filename, nodename, label|
source = File.binread(filename)
gsubs.each {|from, to| source.gsub!(from, to) }
node = (tree[nodename] ||= make_node(label, filename))
node.files << filename
source.scan(pattern) {|s| node.links << find_nodename_from_pattern_match(nodenames, s) }
node.links.delete(nodename)
end
tree
end
private
def make_node(label, filename)
Node.new(File.basename(label), [], Set.new, calc_cluster_name(filename))
end
def calc_cluster_name(filename)
File.basename(File.dirname(filename))
end
def calc_label(filepath)
filepath
end
def make_pattern(filepath)
Regexp.escape(File.basename(filepath, '.*')).gsub(/[A-Z]/, '_\0').gsub('_', '_?')
end
def find_nodename_from_pattern_match(nodenames, matched)
candidates = nodenames.grep(/(?:\A|\/)#{Regexp.escape matched.gsub('_', '')}\./i)
case candidates.size
when 1
candidates.first
when 0
raise "unexpected: no matching node for the match '#{matched}'"
else
raise "ambiguous match #{candidates.inspect} for the match '#{matched}'"
end
end
def calc_nodename(filepath)
calc_label(filepath).downcase.gsub('_', '')
end
end
class GraphPrinter
def initialize(io)
@io = io
end
def print_cluster(graph, clusters)
links = {}
outer_links = Set.new
graph.each do |nodename, node|
node.links.each do |destname|
if clusters[node.cluster].any? {|n, _| n == destname }
(links[node.cluster] ||= Set.new) << [nodename, destname]
else
outer_links << [nodename, destname]
end
end
end
print_digraph do
indent = ' '
passive_cluster_names = clusters.keys - links.keys
passive_cluster_names.each_with_index do |cluster_name, i|
print_subgraph(i, cluster_name) do
clusters[cluster_name].each do |name, node|
print_node(graph, name, node, indent)
end
end
end
links.each_with_index do |(cluster_name, cluster_links), i|
print_subgraph(passive_cluster_names.size + i, cluster_name) do
clusters[cluster_name].each do |name, node|
print_node(graph, name, node, indent)
end
cluster_links.each {|from, to| print_link(from, to, indent) }
end
end
indent = ' '
outer_links.each {|from, to| print_link(from, to, indent) }
end
end
def print_flat(dep)
print_digraph do
@io.puts
@io.puts ' // nodes'
indent = ' '
dep.each do |node_name, node|
print_node(dep, node_name, node, indent)
end
@io.puts
@io.puts ' // links'
dep.each do |s, node|
node.links.each do |d|
print_link s, d, indent
end
end
end
end
private
def print_subgraph(number, label)
@io.puts " subgraph cluster#{number} {"
@io.puts " label = #{label.inspect};"
@io.puts %{ fontcolor = "#123456"; fontsize = 30; fontname="Arial, Helvetica";}
yield
@io.puts " }"
end
def print_link(from, to, indent=nil)
@io.puts %{#{indent}"#{from}" -> "#{to}" [title="#{from} -> #{to}"];} if from != to
end
def print_node(graph, node_name, node, indent=nil)
node.files.each {|f| @io.puts %{#{indent}/* #{f} */} }
fan_in = graph.count {|n,d| d.links.include?(node_name) }
fan_out = node.links.size
@io.puts %{#{indent}"#{node_name}" [label = "#{node.label}|{#{fan_in} in|#{fan_out} out}", shape = Mrecord];}
end
def print_digraph
@io.puts '// try tred tool of graphviz if you want simpler (transitively reduced) graph'
@io.puts 'digraph {'
@io.puts ' overlap = false;'
@io.puts ' rankdir = LR;'
@io.puts ' node [style = filled, fontcolor = "#123456", fillcolor = white, fontsize = 30, fontname="Arial, Helvetica"];'
@io.puts ' edge [color = "#661122"];'
@io.puts ' bgcolor = "transparent";'
yield
@io.puts '}'
end
end
end
Dep.main if $0 == __FILE__
@snipsnipsnip
Copy link
Author

ファイル名だけで文字の大小を無視して探索するようにしたらわりとlanguage-agnosticになった気がする
RubyとCとJavaで試したが概ね意味のあるものを生成してくれる

@snipsnipsnip
Copy link
Author

Cのヘッダとソースは混ぜこんで処理しているが、もうすこしうまい方法があるか考え中

@snipsnipsnip
Copy link
Author

フレームワーク入りのPHPのコードで試したら別のディレクトリで同ファイル名のものがあった オプションが必要そう

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment