24 require_once __DIR__ .
'/Maintenance.php';
34 private $mFiles = [], $mFailures = [], $mWarnings = [];
35 private $mIgnorePaths = [], $mNoStyleCheckPaths = [];
38 parent::__construct();
39 $this->
addDescription(
'Check syntax for all PHP files in MediaWiki' );
40 $this->
addOption(
'with-extensions',
'Also recurse the extensions folder' );
43 'Specific path (file or directory) to check, either with absolute path or '
44 .
'relative to the root of this MediaWiki installation',
50 'Text file containing list of files or directories to check',
56 'Check only files that were modified (requires Git command-line client)'
58 $this->
addOption(
'syntax-only',
'Check for syntax validity only, skip code style warnings' );
66 $this->buildFileList();
68 $this->
output(
"Checking syntax (using php -l, this can take a long time)\n" );
69 foreach ( $this->mFiles
as $f ) {
70 $this->checkFileWithCli( $f );
71 if ( !$this->
hasOption(
'syntax-only' ) ) {
72 $this->checkForMistakes( $f );
75 $this->
output(
"\nDone! " .
count( $this->mFiles ) .
" files checked, " .
76 count( $this->mFailures ) .
" failures and " .
count( $this->mWarnings ) .
77 " warnings found\n" );
83 private function buildFileList() {
86 $this->mIgnorePaths = [
89 $this->mNoStyleCheckPaths = [
92 "EmailPage/PHPMailer",
93 "FCKeditor/fckeditor/",
105 if ( !$this->addPath(
$path ) ) {
106 $this->
error(
"Error: can't find file or directory $path\n",
true );
110 } elseif ( $this->
hasOption(
'list-file' ) ) {
112 MediaWiki\suppressWarnings();
113 $f = fopen( $file,
'r' );
114 MediaWiki\restoreWarnings();
116 $this->
error(
"Can't open file $file\n",
true );
118 $path = trim( fgets( $f ) );
120 $this->addPath(
$path );
125 } elseif ( $this->
hasOption(
'modified' ) ) {
126 $this->
output(
"Retrieving list from Git... " );
127 $files = $this->getGitModifiedFiles(
$IP );
128 $this->
output(
"done\n" );
129 foreach ( $files
as $file ) {
130 if ( $this->isSuitableFile( $file ) && !is_dir( $file ) ) {
131 $this->mFiles[] = $file;
138 $this->
output(
'Building file list...',
'listfiles' );
146 $IP .
'/maintenance',
149 if ( $this->
hasOption(
'with-extensions' ) ) {
150 $dirs[] = $IP .
'/extensions';
154 $this->addDirectoryContent( $d );
158 if ( file_exists(
"$IP/LocalSettings.php" ) ) {
159 $this->mFiles[] =
"$IP/LocalSettings.php";
162 $this->
output(
'done.',
'listfiles' );
170 private function getGitModifiedFiles(
$path ) {
173 if ( !is_dir(
"$path/.git" ) ) {
174 $this->
error(
"Error: Not a Git repository!\n",
true );
187 $cmd =
"cd $ePath && git merge-base master HEAD";
191 $this->
error(
"Error retrieving base SHA1 from Git!\n",
true );
196 $cmd =
"cd $ePath && git diff --name-only --diff-filter AM $eBase";
200 $this->
error(
"Error retrieving list from Git!\n",
true );
203 $wgMaxShellMemory = $oldMaxShellMemory;
206 $filename = strtok(
$output,
"\n" );
207 while ( $filename !==
false ) {
208 if ( $filename !==
'' ) {
209 $arr[] =
"$path/$filename";
211 $filename = strtok(
"\n" );
222 private function isSuitableFile( $file ) {
223 $file = str_replace(
'\\',
'/', $file );
224 $ext = pathinfo( $file, PATHINFO_EXTENSION );
225 if (
$ext !=
'php' &&
$ext !=
'inc' &&
$ext !=
'php5' ) {
228 foreach ( $this->mIgnorePaths
as $regex ) {
230 if ( preg_match(
"~{$regex}~", $file, $m ) ) {
243 private function addPath(
$path ) {
246 return $this->addFileOrDir(
$path ) || $this->addFileOrDir(
"$IP/$path" );
254 private function addFileOrDir(
$path ) {
255 if ( is_dir(
$path ) ) {
256 $this->addDirectoryContent(
$path );
257 } elseif ( file_exists(
$path ) ) {
258 $this->mFiles[] =
$path;
271 private function addDirectoryContent(
$dir ) {
272 $iterator =
new RecursiveIteratorIterator(
273 new RecursiveDirectoryIterator(
$dir ),
274 RecursiveIteratorIterator::SELF_FIRST
276 foreach ( $iterator
as $file ) {
277 if ( $this->isSuitableFile( $file->getRealPath() ) ) {
278 $this->mFiles[] = $file->getRealPath();
288 private function checkFileWithCli( $file ) {
290 if ( strpos(
$res,
'No syntax errors detected' ) ===
false ) {
291 $this->mFailures[$file] =
$res;
306 private function checkForMistakes( $file ) {
307 foreach ( $this->mNoStyleCheckPaths
as $regex ) {
309 if ( preg_match(
"~{$regex}~", $file, $m ) ) {
314 $text = file_get_contents( $file );
315 $tokens = token_get_all( $text );
317 $this->checkEvilToken( $file,
$tokens,
'@',
'Error supression operator (@)' );
318 $this->checkRegex( $file, $text,
'/^[\s\r\n]+<\?/',
'leading whitespace' );
319 $this->checkRegex( $file, $text,
'/\?>[\s\r\n]*$/',
'trailing ?>' );
320 $this->checkRegex( $file, $text,
'/^[\xFF\xFE\xEF]/',
'byte-order mark' );
323 private function checkRegex( $file, $text, $regex, $desc ) {
324 if ( !preg_match( $regex, $text ) ) {
328 if ( !isset( $this->mWarnings[$file] ) ) {
329 $this->mWarnings[$file] = [];
331 $this->mWarnings[$file][] = $desc;
332 $this->
output(
"Warning in file $file: $desc found.\n" );
335 private function checkEvilToken( $file,
$tokens, $evilToken, $desc ) {
336 if ( !in_array( $evilToken,
$tokens ) ) {
340 if ( !isset( $this->mWarnings[$file] ) ) {
341 $this->mWarnings[$file] = [];
343 $this->mWarnings[$file][] = $desc;
344 $this->
output(
"Warning in file $file: $desc found.\n" );