How many times have you come into a software project facing a source-code tree that you are unfamiliar with, and said to yourself, "Man, I wish I could visualize this thing". But of course there isn't a visualization tool for PHP, JavaScript, ActionScript, etc.
Until now. This 100-line Ruby script will examine your source-code tree and generate a .dot file that you can visualize using any
GraphViz viewer. I like
ZGRViewer because it does anti-aliasing, although it is a little cumbersome to use. (In ZGRViewer, be sure to try the "fdp" tool - it's better than the "neato" tool because it does clustering).
I call it the Language Independent Visualizer, or LIVER for short. It works with Java, PHP, JavaScript, ActionScript, and – with a little modification – pretty much any language in which you can identify the top of a function block using a regular expression.
JavaScript:
Java:
PHP:
ActionScript:
The Ruby script is below. Simply change the $source_directory at the top, then run it:
ruby liver.rb$source_directory = 'C:\p4b\xnapps\bazel\flash-sources\slideshow\src'
$file_granularity = true # true for file-level granularity; false for function-level granularity
#
# Extracts function names from all files in $source_directory
#
def parse_function_names()
require 'find'
Find.find($source_directory) { |path|
next if File.ftype(path) != 'file'
basename = File.basename(path)
line_number = 0
open(path).each { |line|
line_number += 1
name = function_name(line, line_number, basename)
if name
put_in_set_map(name, basename + '@' + name, $name_to_functions)
put_in_set_map(basename, name, $filename_to_names)
end
}
}
end
# @todo Make a SetMap class [Jon Aquino 2007-07-06]
#
# Adds the value to the set map at the given name. A set map has as its
# values arrays with unique elements.
#
def put_in_set_map(name, value, set_map)
set_map[name] = [] if ! set_map[name]
set_map[name] << value
set_map[name].uniq!
end
#
# Returns the array of values for the given name, or an empty array if none exist
#
def get_from_set_map(name, set_map)
return set_map[name] ? set_map[name] : []
end
#
# Builds a map of function names to function names referenced
#
def parse_function_references()
require 'find'
Find.find($source_directory) { |path|
next if File.ftype(path) != 'file'
basename = File.basename(path)
current_function = nil
line_number = 0
open(path).each { |line|
line_number += 1
name = function_name(line, line_number, basename)
if name
current_function = basename + '@' + name
next
end
next if ! current_function
tokens = line.split(/[^a-z0-9_]+/i)
tokens.each { |token|
next if get_from_set_map(token, $name_to_functions).length != 1
next if token == current_function.split('@').last
put_in_set_map(current_function, get_from_set_map(token, $name_to_functions)[0], $function_to_referenced_functions)
put_in_set_map(get_from_set_map(token, $name_to_functions)[0], current_function, $referenced_function_to_functions)
}
}
}
end
#
# Returns the function name in the given line of code, or nil if
# it is not the first line of a function definition.
#
def function_name(line, line_number, basename)
if $file_granularity
return line_number == 1 ? basename.sub(/\.[a-z]+$/i, '') : nil
end
if line =~ /function\s+([a-z0-9_]+)/i # PHP/JavaScript
return $1
elsif line =~ /([a-z0-9_]+)\s*:\s*function/i # JavaScript
return $1
elsif line =~ /([a-z0-9_]+)\s*=\s*function/i # JavaScript
return $1
elsif line =~ /(public|private|protected)\s+[a-z]+\s+([a-z0-9_]+)\(/i # Java
return $2
end
return nil
end
#
# Converts the function name into a unique alphanumeric name
#
def node_name(function_name)
if ! node_name_exists(function_name)
$node_name_index += 1
$function_to_node_name[function_name] = 'Node' + $node_name_index.to_s
end
return $function_to_node_name[function_name]
end
#
# Returns whether the function has been assigned a node name
#
def node_name_exists(function_name)
return $function_to_node_name[function_name]
end
#
# Removes functions with too many or too few connections
#
def purge_functions
$filename_to_names.each { |filename, names|
names_to_purge = []
names.each { |name|
inbound_connections = get_from_set_map(filename + '@' + name, $referenced_function_to_functions).length
outbound_connections = get_from_set_map(filename + '@' + name, $function_to_referenced_functions).length
if inbound_connections + outbound_connections == 0 or inbound_connections > 10
names_to_purge << name
end
}
names_to_purge.each { |name|
names.delete(name)
get_from_set_map(filename + '@' + name, $function_to_referenced_functions).each { |referenced_function|
get_from_set_map(referenced_function, $referenced_function_to_functions).delete(filename + '@' + name)
}
get_from_set_map(filename + '@' + name, $referenced_function_to_functions).each { |function|
get_from_set_map(function, $function_to_referenced_functions).delete(filename + '@' + name)
}
$function_to_referenced_functions.delete(filename + '@' + name)
$referenced_function_to_functions.delete(filename + '@' + name)
}
}
end
# Colors from http://colorbrewer.org [Jon Aquino 2007-07-06]
colors = ['8DD3C7', 'FFFFB3', 'BEBADA', 'FB8072', '80B1D3', 'FDB462', 'B3DE69', 'FCCDE5', 'D9D9D9', 'BC80BD', 'CCEBC5', 'FFED6F', '7FC97F', 'BEAED4', 'FDC086', 'FFFF99', '386CB0', 'F0027F', 'BF5B17', '666666', 'DEEBF7', '9ECAE1', '3182BD', 'EFF3FF', 'BDD7E7', '6BAED6', '2171B5', '08519C', 'C6DBEF', '4292C6', '084594', 'F7FBFF', '08306B', 'D8B365', 'F5F5F5', '5AB4AC', 'A6611A', 'DFC27D', '80CDC1', '018571', '8C510A', 'F6E8C3', 'C7EAE5', '01665E', 'BF812D', '35978F', '543005', '003C30', 'E5F5F9', '99D8C9', '2CA25F', 'EDF8FB', 'B2E2E2', '66C2A4', '238B45', '006D2C', 'CCECE6', '41AE76', '005824', 'F7FCFD', '00441B', 'E0ECF4', '9EBCDA', '8856A7', 'B3CDE3', '8C96C6', '88419D', '810F7C', 'BFD3E6', '8C6BB1', '6E016B', '4D004B', '1B9E77', 'D95F02', '7570B3', 'E7298A', '66A61E', 'E6AB02', 'A6761D', 'E0F3DB', 'A8DDB5', '43A2CA', 'F0F9E8', 'BAE4BC', '7BCCC4', '2B8CBE', '0868AC', '4EB3D3', '08589E', 'F7FCF0', '084081', 'E5F5E0', 'A1D99B', '31A354', 'EDF8E9', 'BAE4B3', '74C476', 'C7E9C0', '41AB5D', '005A32', 'F7FCF5', 'F0F0F0', 'BDBDBD', '636363', 'F7F7F7', 'CCCCCC', '969696', '525252', '252525', '737373', 'FFFFFF', '000000', 'FEE6CE', 'FDAE6B', 'E6550D', 'FEEDDE', 'FDBE85', 'FD8D3C', 'D94701', 'A63603', 'FDD0A2', 'F16913', 'D94801', '8C2D04', 'FFF5EB', '7F2704', 'FEE8C8', 'FDBB84', 'E34A33', 'FEF0D9', 'FDCC8A', 'FC8D59', 'D7301F', 'B30000', 'FDD49E', 'EF6548', '990000', 'FFF7EC', '7F0000', 'A6CEE3', '1F78B4', 'B2DF8A', '33A02C', 'FB9A99', 'E31A1C', 'FDBF6F', 'FF7F00', 'CAB2D6', '6A3D9A', 'B15928', 'FBB4AE', 'DECBE4', 'FED9A6', 'FFFFCC', 'E5D8BD', 'FDDAEC', 'F2F2F2', 'B3E2CD', 'FDCDAC', 'CBD5E8', 'F4CAE4', 'E6F5C9', 'FFF2AE', 'F1E2CC', 'E9A3C9', 'A1D76A', 'D01C8B', 'F1B6DA', 'B8E186', '4DAC26', 'C51B7D', 'FDE0EF', 'E6F5D0', '4D9221', 'DE77AE', '7FBC41', '8E0152', '276419', 'AF8DC3', '7FBF7B', '7B3294', 'C2A5CF', 'A6DBA0', '008837', '762A83', 'E7D4E8', 'D9F0D3', '1B7837', '9970AB', '5AAE61', '40004B', 'ECE7F2', 'A6BDDB', 'F1EEF6', 'BDC9E1', '74A9CF', '0570B0', '045A8D', 'D0D1E6', '3690C0', '034E7B', 'FFF7FB', '023858', 'ECE2F0', '1C9099', 'F6EFF7', '67A9CF', '02818A', '016C59', '016450', '014636', 'F1A340', '998EC3', 'E66101', 'FDB863', 'B2ABD2', '5E3C99', 'B35806', 'FEE0B6', 'D8DAEB', '542788', 'E08214', '8073AC', '7F3B08', '2D004B', 'E7E1EF', 'C994C7', 'DD1C77', 'D7B5D8', 'DF65B0', 'CE1256', '980043', 'D4B9DA', '91003F', 'F7F4F9', '67001F', 'EFEDF5', 'BCBDDC', '756BB1', 'F2F0F7', 'CBC9E2', '9E9AC8', '6A51A3', '54278F', 'DADAEB', '807DBA', '4A1486', 'FCFBFD', '3F007D', 'EF8A62', 'CA0020', 'F4A582', '92C5DE', '0571B0', 'B2182B', 'FDDBC7', 'D1E5F0', '2166AC', 'D6604D', '4393C3', '053061', '999999', 'BABABA', '404040', 'E0E0E0', '4D4D4D', '878787', '1A1A1A', 'FDE0DD', 'FA9FB5', 'C51B8A', 'FEEBE2', 'FBB4B9', 'F768A1', 'AE017E', '7A0177', 'FCC5C0', 'DD3497', 'FFF7F3', '49006A', 'FEE0D2', 'FC9272', 'DE2D26', 'FEE5D9', 'FCAE91', 'FB6A4A', 'CB181D', 'A50F15', 'FCBBA1', 'EF3B2C', '99000D', 'FFF5F0', '67000D', 'FFFFBF', '91BFDB', 'D7191C', 'FDAE61', 'ABD9E9', '2C7BB6', 'D73027', 'FEE090', 'E0F3F8', '4575B4', 'F46D43', '74ADD1', 'A50026', '313695', '91CF60', 'A6D96A', '1A9641', 'FEE08B', 'D9EF8B', '1A9850', '66BD63', '006837', 'E41A1C', '377EB8', '4DAF4A', '984EA3', 'FFFF33', 'A65628', 'F781BF', '66C2A5', 'FC8D62', '8DA0CB', 'E78AC3', 'A6D854', 'FFD92F', 'E5C494', 'B3B3B3', '99D594', 'ABDDA4', '2B83BA', 'D53E4F', 'E6F598', '3288BD', '9E0142', '5E4FA2', 'F7FCB9', 'ADDD8E', 'C2E699', '78C679', '238443', 'D9F0A3', 'FFFFE5', '004529', 'EDF8B1', '7FCDBB', '2C7FB8', 'A1DAB4', '41B6C4', '225EA8', '253494', 'C7E9B4', '1D91C0', '0C2C84', 'FFFFD9', '081D58', 'FFF7BC', 'FEC44F', 'D95F0E', 'FFFFD4', 'FED98E', 'FE9929', 'CC4C02', '993404', 'FEE391', 'EC7014', '662506', 'FFEDA0', 'FEB24C', 'F03B20', 'FFFFB2', 'FECC5C', 'BD0026', 'FED976', 'FC4E2A', 'B10026', '800026']
$name_to_functions = {}
$filename_to_names = {}
$function_to_referenced_functions = {}
$referenced_function_to_functions = {}
$function_to_node_name = {}
$node_name_index = 0
parse_function_names()
parse_function_references()
purge_functions()
purge_functions()
purge_functions()
puts 'digraph Dependencies {'
$n = 0
$filename_to_names.each { |filename, names|
next if names.length == 0
color = colors[$n % colors.length]
if $file_granularity
puts ' ' + node_name(filename + '@' + names.first) + ' [label="' + filename + '", style=filled, color="#' + color + '"];'
else
puts ' subgraph cluster' + $n.to_s + ' {'
puts ' node [style=filled, color="#' + color + '"];'
names.each { |name|
puts ' ' + node_name(filename + '@' + name) + ' [label="' + name + '"];'
}
puts ' label="' + filename + '";'
puts ' }'
end
$n += 1
names.each { |name|
referenced_functions = get_from_set_map(filename + '@' + name, $function_to_referenced_functions)
referenced_functions.each { |referenced_function|
puts ' ' + node_name(filename + '@' + name) + ' -> ' + node_name(referenced_function)
}
}
}
puts '}'