]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - maintenance/checkSyntax.php
MediaWiki 1.30.2 renames
[autoinstalls/mediawiki.git] / maintenance / checkSyntax.php
1 <?php
2 /**
3  * Check syntax of all PHP files in MediaWiki
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Maintenance
22  */
23
24 require_once __DIR__ . '/Maintenance.php';
25
26 /**
27  * Maintenance script to check syntax of all PHP files in MediaWiki.
28  *
29  * @ingroup Maintenance
30  */
31 class CheckSyntax extends Maintenance {
32
33         // List of files we're going to check
34         private $mFiles = [], $mFailures = [], $mWarnings = [];
35         private $mIgnorePaths = [], $mNoStyleCheckPaths = [];
36
37         public function __construct() {
38                 parent::__construct();
39                 $this->addDescription( 'Check syntax for all PHP files in MediaWiki' );
40                 $this->addOption( 'with-extensions', 'Also recurse the extensions folder' );
41                 $this->addOption(
42                         'path',
43                         'Specific path (file or directory) to check, either with absolute path or '
44                                 . 'relative to the root of this MediaWiki installation',
45                         false,
46                         true
47                 );
48                 $this->addOption(
49                         'list-file',
50                         'Text file containing list of files or directories to check',
51                         false,
52                         true
53                 );
54                 $this->addOption(
55                         'modified',
56                         'Check only files that were modified (requires Git command-line client)'
57                 );
58                 $this->addOption( 'syntax-only', 'Check for syntax validity only, skip code style warnings' );
59         }
60
61         public function getDbType() {
62                 return Maintenance::DB_NONE;
63         }
64
65         public function execute() {
66                 $this->buildFileList();
67
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 );
73                         }
74                 }
75                 $this->output( "\nDone! " . count( $this->mFiles ) . " files checked, " .
76                         count( $this->mFailures ) . " failures and " . count( $this->mWarnings ) .
77                         " warnings found\n" );
78         }
79
80         /**
81          * Build the list of files we'll check for syntax errors
82          */
83         private function buildFileList() {
84                 global $IP;
85
86                 $this->mIgnorePaths = [
87                 ];
88
89                 $this->mNoStyleCheckPaths = [
90                         // Third-party code we don't care about
91                         "/activemq_stomp/",
92                         "EmailPage/PHPMailer",
93                         "FCKeditor/fckeditor/",
94                         '\bphplot-',
95                         "/svggraph/",
96                         "\bjsmin.php$",
97                         "PEAR/File_Ogg/",
98                         "QPoll/Excel/",
99                         "/geshi/",
100                         "/smarty/",
101                 ];
102
103                 if ( $this->hasOption( 'path' ) ) {
104                         $path = $this->getOption( 'path' );
105                         if ( !$this->addPath( $path ) ) {
106                                 $this->error( "Error: can't find file or directory $path\n", true );
107                         }
108
109                         return; // process only this path
110                 } elseif ( $this->hasOption( 'list-file' ) ) {
111                         $file = $this->getOption( 'list-file' );
112                         MediaWiki\suppressWarnings();
113                         $f = fopen( $file, 'r' );
114                         MediaWiki\restoreWarnings();
115                         if ( !$f ) {
116                                 $this->error( "Can't open file $file\n", true );
117                         }
118                         $path = trim( fgets( $f ) );
119                         while ( $path ) {
120                                 $this->addPath( $path );
121                         }
122                         fclose( $f );
123
124                         return;
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;
132                                 }
133                         }
134
135                         return;
136                 }
137
138                 $this->output( 'Building file list...', 'listfiles' );
139
140                 // Only check files in these directories.
141                 // Don't just put $IP, because the recursive dir thingie goes into all subdirs
142                 $dirs = [
143                         $IP . '/includes',
144                         $IP . '/mw-config',
145                         $IP . '/languages',
146                         $IP . '/maintenance',
147                         $IP . '/skins',
148                 ];
149                 if ( $this->hasOption( 'with-extensions' ) ) {
150                         $dirs[] = $IP . '/extensions';
151                 }
152
153                 foreach ( $dirs as $d ) {
154                         $this->addDirectoryContent( $d );
155                 }
156
157                 // Manually add two user-editable files that are usually sources of problems
158                 if ( file_exists( "$IP/LocalSettings.php" ) ) {
159                         $this->mFiles[] = "$IP/LocalSettings.php";
160                 }
161
162                 $this->output( 'done.', 'listfiles' );
163         }
164
165         /**
166          * Returns a list of tracked files in a Git work tree differing from the master branch.
167          * @param string $path Path to the repository
168          * @return array Resulting list of changed files
169          */
170         private function getGitModifiedFiles( $path ) {
171                 global $wgMaxShellMemory;
172
173                 if ( !is_dir( "$path/.git" ) ) {
174                         $this->error( "Error: Not a Git repository!\n", true );
175                 }
176
177                 // git diff eats memory.
178                 $oldMaxShellMemory = $wgMaxShellMemory;
179                 if ( $wgMaxShellMemory < 1024000 ) {
180                         $wgMaxShellMemory = 1024000;
181                 }
182
183                 $ePath = wfEscapeShellArg( $path );
184
185                 // Find an ancestor in common with master (rather than just using its HEAD)
186                 // to prevent files only modified there from showing up in the list.
187                 $cmd = "cd $ePath && git merge-base master HEAD";
188                 $retval = 0;
189                 $output = wfShellExec( $cmd, $retval );
190                 if ( $retval !== 0 ) {
191                         $this->error( "Error retrieving base SHA1 from Git!\n", true );
192                 }
193
194                 // Find files in the working tree that changed since then.
195                 $eBase = wfEscapeShellArg( rtrim( $output, "\n" ) );
196                 $cmd = "cd $ePath && git diff --name-only --diff-filter AM $eBase";
197                 $retval = 0;
198                 $output = wfShellExec( $cmd, $retval );
199                 if ( $retval !== 0 ) {
200                         $this->error( "Error retrieving list from Git!\n", true );
201                 }
202
203                 $wgMaxShellMemory = $oldMaxShellMemory;
204
205                 $arr = [];
206                 $filename = strtok( $output, "\n" );
207                 while ( $filename !== false ) {
208                         if ( $filename !== '' ) {
209                                 $arr[] = "$path/$filename";
210                         }
211                         $filename = strtok( "\n" );
212                 }
213
214                 return $arr;
215         }
216
217         /**
218          * Returns true if $file is of a type we can check
219          * @param string $file
220          * @return bool
221          */
222         private function isSuitableFile( $file ) {
223                 $file = str_replace( '\\', '/', $file );
224                 $ext = pathinfo( $file, PATHINFO_EXTENSION );
225                 if ( $ext != 'php' && $ext != 'inc' && $ext != 'php5' ) {
226                         return false;
227                 }
228                 foreach ( $this->mIgnorePaths as $regex ) {
229                         $m = [];
230                         if ( preg_match( "~{$regex}~", $file, $m ) ) {
231                                 return false;
232                         }
233                 }
234
235                 return true;
236         }
237
238         /**
239          * Add given path to file list, searching it in include path if needed
240          * @param string $path
241          * @return bool
242          */
243         private function addPath( $path ) {
244                 global $IP;
245
246                 return $this->addFileOrDir( $path ) || $this->addFileOrDir( "$IP/$path" );
247         }
248
249         /**
250          * Add given file to file list, or, if it's a directory, add its content
251          * @param string $path
252          * @return bool
253          */
254         private function addFileOrDir( $path ) {
255                 if ( is_dir( $path ) ) {
256                         $this->addDirectoryContent( $path );
257                 } elseif ( file_exists( $path ) ) {
258                         $this->mFiles[] = $path;
259                 } else {
260                         return false;
261                 }
262
263                 return true;
264         }
265
266         /**
267          * Add all suitable files in given directory or its subdirectories to the file list
268          *
269          * @param string $dir Directory to process
270          */
271         private function addDirectoryContent( $dir ) {
272                 $iterator = new RecursiveIteratorIterator(
273                         new RecursiveDirectoryIterator( $dir ),
274                         RecursiveIteratorIterator::SELF_FIRST
275                 );
276                 foreach ( $iterator as $file ) {
277                         if ( $this->isSuitableFile( $file->getRealPath() ) ) {
278                                 $this->mFiles[] = $file->getRealPath();
279                         }
280                 }
281         }
282
283         /**
284          * Check a file for syntax errors using php -l
285          * @param string $file Path to a file to check for syntax errors
286          * @return bool
287          */
288         private function checkFileWithCli( $file ) {
289                 $res = exec( 'php -l ' . wfEscapeShellArg( $file ) );
290                 if ( strpos( $res, 'No syntax errors detected' ) === false ) {
291                         $this->mFailures[$file] = $res;
292                         $this->output( $res . "\n" );
293
294                         return false;
295                 }
296
297                 return true;
298         }
299
300         /**
301          * Check a file for non-fatal coding errors, such as byte-order marks in the beginning
302          * or pointless ?> closing tags at the end.
303          *
304          * @param string $file String Path to a file to check for errors
305          */
306         private function checkForMistakes( $file ) {
307                 foreach ( $this->mNoStyleCheckPaths as $regex ) {
308                         $m = [];
309                         if ( preg_match( "~{$regex}~", $file, $m ) ) {
310                                 return;
311                         }
312                 }
313
314                 $text = file_get_contents( $file );
315                 $tokens = token_get_all( $text );
316
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' );
321         }
322
323         private function checkRegex( $file, $text, $regex, $desc ) {
324                 if ( !preg_match( $regex, $text ) ) {
325                         return;
326                 }
327
328                 if ( !isset( $this->mWarnings[$file] ) ) {
329                         $this->mWarnings[$file] = [];
330                 }
331                 $this->mWarnings[$file][] = $desc;
332                 $this->output( "Warning in file $file: $desc found.\n" );
333         }
334
335         private function checkEvilToken( $file, $tokens, $evilToken, $desc ) {
336                 if ( !in_array( $evilToken, $tokens ) ) {
337                         return;
338                 }
339
340                 if ( !isset( $this->mWarnings[$file] ) ) {
341                         $this->mWarnings[$file] = [];
342                 }
343                 $this->mWarnings[$file][] = $desc;
344                 $this->output( "Warning in file $file: $desc found.\n" );
345         }
346 }
347
348 $maintClass = "CheckSyntax";
349 require_once RUN_MAINTENANCE_IF_MAIN;