Completed
Push — try/code-signature-diff ( c1d5fe...c0ff74 )
by
unknown
70:30 queued 60:02
created

Analyzer::get_differences()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Automattic\Jetpack\Analyzer;
4
5
use PhpParser\Error;
6
use PhpParser\NodeDumper;
7
use PhpParser\ParserFactory;
8
use PhpParser\Node;
9
use PhpParser\Node\Stmt\Function_;
10
use PhpParser\Node\Stmt\ClassMethod_;
11
use PhpParser\NodeTraverser;
12
use PhpParser\NodeVisitorAbstract;
13
use PhpParser\NodeVisitor\NameResolver;
14
15
// const STATE_NONE = 0;
16
// const STATE_CLASS_DECLARATION = 1;
17
18
const VIS_PUBLIC  = 0;
19
const VIS_PRIVATE = 1;
20
21
class Analyzer extends NodeVisitorAbstract {
22
	private $declarations;
23
	private $base_path;
24
	private $current_path;
25
	private $current_relative_path;
26
	private $current_class;
27
28
	function __construct( $base_path ) {
29
		$this->parser       = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 );
30
		$this->declarations = array();
31
		$this->base_path    = $this->slashit( $base_path );
32
	}
33
34
	private function slashit( $path ) {
35
		$path .= ( substr( $path, -1 ) == '/' ? '' : '/' );
36
		return $path;
37
	}
38
39
	protected function add_declaration( $declaration ) {
40
		$this->declarations[] = $declaration;
41
	}
42
43
	public function print_declarations() {
44
		echo $this->save_declarations( 'php://memory' );
45
	}
46
47
	/**
48
	 * Saves the declarations to a file and returns the file contents
49
	 */
50
	public function save_declarations( $file_path ) {
51
		$handle = fopen( $file_path, 'r+');
52
		foreach ( $this->declarations as $dec ) {
53
			fputcsv( $handle, $dec->to_csv_array() );
54
		}
55
		rewind( $handle );
56
		$contents = stream_get_contents( $handle );
57
		fclose( $handle );
58
		return $contents;
59
	}
60
61
	public function get_declarations() {
62
		return $this->declarations;
63
	}
64
65
	public function load_declarations( $file_path ) {
66
		$row = 1;
67
		if ( ( $handle = fopen( $file_path , "r" ) ) !== FALSE ) {
68
			while ( ( $data = fgetcsv( $handle, 1000, "," ) ) !== FALSE ) {
69
				$num = count( $data );
0 ignored issues
show
Unused Code introduced by
$num is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
70
				list( $type, $file, $line, $class_name, $name, $static, $params_json ) = $data;
71
72
				switch( $type ) {
73
					case 'class':
74
						$this->add_declaration( new Class_Declaration( $file, $line, $class_name ) );
75
						break;
76
77
					case 'property':
78
						$this->add_declaration( new Class_Property_Declaration( $file, $line, $class_name, $name, $static ) );
79
						break;
80
81 View Code Duplication
					case 'method':
82
						$params = json_decode( $params_json, TRUE );
83
						$declaration = new Class_Method_Declaration( $file, $line, $class_name, $name, $static );
84
						if ( is_array( $params ) ) {
85
							foreach( $params as $param ) {
86
								$declaration->add_param( $param->name, $param->default, $param->type, $param->byRef, $param->variadic );
87
							}
88
						}
89
90
						$this->add_declaration( $declaration );
91
92
						break;
93
94 View Code Duplication
					case 'function':
95
						$params = json_decode( $params_json, TRUE );
96
						$declaration = new Function_Declaration( $file, $line, $name );
97
						if ( is_array( $params ) ) {
98
							foreach( $params as $param ) {
99
								$declaration->add_param( $param->name, $param->default, $param->type, $param->byRef, $param->variadic );
100
							}
101
						}
102
103
						$this->add_declaration( $declaration );
104
105
						break;
106
				}
107
				$row++;
108
			}
109
			fclose($handle);
110
		}
111
	}
