Completed
Push — master ( 19c41e...efb497 )
by Paweł
03:01
created

code_review::getVersionsList()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 9
Bugs 1 Features 4
Metric Value
cc 5
eloc 14
c 9
b 1
f 4
nc 6
nop 0
dl 0
loc 21
ccs 0
cts 17
cp 0
crap 30
rs 8.7624
1
<?php
2
/**
3
 * Main plugin class. 
4
 * Storage for various handlers.
5
 * @author Paweł Sroka ([email protected])
6
 */
7
class code_review {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
8
9
	/**
10
	 * Config array to allow mocking of configuration.
11
	 *
12
	 * @var array
13
	 */
14
	protected static $config = array();
15
16
	/**
17
	 * @codeCoverageIgnore
18
	 */
19
	public static function boot() {
20
		if (version_compare(elgg_get_version(true), '1.9', '<')) {
21
			$autoloader = new CodeReviewAutoloader();
22
			$autoloader->register();
23
		}
24
25
		$enginePath = elgg_get_config('path') . 'engine/';
26
		if (function_exists('elgg_get_engine_path')) {
27
			$enginePath = elgg_get_engine_path() . '/';
28
		}
29
		self::initConfig(array(
30
			'engine_path' => $enginePath,
31
			'path' => elgg_get_config('path'),
32
			'pluginspath' => elgg_get_plugins_path(),
33
			'plugins_getter' => 'elgg_get_plugins',
34
		));
35
	}
36
37
	/**
38
	 * @return array
39
	 */
40 1
	public static function getConfig() {
41 1
		return self::$config;
42
	}
43
44
	/**
45
	 * @param array $options
46
	 *
47
	 * @todo Move into CodeReviewConfig instead
48
	 */
49 3
	public static function initConfig(array $options) {
50 3
		self::$config = $options;
51
52
		$names = array(
53 3
			'T_NAMESPACE',
54 3
			'T_NS_C',
55 3
			'T_NS_SEPARATOR',
56 3
		);
57
58 3
		foreach ($names as $name) {
59 3
			if (!defined($name)) {
60
				// just define it with value unused by tokenizer to avoid errors on old PHP versions
61
				define($name, 10000);
62
			}
63 3
		}
64 3
	}
65
66
	/**
67
	 * @codeCoverageIgnore
68
	 */
69
	public static function init() {
70
71
		elgg_register_event_handler('pagesetup', 'system', array(__CLASS__, 'pagesetup'));
72
73
		elgg_register_plugin_hook_handler('register', 'menu:code_review', array(__CLASS__, 'menu_register'));
74
75
		elgg_register_ajax_view('graphics/ajax_loader');
76
		elgg_register_ajax_view('code_review/analysis');
77
		
78
		elgg_register_js('code_review', elgg_get_config('wwwroot') . 'mod/'
79
			. __CLASS__ . '/views/default/js/code_review.js');
80
	}
81
82
	/**
83
	 * @codeCoverageIgnore
84
	 */
85
	public static function pagesetup() {
86
		if (elgg_get_context() == 'admin') {
87
			elgg_register_menu_item('page', array(
88
				'name' => 'code/diagnostic',
89
				'href' => 'admin/code/diagnostic',
90
				'text' => elgg_echo('admin:code:diagnostic'),
91
				'context' => 'admin',
92
				'section' => 'develop'
93
			));
94
		}
95
	}
96
97
	/**
98
	 * @codeCoverageIgnore
99
	 */
100
	public static function menu_register() {
101
		$result = array();
102
		$result[] = ElggMenuItem::factory(array(
103
			'name' => 'admin/code/diagnostic',
104
			'href' => 'admin/code/diagnostic',
105
			'text' => elgg_echo('admin:code:diagnostic'),
106
		));
107
		$result[] = ElggMenuItem::factory(array(
108
			'name' => 'admin/code/diagnostic/deprecated_list',
109
			'href' => 'admin/code/diagnostic/deprecated_list',
110
			'text' => elgg_echo('admin:code:diagnostic:deprecated_list'),
111
//			'target' => '_blank',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
112
		));
113
		$result[] = ElggMenuItem::factory(array(
114
			'name' => 'admin/code/diagnostic/private_list',
115
			'href' => 'admin/code/diagnostic/private_list',
116
			'text' => elgg_echo('admin:code:diagnostic:private_list'),
117
//			'target' => '_blank',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
118
		));
119
		$result[] = ElggMenuItem::factory(array(
120
			'name' => 'admin/code/diagnostic/functions_list',
121
			'href' => 'admin/code/diagnostic/functions_list',
122
			'text' => elgg_echo('admin:code:diagnostic:functions_list'),
123
		));
124
		return $result;
125
	}
126
127
	/**
128
	 * @param string $subPath
129
	 * @return RegexIterator
130
	 */
131 2
	public static function getPhpIterator($subPath = '/') {
132 2
		$i = new RecursiveDirectoryIterator(self::$config['engine_path'] . $subPath, RecursiveDirectoryIterator::SKIP_DOTS);
133 2
		$i = new RecursiveIteratorIterator($i, RecursiveIteratorIterator::LEAVES_ONLY);
134 2
		$i = new RegexIterator($i, "/.*\.php/");
135 2
		return $i;
136
	}
137
138
	public static function getVersionsList() {
139
		$i = self::getPhpIterator('lib/');
140
		$i = new RegexIterator($i, "/deprecated-.*/");
141
		
142
		$vv = array();
143
		
144
		foreach ($i as $file) {
145
			if ($file instanceof SplFileInfo) {
146
				if (preg_match('#^deprecated-([0-9\.]*)$#', $file->getBasename('.php'), $matches)) {
147
					$version = $matches[1];
148
				} else {
149
					$version = null;
150
				}
151
				if ($version) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $version of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
152
					$vv[] = $version;
153
				}
154
			}
155
		}
156
		usort($vv, 'version_compare');
157
		return $vv;
158
	}
