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> ".$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?>