<?php
class dotrunner
{
var $graphVizBin;
var $options;
var $title;
var $outputFileName;
var $dotCmdOutput;
var $tmpfile;
var $cmd;
var $exclude_fns;
var $builtins;
var $callback;
function dotrunner()
{
$this->options=array(
'output'=>'vertical', /* horizontal or vertical */
'exclude_undefined'=>false,
'exclude_builtins'=>true
);
$this->graphVizBin='/usr/bin/dot';
$this->callback=false;
}
function genGraph($source,$title,$callback=false)
{
ob_start();
$this->title=$title;
$tokens=token_get_all($source);
unset($source);
$context=array(array(T_FUNCTION,'_main'));
$calls=array();
$fns=array("_main"=>1);
parse_tokens($tokens,$context,$calls,$fns);
$this->exclude_fns=array();
$this->builtins=get_defined_functions();
$this->builtins=$this->builtins['internal'];
if ($this->options['exclude_builtins']) {
// NB copy - since this is the list of things we are not showing
// which may be appended to later
$this->exclude_fns=$this->builtins;
}
$dotwriter=new dotwriter($this,$calls,$fns,$callback);
$dotwriter->write_dot_file();
$dotfile=ob_get_contents();
ob_end_clean();
$this->tmpfile=$this->write_tmp_file(
$dotfile, dirname($this->outputFileName));
$run=$this->graphVizBin . " -Tjpg -o " . $this->outputFileName . " " . $this->tmpfile;
$this->dotCmdOutput=`$run`;
$this->cmd=$run;
if ($callback) {
$run=$this->graphVizBin . " -Tcmapx -o " . $this->outputFileName . ".cmapx " . $this->tmpfile;
$this->dotCmdOutput.=`$run`;
$this->cmd .='; ' . $run;
}
return $this->outputFileName;
}
function write_tmp_file(&$dotfile, $indir)
{
$tmpfile=tempnam($indir,'calls');
if ($oh=fopen($tmpfile,'w')) {
fputs($oh, $dotfile);
fclose($oh);
return($tmpfile);
}
trigger_error("Unable to write to temporary file $tmpfile");
}
function cleanUp()
{
unlink($this->tmpfile);
}
}
class dotwriter
{
var $calls;
var $fns;
var $ctrl;
var $imapURLGen;
function dotwriter(&$ctrl,&$calls,&$fns,$callback)
{
$this->ctrl=$ctrl;
$this->calls=&$calls;
$this->fns=$fns;
$this->imapURLGen=$callback;
}
function write_readable()
{
$uncalled=$this->get_uncalled();
foreach ($uncalled as $fname=>$dummy) {
print $fname . " is not explicitly called\n";
}
foreach ($this->calls as $call) {
list($calling, $called, $cond, $loop)=$call;
print "$calling -> $called ";
if ($cond) {
print "conditionally ";
}
if ($loop) {
print "in loop ";
}
if (!isset($this->fns[$called])) {
print "external";
}
print "\n";
}
}
function write_dot_file()
{
print "digraph \"" . $this->ctrl->title . "\" {\n";
if (strtoupper(substr($this->ctrl->options['output'],0,1))=='V') {
print "rankdir=LR;\n";
}
print "fontsize=8;\n";
$uncalled=$this->get_uncalled();
print "node [ shape = polygon, sides = 4 ];\n";
$url='';
foreach ($uncalled as $fname=>$dummy) {
if ($this->imapURLGen) {
$url=',URL="' . call_user_func($this->imapURLGen,$this->ctrl->title, $fname) . '"';
}
print "\"$fname\" [color=lightblue2,style=filled" . $url . "];\n";
}
$done_already=array();
foreach ($this->calls as $call) {
list($calling, $called, $cond, $loop)=$call;
$do="$calling -> $called";
if (in_array($do,$done_already)) {
print "/* not repeating $do */\n";
continue;
}
$url='';
if ($this->imapURLGen) {
$url=',URL="' . call_user_func($this->imapURLGen,$this->ctrl->title, $called) . '"';
}
$done_already[]=$do;
// the array $exclude_fns contains a list of fns we don't
// want to see in the output.
if ($this->ctrl->options['exclude_undefined']) {
if (!isset($this->fns[$called])) {
$this->ctrl->exclude_fns[]=$called;
}
}
if (!in_array($called, $this->ctrl->exclude_fns)) {
$edge=" edge [color=black];\n";
$nodes=" \"$calling\" -> \"$called\"";
$style=array();
if ($cond) {
$style[]="style=dashed";
}
if (in_array($called, $this->ctrl->builtins)) {
$nodes="\"$called\" [color=green,style=filled" . $url . "];\n$nodes";
} elseif (!isset($this->fns[$called])) {
$nodes="\"$called\" [color=salmon2,style=filled" . $url . "];\n$nodes";
}
if (count($style)) {
$nodes.=' [' . implode(',',$style) . ']';
}
if ($loop) {
$edge=" edge [color=red];\n";
}
print $edge . $nodes . ";\n";
}
}
print "}\n";
}
function get_uncalled()
{
$uncalled=$this->fns;
foreach ($this->calls as $call) {
list($calling,$called,$cond,$loop)=$call;
unset($uncalled[$called]);
}
return $uncalled;
}
/**
* this method never called - as it calls an undefined fn
*/
function dummy()
{
no_such_function();
}
}// end of class dotwriter
function get_next_string($offset, &$src,&$context,$curr_tok)
{
// find the next t_string in array $src and return it
$found=false;
for ($x=1; ($x<20) && ($found===false); $x++) {
if (is_array($src[$offset+$x])) {
list($tok, $val)=$src[$offset+$x];
if ($tok==T_STRING) {
$found=array($val,$x);
}
}
}
if ($found===false) {
$additional=collapse_context($context);
list($line,$some_code)=get_lines(false,$offset, $src,$additional);
trigger_error("Unable to find T_STRING identifier at $line in parsed file <pre>$some_code</pre>");
}
// are we defining a function inside a class?
if ($curr_tok==T_FUNCTION) {
$last=count($context)-1;
for ($x=$last; $x>0; $x--) {
list ($ctok,$val) = $context[$x];
if ($ctok==T_CLASS) {
$found[0]='::' . $found[0];
}
}
}
return($found);
}
function end_block(&$context,$offset,&$src)
{
strip_context_to('{',$context,$offset,$src);
$copy=$context;
$last=count($context)-1;
$found=false;
for ($x=$last; ($x>0 && !$found); $x--) {
list($tok,$val)=array_pop($copy);
switch ($tok) {
case T_FUNCTION:
case T_CLASS:
case T_IF:
case T_SWITCH:
case T_FOR:
case T_FOREACH:
case T_CLASS:
case T_ELSE:
case T_ELSEIF:
case T_WHILE:
case T_DO:
$found=true;
$context=$copy;
break;
default:
break;
}
}
if (!$found) {
$additional=collapse_context($context);
list($line,$some_code)=get_lines(false,$offset, $src,$additional);
trigger_error("Unmatched '}' at $line in parsed file<pre>$some_code</pre>");
}
return($found);
}
function strip_context_to($char, &$context,$offset,&$src)
{
$copy=$context;
$last=count($context)-1;
for ($x=$last; $x>0; $x--) {
list($tok,$val)=array_pop($copy);
if ($val==$char) {
$context=$copy;
return(true);
}
}
$additional=collapse_context($context);
list($line,$some_code)=get_lines(false,$offset, $src,$additional);
trigger_error("Could not find '$char' in context stack at $line in parsed file<pre>$some_code</pre>");
return(false);
}
// $additional=collapse_context($context);
function collapse_context($stack)
{
$curr='';
foreach($stack as $ival) {
list($tok,$val)=$ival;
$curr.="$tok($val)|";
}
$curr.="\n";
return("context: $curr");
}
function find_calling_fn(&$context)
{
$last=count($context)-1;
$loop=0;
$cond=0;
for ($x=$last; $x>=0; $x--) {
list($tok,$val)=$context[$x];
if ($tok==T_FUNCTION) {
$calling=$val;
break;
}
switch ($tok) {
case T_IF:
case T_ELSE:
case T_ELSEIF:
case T_SWITCH:
$cond=1;
break;
case T_FOR:
case T_FOREACH:
case T_DO:
case T_WHILE:
$loop=1;
break;
default:
break;
}
}
return(array($calling, $cond, $loop));
}
function dump_calls($calls)
{
foreach ($calls as $call) {
list($calling, $called, $cond, $loop)=$call;
print "$calling -> $called ";
if ($cond) {
print "conditionally ";
}
if ($loop) {
print "in loop";
}
print "\n";
}
}
function followed_by($char,&$tokens,$offset)
{
$last=count($tokens)-1;
for ($x=1; $x<=$last; $x++) {
$tok=$tokens[$offset+$x];
if (is_array($tok) && ($tok[0]==T_WHITESPACE)) {
continue;
}
if ($tok=='(') {
return(true);
} else {
return(false);
}
}
}
function prefixed_by($tok_const, &$tokens, $offset)
{
for ($x=$offset-1; $x>0; $x--) {
$tok=$tokens[$x];
if (is_array($tok) && ($tok[0]==$tok_const)) {
return(true);
} elseif (is_array($tok) && ($tok[0]==T_WHITESPACE)) {
continue;
} elseif ($tok===$tok_const) {
return(true);
}
return(false);
}
}
function parse_tokens(&$tokens,&$context,&$calls,&$fns)
{
// global $tokens, $context;
$last=count($tokens);
for ($x=0; $x<$last; $x++) {
if (is_array($tokens[$x])) {
list($tok, $val)=$tokens[$x];
switch($tok) {
case T_IF:
case T_ELSE:
case T_ELSEIF:
case T_SWITCH:
$context[]=array($tok,'COND');
break;
case T_FOR:
case T_FOREACH:
case T_WHILE:
case T_DO:
$context[]=array($tok,'LOOP');
break;
case T_FUNCTION:
case T_CLASS:
list($named,$offset)=get_next_string($x, $tokens,$context,$tok);
if ($tok==T_CLASS) $named='::' . $named;
// possibly need to handle MAGIC methods here
$x+=$offset;
$context[]=array($tok,$named);
$fns[$named]=1;
break;
case T_STRING:
if (followed_by('(',$tokens,$x)) {
$called=$val;
if (prefixed_by(T_OBJECT_OPERATOR, $tokens,$x)
|| prefixed_by(T_NEW, $tokens, $x)) {
$called='::' . $called;
}
list($calling,$cond,$loop)=find_calling_fn($context);
$calls[]=array($calling, $called, $cond, $loop);
}
get_lines($val,false,$loop);
break;
case T_WHITESPACE:
case T_INLINE_HTML:
case T_COMMENT: // for PHP5 need to check T_DOC_COMMENT
get_lines($val,false,$loop);
default:
break;
}
} else {
switch($tokens[$x]) {
case '{':
case '(':
case '[':
$context[]=array(0,$tokens[$x]);
break;
case ']':
strip_context_to('[',$context,$x,$tokens);
break;
case ')':
strip_context_to('(',$context,$x,$tokens);
break;
case '}':
end_block($context,$x,$tokens);
break;
default:
break;
}
}
}
}
function get_lines($token=false,$offset=-1, &$src,$additional='')
{
static $linecount;
if (!$linecount) {
$linecount=0;
}
if ($token===false) {
$some_code=$additional . rebuild_code($offset, $src);
return(array($linecount,$some_code));
}
$count=0;
while ($token=strstr($token, "\n")) {
$token=substr($token,1);
$count++;
}
$linecount+=$count;
return(array($count,'n/a'));
}
function rebuild_code($offset, &$src)
{
$out='';
$start=$offset-5;
if ($start<0) $start=0;
$end=$offset+5;
if ($end>count($src)) $end=count($src);
for (; $start<$end; $start++) {
if (is_array($src[$start])) {
$tok=$src[$start];
$out.=$tok[1];
} else {
$out.=$src[$start];
}
}
return(htmlentities($out));
}
|