PHP Classes

File: dotwriter.inc.php

Recommend this page to a friend!
  Classes of Colin McKinnon   Code Graph   dotwriter.inc.php   Download  
File: dotwriter.inc.php
Role: Class source
Content type: text/plain
Description: includeable parser/generator
Class: Code Graph
Generate call graphs of PHP code with GraphViz
Author: By
Last change:
Date: 13 years ago
Size: 11,213 bytes
 

Contents

Class file image Download
<?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)); }