112
113
	public function scan() {
114
		$exclude = array( '.git', 'vendor', 'tests', 'docker', 'bin', 'scss', 'images', 'docs', 'languages', 'node_modules' );
115
		$filter  = function ( $file, $key, $iterator ) use ( $exclude ) {
116
			if ( $iterator->hasChildren() && ! in_array( $file->getFilename(), $exclude ) ) {
117
				return true;
118
			}
119
			return $file->isFile();
120
		};
121
122
		$inner_iterator = new \RecursiveDirectoryIterator( $this->base_path, \RecursiveDirectoryIterator::SKIP_DOTS );
123
124
		$iterator = new \RecursiveIteratorIterator(
125
			new \RecursiveCallbackFilterIterator( $inner_iterator, $filter )
126
		);
127
128
		$display = array( 'php' );
129
		foreach ( $iterator as $file ) {
130
			if ( in_array( strtolower( array_pop( explode( '.', $file ) ) ), $display ) ) {
0 ignored issues
show
Bug introduced by
explode('.', $file) cannot be passed to array_pop() as the parameter $array expects a reference.
Loading history...
131
				$this->file( $file );
132
			}
133
		}
134
	}
135
136
	public function file( $file_path ) {
137
		$this->current_path = $file_path;
138
		$this->current_relative_path = str_replace( $this->base_path, '', $file_path );
139
140
		$source = file_get_contents( $file_path );
141
		try {
142
			$ast = $this->parser->parse( $source );
143
		} catch ( Error $error ) {
0 ignored issues
show
Bug introduced by
The class PhpParser\Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
144
			echo "Parse error: {$error->getMessage()}\n";
145
			return;
146
		}
147
148
		// $dumper = new NodeDumper;
149
		// echo $dumper->dump($ast) . "\n";
150
151
		$traverser = new NodeTraverser();
152
		$nameResolver = new NameResolver();
153
		$traverser->addVisitor( $nameResolver );
154
155
		// Resolve names
156
		$ast = $traverser->traverse( $ast );
157
158
		// now scan for public methods etc
159
		$traverser = new NodeTraverser();
160
		$traverser->addVisitor( $this );
161
		$ast = $traverser->traverse( $ast );
0 ignored issues
show
Unused Code introduced by
$ast is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
162
	}
163
164
	public function enterNode( Node $node ) {
165
		if ( $node instanceof Node\Stmt\Class_ ) {
0 ignored issues
show
Bug introduced by
The class PhpParser\Node\Stmt\Class_ does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
166
			// $this->current_class = $node->name->name;
167
			$this->current_class = implode( '\\', $node->namespacedName->parts );
168
169
			$this->add_declaration( new Class_Declaration( $this->current_relative_path, $node->getLine(), $node->name->name ) );
170
		}
171
		if ( $node instanceof Node\Stmt\Property && $node->isPublic() ) {
0 ignored issues
show
Bug introduced by
The class PhpParser\Node\Stmt\Property does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
172
			$this->add_declaration( new Class_Property_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->props[0]->name->name, $node->isStatic() ) );
173
		}
174
		if ( $node instanceof Node\Stmt\ClassMethod && $node->isPublic() ) {
0 ignored issues
show
Bug introduced by
The class PhpParser\Node\Stmt\ClassMethod does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
175
			// ClassMethods are also listed inside interfaces, which means current_class is null
176
			// so we ignore these
177
			if ( ! $this->current_class ) {
178
				return;
179
			}
180
			$method = new Class_Method_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->name->name, $node->isStatic() );
181 View Code Duplication
			foreach ( $node->getParams() as $param ) {
182
				$method->add_param( $param->var->name, $param->default, $param->type, $param->byRef, $param->variadic );
183
			}
184
			$this->add_declaration( $method );
185
		}
186
		if ( $node instanceof Node\Stmt\Function_ ) {
0 ignored issues
show
Bug introduced by
The class PhpParser\Node\Stmt\Function_ does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
187
			$function = new Function_Declaration( $this->current_relative_path, $node->getLine(), $node->name->name );
188 View Code Duplication
			foreach ( $node->getParams() as $param ) {
189
				$function->add_param( $param->var->name, $param->default, $param->type, $param->byRef, $param->variadic );
190
			}
191
			$this->add_declaration( $function  );
192
		}
193
	}
194
195
	public function leaveNode( Node $node ) {
196
		if ( $node instanceof Node\Stmt\Class_ ) {
0 ignored issues
show
Bug introduced by
The class PhpParser\Node\Stmt\Class_ does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
197
			$this->current_class = null;
198
		}
199
	}
