MediaWiki  REL1_19
checkSyntax.php
Go to the documentation of this file.
00001 <?php
00024 require_once( dirname( __FILE__ ) . '/Maintenance.php' );
00025 
00026 class CheckSyntax extends Maintenance {
00027 
00028         // List of files we're going to check
00029         private $mFiles = array(), $mFailures = array(), $mWarnings = array();
00030         private $mIgnorePaths = array(), $mNoStyleCheckPaths = array();
00031 
00032         public function __construct() {
00033                 parent::__construct();
00034                 $this->mDescription = "Check syntax for all PHP files in MediaWiki";
00035                 $this->addOption( 'with-extensions', 'Also recurse the extensions folder' );
00036                 $this->addOption( 'path', 'Specific path (file or directory) to check, either with absolute path or relative to the root of this MediaWiki installation',
00037                         false, true );
00038                 $this->addOption( 'list-file', 'Text file containing list of files or directories to check', false, true );
00039                 $this->addOption( 'modified', 'Check only files that were modified (requires SVN command-line client)' );
00040                 $this->addOption( 'syntax-only', 'Check for syntax validity only, skip code style warnings' );
00041         }
00042 
00043         public function getDbType() {
00044                 return Maintenance::DB_NONE;
00045         }
00046 
00047         public function execute() {
00048                 $this->buildFileList();
00049 
00050                 // ParseKit is broken on PHP 5.3+, disabled until this is fixed
00051                 $useParseKit = function_exists( 'parsekit_compile_file' ) && version_compare( PHP_VERSION, '5.3', '<' );
00052 
00053                 $str = 'Checking syntax (using ' . ( $useParseKit ?
00054                         'parsekit' : ' php -l, this can take a long time' ) . ")\n";
00055                 $this->output( $str );
00056                 foreach ( $this->mFiles as $f ) {
00057                         if ( $useParseKit ) {
00058                                 $this->checkFileWithParsekit( $f );
00059                         } else {
00060                                 $this->checkFileWithCli( $f );
00061                         }
00062                         if ( !$this->hasOption( 'syntax-only' ) ) {
00063                                 $this->checkForMistakes( $f );
00064                         }
00065                 }
00066                 $this->output( "\nDone! " . count( $this->mFiles ) . " files checked, " .
00067                         count( $this->mFailures ) . " failures and " . count( $this->mWarnings ) .
00068                         " warnings found\n" );
00069         }
00070 
00074         private function buildFileList() {
00075                 global $IP;
00076 
00077                 $this->mIgnorePaths = array(
00078                         // Compat stuff, explodes on PHP 5.3
00079                         "includes/NamespaceCompat.php$",
00080                         );
00081 
00082                 $this->mNoStyleCheckPaths = array(
00083                         // Third-party code we don't care about
00084                         "/activemq_stomp/",
00085                         "EmailPage/PHPMailer",
00086                         "FCKeditor/fckeditor/",
00087                         '\bphplot-',
00088                         "/svggraph/",
00089                         "\bjsmin.php$",
00090                         "PEAR/File_Ogg/",
00091                         "QPoll/Excel/",
00092                         "/geshi/",
00093                         "/smarty/",
00094                         );
00095 
00096                 if ( $this->hasOption( 'path' ) ) {
00097                         $path = $this->getOption( 'path' );
00098                         if ( !$this->addPath( $path ) ) {
00099                                 $this->error( "Error: can't find file or directory $path\n", true );
00100                         }
00101                         return; // process only this path
00102                 } elseif ( $this->hasOption( 'list-file' ) ) {
00103                         $file = $this->getOption( 'list-file' );
00104                         wfSuppressWarnings();
00105                         $f = fopen( $file, 'r' );
00106                         wfRestoreWarnings();
00107                         if ( !$f ) {
00108                                 $this->error( "Can't open file $file\n", true );
00109                         }
00110                         $path = trim( fgets( $f ) );
00111                         while ( $path ) {
00112                                 $this->addPath( $path );
00113                         }
00114                         fclose( $f );
00115                         return;
00116                 } elseif ( $this->hasOption( 'modified' ) ) {
00117                         $this->output( "Retrieving list from Subversion... " );
00118                         $parentDir = wfEscapeShellArg( dirname( __FILE__ ) . '/..' );
00119                         $retval = null;
00120                         $output = wfShellExec( "svn status --ignore-externals $parentDir", $retval );
00121                         if ( $retval ) {
00122                                 $this->error( "Error retrieving list from Subversion!\n", true );
00123                         } else {
00124                                 $this->output( "done\n" );
00125                         }
00126 
00127                         preg_match_all( '/^\s*[AM].{7}(.*?)\r?$/m', $output, $matches );
00128                         foreach ( $matches[1] as $file ) {
00129                                 if ( $this->isSuitableFile( $file ) && !is_dir( $file ) ) {
00130                                         $this->mFiles[] = $file;
00131                                 }
00132                         }
00133                         return;
00134                 }
00135 
00136                 $this->output( 'Building file list...', 'listfiles' );
00137 
00138                 // Only check files in these directories.
00139                 // Don't just put $IP, because the recursive dir thingie goes into all subdirs
00140                 $dirs = array(
00141                         $IP . '/includes',
00142                         $IP . '/mw-config',
00143                         $IP . '/languages',
00144                         $IP . '/maintenance',
00145                         $IP . '/skins',
00146                 );
00147                 if ( $this->hasOption( 'with-extensions' ) ) {
00148                         $dirs[] = $IP . '/extensions';
00149                 }
00150 
00151                 foreach ( $dirs as $d ) {
00152                         $this->addDirectoryContent( $d );
00153                 }
00154 
00155                 // Manually add two user-editable files that are usually sources of problems
00156                 if ( file_exists( "$IP/LocalSettings.php" ) ) {
00157                         $this->mFiles[] = "$IP/LocalSettings.php";
00158                 }
00159                 if ( file_exists( "$IP/AdminSettings.php" ) ) {
00160                         $this->mFiles[] = "$IP/AdminSettings.php";
00161                 }
00162 
00163                 $this->output( 'done.', 'listfiles' );
00164         }
00165 
00171         private function isSuitableFile( $file ) {
00172                 $file = str_replace( '\\', '/', $file );
00173                 $ext = pathinfo( $file, PATHINFO_EXTENSION );
00174                 if ( $ext != 'php' && $ext != 'inc' && $ext != 'php5' )
00175                         return false;
00176                 foreach ( $this->mIgnorePaths as $regex ) {
00177                         $m = array();
00178                         if ( preg_match( "~{$regex}~", $file, $m ) )
00179                                 return false;
00180                 }
00181                 return true;
00182         }
00183 
00189         private function addPath( $path ) {
00190                 global $IP;
00191                 return $this->addFileOrDir( $path ) || $this->addFileOrDir( "$IP/$path" );
00192         }
00193 
00199         private function addFileOrDir( $path ) {
00200                 if ( is_dir( $path ) ) {
00201                         $this->addDirectoryContent( $path );
00202                 } elseif ( file_exists( $path ) ) {
00203                         $this->mFiles[] = $path;
00204                 } else {
00205                         return false;
00206                 }
00207                 return true;
00208         }
00209 
00215         private function addDirectoryContent( $dir ) {
00216                 $iterator = new RecursiveIteratorIterator(
00217                         new RecursiveDirectoryIterator( $dir ),
00218                         RecursiveIteratorIterator::SELF_FIRST
00219                 );
00220                 foreach ( $iterator as $file ) {
00221                         if ( $this->isSuitableFile( $file->getRealPath() ) ) {
00222                                 $this->mFiles[] = $file->getRealPath();
00223                         }
00224                 }
00225         }
00226 
00233         private function checkFileWithParsekit( $file ) {
00234                 static $okErrors = array(
00235                         'Redefining already defined constructor',
00236                         'Assigning the return value of new by reference is deprecated',
00237                 );
00238                 $errors = array();
00239                 parsekit_compile_file( $file, $errors, PARSEKIT_SIMPLE );
00240                 $ret = true;
00241                 if ( $errors ) {
00242                         foreach ( $errors as $error ) {
00243                                 foreach ( $okErrors as $okError ) {
00244                                         if ( substr( $error['errstr'], 0, strlen( $okError ) ) == $okError ) {
00245                                                 continue 2;
00246                                         }
00247                                 }
00248                                 $ret = false;
00249                                 $this->output( "Error in $file line {$error['lineno']}: {$error['errstr']}\n" );
00250                                 $this->mFailures[$file] = $errors;
00251                         }
00252                 }
00253                 return $ret;
00254         }
00255 
00261         private function checkFileWithCli( $file ) {
00262                 $res = exec( 'php -l ' . wfEscapeShellArg( $file ) );
00263                 if ( strpos( $res, 'No syntax errors detected' ) === false ) {
00264                         $this->mFailures[$file] = $res;
00265                         $this->output( $res . "\n" );
00266                         return false;
00267                 }
00268                 return true;
00269         }
00270 
00278         private function checkForMistakes( $file ) {
00279                 foreach ( $this->mNoStyleCheckPaths as $regex ) {
00280                         $m = array();
00281                         if ( preg_match( "~{$regex}~", $file, $m ) )
00282                                 return;
00283                 }
00284 
00285                 $text = file_get_contents( $file );
00286                 $tokens = token_get_all( $text );
00287 
00288                 $this->checkEvilToken( $file, $tokens, '@', 'Error supression operator (@)');
00289                 $this->checkRegex( $file, $text, '/^[\s\r\n]+<\?/', 'leading whitespace' );
00290                 $this->checkRegex( $file, $text, '/\?>[\s\r\n]*$/', 'trailing ?>' );
00291                 $this->checkRegex( $file, $text, '/^[\xFF\xFE\xEF]/', 'byte-order mark' );
00292         }
00293 
00294         private function checkRegex( $file, $text, $regex, $desc ) {
00295                 if ( !preg_match( $regex, $text ) ) {
00296                         return;
00297                 }
00298 
00299                 if ( !isset( $this->mWarnings[$file] ) ) {
00300                         $this->mWarnings[$file] = array();
00301                 }
00302                 $this->mWarnings[$file][] = $desc;
00303                 $this->output( "Warning in file $file: $desc found.\n" );
00304         }
00305 
00306         private function checkEvilToken( $file, $tokens, $evilToken, $desc ) {
00307                 if ( !in_array( $evilToken, $tokens ) ) {
00308                         return;
00309                 }
00310 
00311                 if ( !isset( $this->mWarnings[$file] ) ) {
00312                         $this->mWarnings[$file] = array();
00313                 }
00314                 $this->mWarnings[$file][] = $desc;
00315                 $this->output( "Warning in file $file: $desc found.\n" );
00316         }
00317 }
00318 
00319 $maintClass = "CheckSyntax";
00320 require_once( RUN_MAINTENANCE_IF_MAIN );
00321