00001 <?php
00023 require_once( dirname( __FILE__ ) . '/Maintenance.php' );
00024
00025 class CheckSyntax extends Maintenance {
00026
00027
00028 private $mFiles = array(), $mFailures = array(), $mWarnings = array();
00029 private $mIgnorePaths = array(), $mNoStyleCheckPaths = array();
00030
00031 public function __construct() {
00032 parent::__construct();
00033 $this->mDescription = "Check syntax for all PHP files in MediaWiki";
00034 $this->addOption( 'with-extensions', 'Also recurse the extensions folder' );
00035 $this->addOption( 'path', 'Specific path (file or directory) to check, either with absolute path or relative to the root of this MediaWiki installation',
00036 false, true);
00037 $this->addOption( 'list-file', 'Text file containing list of files or directories to check', false, true);
00038 $this->addOption( 'modified', 'Check only files that were modified (requires SVN command-line client)' );
00039 $this->addOption( 'syntax-only', 'Check for syntax validity only, skip code style warnings' );
00040 }
00041
00042 public function getDbType() {
00043 return Maintenance::DB_NONE;
00044 }
00045
00046 public function execute() {
00047 $this->buildFileList();
00048
00049
00050 $useParseKit = function_exists( 'parsekit_compile_file' ) && version_compare( PHP_VERSION, '5.3', '<' );
00051
00052 $str = 'Checking syntax (using ' . ( $useParseKit ?
00053 'parsekit)' : ' php -l, this can take a long time)' );
00054 $this->output( $str );
00055 foreach( $this->mFiles as $f ) {
00056 if( $useParseKit ) {
00057 $this->checkFileWithParsekit( $f );
00058 } else {
00059 $this->checkFileWithCli( $f );
00060 }
00061 if( !$this->hasOption( 'syntax-only' ) ) {
00062 $this->checkForMistakes( $f );
00063 }
00064 }
00065 $this->output( "\nDone! " . count( $this->mFiles ) . " files checked, " .
00066 count( $this->mFailures ) . " failures and " . count( $this->mWarnings ) .
00067 " warnings found\n" );
00068 }
00069
00073 private function buildFileList() {
00074 global $IP;
00075
00076 $this->mIgnorePaths = array(
00077
00078 "includes/NamespaceCompat.php$",
00079 "DiscussionThreading/REV",
00080 );
00081
00082 $this->mNoStyleCheckPaths = array(
00083
00084 "/activemq_stomp/",
00085 "EmailPage/phpMailer",
00086 "FCKeditor/fckeditor/",
00087 '\bphplot-',
00088 "/svggraph/",
00089 "\bjsmin.php$",
00090 "OggHandler/PEAR/",
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;
00102 } elseif ( $this->hasOption( 'list-file' ) ) {
00103 $file = $this->getOption( 'list-file' );
00104 $f = @fopen( $file, 'r' );
00105 if ( !$f ) {
00106 $this->error( "Can't open file $file\n", true );
00107 }
00108 while( $path = trim( fgets( $f ) ) ) {
00109 $this->addPath( $path );
00110 }
00111 fclose( $f );
00112 return;
00113 } elseif ( $this->hasOption( 'modified' ) ) {
00114 $this->output( "Retrieving list from Subversion... " );
00115 $parentDir = wfEscapeShellArg( dirname( __FILE__ ) . '/..' );
00116 $output = wfShellExec( "svn status --ignore-externals $parentDir", $retval );
00117 if ( $retval ) {
00118 $this->error( "Error retrieving list from Subversion!\n", true );
00119 } else {
00120 $this->output( "done\n" );
00121 }
00122
00123 preg_match_all( '/^\s*[AM].{7}(.*?)\r?$/m', $output, $matches );
00124 foreach ( $matches[1] as $file ) {
00125 if ( self::isSuitableFile( $file ) && !is_dir( $file ) ) {
00126 $this->mFiles[] = $file;
00127 }
00128 }
00129 return;
00130 }
00131
00132 $this->output( 'Building file list...', 'listfiles' );
00133
00134
00135
00136 $dirs = array(
00137 $IP . '/includes',
00138 $IP . '/config',
00139 $IP . '/languages',
00140 $IP . '/maintenance',
00141 $IP . '/skins',
00142 );
00143 if( $this->hasOption( 'with-extensions' ) ) {
00144 $dirs[] = $IP . '/extensions';
00145 }
00146
00147 foreach( $dirs as $d ) {
00148 $this->addDirectoryContent( $d );
00149 }
00150
00151
00152 if ( file_exists( "$IP/LocalSettings.php" ) ) {
00153 $this->mFiles[] = "$IP/LocalSettings.php";
00154 }
00155 if ( file_exists( "$IP/AdminSettings.php" ) ) {
00156 $this->mFiles[] = "$IP/AdminSettings.php";
00157 }
00158
00159 $this->output( 'done.', 'listfiles' );
00160 }
00161
00165 private function isSuitableFile( $file ) {
00166 $ext = pathinfo( $file, PATHINFO_EXTENSION );
00167 if ( $ext != 'php' && $ext != 'inc' && $ext != 'php5' )
00168 return false;
00169 foreach( $this->mIgnorePaths as $regex ) {
00170 $m = array();
00171 if ( preg_match( "~{$regex}~", $file, $m ) )
00172 return false;
00173 }
00174 return true;
00175 }
00176
00180 private function addPath( $path ) {
00181 global $IP;
00182 return $this->addFileOrDir( $path ) || $this->addFileOrDir( "$IP/$path" );
00183 }
00184
00188 private function addFileOrDir( $path ) {
00189 if ( is_dir( $path ) ) {
00190 $this->addDirectoryContent( $path );
00191 } elseif ( file_exists( $path ) ) {
00192 $this->mFiles[] = $path;
00193 } else {
00194 return false;
00195 }
00196 return true;
00197 }
00198
00204 private function addDirectoryContent( $dir ) {
00205 $iterator = new RecursiveIteratorIterator(
00206 new RecursiveDirectoryIterator( $dir ),
00207 RecursiveIteratorIterator::SELF_FIRST
00208 );
00209 foreach ( $iterator as $file ) {
00210 if ( $this->isSuitableFile( $file->getRealPath() ) ) {
00211 $this->mFiles[] = $file->getRealPath();
00212 }
00213 }
00214 }
00215
00222 private function checkFileWithParsekit( $file ) {
00223 static $okErrors = array(
00224 'Redefining already defined constructor',
00225 'Assigning the return value of new by reference is deprecated',
00226 );
00227 $errors = array();
00228 parsekit_compile_file( $file, $errors, PARSEKIT_SIMPLE );
00229 $ret = true;
00230 if ( $errors ) {
00231 foreach ( $errors as $error ) {
00232 foreach ( $okErrors as $okError ) {
00233 if ( substr( $error['errstr'], 0, strlen( $okError ) ) == $okError ) {
00234 continue 2;
00235 }
00236 }
00237 $ret = false;
00238 $this->output( "Error in $file line {$error['lineno']}: {$error['errstr']}\n" );
00239 $this->mFailures[$file] = $errors;
00240 }
00241 }
00242 return $ret;
00243 }
00244
00250 private function checkFileWithCli( $file ) {
00251 $res = exec( 'php -l ' . wfEscapeShellArg( $file ) );
00252 if( strpos( $res, 'No syntax errors detected' ) === false ) {
00253 $this->mFailures[$file] = $res;
00254 $this->output( $res . "\n" );
00255 return false;
00256 }
00257 return true;
00258 }
00259
00267 private function checkForMistakes( $file ) {
00268 foreach( $this->mNoStyleCheckPaths as $regex ) {
00269 $m = array();
00270 if ( preg_match( "~{$regex}~", $file, $m ) )
00271 return;
00272 }
00273
00274 $text = file_get_contents( $file );
00275
00276 $this->checkRegex( $file, $text, '/^[\s\r\n]+<\?/', 'leading whitespace' );
00277 $this->checkRegex( $file, $text, '/\?>[\s\r\n]*$/', 'trailing ?>' );
00278 $this->checkRegex( $file, $text, '/^[\xFF\xFE\xEF]/', 'byte-order mark' );
00279 }
00280
00281 private function checkRegex( $file, $text, $regex, $desc ) {
00282 if ( !preg_match( $regex, $text ) ) {
00283 return;
00284 }
00285
00286 if ( !isset( $this->mWarnings[$file] ) ) {
00287 $this->mWarnings[$file] = array();
00288 }
00289 $this->mWarnings[$file][] = $desc;
00290 $this->output( "Warning in file $file: $desc found.\n" );
00291 }
00292 }
00293
00294 $maintClass = "CheckSyntax";
00295 require_once( DO_MAINTENANCE );
00296