2 //==============================================================================
4 // Description: Simple static analysis of a PHP file.
6 // Author: johanp@aditus.nu
7 // Version: $Id: jplintphp.php,v 1.14 2003/03/01 21:51:18 aditus Exp $
11 // Copyright (C) 2001,2002 Johan Persson
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
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 //==============================================================================
25 //-------------------------------------------------------------------
26 // Some testcode to get the ereg expressions correct. Why does this
27 // always has to be a bloody pain... :-)
28 //------------------------------------------------------------------------
29 //$aLine = 'var $txt1 = array(), $txt2 = "" , $txt3 = "kalle" ;';
30 //$pattern='/^var\s+(\$\w+)?\s*=?(array\(\)|\d+\.\d+|\d+|"\w*")?(,\s*(\$\w+)?\s*=?(array\(\)|\d+\.\d+|\d+|"\w*")?)*/';
31 //$vdec = '(\$\w+)?\s*=?\s*(array\(\)|\d+\.\d+|\d+|"\w*")?';
32 //$vlist = "\s*$vdec\s*,?";
33 //$pattern = "/^var $vlist$vlist$vlist$vlist$vlist;/";
34 //echo "pattern=$pattern<p>";
38 // $aLine = 'function GanttVLine($aDate,$aTitle="",$aColor="black",$aWeight="_{33}*45/12",$aStyle="dashed")';
39 $aLine = 'function SetFrameBevel($aDepth=3,$aBorder=false,$aBorderColor=\'black@0.4\',$aColor1=\'white\',$aColor2=\'darkgray\',$aFlg=true)';
41 $quotchars = "[\w|£|#|$|%|&|\.|@\[\]|+|*|\/|-|\{|\}]+";
42 $argdef = '\s*(\&?\$\w+)*=?(""|'."''".'|'."'".$quotchars."'".'|\d+|\d+\.\d+|\w+|"'.$quotchars.'"|array\(\d*,?\d*,?\d*,?\))?\s*,?';
44 $pattern = '/^function\s+(\w+)\s*\(\s*'.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.'\s*\)/i';
46 $flg=preg_match($pattern,trim($aLine),$matches);
50 $numArgs = ceil((count($matches)-2)/2);
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];
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>";
65 echo "No match found.<br>";
71 // Base class for PHP class properties (Class, methods)
74 function Prop($aName) {
75 $this->iName = $aName;
82 // Stores properties for a class definition, name, methods, file etc
83 class ClassProp extends Prop {
87 var $iFuncs,$iFuncNbr=0;
88 var $iVars=array(),$iVarNbr=0,$iUsed=array();
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();
101 function AddVar($aVar,$aVal="") {
102 $this->iVars[$this->iVarNbr] = array($aVar,$aVal);
103 $this->iUsed[$this->iVarNbr] = false;
107 function IaAllVarsUsed() {
108 for($i=0; count($this->iVars); ++$i)
109 if( !$this->iUsed[$i] )
114 function AddFunc($aFunc) {
116 // Sanity check. Make sure that a function with this name isn't
117 // alrey defined in this class
119 for($i=0; $i<$this->iFuncNbr && !$found; ++$i) {
120 $found = ($aFunc->iName == $this->iFuncs[$i]->iName);
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";
128 $this->iFuncs[$this->iFuncNbr]=$aFunc;
132 function GetFileName() {
133 return $this->iFileName;
136 function FormatVar($aVar) {
137 return "<i>".$aVar."</i>";
140 function FormatClass($aClass,$aParent) {
141 $res = "<hr>CLASS <b>".$aClass."</b>";
143 $res .= " INHERITS <b>".$aParent."</b>";
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> ".$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>";
159 $res .= "<p><b>METHODS</b><br>";
160 for( $i=0; $i<count($this->iFuncs); ++$i) {
161 $res .= $this->iFuncs[$i]->ToString();
168 // Stores properties for a class method
169 class FuncProp extends Prop {
171 var $iArgs=array(),$iArgsVal=array(), $iArgsDes=array();
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;
188 // Some Java style ToString() methods
189 function ToString() {
190 $res = $this->iClassName."::<b>".$this->iName."</b>";
192 if( $this->iNumArgs > 0 ) {
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];
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.
213 var $iClasses=null,$iClassCnt;
214 var $iCurrClass=null;
215 var $iGlobalFuncs=null;
217 var $iInComment=0,$iHyphenMarks=0,$iQuoteMarks=0,$iInString=0;
219 var $iFp=null, $iCurrFile="";
220 var $iPrevLine,$iNextLine;
222 var $iCommentBreak=true,$iLastComment="";
224 function Parser($aFile) {
225 $this->iClasses=array();
226 $this->iWarnings = array();
228 $this->iGlobalFuncs = array();
229 $this->iCurrFileName=$aFile;
230 $fp = @fopen($aFile,"r");
232 die("Parser: Can't open file $aFile");
235 $this->iCurrFile=$aFile;
238 function MapClass($aClass) {
239 echo $aClass->ToString().'<p>';
242 function MapGlobalFunc($aFunc) {
243 echo $aFunc->ToString().'<p>';
247 function DoMapClasses() {
248 for($i=0; $i<count($this->iClasses); ++$i) {
249 $this->MapClass($this->iClasses[$i]);
253 function DoMapGlobalFuncs() {
254 $n = count($this->iGlobalFuncs);
255 for( $i=0; $i<$n; ++$i ) {
256 $this->MapGlobalFunc($this->iGlobalFuncs[$i]);
260 function StartIndicator($aFilename) {
261 echo "<h2>File: $aFilename </h2>\n";
266 // Read line by line to find each class and all methods
267 // defined within that class
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;
279 $buffer = $this->iNextLine;
281 $this->ParseLine($buffer,$lnbr);
288 function GetWarnings() {
290 for($i=0; $i<count($this->iWarnings); ++$i)
291 $res .= $this->iWarnings[$i]."<br>";
295 function GetUnusedClassVariables() {
297 for($i=0; $i<count($this->iClasses); ++$i) {
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>";
306 $res .= "<b>Warning:</b>Unused variables in Class ".$this->iClasses[$i]->GetName()." (".$var.")<br>";
311 function CheckUsedVars($aLine,$aLineNbr) {
312 $n = count($this->iCurrClass->iVars);
315 for( $i=0; $i<$n; ++$i) {
316 $pattern = "/this->".substr($this->iCurrClass->iVars[$i][0],1)."/";
317 $isVarUsed=preg_match($pattern,trim($aLine));
319 $pattern = "/[^>\w]".trim(substr($this->iCurrClass->iVars[$i][0],1))."[^\w]/";
320 $isVarUsedWithoutThis=preg_match($pattern,trim($aLine));
324 $this->iCurrClass->iUsed[$i]=true;
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.";
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;/";
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]) )
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>";
351 $this->iCurrClass->AddVar($matches[1+$i*2],$matches[2+$i*2]);
356 // Factory function for classes
357 function &NewClassProp($aParent,$aName,$aLineNbr,$aFileName) {
358 return new ClassProp($aParent,$aName,$aLineNbr,$aFileName);
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);
366 function LineIndicatorMinor($aLineNbr) {
367 echo "..$aLineNbr..";
371 function LineIndicatorMajor($aLineNbr) {
375 // Maintain brace count ignoring braces within strings and comments.
376 function BraceCount($aLine) {
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;
386 if( $prevc != '\\' && $c=="'" && !$this->iQuoteMarks )
387 $this->iHyphenMarks = $this->iHyphenMarks ? 0 : 1;
389 $this->iInString = $this->iHyphenMarks || $this->iQuoteMarks ? 1 : 0;
391 if( ($cc == '//' || $cc == '#') && !$this->iInString )
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;
401 //echo " $this->iBraceCnt ($this->iInComment, $this->iInString) : ".htmlentities($aLine)."<br>\n";
404 function ParseLine($aLine,$aLineNbr) {
407 if( $aLineNbr % 50 == 0 ) {
408 $this->LineIndicatorMinor($aLineNbr);
409 if( $aLineNbr % 500 == 0 )
410 $this->LineIndicatorMajor($aLineNbr);
414 $aLine = trim($aLine);
415 if( $aLine=='' ) return;
417 $pattern = '/^\s*\/\//';
418 if( !$this->iInString && preg_match($pattern,$aLine) ) {
419 if( $this->iCommentBreak ) {
420 $this->iLastComment = trim($aLine);
421 $this->iCommentBreak = false;
424 $this->iLastComment .= $aLine;
428 $this->iCommentBreak = true;
430 if( $this->iBraceCnt < 0 )
431 die("Syntax error in PHP file. Unmatched braces on line $aLineNbr");
433 if( $this->iBraceCnt > 0 ) {
434 if( $this->ParseClassVars($aLine) ) {
437 $this->CheckUsedVars($aLine,$aLineNbr);
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) ) {
446 if( isset($matches[4]) ) // Inheritance?
447 $parent = $matches[4];
450 $this->iClasses[$this->iClassCnt] = $this->NewClassProp($parent,$name,$aLineNbr,$this->iCurrFileName);
451 $this->iCurrClass = &$this->iClasses[$this->iClassCnt];
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
465 $quotchars = "[\w|£|#|$|%|&|\.|@\[\]|+|*|\/|-|\{|\}]+";
466 $argdef = '\s*(\&?\$\w+)*=?(""|'."''".'|'."'".$quotchars."'".'|-?\d+|-?\d+\.\d+|\w+|"'.$quotchars.'"|array\(-?\d*,?-?\d*,?-?\d*,?\))?\s*,?';
468 $pattern = '/^function\s+(\w+)\s*\(\s*'.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.$argdef.'\s*\)/i';
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];
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];
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);
489 die("Syntax error in PHP file. Function definition within function. (".$this->iBraceCnt.")");
491 // Clear comment once we used it
492 $this->iLastComment = "" ;
495 $this->BraceCount($aLine);
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
508 var $iParser,$aFileName;
510 function Driver($aFile) {
511 $this->iParser = $this->NewParser($aFile);
514 function NewParser($aFile) {
515 return new Parser($aFile);
519 $this->iParser->Start();
520 $this->iParser->End();
521 $this->PostProcessing();
524 function PostProcessing() {
525 $this->iParser->DoMapClasses();
526 $this->iParser->DoMapGlobalFuncs();