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 ) {
174 if ( !is_dir(
"$path/.git" ) ) {
175 $this->
error(
"Error: Not a Git repository!\n",
true );
179 $oldMaxShellMemory = $wgMaxShellMemory;
180 if ( $wgMaxShellMemory < 1024000 ) {
181 $wgMaxShellMemory = 1024000;
188 $cmd =
"cd $ePath && git merge-base master HEAD";
192 $this->
error(
"Error retrieving base SHA1 from Git!\n",
true );
197 $cmd =
"cd $ePath && git diff --name-only --diff-filter AM $eBase";
201 $this->
error(
"Error retrieving list from Git!\n",
true );
204 $wgMaxShellMemory = $oldMaxShellMemory;
207 $filename = strtok(
$output,
"\n" );
208 while ( $filename !==
false ) {
209 if ( $filename !==
'' ) {
210 $arr[] =
"$path/$filename";
212 $filename = strtok(
"\n" );
223 private function isSuitableFile( $file ) {
224 $file = str_replace(
'\\',
'/', $file );
225 $ext = pathinfo( $file, PATHINFO_EXTENSION );
226 if (
$ext !=
'php' &&
$ext !=
'inc' &&
$ext !=
'php5' ) {
229 foreach ( $this->mIgnorePaths
as $regex ) {
231 if ( preg_match(
"~{$regex}~", $file, $m ) ) {
244 private function addPath(
$path ) {
247 return $this->addFileOrDir(
$path ) || $this->addFileOrDir(
"$IP/$path" );
255 private function addFileOrDir(
$path ) {
256 if ( is_dir(
$path ) ) {
257 $this->addDirectoryContent(
$path );
258 } elseif ( file_exists(
$path ) ) {
259 $this->mFiles[] =
$path;
272 private function addDirectoryContent(
$dir ) {
273 $iterator =
new RecursiveIteratorIterator(
274 new RecursiveDirectoryIterator(
$dir ),
275 RecursiveIteratorIterator::SELF_FIRST
277 foreach ( $iterator
as $file ) {
278 if ( $this->isSuitableFile( $file->getRealPath() ) ) {
279 $this->mFiles[] = $file->getRealPath();
289 private function checkFileWithCli( $file ) {
291 if ( strpos(
$res,
'No syntax errors detected' ) ===
false ) {
292 $this->mFailures[$file] =
$res;
307 private function checkForMistakes( $file ) {
308 foreach ( $this->mNoStyleCheckPaths
as $regex ) {
310 if ( preg_match(
"~{$regex}~", $file, $m ) ) {
315 $text = file_get_contents( $file );
316 $tokens = token_get_all( $text );
318 $this->checkEvilToken( $file,
$tokens,
'@',
'Error supression operator (@)' );
319 $this->checkRegex( $file, $text,
'/^[\s\r\n]+<\?/',
'leading whitespace' );
320 $this->checkRegex( $file, $text,
'/\?>[\s\r\n]*$/',
'trailing ?>' );
321 $this->checkRegex( $file, $text,
'/^[\xFF\xFE\xEF]/',
'byte-order mark' );
324 private function checkRegex( $file, $text, $regex, $desc ) {
325 if ( !preg_match( $regex, $text ) ) {
329 if ( !isset( $this->mWarnings[$file] ) ) {
330 $this->mWarnings[$file] = [];
332 $this->mWarnings[$file][] = $desc;
333 $this->
output(
"Warning in file $file: $desc found.\n" );
336 private function checkEvilToken( $file,
$tokens, $evilToken, $desc ) {
337 if ( !in_array( $evilToken,
$tokens ) ) {
341 if ( !isset( $this->mWarnings[$file] ) ) {
342 $this->mWarnings[$file] = [];
344 $this->mWarnings[$file][] = $desc;
345 $this->
output(
"Warning in file $file: $desc found.\n" );