159
160
	/**
161
	 * @val string
162
	 */
163
	const DEPRECATED_TAG_PREFIX = 'deprecated';
164
165
166
	/**
167
	 * @val string
168
	 */
169
	const PRIVATE_TAG_PREFIX = 'private';
170
171
	/**
172
	 * Filtering predicate
173
	 *
174
	 * @param $e
175
	 * @return bool
176
	 */
177 1
	public static function filterTagsByDeprecatedPrefix($e) {
178 1
		return strpos($e, self::DEPRECATED_TAG_PREFIX) === 0;
179
	}
180
181
	/**
182
	 * Filtering predicate
183
	 *
184
	 * @param $e
185
	 * @return bool
186
	 */
187
	public static function filterTagsByPrivatePrefix($e) {
188
		return strpos($e, self::PRIVATE_TAG_PREFIX) === 0;
189
	}
190
191 1
	private static function getDeprecatedInfoFromDocBlock($deprecatedInfo) {
192 1
		if (strpos($deprecatedInfo, '@' . self::DEPRECATED_TAG_PREFIX) === false){
193 1
			return false;
194
		} else {
195 1
			$deprecatedInfo = explode('* @', $deprecatedInfo);
196 1
			$deprecatedInfo = array_filter($deprecatedInfo, array(__CLASS__, 'filterTagsByDeprecatedPrefix'));
197 1
			$deprecatedInfo = array_shift($deprecatedInfo);
198 1
			$deprecatedInfo = substr($deprecatedInfo, strlen(self::DEPRECATED_TAG_PREFIX));
199
200
			//strip leading whitechars and stars and closing tags
201 1
			$deprecatedInfo = preg_replace('#\n\s*(?:\*\/?\s*)+#', "\n", $deprecatedInfo);
202
			//save and strip leading version info
203 1
			$version = null;
204 1
			preg_match('#^\s*([0-9]+\.[0-9]+)#', $deprecatedInfo, $matches);
205 1
			if ($matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
206 1
				$version = $matches[1];
207 1
			}
208 1
			$deprecatedInfo = preg_replace('#\s*[0-9](?:\.[0-9]+){1,2}\.?\s*#', "", $deprecatedInfo);
209
			//strip embedded @link docblock entries
210 1
			$deprecatedInfo = preg_replace('#\{\@[a-z]+\s([^\}]+)\}#', '$1', $deprecatedInfo);
211
			//trim possible whitechars at the end
212 1
			$deprecatedInfo = trim($deprecatedInfo);
213
214 1
			$shortDeprecatedInfo = $deprecatedInfo;
215 1
			if (($pos = strpos($shortDeprecatedInfo, "\n")) !== false) {
216
				$shortDeprecatedInfo = substr($shortDeprecatedInfo, 0, $pos);
217
			}
218
219
			$result = array(
220 1
				'deprecated' => true,
221
//				'fixinfo' => strlen($deprecatedInfo) > 0 ? $deprecatedInfo : false,
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
222 1
				'fixinfoshort' => strlen($shortDeprecatedInfo) > 0 ? $shortDeprecatedInfo : false,
223 1
			);
224 1
			if ($version) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $version of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
225 1
				$result['version'] = $version;
226 1
			}
227 1
			return $result;
228
		}
229
	}
230
231
	private static function getPrivateInfoFromDocBlock($privateInfo) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
232
		if (strpos($privateInfo, '@' . self::PRIVATE_TAG_PREFIX) === false){
233
			return false;
234
		} else {
235
			$result = array(
236
				'private' => true
237
			);
238
			return $result;
239
		}
