CheckSyntax::getDbType()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 31 and the first side effect is on line 24.

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.

Loading history...
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 = [];
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
35
	private $mIgnorePaths = [], $mNoStyleCheckPaths = [];
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
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