2009-08-07 04:19:09 +00:00
< ? php
/**
* Check syntax of all PHP files in MediaWiki
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 2 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License along
* with this program ; if not , write to the Free Software Foundation , Inc . ,
* 51 Franklin Street , Fifth Floor , Boston , MA 02110 - 1301 , USA .
* http :// www . gnu . org / copyleft / gpl . html
*
2010-09-05 13:15:48 +00:00
* @ file
2009-08-07 04:19:09 +00:00
* @ ingroup Maintenance
*/
2010-12-04 03:20:14 +00:00
2012-08-27 19:03:15 +00:00
require_once ( __DIR__ . '/Maintenance.php' );
2009-08-07 04:19:09 +00:00
2012-06-16 20:35:13 +00:00
/**
* Maintenance script to check syntax of all PHP files in MediaWiki .
*
* @ ingroup Maintenance
*/
2010-02-24 17:21:48 +00:00
class CheckSyntax extends Maintenance {
2009-08-07 04:19:09 +00:00
// List of files we're going to check
2009-11-17 17:26:19 +00:00
private $mFiles = array (), $mFailures = array (), $mWarnings = array ();
2009-12-05 15:03:20 +00:00
private $mIgnorePaths = array (), $mNoStyleCheckPaths = array ();
2009-08-07 04:19:09 +00:00
public function __construct () {
parent :: __construct ();
$this -> mDescription = " Check syntax for all PHP files in MediaWiki " ;
$this -> addOption ( 'with-extensions' , 'Also recurse the extensions folder' );
2009-11-17 18:57:35 +00:00
$this -> addOption ( 'path' , 'Specific path (file or directory) to check, either with absolute path or relative to the root of this MediaWiki installation' ,
2010-05-22 16:50:39 +00:00
false , true );
$this -> addOption ( 'list-file' , 'Text file containing list of files or directories to check' , false , true );
2012-04-06 04:13:46 +00:00
$this -> addOption ( 'modified' , 'Check only files that were modified (requires Git command-line client)' );
2009-12-04 15:49:06 +00:00
$this -> addOption ( 'syntax-only' , 'Check for syntax validity only, skip code style warnings' );
2009-08-07 04:19:09 +00:00
}
2010-03-10 12:59:44 +00:00
public function getDbType () {
2009-08-09 13:31:15 +00:00
return Maintenance :: DB_NONE ;
}
2009-08-07 04:19:09 +00:00
public function execute () {
$this -> buildFileList ();
2009-08-24 15:08:58 +00:00
// ParseKit is broken on PHP 5.3+, disabled until this is fixed
$useParseKit = function_exists ( 'parsekit_compile_file' ) && version_compare ( PHP_VERSION , '5.3' , '<' );
2010-05-22 16:50:39 +00:00
$str = 'Checking syntax (using ' . ( $useParseKit ?
2010-06-06 13:31:44 +00:00
'parsekit' : ' php -l, this can take a long time' ) . " ) \n " ;
2010-01-26 13:40:00 +00:00
$this -> output ( $str );
2010-05-22 16:50:39 +00:00
foreach ( $this -> mFiles as $f ) {
if ( $useParseKit ) {
2009-08-19 00:46:18 +00:00
$this -> checkFileWithParsekit ( $f );
} else {
$this -> checkFileWithCli ( $f );
}
2010-05-22 16:50:39 +00:00
if ( ! $this -> hasOption ( 'syntax-only' ) ) {
2009-12-04 14:38:43 +00:00
$this -> checkForMistakes ( $f );
}
2009-08-19 00:46:18 +00:00
}
$this -> output ( " \n Done! " . count ( $this -> mFiles ) . " files checked, " .
2009-11-13 22:18:32 +00:00
count ( $this -> mFailures ) . " failures and " . count ( $this -> mWarnings ) .
" warnings found \n " );
2009-08-07 04:19:09 +00:00
}
/**
* Build the list of files we ' ll check for syntax errors
*/
private function buildFileList () {
global $IP ;
2009-12-05 15:03:20 +00:00
$this -> mIgnorePaths = array (
// Compat stuff, explodes on PHP 5.3
" includes/NamespaceCompat.php $ " ,
);
2010-12-04 03:20:14 +00:00
2009-12-05 15:03:20 +00:00
$this -> mNoStyleCheckPaths = array (
// Third-party code we don't care about
" /activemq_stomp/ " ,
2010-11-21 15:44:46 +00:00
" EmailPage/PHPMailer " ,
2009-12-05 15:03:20 +00:00
" FCKeditor/fckeditor/ " ,
'\bphplot-' ,
" /svggraph/ " ,
" \ bjsmin.php $ " ,
2010-03-08 20:39:47 +00:00
" PEAR/File_Ogg/ " ,
2009-12-05 15:03:20 +00:00
" QPoll/Excel/ " ,
2010-01-03 15:27:25 +00:00
" /geshi/ " ,
2009-12-05 15:03:20 +00:00
" /smarty/ " ,
);
2009-11-17 18:57:35 +00:00
if ( $this -> hasOption ( 'path' ) ) {
$path = $this -> getOption ( 'path' );
if ( ! $this -> addPath ( $path ) ) {
$this -> error ( " Error: can't find file or directory $path\n " , true );
2009-11-14 11:47:15 +00:00
}
2009-11-17 18:57:35 +00:00
return ; // process only this path
} elseif ( $this -> hasOption ( 'list-file' ) ) {
$file = $this -> getOption ( 'list-file' );
2011-07-06 21:57:44 +00:00
wfSuppressWarnings ();
$f = fopen ( $file , 'r' );
wfRestoreWarnings ();
2009-11-17 18:57:35 +00:00
if ( ! $f ) {
$this -> error ( " Can't open file $file\n " , true );
}
2010-11-01 00:07:17 +00:00
$path = trim ( fgets ( $f ) );
while ( $path ) {
2009-11-17 18:57:35 +00:00
$this -> addPath ( $path );
}
fclose ( $f );
return ;
2009-11-17 17:26:19 +00:00
} elseif ( $this -> hasOption ( 'modified' ) ) {
2012-04-06 04:13:46 +00:00
$this -> output ( " Retrieving list from Git... " );
$files = $this -> getGitModifiedFiles ( $IP );
$this -> output ( " done \n " );
foreach ( $files as $file ) {
2010-07-25 18:00:32 +00:00
if ( $this -> isSuitableFile ( $file ) && ! is_dir ( $file ) ) {
2009-11-18 17:57:32 +00:00
$this -> mFiles [] = $file ;
}
}
2009-11-17 17:26:19 +00:00
return ;
2009-11-14 11:47:15 +00:00
}
2010-01-26 13:40:00 +00:00
$this -> output ( 'Building file list...' , 'listfiles' );
2009-11-14 11:47:15 +00:00
2010-12-04 03:20:14 +00:00
// Only check files in these directories.
2009-08-07 04:19:09 +00:00
// Don't just put $IP, because the recursive dir thingie goes into all subdirs
2010-05-22 16:50:39 +00:00
$dirs = array (
2009-08-07 04:19:09 +00:00
$IP . '/includes' ,
2011-05-15 14:15:43 +00:00
$IP . '/mw-config' ,
2009-08-07 04:19:09 +00:00
$IP . '/languages' ,
$IP . '/maintenance' ,
$IP . '/skins' ,
);
2010-05-22 16:50:39 +00:00
if ( $this -> hasOption ( 'with-extensions' ) ) {
2009-08-07 04:19:09 +00:00
$dirs [] = $IP . '/extensions' ;
}
2010-05-22 16:50:39 +00:00
foreach ( $dirs as $d ) {
2009-11-17 18:57:35 +00:00
$this -> addDirectoryContent ( $d );
2009-08-07 04:19:09 +00:00
}
2009-11-14 11:47:15 +00:00
// Manually add two user-editable files that are usually sources of problems
if ( file_exists ( " $IP /LocalSettings.php " ) ) {
$this -> mFiles [] = " $IP /LocalSettings.php " ;
}
if ( file_exists ( " $IP /AdminSettings.php " ) ) {
$this -> mFiles [] = " $IP /AdminSettings.php " ;
}
2010-01-26 13:40:00 +00:00
$this -> output ( 'done.' , 'listfiles' );
2009-08-07 04:19:09 +00:00
}
2010-12-04 03:20:14 +00:00
2012-04-06 04:13:46 +00:00
/**
* Returns a list of tracked files in a Git work tree differing from the master branch .
* @ param $path string : Path to the repository
* @ return array : Resulting list of changed files
*/
private function getGitModifiedFiles ( $path ) {
global $wgMaxShellMemory ;
if ( ! is_dir ( " $path /.git " ) ) {
$this -> error ( " Error: Not a Git repository! \n " , true );
}
// git diff eats memory.
$oldMaxShellMemory = $wgMaxShellMemory ;
if ( $wgMaxShellMemory < 1024000 ) {
$wgMaxShellMemory = 1024000 ;
}
$ePath = wfEscapeShellArg ( $path );
// Find an ancestor in common with master (rather than just using its HEAD)
// to prevent files only modified there from showing up in the list.
$cmd = " cd $ePath && git merge-base master HEAD " ;
$retval = 0 ;
$output = wfShellExec ( $cmd , $retval );
if ( $retval !== 0 ) {
$this -> error ( " Error retrieving base SHA1 from Git! \n " , true );
}
// Find files in the working tree that changed since then.
$eBase = wfEscapeShellArg ( rtrim ( $output , " \n " ) );
$cmd = " cd $ePath && git diff --name-only --diff-filter AM $eBase " ;
$retval = 0 ;
$output = wfShellExec ( $cmd , $retval );
if ( $retval !== 0 ) {
$this -> error ( " Error retrieving list from Git! \n " , true );
}
$wgMaxShellMemory = $oldMaxShellMemory ;
$arr = array ();
$filename = strtok ( $output , " \n " );
while ( $filename !== false ) {
if ( $filename !== '' ) {
$arr [] = " $path / $filename " ;
}
$filename = strtok ( " \n " );
}
return $arr ;
}
2009-11-18 17:57:32 +00:00
/**
* Returns true if $file is of a type we can check
2011-10-18 17:31:54 +00:00
* @ param $file string
* @ return bool
2009-11-18 17:57:32 +00:00
*/
2010-01-03 15:02:55 +00:00
private function isSuitableFile ( $file ) {
2010-04-08 15:55:59 +00:00
$file = str_replace ( '\\' , '/' , $file );
2009-11-18 17:57:32 +00:00
$ext = pathinfo ( $file , PATHINFO_EXTENSION );
2010-01-03 15:02:55 +00:00
if ( $ext != 'php' && $ext != 'inc' && $ext != 'php5' )
return false ;
2010-05-22 16:50:39 +00:00
foreach ( $this -> mIgnorePaths as $regex ) {
2010-01-03 15:02:55 +00:00
$m = array ();
if ( preg_match ( " ~ { $regex } ~ " , $file , $m ) )
return false ;
}
return true ;
2009-11-18 17:57:32 +00:00
}
2009-08-07 04:19:09 +00:00
2009-11-17 18:57:35 +00:00
/**
* Add given path to file list , searching it in include path if needed
2011-10-18 17:31:54 +00:00
* @ param $path string
* @ return bool
2009-11-17 18:57:35 +00:00
*/
private function addPath ( $path ) {
global $IP ;
return $this -> addFileOrDir ( $path ) || $this -> addFileOrDir ( " $IP / $path " );
}
/**
2011-10-18 17:31:54 +00:00
* Add given file to file list , or , if it ' s a directory , add its content
* @ param $path string
* @ return bool
*/
2009-11-17 18:57:35 +00:00
private function addFileOrDir ( $path ) {
if ( is_dir ( $path ) ) {
$this -> addDirectoryContent ( $path );
} elseif ( file_exists ( $path ) ) {
$this -> mFiles [] = $path ;
} else {
return false ;
}
return true ;
}
/**
* Add all suitable files in given directory or its subdirectories to the file list
*
* @ param $dir String : directory to process
*/
private function addDirectoryContent ( $dir ) {
$iterator = new RecursiveIteratorIterator (
2010-05-22 16:50:39 +00:00
new RecursiveDirectoryIterator ( $dir ),
2009-11-17 18:57:35 +00:00
RecursiveIteratorIterator :: SELF_FIRST
);
foreach ( $iterator as $file ) {
2010-01-03 15:02:55 +00:00
if ( $this -> isSuitableFile ( $file -> getRealPath () ) ) {
2009-11-17 18:57:35 +00:00
$this -> mFiles [] = $file -> getRealPath ();
}
}
}
2009-08-07 04:19:09 +00:00
/**
2009-08-19 00:46:18 +00:00
* Check a file for syntax errors using Parsekit . Shamelessly stolen
* from tools / lint . php by TimStarling
* @ param $file String Path to a file to check for syntax errors
2009-08-10 17:20:18 +00:00
* @ return boolean
2009-08-07 04:19:09 +00:00
*/
2009-08-19 00:46:18 +00:00
private function checkFileWithParsekit ( $file ) {
static $okErrors = array (
'Redefining already defined constructor' ,
'Assigning the return value of new by reference is deprecated' ,
);
$errors = array ();
parsekit_compile_file ( $file , $errors , PARSEKIT_SIMPLE );
$ret = true ;
if ( $errors ) {
foreach ( $errors as $error ) {
foreach ( $okErrors as $okError ) {
if ( substr ( $error [ 'errstr' ], 0 , strlen ( $okError ) ) == $okError ) {
continue 2 ;
}
}
$ret = false ;
$this -> output ( " Error in $file line { $error [ 'lineno' ] } : { $error [ 'errstr' ] } \n " );
$this -> mFailures [ $file ] = $errors ;
2009-08-07 04:19:09 +00:00
}
}
2009-08-19 00:46:18 +00:00
return $ret ;
}
/**
* Check a file for syntax errors using php - l
* @ param $file String Path to a file to check for syntax errors
* @ return boolean
*/
private function checkFileWithCli ( $file ) {
2010-05-22 16:50:39 +00:00
$res = exec ( 'php -l ' . wfEscapeShellArg ( $file ) );
if ( strpos ( $res , 'No syntax errors detected' ) === false ) {
2009-08-19 00:46:18 +00:00
$this -> mFailures [ $file ] = $res ;
$this -> output ( $res . " \n " );
return false ;
}
return true ;
2009-08-07 04:19:09 +00:00
}
2009-11-13 22:18:32 +00:00
/**
* Check a file for non - fatal coding errors , such as byte - order marks in the beginning
* or pointless ?> closing tags at the end.
*
* @ param $file String String Path to a file to check for errors
* @ return boolean
*/
private function checkForMistakes ( $file ) {
2010-05-22 16:50:39 +00:00
foreach ( $this -> mNoStyleCheckPaths as $regex ) {
2010-01-03 15:02:55 +00:00
$m = array ();
if ( preg_match ( " ~ { $regex } ~ " , $file , $m ) )
return ;
}
2009-11-13 22:18:32 +00:00
$text = file_get_contents ( $file );
2011-05-15 14:32:49 +00:00
$tokens = token_get_all ( $text );
2009-11-13 22:18:32 +00:00
2011-05-15 14:32:49 +00:00
$this -> checkEvilToken ( $file , $tokens , '@' , 'Error supression operator (@)' );
2009-11-13 22:18:32 +00:00
$this -> checkRegex ( $file , $text , '/^[\s\r\n]+<\?/' , 'leading whitespace' );
$this -> checkRegex ( $file , $text , '/\?>[\s\r\n]*$/' , 'trailing ?>' );
$this -> checkRegex ( $file , $text , '/^[\xFF\xFE\xEF]/' , 'byte-order mark' );
}
private function checkRegex ( $file , $text , $regex , $desc ) {
if ( ! preg_match ( $regex , $text ) ) {
return ;
}
if ( ! isset ( $this -> mWarnings [ $file ] ) ) {
$this -> mWarnings [ $file ] = array ();
}
$this -> mWarnings [ $file ][] = $desc ;
$this -> output ( " Warning in file $file : $desc found. \n " );
}
2011-05-15 14:32:49 +00:00
private function checkEvilToken ( $file , $tokens , $evilToken , $desc ) {
if ( ! in_array ( $evilToken , $tokens ) ) {
return ;
}
if ( ! isset ( $this -> mWarnings [ $file ] ) ) {
$this -> mWarnings [ $file ] = array ();
}
$this -> mWarnings [ $file ][] = $desc ;
$this -> output ( " Warning in file $file : $desc found. \n " );
}
2009-08-07 04:19:09 +00:00
}
2010-02-24 17:21:48 +00:00
$maintClass = " CheckSyntax " ;
2011-01-13 22:58:55 +00:00
require_once ( RUN_MAINTENANCE_IF_MAIN );
2009-08-19 00:46:18 +00:00