200
201
	public function find_differences( $analyzer ) {
202
		// check the analyzers have been run
203
		if ( count( $analyzer->get_declarations() ) === 0 ) {
204
			$analyzer->scan();
205
		}
206
207
		if ( count( $this->get_declarations() ) === 0 ) {
208
			$this->scan();
209
		}
210
211
		$differences = new Declaration_Differences();
212
		$total = 0;
213
		// for each declaration, see if it exists in the current analyzer's declarations
214
		// if not, add it to the list of differences - either as missing or different
215
		foreach( $analyzer->get_declarations() as $prev_declaration ) {
216
			$matched = false;
217
			foreach( $this->declarations as $declaration ) {
218
				if ( $prev_declaration->match( $declaration ) ) {
219
					$matched = true;
220
					break;
221
				}
222
			}
223
			if ( ! $matched ) {
224
				$differences->add_difference( new Difference_Missing( $prev_declaration ) );
225
			}
226
			$total += 1;
227
		}
228
229
		echo "Total: $total\n";
230
		echo "Missing: " . count( $differences->get_differences() ) . "\n";
231
		return $differences;
232
	}
233
234
	public function check_file_compatibility( $file_path ) {
235
		$source = file_get_contents( $file_path );
236
		try {
237
			$ast = $this->parser->parse( $source );
238
		} catch ( Error $error ) {
0 ignored issues
show
Bug introduced by
The class PhpParser\Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
239
			echo "Parse error: {$error->getMessage()}\n";
240
			return;
241
		}
242
243
		// $dumper = new NodeDumper;
244
		// echo $dumper->dump($ast) . "\n";
245
246
		$traverser = new NodeTraverser();
247
		$invocation_finder = new Invocation_Finder( $this );
248
		$traverser->addVisitor( $invocation_finder );
249
		$ast = $traverser->traverse( $ast );
0 ignored issues
show
Unused Code introduced by
$ast is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
250
	}
251
}
252
253
class Declaration_Differences {
254
	private $differences;
255
	private $parser;
256
257
	function __construct() {
258
		$this->parser       = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 );
259
		$this->differences = array();
260
	}
261
262
	public function get_differences() {
263
		return $this->differences;
264
	}
265
266
	public function add_difference( $difference ) {
267
		$this->differences[] = $difference;
268
	}
269
}
270
271
class Invocation_Finder extends NodeVisitorAbstract {
272
	public $analyzer;
273
274
	public function __construct( $analyzer ) {
275
		$this->analyzer = $analyzer;
276
	}
277
278
	public function enterNode( Node $node ) {
279
280
		// if ( $node instanceof Node\Stmt\Class_ ) {
281
		// 	$this->current_class = $node->name->name;
282
		// 	$this->add_declaration( new Class_Declaration( $this->current_relative_path, $node->getLine(), $node->name->name ) );
283
		// }
284
		// if ( $node instanceof Node\Stmt\Property && $node->isPublic() ) {
285
		// 	$this->add_declaration( new Class_Property_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->props[0]->name->name, $node->isStatic() ) );
286
		// }
287
		// if ( $node instanceof Node\Stmt\ClassMethod && $node->isPublic() ) {
288
		// 	// ClassMethods are also listed inside interfaces, which means current_class is null
289
		// 	// so we ignore these
290
		// 	if ( ! $this->current_class ) {
291
		// 		return;
292
		// 	}
293
		// 	$method = new Class_Method_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->name->name, $node->isStatic() );
294
		// 	foreach ( $node->getParams() as $param ) {
295
		// 		$method->add_param( $param->var->name, $param->default, $param->type, $param->byRef, $param->variadic );
296
		// 	}
297
		// 	$this->add_declaration( $method );
298
		// }
299
	}
300
301
	public function leaveNode( Node $node ) {
302
		// if ( $node instanceof Node\Stmt\Class_ ) {
303
		// 	$this->current_class = null;
304
		// }
305
	}
306
}
307
308
class Difference_Missing {
309
	public $declaration;
310
311
	function __construct( $declaration ) {
312
		$this->declaration = $declaration;
313
	}
314
315
	public function to_csv() {
316
		return 'missing,' . $this->declaration->path . ',' . $this->declaration->type() . ',' . $this->declaration->display_name();
317
	}
318
}
319
320
/*
321
class Difference_Params {
322
	public $declaration;
323
324
	function __construct( $declaration ) {
325
		$this->declaration = $declaration;
326
	}
327
328
	public function to_csv() {
329
		return 'params,' . implode( ',', $this->declaration->to_csv_array() );
330
	}
331
}
332
*/
333
334
abstract class Declaration {
335
	public $path;
336
	public $line;
337
338
	function __construct( $path, $line ) {
339
		$this->path = $path;
340
		$this->line = $line;
341
	}
342
343
	function match( $other ) {
344
		return get_class( $other ) === get_class( $this )
345
			&& $other->name === $this->name
0 ignored issues
show
Bug introduced by
The property name does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
346
			&& $other->path === $this->path;
347
	}
348
349
	// a simple name, like 'method'
350
	abstract function type();
351
352
	// e.g. Jetpack::get_file_url_for_environment()
353
	abstract function display_name();
354
}
355
356
class Class_Declaration extends Declaration {
357
	public $class_name;
358
359
	function __construct( $path, $line, $class_name ) {
360
		$this->class_name = $class_name;
361
		parent::__construct( $path, $line );
362
	}
363
364
	function to_csv_array() {
365
		return array(
366
			$this->type(),
367
			$this->path,
368
			$this->line,
369
			$this->class_name
370
		);
371
	}
372
373
	function type() {
374
		return 'class';
375
	}
376
377
	function display_name() {
378
		return $this->class_name;
379
	}
380
}
381
382
/**
383
 * We only log public class methods, whether they are static, and their parameters
384
 */
