1<?php
2//==============================================================================
3// Name:	JPLINTPHP.PHP
4// Description:	Simple static analysis of a PHP file.
5// Created:	01/12/03
6// Author:	johanp@aditus.nu
7// Version: 	$Id: jplintphp.php,v 1.1.1.1 2005/11/30 23:01:58 gth2 Exp $
8//
9// License:	QPL 1.0
10//
11// Copyright (C) 2001,2002 Johan Persson
12//
13// Long Description:
14// Parses a correct PHP file for classes and methods and does some rudimentary
15// checks and warns for:
16// 1) ... unused instance variables exists
17// 2) ... possible forgotten $this-> qualifier for access to instance variables
18//
19// Please note that the PHP file MUST be syntactically correct since
20// the parsing is very simple and can't cope with recovery after syntax errors.
21//==============================================================================
22
23
24//-------------------------------------------------------------------
25// Some testcode to get the ereg expressions correct. Why does this
26// always has to be a bloody pain... :-)
27//------------------------------------------------------------------------
28//$aLine = 'var $txt1 = array(), $txt2 = "" , $txt3 = "kalle" ;';
29//$pattern='/^var\s+(\$\w+)?\s*=?(array\(\)|\d+\.\d+|\d+|"\w*")?(,\s*(\$\w+)?\s*=?(array\(\)|\d+\.\d+|\d+|"\w*")?)*/';
30//$vdec = '(\$\w+)?\s*=?\s*(array\(\)|\d+\.\d+|\d+|"\w*")?';
31//$vlist = "\s*$vdec\s*,?";
32//$pattern = "/^var $vlist$vlist$vlist$vlist$vlist;/";
33//echo "pattern=$pattern<p>";
34
35/*
36if( false ) {
37    $aLine = 'function GanttVLine($aDate,$aTitle="",$aColor="black",$aWeight="_{33}*45/12",$aStyle="dashed")';
38
39    //$aLine   = "function LogAction(&\$aAA, \$aStr=array(0,0,0),\$aLineBreak=true,\$lastarg='x')";
40    //$argdef  = '\s*(\&?\$\w+)*=?(""|'."''".'|'."'.+'".'|\d+|\d+\.\d+|\w+|".+"|array\(\d*,?\d*,?\d*,?\))?\s*,?';
41    $quotchars = "[\w|�|#|$|%|&|@\[\]|+|*|\/|-|\{|\}]+";
42    $argdef  = '\s*(\&?\$\w+)*=?(""|'."''".'|'."'".$quotchars."'".'|\d+|\d+\.\d+|\w+|"'.$quotchars.'"|array\(\d*,?\d*,?\d*,?\))?\s*,?';
43
44    $pattern = '/^function\s+(\w+)\s*\(\s*'.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.'\s*\)/i';
45
46    $flg=preg_match($pattern,trim($aLine),$matches);
47    if( $flg )
48    {
49
50	$numArgs = ceil((count($matches)-2)/2);
51	$fname = $matches[1];
52	$args=array();
53	$argsval=array();
54	for($i=0; $i<$numArgs; ++$i) {
55	    $args[$i]=$matches[2+$i*2];
56	    if( isset($matches[3+$i*2]) )
57		$argsval[$i]=$matches[3+$i*2];
58	}
59
60	echo "Number of args: ".ceil((count($matches)-1)/2)."<br>";
61	for($i=0;$i<count($matches); ++$i)
62	    echo "<pre>$i:$matches[$i]</pre>";
63    }
64    else
65	echo "No match found.<br>";
66    exit();
67}
68
69*/
70
71// Base class for PHP class properties (Class, methods)
72class Prop {
73    var $iName;
74    function Prop($aName) {
75	$this->iName = $aName;
76    }
77    function GetName() {
78	return $this->iName;
79    }
80}
81
82// Stores properties for a class definition, name, methods, file etc
83class ClassProp extends Prop {
84    var $iParent;
85    var $iFileName;
86    var $iLineNbr;
87    var $iFuncs,$iFuncNbr=0;
88    var $iVars=array(),$iVarNbr=0,$iUsed=array();
89
90    function ClassProp($aParent,$aName,$aLineNbr,$aFile) {
91	$this->iName = $aName;
92	$this->iParent = $aParent;
93	$this->iLineNbr = $aLineNbr;
94	$this->iFileName = $aFile;
95	$this->iFuncs=array();
96	$this->iVars=array();
97	$this->iUsed=array();
98	$this->iFuncNbr = 0;
99    }
100
101    function AddVar($aVar,$aVal="") {
102	$this->iVars[$this->iVarNbr] = array($aVar,$aVal);
103	$this->iUsed[$this->iVarNbr] = false;
104	$this->iVarNbr++;
105    }
106
107    function IaAllVarsUsed() {
108	for($i=0; count($this->iVars); ++$i)
109	    if( !$this->iUsed[$i] )
110		return false;
111	return true;
112    }
113
114    function AddFunc($aFunc) {
115
116    // Sanity check. Make sure that a function with this name isn't
117    // alrey defined in this class
118    $found = false;
119    for($i=0; $i<$this->iFuncNbr && !$found; ++$i) {
120    	$found = ($aFunc->iName == $this->iFuncs[$i]->iName);
121    }
122    if( $found ) {
123    	echo "<br><font color=red><b>Semantic error in PHP file:</font></b> Function <b>$aFunc->iName</b> is multiple defined in class <b>$this->iName</b>. Skipping.<br>\n";
124		return;
125    }
126
127
128	$this->iFuncs[$this->iFuncNbr]=$aFunc;
129	$this->iFuncNbr++;
130    }
131
132    function GetFileName() {
133	return $this->iFileName;
134    }
135
136    function FormatVar($aVar) {
137	return "<i>".$aVar."</i>";
138    }
139
140    function FormatClass($aClass,$aParent) {
141	$res = "<hr>CLASS <b>".$aClass."</b>";
142	if( $aParent != "" )
143	    $res .= " INHERITS <b>".$aParent."</b>";
144	return $res;
145    }
146
147    // Some Java style ToString() methods
148    function ToString() {
149	$res = $this->FormatClass($this->iName,$this->iParent);
150	$res .= "(Defined in: ".$this->iFileName.":".$this->iLineNbr.")<br>" ;
151	$res .= "<br><b>VARS</b>";
152	for( $i=0; $i<count($this->iVars); ++$i) {
153	    $res .= "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;".$this->FormatVar($this->iVars[$i][0]);
154	    if($this->iVars[$i][1] != "")
155		$res .= " = ".$this->FormatVar($this->iVars[$i][1]);
156	    if( !$this->iUsed[$i] )
157		$res .= "<font color=\"red\"> ** NOT USED **</font>";
158	}
159	$res .= "<p><b>METHODS</b><br>";
160	for( $i=0; $i<count($this->iFuncs); ++$i) {
161	    $res .= $this->iFuncs[$i]->ToString();
162	}
163	return $res;
164    }
165
166}
167
168// Stores properties for a class method
169class FuncProp extends Prop {
170    var $iNumArgs;
171    var $iArgs=array(),$iArgsVal=array(), $iArgsDes=array();
172    var $iClassName;
173    var $iLineNbr;
174    var $iFileName;
175    var $iShortComment;
176
177    function FuncProp($aClassName,$aName,$aLineNbr,$aArgs,$aArgsVal,$aShortComment="",$aFileName="") {
178	$this->iName = $aName;
179	$this->iClassName = $aClassName;
180	$this->iNumArgs = count($aArgs);
181	$this->iArgs = $aArgs;
182	$this->iArgsVal = $aArgsVal;
183	$this->iLineNbr = $aLineNbr;
184	$this->iShortComment = $aShortComment;
185	$this->iFileName = $aFileName;
186    }
187
188// Some Java style ToString() methods
189    function ToString() {
190	$res = $this->iClassName."::<b>".$this->iName."</b>";
191
192	if( $this->iNumArgs > 0 ) {
193	    $res .= "(";
194	    for($i=0; $i<$this->iNumArgs; ++$i) {
195		if($i!=0) $res .= ", ";
196		$res .= "<i>".$this->iArgs[$i];
197		if( isset($this->iArgsVal[$i]) && $this->iArgsVal[$i]!="" )
198		    $res .= " = ".$this->iArgsVal[$i];
199		$res .= "</i>";
200	    }
201	    $res .= ")";
202	}
203	else
204	    $res .= "()";
205	return $res."<br>";
206    }
207}
208
209// The actual parser class. very simple. Read all the line
210// for a given file and try to figure out if there is a function or
211// class definiton on that line.
212class Parser {
213    var $iClasses=null,$iClassCnt;
214    var $iCurrClass=null;
215    var $iGlobalFuncs=null;
216    var $iBraceCnt=0;
217    var $iInComment=0,$iHyphenMarks=0,$iQuoteMarks=0,$iInString=0;
218    var $iCurrFileName;
219    var $iFp=null, $iCurrFile="";
220    var $iPrevLine,$iNextLine;
221    var $iWarnings=null;
222    var $iCommentBreak=true,$iLastComment="";
223
224    function Parser($aFile) {
225	$this->iClasses=array();
226	$this->iWarnings = array();
227	$this->iClassCnt=0;
228	$this->iGlobalFuncs = array();
229	$this->iCurrFileName=$aFile;
230	$fp = @fopen($aFile,"r");
231	if( !$fp ) {
232	    die("Parser: Can't open file $aFile");
233	}
234	$this->iFp = $fp;
235	$this->iCurrFile=$aFile;
236    }
237
238    function MapClass($aClass) {
239	echo $aClass->ToString().'<p>';
240    }
241
242	function MapGlobalFunc($aFunc) {
243		echo $aFunc->ToString().'<p>';
244	}
245
246
247    function DoMapClasses() {
248	for($i=0; $i<count($this->iClasses); ++$i) {
249	    $this->MapClass($this->iClasses[$i]);
250	}
251    }
252
253    function DoMapGlobalFuncs() {
254    	$n = count($this->iGlobalFuncs);
255    	for( $i=0; $i<$n; ++$i ) {
256      		$this->MapGlobalFunc($this->iGlobalFuncs[$i]);
257      	}
258    }
259
260	function StartIndicator($aFilename) {
261		echo "<h2>File: $aFilename </h2>\n";
262		flush();
263	}
264
265    function Start() {
266	// Read line by line to find each class and all methods
267	// defined within that class
268	$lnbr=1;
269	$this->iPrevLine = "";
270	$this->iNextLine = fgets($this->iFp,256);
271	$this->StartIndicator($this->iCurrFileName);
272	while( !feof($this->iFp) ) {
273	    $buffer = $this->iNextLine;
274	    $this->iNextLine = fgets($this->iFp,256);
275	    $this->ParseLine($buffer,$lnbr);
276	    $this->iPrevLine = $buffer;
277	    ++$lnbr;
278	}
279	$buffer = $this->iNextLine;
280	$this->iNextLine="";
281	$this->ParseLine($buffer,$lnbr);
282    }
283
284    function End() {
285	fclose($this->iFp);
286    }
287
288    function GetWarnings() {
289	$res="";
290	for($i=0; $i<count($this->iWarnings); ++$i)
291	    $res .= $this->iWarnings[$i]."<br>";
292	return $res;
293    }
294
295    function GetUnusedClassVariables() {
296	$res = "";
297	for($i=0; $i<count($this->iClasses); ++$i) {
298	    $var = "";
299	    for($j=0; $j<count($this->iClasses[$i]->iVars); ++$j) {
300		if( !$this->iClasses[$i]->iUsed[$j] ) {
301		    if( $var != "" ) $var .= ", ";
302		    $var .= "<i>".$this->iClasses[$i]->iVars[$j][0]."</i>";
303		}
304	    }
305	    if( $var != "" )
306		$res .= "<b>Warning:</b>Unused variables in Class ".$this->iClasses[$i]->GetName()." (".$var.")<br>";
307	}
308	return $res;
309    }
310
311    function CheckUsedVars($aLine,$aLineNbr) {
312	$n = count($this->iCurrClass->iVars);
313	if( $n==0 )	return;
314	$ret=false;
315	for( $i=0; $i<$n; ++$i) {
316	    $pattern = "/this->".substr($this->iCurrClass->iVars[$i][0],1)."/";
317	    $isVarUsed=preg_match($pattern,trim($aLine));
318
319	    $pattern = "/[^>\w]".trim(substr($this->iCurrClass->iVars[$i][0],1))."[^\w]/";
320	    $isVarUsedWithoutThis=preg_match($pattern,trim($aLine));
321
322	    if( $isVarUsed ) {
323		$ret=true;
324		$this->iCurrClass->iUsed[$i]=true;
325	    }
326	    elseif( $isVarUsedWithoutThis ) {
327		$this->iWarnings[] = "<b>Warning:</b> Possible use of <b>".$this->iCurrClass->iVars[$i][0]."</b> (Class ".$this->iCurrClass->GetName().") in ".
328		     $this->iCurrFileName.":".$aLineNbr." without a '\$this' qualifier.";
329	    }
330	}
331	return $ret;
332    }
333
334    function ParseClassVars($aLine) {
335	// Instance variables in $matches[$i], $i=1,2,3,...
336        $vdec = '(\$\w+)?\s*=?\s*(array\(\)|\d+\.\d+|\d+|".*"'."|'.*'".')?';
337        $vlist = "\s*$vdec\s*,?";
338        $pattern = "/^var $vlist$vlist$vlist$vlist$vlist;/";
339
340
341	$isVar=preg_match($pattern,trim($aLine),$matches);
342	if( !$isVar ) return false;
343	$n = ceil((count($matches)-1)/2);
344	for($i=0; $i<ceil((count($matches)-1)/2); ++$i) {
345	    if( !isset($matches[2+$i*2]) )
346		$matches[2+$i*2]="";
347	    if( trim($matches[1+$i*2]) == "" ) {
348		echo "****DEBUG #$i: line=$aLine<br>m1=".$matches[$i*2]."m2=".$matches[1+$i*2]."m3=".$matches[2+$i*2]."<p>";
349            }
350	    else
351		$this->iCurrClass->AddVar($matches[1+$i*2],$matches[2+$i*2]);
352	}
353	return true;
354    }
355
356    // Factory function for classes
357    function &NewClassProp($aParent,$aName,$aLineNbr,$aFileName) {
358	return new ClassProp($aParent,$aName,$aLineNbr,$aFileName);
359    }
360
361    // Factory function for methods
362    function &NewFuncProp($aClassName,$aName,$aLineNbr,$aArgs,$aArgsVal,$aShortComment) {
363	return new FuncProp($aClassName,$aName,$aLineNbr,$aArgs,$aArgsVal,$aShortComment,$this->iCurrFileName);
364    }
365
366	function LineIndicatorMinor($aLineNbr) {
367		echo "..$aLineNbr..";
368		flush();
369	}
370
371	function LineIndicatorMajor($aLineNbr) {
372		echo "<br>\n";
373	}
374
375	// Maintain brace count ignoring braces within strings and comments.
376	function BraceCount($aLine) {
377		$n = strlen($aLine);
378		$done = false;
379		$prevc='';
380		for( $i=0; $i<$n && !$done; ++$i ) {
381			$cc = substr($aLine,$i,2);
382			$c = substr($cc,0,1);
383			if( $prevc != '\\' && $c=='"' && !$this->iHyphenMarks )
384				$this->iQuoteMarks = $this->iQuoteMarks ? 0 : 1;
385
386			if( $prevc != '\\' && $c=="'" && !$this->iQuoteMarks )
387				$this->iHyphenMarks = $this->iHyphenMarks ? 0 : 1;
388
389			$this->iInString = $this->iHyphenMarks || $this->iQuoteMarks ? 1 : 0;
390
391			if( ($cc == '//' || $cc == '#') && !$this->iInString )
392				$done=true;
393			else {
394				if( $cc == '/*' && !$this->iInString ) $this->iInComment = true;
395				elseif( $cc == '*/' && !$this->iInString )  $this->iInComment = false;
396				elseif( $c == '{' && !$this->iInComment && !$this->iInString ) ++$this->iBraceCnt;
397				elseif( $c == '}' && !$this->iInComment && !$this->iInString ) --$this->iBraceCnt;
398			}
399			$prevc = $c;
400		}
401		//echo " $this->iBraceCnt ($this->iInComment, $this->iInString) : ".htmlentities($aLine)."<br>\n";
402	}
403
404    function ParseLine($aLine,$aLineNbr) {
405
406
407    if( $aLineNbr % 50 == 0 )	{
408    	$this->LineIndicatorMinor($aLineNbr);
409    	if( $aLineNbr % 500 == 0 )
410    		$this->LineIndicatorMajor($aLineNbr);
411    }
412
413
414    $aLine = trim($aLine);
415    if( $aLine=='' ) return;
416
417	$pattern = '/^\s*\/\//';
418	if( !$this->iInString && preg_match($pattern,$aLine) ) {
419	    if( $this->iCommentBreak ) {
420		$this->iLastComment = trim($aLine);
421		$this->iCommentBreak = false;
422	    }
423	    else
424		$this->iLastComment .= $aLine;
425	    return;
426	}
427	else
428	    $this->iCommentBreak = true;
429
430	if( $this->iBraceCnt < 0 )
431	    die("Syntax error in PHP file. Unmatched braces on line $aLineNbr");
432
433	if( $this->iBraceCnt > 0 ) {
434	    if( $this->ParseClassVars($aLine) ) {
435 		return;
436	    }
437	    $this->CheckUsedVars($aLine,$aLineNbr);
438	}
439
440	// Is this a class definition of the form
441	// class classname {extends parentclass} \{
442	$pattern="/^(class)\s+(\w+)\s*(extends\s+(\w+\s*))?/i";
443	//$isClass=preg_match($pattern,trim($aLine),$matches);
444	if( !$this->iInString && preg_match($pattern,$aLine,$matches) ) {
445	    $name = $matches[2];
446	    if( isset($matches[4]) )	// Inheritance?
447		$parent = $matches[4];
448	    else
449		$parent = "";
450	    $this->iClasses[$this->iClassCnt] = $this->NewClassProp($parent,$name,$aLineNbr,$this->iCurrFileName);
451	    $this->iCurrClass = &$this->iClasses[$this->iClassCnt];
452	    $this->iClassCnt++;
453	}
454	else {
455	    // Look for a function definition with arguments which may have default
456	    // values. The pattern below works for up to 10 arguments
457	    // $matches[1]=function name
458	    // $matches[2+($i)*2]=argument $i name [i=0,1,2,...]
459	    // $matches[3+($i)*2]=argument $i value
460	    // Number of arguments=ceil((count($matches)-2)/2)
461	    // Note: We must use ceil() since if the last argument has no initialization
462	    // the last two entries wont exist and we will get a floating point
463	    // number back.
464
465	    $quotchars = "[\w|�|#|$|%|&|@\[\]|+|*|\/|-|\{|\}]+";
466	    $argdef  = '\s*(\&?\$\w+)*=?(""|'."''".'|'."'".$quotchars."'".'|-?\d+|-?\d+\.\d+|\w+|"'.$quotchars.'"|array\(-?\d*,?-?\d*,?-?\d*,?\))?\s*,?';
467
468	    $pattern = '/^function\s+(\w+)\s*\(\s*'.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.'\s*\)/i';
469
470	    //$isFunction=preg_match($pattern,trim($aLine),$matches);
471	    if( !$this->iInString && preg_match($pattern,$aLine,$matches) ) {
472			$numArgs = ceil((count($matches)-2)/2);
473			$fname = $matches[1];
474			$args=array();
475			$argsval=array();
476			for($i=0; $i<$numArgs; ++$i) {
477			    $args[$i]=$matches[2+$i*2];
478		    	if( isset($matches[3+$i*2]) )
479				$argsval[$i]=$matches[3+$i*2];
480			}
481			if( isset($this->iCurrClass) && $this->iCurrClass!=null && $this->iBraceCnt==1 )
482		    	$this->iCurrClass->AddFunc($this->NewFuncProp($this->iCurrClass->GetName(),$fname,$aLineNbr,$args,$argsval,$this->iLastComment));
483			elseif( $this->iBraceCnt==0 ) {
484				// Add a global function
485				//$aClassName,$aName,$aLineNbr,$aArgs,$aArgsVal,$aShortComment,$aFileName=""
486				$this->iGlobalFuncs[] = $this->NewFuncProp('',$fname,$aLineNbr,$args,$argsval,$this->iPrevLine);
487			}
488			else
489			    die("Syntax error in PHP file. Function definition within function. (".$this->iBraceCnt.")");
490
491	        // Clear comment once we used it
492    	    $this->iLastComment = "" ;
493	    }
494	}
495	$this->BraceCount($aLine);
496    }
497}
498
499
500// Class Driver
501// Parse a file and get all the functions and classed defined in that
502// file. The methods and classes are stored in the properties
503// iClasses and iFuncs and are each instances of ClassProp and FuncProp respectively
504// To use this class just inherit the class and implement
505// your own overloaded version of PostProcessing() (currently it just prints out the
506// found methods)
507class LintDriver {
508    var $iParser,$aFileName;
509
510    function Driver($aFile) {
511	$this->iParser = $this->NewParser($aFile);
512    }
513
514    function NewParser($aFile) {
515	return new Parser($aFile);
516    }
517
518    function Run() {
519	$this->iParser->Start();
520	$this->iParser->End();
521	$this->PostProcessing();
522    }
523
524    function PostProcessing() {
525	$this->iParser->DoMapClasses();
526	$this->iParser->DoMapGlobalFuncs();
527    }
528}
529
530// EOF
531?>