MediaWiki
REL1_24
|
00001 <?php 00024 require_once __DIR__ . '/Maintenance.php'; 00025 00031 class CheckSyntax extends Maintenance { 00032 00033 // List of files we're going to check 00034 private $mFiles = array(), $mFailures = array(), $mWarnings = array(); 00035 private $mIgnorePaths = array(), $mNoStyleCheckPaths = array(); 00036 00037 public function __construct() { 00038 parent::__construct(); 00039 $this->mDescription = "Check syntax for all PHP files in MediaWiki"; 00040 $this->addOption( 'with-extensions', 'Also recurse the extensions folder' ); 00041 $this->addOption( 00042 'path', 00043 'Specific path (file or directory) to check, either with absolute path or ' 00044 . 'relative to the root of this MediaWiki installation', 00045 false, 00046 true 00047 ); 00048 $this->addOption( 00049 'list-file', 00050 'Text file containing list of files or directories to check', 00051 false, 00052 true 00053 ); 00054 $this->addOption( 00055 'modified', 00056 'Check only files that were modified (requires Git command-line client)' 00057 ); 00058 $this->addOption( 'syntax-only', 'Check for syntax validity only, skip code style warnings' ); 00059 } 00060 00061 public function getDbType() { 00062 return Maintenance::DB_NONE; 00063 } 00064 00065 public function execute() { 00066 $this->buildFileList(); 00067 00068 // ParseKit is broken on PHP 5.3+, disabled until this is fixed 00069 $useParseKit = function_exists( 'parsekit_compile_file' ) 00070 && version_compare( PHP_VERSION, '5.3', '<' ); 00071 00072 $str = 'Checking syntax (using ' . ( $useParseKit ? 00073 'parsekit' : ' php -l, this can take a long time' ) . ")\n"; 00074 $this->output( $str ); 00075 foreach ( $this->mFiles as $f ) { 00076 if ( $useParseKit ) { 00077 $this->checkFileWithParsekit( $f ); 00078 } else { 00079 $this->checkFileWithCli( $f ); 00080 } 00081 if ( !$this->hasOption( 'syntax-only' ) ) { 00082 $this->checkForMistakes( $f ); 00083 } 00084 } 00085 $this->output( "\nDone! " . count( $this->mFiles ) . " files checked, " . 00086 count( $this->mFailures ) . " failures and " . count( $this->mWarnings ) . 00087 " warnings found\n" ); 00088 } 00089 00093 private function buildFileList() { 00094 global $IP; 00095 00096 $this->mIgnorePaths = array( 00097 // Compat stuff, explodes on PHP 5.3 00098 "includes/NamespaceCompat.php$", 00099 ); 00100 00101 $this->mNoStyleCheckPaths = array( 00102 // Third-party code we don't care about 00103 "/activemq_stomp/", 00104 "EmailPage/PHPMailer", 00105 "FCKeditor/fckeditor/", 00106 '\bphplot-', 00107 "/svggraph/", 00108 "\bjsmin.php$", 00109 "PEAR/File_Ogg/", 00110 "QPoll/Excel/", 00111 "/geshi/", 00112 "/smarty/", 00113 ); 00114 00115 if ( $this->hasOption( 'path' ) ) { 00116 $path = $this->getOption( 'path' ); 00117 if ( !$this->addPath( $path ) ) { 00118 $this->error( "Error: can't find file or directory $path\n", true ); 00119 } 00120 00121 return; // process only this path 00122 } elseif ( $this->hasOption( 'list-file' ) ) { 00123 $file = $this->getOption( 'list-file' ); 00124 wfSuppressWarnings(); 00125 $f = fopen( $file, 'r' ); 00126 wfRestoreWarnings(); 00127 if ( !$f ) { 00128 $this->error( "Can't open file $file\n", true ); 00129 } 00130 $path = trim( fgets( $f ) ); 00131 while ( $path ) { 00132 $this->addPath( $path ); 00133 } 00134 fclose( $f ); 00135 00136 return; 00137 } elseif ( $this->hasOption( 'modified' ) ) { 00138 $this->output( "Retrieving list from Git... " ); 00139 $files = $this->getGitModifiedFiles( $IP ); 00140 $this->output( "done\n" ); 00141 foreach ( $files as $file ) { 00142 if ( $this->isSuitableFile( $file ) && !is_dir( $file ) ) { 00143 $this->mFiles[] = $file; 00144 } 00145 } 00146 00147 return; 00148 } 00149 00150 $this->output( 'Building file list...', 'listfiles' ); 00151 00152 // Only check files in these directories. 00153 // Don't just put $IP, because the recursive dir thingie goes into all subdirs 00154 $dirs = array( 00155 $IP . '/includes', 00156 $IP . '/mw-config', 00157 $IP . '/languages', 00158 $IP . '/maintenance', 00159 $IP . '/skins', 00160 ); 00161 if ( $this->hasOption( 'with-extensions' ) ) { 00162 $dirs[] = $IP . '/extensions'; 00163 } 00164 00165 foreach ( $dirs as $d ) { 00166 $this->addDirectoryContent( $d ); 00167 } 00168 00169 // Manually add two user-editable files that are usually sources of problems 00170 if ( file_exists( "$IP/LocalSettings.php" ) ) { 00171 $this->mFiles[] = "$IP/LocalSettings.php"; 00172 } 00173 00174 $this->output( 'done.', 'listfiles' ); 00175 } 00176 00182 private function getGitModifiedFiles( $path ) { 00183 00184 global $wgMaxShellMemory; 00185 00186 if ( !is_dir( "$path/.git" ) ) { 00187 $this->error( "Error: Not a Git repository!\n", true ); 00188 } 00189 00190 // git diff eats memory. 00191 $oldMaxShellMemory = $wgMaxShellMemory; 00192 if ( $wgMaxShellMemory < 1024000 ) { 00193 $wgMaxShellMemory = 1024000; 00194 } 00195 00196 $ePath = wfEscapeShellArg( $path ); 00197 00198 // Find an ancestor in common with master (rather than just using its HEAD) 00199 // to prevent files only modified there from showing up in the list. 00200 $cmd = "cd $ePath && git merge-base master HEAD"; 00201 $retval = 0; 00202 $output = wfShellExec( $cmd, $retval ); 00203 if ( $retval !== 0 ) { 00204 $this->error( "Error retrieving base SHA1 from Git!\n", true ); 00205 } 00206 00207 // Find files in the working tree that changed since then. 00208 $eBase = wfEscapeShellArg( rtrim( $output, "\n" ) ); 00209 $cmd = "cd $ePath && git diff --name-only --diff-filter AM $eBase"; 00210 $retval = 0; 00211 $output = wfShellExec( $cmd, $retval ); 00212 if ( $retval !== 0 ) { 00213 $this->error( "Error retrieving list from Git!\n", true ); 00214 } 00215 00216 $wgMaxShellMemory = $oldMaxShellMemory; 00217 00218 $arr = array(); 00219 $filename = strtok( $output, "\n" ); 00220 while ( $filename !== false ) { 00221 if ( $filename !== '' ) { 00222 $arr[] = "$path/$filename"; 00223 } 00224 $filename = strtok( "\n" ); 00225 } 00226 00227 return $arr; 00228 } 00229 00235 private function isSuitableFile( $file ) { 00236 $file = str_replace( '\\', '/', $file ); 00237 $ext = pathinfo( $file, PATHINFO_EXTENSION ); 00238 if ( $ext != 'php' && $ext != 'inc' && $ext != 'php5' ) { 00239 return false; 00240 } 00241 foreach ( $this->mIgnorePaths as $regex ) { 00242 $m = array(); 00243 if ( preg_match( "~{$regex}~", $file, $m ) ) { 00244 return false; 00245 } 00246 } 00247 00248 return true; 00249 } 00250 00256 private function addPath( $path ) { 00257 global $IP; 00258 00259 return $this->addFileOrDir( $path ) || $this->addFileOrDir( "$IP/$path" ); 00260 } 00261 00267 private function addFileOrDir( $path ) { 00268 if ( is_dir( $path ) ) { 00269 $this->addDirectoryContent( $path ); 00270 } elseif ( file_exists( $path ) ) { 00271 $this->mFiles[] = $path; 00272 } else { 00273 return false; 00274 } 00275 00276 return true; 00277 } 00278 00284 private function addDirectoryContent( $dir ) { 00285 $iterator = new RecursiveIteratorIterator( 00286 new RecursiveDirectoryIterator( $dir ), 00287 RecursiveIteratorIterator::SELF_FIRST 00288 ); 00289 foreach ( $iterator as $file ) { 00290 if ( $this->isSuitableFile( $file->getRealPath() ) ) { 00291 $this->mFiles[] = $file->getRealPath(); 00292 } 00293 } 00294 } 00295 00302 private function checkFileWithParsekit( $file ) { 00303 static $okErrors = array( 00304 'Redefining already defined constructor', 00305 'Assigning the return value of new by reference is deprecated', 00306 ); 00307 $errors = array(); 00308 parsekit_compile_file( $file, $errors, PARSEKIT_SIMPLE ); 00309 $ret = true; 00310 if ( $errors ) { 00311 foreach ( $errors as $error ) { 00312 foreach ( $okErrors as $okError ) { 00313 if ( substr( $error['errstr'], 0, strlen( $okError ) ) == $okError ) { 00314 continue 2; 00315 } 00316 } 00317 $ret = false; 00318 $this->output( "Error in $file line {$error['lineno']}: {$error['errstr']}\n" ); 00319 $this->mFailures[$file] = $errors; 00320 } 00321 } 00322 00323 return $ret; 00324 } 00325 00331 private function checkFileWithCli( $file ) { 00332 $res = exec( 'php -l ' . wfEscapeShellArg( $file ) ); 00333 if ( strpos( $res, 'No syntax errors detected' ) === false ) { 00334 $this->mFailures[$file] = $res; 00335 $this->output( $res . "\n" ); 00336 00337 return false; 00338 } 00339 00340 return true; 00341 } 00342 00349 private function checkForMistakes( $file ) { 00350 foreach ( $this->mNoStyleCheckPaths as $regex ) { 00351 $m = array(); 00352 if ( preg_match( "~{$regex}~", $file, $m ) ) { 00353 return; 00354 } 00355 } 00356 00357 $text = file_get_contents( $file ); 00358 $tokens = token_get_all( $text ); 00359 00360 $this->checkEvilToken( $file, $tokens, '@', 'Error supression operator (@)' ); 00361 $this->checkRegex( $file, $text, '/^[\s\r\n]+<\?/', 'leading whitespace' ); 00362 $this->checkRegex( $file, $text, '/\?>[\s\r\n]*$/', 'trailing ?>' ); 00363 $this->checkRegex( $file, $text, '/^[\xFF\xFE\xEF]/', 'byte-order mark' ); 00364 } 00365 00366 private function checkRegex( $file, $text, $regex, $desc ) { 00367 if ( !preg_match( $regex, $text ) ) { 00368 return; 00369 } 00370 00371 if ( !isset( $this->mWarnings[$file] ) ) { 00372 $this->mWarnings[$file] = array(); 00373 } 00374 $this->mWarnings[$file][] = $desc; 00375 $this->output( "Warning in file $file: $desc found.\n" ); 00376 } 00377 00378 private function checkEvilToken( $file, $tokens, $evilToken, $desc ) { 00379 if ( !in_array( $evilToken, $tokens ) ) { 00380 return; 00381 } 00382 00383 if ( !isset( $this->mWarnings[$file] ) ) { 00384 $this->mWarnings[$file] = array(); 00385 } 00386 $this->mWarnings[$file][] = $desc; 00387 $this->output( "Warning in file $file: $desc found.\n" ); 00388 } 00389 } 00390 00391 $maintClass = "CheckSyntax"; 00392 require_once RUN_MAINTENANCE_IF_MAIN;