PHP Classes

File: parse_gen.php

Recommend this page to a friend!
  Classes of Colin McKinnon   Code Graph   parse_gen.php   Download  
File: parse_gen.php
Role: Example script
Content type: text/plain
Description: source code and front end
Class: Code Graph
Generate call graphs of PHP code with GraphViz
Author: By
Last change:
Date: 13 years ago
Size: 14,793 bytes
 

Contents

Class file image Download
<?php // dot -o parse.jpg -Tjpg parsed.dot /** * parse_gen.php * * code to create a node graph of a PHP source file * C. McKinnon 23rd June 2006 * */ /** * init */ error_reporting(E_NONE); set_time_limit(600); $options=array( 'output'=>'vertical', /* horizontal or vertical */ 'exclude_undefined'=>false, 'exclude_builtins'=>true ); class dotwriter { var $calls; var $fns; function dotwriter(&$calls,&$fns) { $this->calls=&$calls; $this->fns=$fns; } 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() { global $src_file,$options,$exclude_fns,$builtins; print "digraph \"" . basename($src_file) . "\" {\n"; if (strtoupper(substr($options['output'],0,1))=='V') { print "rankdir=LR;\n"; } print "fontsize=8;\n"; $uncalled=$this->get_uncalled(); print "node [ shape = polygon, sides = 4 ];\n"; foreach ($uncalled as $fname=>$dummy) { print "\"$fname\" [color=lightblue2,style=filled];\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; } $done_already[]=$do; // the array $exclude_fns contains a list of fns we don't // want to see in the output. if ($options['exclude_undefined']) { if (!isset($this->fns[$called])) { $exclude_fns[]=$called; } } if (!in_array($called, $exclude_fns)) { $edge=" edge [color=black];\n"; $nodes=" \"$calling\" -> \"$called\""; $style=array(); if ($cond) { $style[]="style=dashed"; } if (in_array($called, $builtins)) { $nodes="\"$called\" [color=green,style=filled];\n$nodes"; } elseif (!isset($this->fns[$called])) { $nodes="\"$called\" [color=salmon2,style=filled];\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(); } } 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)); } function check_params() { global $src_file,$options; if ($_REQUEST['src_file'] && $_REQUEST['opt_output'] && $_REQUEST['opt_undef'] && $_REQUEST['opt_builtin']) { if (file_exists($_REQUEST['src_file'])) { if (strtoupper(substr($_REQUEST['opt_output'],0,1))=='V') { $options['output']='Vertical'; } else { $options['output']='Horizontal'; } if ($_REQUEST['opt_undef']=='YES') { $options['exclude_undefined']=true; } else { $options['exclude_undefined']=false; } if ($_REQUEST['opt_builtin']=='YES') { $options['exclude_builtins']=true; } else { $options['exclude_builtins']=false; } return(true); } else { trigger_error("File not found"); } } return(false); } function write_tmp_file(&$dotfile) { $tmpfile=tempnam('/tmp','calls'); if ($oh=fopen($tmpfile,'w')) { fputs($oh, $dotfile); fclose($oh); return($tmpfile); } } function get_dest_file($src_file) { $src_file=serialize($_GET); // different graphs for difft params $dest="graphs/" . md5($src_file); return($dest); } function clean_cache() { // garbage collectio %%% needs to be safer $base=dirname(__FILE__); if (is_dir($base . '/graphs')) { $cmd="find " . $base . '/graohs/' . " -name \*.jpg -mtime +30 -exec rm -f {} \;"; $chk=`echo "$cmd" | at now`; } } function not_cached($src_file) { clean_cache(); $destfile=get_dest_file($src_file) . '.jpg'; $cachetime=(integer)@filemtime($destfile); $codetime=(integer)@filemtime($src_file); return($cachetime<$codetime); } function do_page($tmpfile,$cmd, $output) { print "<body>\n"; do_form(); do_result($tmpfile,$cmd, $output); print "</body>"; } function do_form() { print "<form method='GET'>\n"; print "file to parse: <input type='text' name='src_file' value='" . $_REQUEST['src_file'] . "' size='80'><br />\n"; print "output vertical or horizontal? <select name='opt_output'> <option selected>Vertical</option> <option>Horizontal</option> </select><br />\n"; print "exclude undefined functions? <select name='opt_undef'> <option selected>YES</option> <option>NO</option></select><br />\n"; print "exclude builtin functions? <select name='opt_builtin'> <option selected>YES</option> <option>NO</option></select><br />\n"; print "<input type='submit' name='submit' value='submit'></form>\n"; } function do_result($tmpfile,$cmd,$output) { global $src_file,$options,$exclude_fns,$builtins; if ($_REQUEST['src_file']) { show_errors(); $destfile=get_dest_file($_REQUEST['src_file']) . '.jpg'; if (file_exists($destfile)) { print "<h3>$src_file</h3>\n"; list($lines_processed)=get_lines(false,false,$lines_processed); if ($lines_processed) { print "parsed $lines_processed lines from input file<br />\n"; } print "<br /><img src='$destfile' alt='callgraph'>\n"; } else { print "expected output in $destfile<br />\n"; print "failed to generate for some reason dot file is shown below"; print "<hr><pre>" . file_get_contents($tmpfile) . "</pre>\n"; } print "<hr><h3>debug</h3>run=$cmd<br />output=<pre>$output</pre>\n"; print "options=<pre>" . var_export($options,true) . "</pre>\n"; print "params=<pre>" . var_export($_GET,true) . "</pre>\n"; // print "exclude_fns=<pre>" . var_export($exclude_fns, true) . "</pre>\n"; // print "builtins=<pre>" . var_export($builtins, true) . "</pre>\n"; unlink($tmpfile); } else { print "<h3>Key</h3><br /><img src='key.jpg' alt='key'>\n"; } } function show_errors() { global $process_errors; $colors=array( E_ERROR => "FF1010", // red E_WARNING => "FF3333", // yellow E_PARSE => "CC00FF", // unlikely E_NOTICE => "00FFFF", E_CORE_ERROR => "CC00FF", E_CORE_WARNING => "FF0000", E_COMPILE_ERROR => "FF0000", E_COMPILE_WARNING => "FF9933", E_USER_ERROR => "FF6666", E_USER_WARNING => "FFFF33", E_USER_NOTICE => "CCCCFF", E_STRICT => "FF99FF" ); if (count($process_errors)) { print "<h3>Errors occurred during processing</h3>\n"; print "<table bgcolor='#000000'>"; foreach($process_errors as $item) { list($errno, $msg)=$item; $bg=$colors[$errno]; if (!$bg) $bg='FFFFFF'; print "<tr><td bgcolor='#$bg'>$errno</td><td bgcolor='#$bg'>$msg</td></tr>\n"; } print "</table>\n"; } } function error_logging($errno, $errmsg, $filename, $line, $vars) { global $process_errors; $errortype = array ( E_ERROR => "Error", E_WARNING => "Warning", E_PARSE => "Parsing Error", E_NOTICE => "Notice", E_CORE_ERROR => "Core Error", E_CORE_WARNING => "Core Warning", E_COMPILE_ERROR => "Compile Error", E_COMPILE_WARNING => "Compile Warning", E_USER_ERROR => "User Error", E_USER_WARNING => "User Warning", E_USER_NOTICE => "User Notice", E_STRICT => "Runtime Notice" ); $process_errors[]=array($errno, $errortype[$errno] . " $errmsg at $line in $filename"); } /** * main * * ----------------------------------------------------------- */ if (check_params()) { $process_errors=array(); if (not_cached($src_file)) { set_error_handler(error_logging); ob_start(); $source=file_get_contents($src_file); $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); $exclude_fns=array(); $builtins=get_defined_functions(); $builtins=$builtins['internal']; if ($options['exclude_builtins']) { // NB copy - since this is the list of things we are not showing // which may be appended to later $exclude_fns=$builtins; } $dotwriter=new dotwriter($calls,$fns); $dotwriter->write_dot_file(); $dotfile=ob_get_contents(); ob_end_clean(); $dest=get_dest_file($src_file) . '.jpg'; $tmpfile=write_tmp_file($dotfile); $run="/usr/bin/dot -Tjpg -o $dest $tmpfile"; $output=`$run`; restore_error_handler(); } } do_page($tmpfile,$run,$output); ?>