PHP Classes
elePHPant
Icontem

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: 5 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));
}