240
	}
241
242
	/**
243
	 * @param string $maxVersion
244
	 * @return array
245
	 */
246 1
	public static function getDeprecatedFunctionsList($maxVersion = '') {
247 1
		$i1 = self::getPhpIterator('lib/');
248 1
		$i1 = new RegexIterator($i1, "/deprecated-.*/");
249 1
		$i2 = self::getPhpIterator('classes/');
250
251 1
		$i = new AppendIterator();
252 1
		$i->append($i1);
253 1
		$i->append($i2);
254
		
255 1
		$functs = array();
256
		
257 1
		foreach ($i as $file) {
258 1
			if ($file instanceof SplFileInfo) {
259 1
				if (preg_match('#^deprecated-([0-9\.]*)$#', $file->getBasename('.php'), $matches)) {
260 1
					$version = $matches[1];
261 1
				} else {
262 1
					$version = null;
263
				}
264
				
265
				//skip versions higher than selected
266 1
				if ($maxVersion && $version && version_compare($version, $maxVersion) > 0) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $version of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
267 1
					continue;
268
				}
269
270 1
				$tokens = new PhpFileParser($file->getPathname());
271 1
				$functs = array_merge($functs, self::getDeprecatedFunctionsFromTokens($tokens, $file, $version));
272 1
			}
273 1
		}
274 1
		return $functs;
275
	}
276
277
	/**
278
	 * @param string $maxVersion
0 ignored issues
show
Bug introduced by
There is no parameter named $maxVersion. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
279
	 * @return array
280
	 */
281 1
	public static function getPrivateFunctionsList() {
282 1
		$i1 = new DirectoryIterator(self::$config['engine_path'] . 'lib/');
283 1
		$i1 = new RegexIterator($i1, "/.*\.php/");
284 1
		$i2 = self::getPhpIterator('classes/');
285
286 1
		$i = new AppendIterator();
287 1
		$i->append($i1);
288 1
		$i->append($i2);
289
290 1
		$functs = array();
291
292 1
		foreach ($i as $file) {
293 1
			if ($file instanceof SplFileInfo) {
294 1
				$tokens = new PhpFileParser($file->getPathname());
295 1
				$functs = array_merge($functs, self::getPrivateFunctionsFromTokens($tokens, $file));
296 1
			}
297 1
		}
298 1
		return $functs;
299
	}
300
301
	/**
302
	 * Redurns deprecated functions from particular file.
303
	 *
304
	 * @param PhpFileParser $tokens
305
	 * @param SplFileInfo   $file
306
	 * @param               $version
307
	 * @return array
308
	 */
309 1
	private static function getDeprecatedFunctionsFromTokens(PhpFileParser $tokens, SplFileInfo $file, $version) {
310 1
		$namespace = '';
311 1
		$className = null;
312 1
		$functs = array();
313 1
		foreach ($tokens as $key => $token) {
314 1
			if ($tokens->isEqualToToken(T_INTERFACE, $key)) {
315
				//we don't process interfaces for deprecated functions
316
				break;
317
			}
318 1
			if ($tokens->isEqualToToken(T_NAMESPACE, $key)) {
319 1
				$pos = $key+2;
320 1
				$namespace = '';
321 1
				while (isset($tokens[$pos]) && $tokens[$pos] !== ';') {
322 1
					$namespace .= $tokens[$pos][1];
323 1
						$pos++;
324 1
				}
325 1
				$namespace = '\\' . $namespace . '\\';
326 1
			}
327 1
			if ($tokens->isEqualToToken(T_CLASS, $key)) {
328
				//mark class name for all following functions
329 1
				$className = $namespace . $tokens[$key+2][1];
330 1
			}
331
332
			//TODO we need to filter out closures
333
334 1
			if ($tokens->isEqualToToken(T_FUNCTION, $key)) {
335 1
				if ($className) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
336 1
					$functionName = $className . '::' . $tokens[$key+2][1];
337
					try {
338 1
						$reflection = new ReflectionMethod($className, $tokens[$key+2][1]);
339 1
					} catch (ReflectionException $e) {
340 1
						break;
341
					}
342
343
				} else {
344 1
					$functionName = $tokens[$key+2][1];
345
					try {
346 1
						$reflection = new ReflectionFunction($functionName);
347 1
					} catch (ReflectionException $e) {
348
						break;
349
					}
350
				}
351
352
				//check if non empty version and try go guess
353
				$data = array(
354 1
					'name' => $functionName,
355 1
					'version' => $version,
356 1
					'file' => $file->getPathname(),
357 1
					'line' => $token[2],
358 1
				);
359
360 1
				$docBlock = $reflection->getDocComment();
361 1
				if ($docBlock) {
362 1
					$info = self::getDeprecatedInfoFromDocBlock($docBlock);
363 1
					if (!$info) {
364 1
						if ($version) {
365
							// no details, but we have version, so everything is deprecated here
366
							$info = array(
367 1
								'deprecated' => true,
368 1
								'version' => $version,
369 1
								'fixinfoshort' => false,
370 1
							);
371 1
						} else {
372
							//skipping - not deprecated
373
							continue;
374
						}
375 1
					}
376 1
					$data = array_merge($data, $info);
377 1
				}
378
379 1
				$functs[strtolower($functionName)] = new CodeReview_Issues_Deprecated($data);
380 1
			}
381 1
		}
