Jon Aquino's Mental Garden

Engineering beautiful software jon aquino labs | personal blog

Thursday, July 05, 2007

Language Independent Visualizer (LIVER)

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:

Image-0080


Java:

Image-0081


PHP:

Image-0078


ActionScript:

Image-0079


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 '}'

4 Comments:

  • Neat idea, but a bit slow to run!

    It would be awesome if you could see some kind of progress in the console window you were running it in...

    By Blogger CloCkWeRX, at 7/08/2007 1:53 AM  

  • hey clockwerx - I suppose to display progress, instead of outputting to stdout you could print progress dots to stdout and save the output directly to the file. Should be any easy modification for you to make - let me know if you need a hand.

    For my stuff it doesn't take *too* long (maybe a minute for hundreds of files). And I don't run it that often anyway.

    By Blogger Jonathan, at 7/08/2007 4:32 PM  

  • very good!, What is the license of this source?

    I'd like to port it to PHP. I think it will run faster

    By Blogger Cesar D. Rodas, at 7/11/2007 2:46 PM  

  • Hi Cesar - I'll go with a fairly liberal license: the MIT license

    By Blogger Jonathan, at 7/11/2007 3:06 PM  

Post a Comment

<< Home