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
|
|
View Code Duplication |
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
|
|
|
|
172
|
|
|
global $wgMaxShellMemory; |
173
|
|
|
|
174
|
|
|
if ( !is_dir( "$path/.git" ) ) { |
175
|
|
|
$this->error( "Error: Not a Git repository!\n", true ); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
// git diff eats memory. |
179
|
|
|
$oldMaxShellMemory = $wgMaxShellMemory; |
180
|
|
|
if ( $wgMaxShellMemory < 1024000 ) { |
181
|
|
|
$wgMaxShellMemory = 1024000; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
$ePath = wfEscapeShellArg( $path ); |
185
|
|
|
|
186
|
|
|
// Find an ancestor in common with master (rather than just using its HEAD) |
187
|
|
|
// to prevent files only modified there from showing up in the list. |
188
|
|
|
$cmd = "cd $ePath && git merge-base master HEAD"; |
189
|
|
|
$retval = 0; |
190
|
|
|
$output = wfShellExec( $cmd, $retval ); |
191
|
|
|
if ( $retval !== 0 ) { |
192
|
|
|
$this->error( "Error retrieving base SHA1 from Git!\n", true ); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
// Find files in the working tree that changed since then. |
196
|
|
|
$eBase = wfEscapeShellArg( rtrim( $output, "\n" ) ); |
197
|
|
|
$cmd = "cd $ePath && git diff --name-only --diff-filter AM $eBase"; |
198
|
|
|
$retval = 0; |
199
|
|
|
$output = wfShellExec( $cmd, $retval ); |
200
|
|
|
if ( $retval !== 0 ) { |
201
|
|
|
$this->error( "Error retrieving list from Git!\n", true ); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
$wgMaxShellMemory = $oldMaxShellMemory; |
205
|
|
|
|
206
|
|
|
$arr = []; |
207
|
|
|
$filename = strtok( $output, "\n" ); |
208
|
|
|
while ( $filename !== false ) { |
209
|
|
|
if ( $filename !== '' ) { |
210
|
|
|
$arr[] = "$path/$filename"; |
211
|
|
|
} |
212
|
|
|
$filename = strtok( "\n" ); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
return $arr; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Returns true if $file is of a type we can check |
220
|
|
|
* @param string $file |
221
|
|
|
* @return bool |
222
|
|
|
*/ |
223
|
|
|
private function isSuitableFile( $file ) { |
224
|
|
|
$file = str_replace( '\\', '/', $file ); |
225
|
|
|
$ext = pathinfo( $file, PATHINFO_EXTENSION ); |
226
|
|
|
if ( $ext != 'php' && $ext != 'inc' && $ext != 'php5' ) { |
227
|
|
|
return false; |
228
|
|
|
} |
229
|
|
|
foreach ( $this->mIgnorePaths as $regex ) { |
230
|
|
|
$m = []; |
231
|
|
|
if ( preg_match( "~{$regex}~", $file, $m ) ) { |
232
|
|
|
return false; |
233
|
|
|
} |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
return true; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Add given path to file list, searching it in include path if needed |
241
|
|
|
* @param string $path |
242
|
|
|
* @return bool |
243
|
|
|
*/ |
244
|
|
|
private function addPath( $path ) { |
245
|
|
|
global $IP; |
246
|
|
|
|
247
|
|
|
return $this->addFileOrDir( $path ) || $this->addFileOrDir( "$IP/$path" ); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Add given file to file list, or, if it's a directory, add its content |
252
|
|
|
* @param string $path |
253
|
|
|
* @return bool |
254
|
|
|
*/ |
255
|
|
|
private function addFileOrDir( $path ) { |
256
|
|
|
if ( is_dir( $path ) ) { |
257
|
|
|
$this->addDirectoryContent( $path ); |
258
|
|
|
} elseif ( file_exists( $path ) ) { |
259
|
|
|
$this->mFiles[] = $path; |
260
|
|
|
} else { |
261
|
|
|
return false; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
return true; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* Add all suitable files in given directory or its subdirectories to the file list |
269
|
|
|
* |
270
|
|
|
* @param string $dir Directory to process |
271
|
|
|
*/ |
272
|
|
|
private function addDirectoryContent( $dir ) { |
273
|
|
|
$iterator = new RecursiveIteratorIterator( |
274
|
|
|
new RecursiveDirectoryIterator( $dir ), |
275
|
|
|
RecursiveIteratorIterator::SELF_FIRST |
276
|
|
|
); |
277
|
|
|
foreach ( $iterator as $file ) { |
278
|
|
|
if ( $this->isSuitableFile( $file->getRealPath() ) ) { |
279
|
|
|
$this->mFiles[] = $file->getRealPath(); |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Check a file for syntax errors using php -l |
286
|
|
|
* @param string $file Path to a file to check for syntax errors |
287
|
|
|
* @return bool |
288
|
|
|
*/ |
289
|
|
|
private function checkFileWithCli( $file ) { |
290
|
|
|
$res = exec( 'php -l ' . wfEscapeShellArg( $file ) ); |
291
|
|
|
if ( strpos( $res, 'No syntax errors detected' ) === false ) { |
292
|
|
|
$this->mFailures[$file] = $res; |
293
|
|
|
$this->output( $res . "\n" ); |
294
|
|
|
|
295
|
|
|
return false; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
return true; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Check a file for non-fatal coding errors, such as byte-order marks in the beginning |
303
|
|
|
* or pointless ?> closing tags at the end. |
304
|
|
|
* |
305
|
|
|
* @param string $file String Path to a file to check for errors |
306
|
|
|
*/ |
307
|
|
|
private function checkForMistakes( $file ) { |
308
|
|
|
foreach ( $this->mNoStyleCheckPaths as $regex ) { |
309
|
|
|
$m = []; |
310
|
|
|
if ( preg_match( "~{$regex}~", $file, $m ) ) { |
311
|
|
|
return; |
312
|
|
|
} |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
$text = file_get_contents( $file ); |
316
|
|
|
$tokens = token_get_all( $text ); |
317
|
|
|
|
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' ); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
View Code Duplication |
private function checkRegex( $file, $text, $regex, $desc ) { |
325
|
|
|
if ( !preg_match( $regex, $text ) ) { |
326
|
|
|
return; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
if ( !isset( $this->mWarnings[$file] ) ) { |
330
|
|
|
$this->mWarnings[$file] = []; |
331
|
|
|
} |
332
|
|
|
$this->mWarnings[$file][] = $desc; |
333
|
|
|
$this->output( "Warning in file $file: $desc found.\n" ); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
View Code Duplication |
private function checkEvilToken( $file, $tokens, $evilToken, $desc ) { |
337
|
|
|
if ( !in_array( $evilToken, $tokens ) ) { |
338
|
|
|
return; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
if ( !isset( $this->mWarnings[$file] ) ) { |
342
|
|
|
$this->mWarnings[$file] = []; |
343
|
|
|
} |
344
|
|
|
$this->mWarnings[$file][] = $desc; |
345
|
|
|
$this->output( "Warning in file $file: $desc found.\n" ); |
346
|
|
|
} |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
$maintClass = "CheckSyntax"; |
350
|
|
|
require_once RUN_MAINTENANCE_IF_MAIN; |
351
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.