385
class Class_Method_Declaration extends Declaration {
386
	public $class_name;
387
	public $name;
388
	public $params;
389
	public $static;
390
391 View Code Duplication
	function __construct( $path, $line, $class_name, $name, $static ) {
392
		$this->class_name = $class_name;
393
		$this->name = $name;
394
		$this->params = array();
395
		$this->static = $static;
396
		parent::__construct( $path, $line );
397
	}
398
399
	// TODO: parse "default" into comparable string form?
400
	function add_param( $name, $default, $type, $byRef, $variadic ) {
401
		$this->params[] = (object) compact( 'name', 'default', 'type', 'byRef', 'variadic' );
402
	}
403
404 View Code Duplication
	function to_csv_array() {
405
		return array(
406
			$this->type(),
407
			$this->path,
408
			$this->line,
409
			$this->class_name,
410
			$this->name,
411
			$this->static,
412
			json_encode( $this->params )
413
		);
414
	}
415
416
	function type() {
417
		return 'method';
418
	}
419
420
	function display_name() {
421
		$sep = $this->static ? '::' : '->';
422
		return $this->class_name . $sep . $this->name . '(' . implode( ', ', array_map( function( $param ) { return '$' . $param->name; }, $this->params ) ) . ')';
423
	}
424
}
425
426
/**
427
 * We only log public class variables
428
 */
429
class Class_Property_Declaration extends Declaration {
430
	public $class_name;
431
	public $name;
432
	public $static;
433
434 View Code Duplication
	function __construct( $path, $line, $class_name, $name, $static ) {
435
		$this->class_name = $class_name;
436
		$this->name = $name;
437
		$this->static = $static;
438
		parent::__construct( $path, $line );
439
	}
440
441 View Code Duplication
	function to_csv_array() {
442
		return array(
443
			$this->type(),
444
			$this->path,
445
			$this->line,
446
			$this->class_name,
447
			$this->name,
448
			$this->static,
449
			''
450
		);
451
	}
452
453
	function type() {
454
		return 'property';
455
	}
456
457
	function display_name() {
458
		$sep = $this->static ? '::$' : '->';
459
		return $this->class_name . $sep . $this->name;
460
	}
461
}
462
463
/**
464
 * We only log public class methods, whether they are static, and their parameters
465
 */
466
class Function_Declaration extends Declaration {
467
	public $name;
468
	public $params;
469
470
	function __construct( $path, $line, $name ) {
471
		$this->name = $name;
472
		$this->params = array();
473
		parent::__construct( $path, $line );
474
	}
475
476
	// TODO: parse "default" into comparable string form?
477
	function add_param( $name, $default, $type, $byRef, $variadic ) {
478
		$this->params[] = (object) compact( 'name', 'default', 'type', 'byRef', 'variadic' );
479
	}
480
481
	function to_csv_array() {
482
		return array(
483
			$this->type(),
484
			$this->path,
485
			$this->line,
486
			'',
487
			$this->name,
488
			'',
489
			json_encode( $this->params )
490
		);
491
	}
492
493
	function type() {
494
		return 'function';
495
	}
496
497
	function display_name() {
498
		return $this->name . '(' . implode( ', ', array_map( function( $param ) { return '$' . $param->name; }, $this->params ) ) . ')';
499
	}
500
}