382 1
		return $functs;
383
	}
384
385
	/**
386
	 * Redurns deprecated functions from particular file.
387
	 *
388
	 * @param PhpFileParser $tokens
389
	 * @param SplFileInfo   $file
390
	 * @param               $version
391
	 * @return array
392
	 */
393 1
	private static function getPrivateFunctionsFromTokens(PhpFileParser $tokens, SplFileInfo $file) {
394 1
		$namespace = '';
395 1
		$className = null;
396 1
		$functs = array();
397 1
		foreach ($tokens as $key => $token) {
398 1
			if ($tokens->isEqualToToken(T_INTERFACE, $key)) {
399
				//we don't process interfaces for deprecated functions
400
				break;
401
			}
402 1
			if ($tokens->isEqualToToken(T_NAMESPACE, $key)) {
403 1
				$pos = $key+2;
404 1
				$namespace = '';
405 1
				while (isset($tokens[$pos]) && $tokens[$pos] !== ';') {
406 1
					$namespace .= $tokens[$pos][1];
407 1
					$pos++;
408 1
				}
409 1
				$namespace = '\\' . $namespace . '\\';
410 1
			}
411 1
			if ($tokens->isEqualToToken(T_CLASS, $key)) {
412
				//mark class name for all following functions
413 1
				$className = $namespace . $tokens[$key+2][1];
414 1
			}
415
416 1
			if ($tokens->isEqualToToken(T_FUNCTION, $key)) {
417 1
				if ($className) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
418 1
					$functionName = $className . '::' . $tokens[$key+2][1];
419
					try {
420 1
						$reflection = new ReflectionMethod($className, $tokens[$key+2][1]);
421 1
					} catch (ReflectionException $e) {
422 1
						break;
423
					}
424
				} else {
425 1
					$functionName = $tokens[$key+2][1];
426
					try {
427 1
						$reflection = new ReflectionFunction($functionName);
428 1
					} catch (ReflectionException $e) {
429
						break;
430
					}
431
				}
432
433
				//check if non empty version and try go guess
434
				$data = array(
435 1
					'name' => $functionName,
436 1
					'file' => $file->getPathname(),
437 1
					'line' => $token[2],
438 1
				);
439
440 1
				$docBlock = $reflection->getDocComment();
441 1
				if ($docBlock) {
442 1
					if (preg_match('/@access\s+private/', $docBlock) < 1) {
443
						//skipping - not private
444 1
						continue;
445
					}
446 1
					$data = new CodeReview_Issues_Private($data);
447 1
				} else {
448
					//non documented means private
449 1
					$data = new CodeReview_Issues_NotDocumented($data);
450
				}
451
452 1
				$functs[strtolower($functionName)] = $data;
453 1
			}
454 1
		}
455 1
		return $functs;
456
	}
457
458
	/**
459
	 * Returns a list of plugin directory names from a base directory.
460
	 * Copied from 1.9 core due to elgg_get_plugin_ids_in_dir removal in 1.9
461
	 *
462
	 * @param string $dir A dir to scan for plugins. Defaults to config's plugins_path.
463
	 *                    Must have a trailing slash.
464
	 *
465
	 * @return array Array of directory names (not full paths)
466
	 */
467 1
	public static function getPluginDirsInDir($dir = null) {
468 1
		if (!$dir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dir of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
469
			$dir = self::$config['pluginspath'];
470
		}
471
472 1
		$plugin_dirs = array();
473 1
		$handle = opendir($dir);
474
475 1
		if ($handle) {
476 1
			while ($plugin_dir = readdir($handle)) {
477
				// must be directory and not begin with a .
478 1
				if (substr($plugin_dir, 0, 1) !== '.' && is_dir($dir . $plugin_dir)) {
479 1
					$plugin_dirs[] = $plugin_dir;
480 1
				}
481 1
			}
482 1
		}
483
484 1
		sort($plugin_dirs);
485
486 1
		return $plugin_dirs;
487
	}
488
}