Completed
Push — try/code-signature-diff ( c0ff74...5d24fe )
by
unknown
118:01 queued 109:00
created

Declaration_Differences::get()   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
27
	function __construct( $base_path ) {
28
		$this->parser       = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 );
29
		$this->base_path    = $this->slashit( $base_path );
30
	}
31
32
	private function slashit( $path ) {
33
		$path .= ( substr( $path, -1 ) == '/' ? '' : '/' );
34
		return $path;
35
	}
36
37
	public function scan() {
38
		$declarations = new Declarations();
39
40
		$exclude = array( '.git', 'vendor', 'tests', 'docker', 'bin', 'scss', 'images', 'docs', 'languages', 'node_modules' );
41
		$filter  = function ( $file, $key, $iterator ) use ( $exclude ) {
42
			if ( $iterator->hasChildren() && ! in_array( $file->getFilename(), $exclude ) ) {
43
				return true;
44
			}
45
			return $file->isFile();
46
		};
47
48
		$inner_iterator = new \RecursiveDirectoryIterator( $this->base_path, \RecursiveDirectoryIterator::SKIP_DOTS );
49
50
		$iterator = new \RecursiveIteratorIterator(
51
			new \RecursiveCallbackFilterIterator( $inner_iterator, $filter )
52
		);
53
54
		$display = array( 'php' );
55
		foreach ( $iterator as $file ) {
56
			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...
57
				$this->file( $file, $declarations );
58
			}
59
		}
60
61
		return $declarations;
62
	}
63
64
	public function file( $file_path, $declarations ) {
65
		$this->current_path = $file_path;
66
		$current_relative_path = str_replace( $this->base_path, '', $file_path );
67
68
		$source = file_get_contents( $file_path );
69
		try {
70
			$ast = $this->parser->parse( $source );
71
		} 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...
72
			echo "Parse error: {$error->getMessage()}\n";
73
			return;
74
		}
75
76
		// $dumper = new NodeDumper;
77
		// echo $dumper->dump($ast) . "\n";
78
79
		$traverser = new NodeTraverser();
80
		$nameResolver = new NameResolver();
81
		$traverser->addVisitor( $nameResolver );
82
83
		// Resolve names
84
		$ast = $traverser->traverse( $ast );
85
86
		// now scan for public methods etc
87
		$traverser = new NodeTraverser();
88
		$declaration_visitor = new Declaration_Visitor( $current_relative_path, $declarations );
89
		$traverser->addVisitor( $declaration_visitor );
90
		$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...
91
	}
92
93
	public function check_file_compatibility( $file_path ) {
94
		$source = file_get_contents( $file_path );
95
		try {
96
			$ast = $this->parser->parse( $source );
97
		} 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...
98
			echo "Parse error: {$error->getMessage()}\n";
99
			return;
100
		}
101
102
		// $dumper = new NodeDumper;
103
		// echo $dumper->dump($ast) . "\n";
104
105
		$traverser = new NodeTraverser();
106
		$invocation_finder = new Invocation_Visitor( $this );
107
		$traverser->addVisitor( $invocation_finder );
108
		$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...
109
	}
110
}
111
112
class Declarations {
113
	private $declarations;
114
	// private $parser;
115
116
	function __construct() {
117
		// $this->parser       = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 );
118
		$this->declarations = array();
119
	}
120
121
	public function get() {
122
		return $this->declarations;
123
	}
124
125
	public function add( $declaration ) {
126
		$this->declarations[] = $declaration;
127
	}
128
129
	public function print() {
130
		echo $this->save( 'php://memory' );
131
	}
132
133
	/**
134
	 * Saves the declarations to a file and returns the file contents
135
	 */
136
	public function save( $file_path ) {
137
		$handle = fopen( $file_path, 'r+');
138
		foreach ( $this->declarations as $dec ) {
139
			fputcsv( $handle, $dec->to_csv_array() );
140
		}
141
		rewind( $handle );
142
		$contents = stream_get_contents( $handle );
143
		fclose( $handle );
144
		return $contents;
145
	}
146
147
	public function load( $file_path ) {
148
		$row = 1;
149
		if ( ( $handle = fopen( $file_path , "r" ) ) !== FALSE ) {
150
			while ( ( $data = fgetcsv( $handle, 1000, "," ) ) !== FALSE ) {
151
				$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...
152
				list( $type, $file, $line, $class_name, $name, $static, $params_json ) = $data;
153
154
				switch( $type ) {
155
					case 'class':
156
						$this->add( new Class_Declaration( $file, $line, $class_name ) );
157
						break;
158
159
					case 'property':
160
						$this->add( new Class_Property_Declaration( $file, $line, $class_name, $name, $static ) );
161
						break;
162
163 View Code Duplication
					case 'method':
164
						$params = json_decode( $params_json, TRUE );
165
						$declaration = new Class_Method_Declaration( $file, $line, $class_name, $name, $static );
166
						if ( is_array( $params ) ) {
167
							foreach( $params as $param ) {
168
								$declaration->add_param( $param->name, $param->default, $param->type, $param->byRef, $param->variadic );
169
							}
170
						}
171
172
						$this->add( $declaration );
173
174
						break;
175
176 View Code Duplication
					case 'function':
177
						$params = json_decode( $params_json, TRUE );
178
						$declaration = new Function_Declaration( $file, $line, $name );
179
						if ( is_array( $params ) ) {
180
							foreach( $params as $param ) {
181
								$declaration->add_param( $param->name, $param->default, $param->type, $param->byRef, $param->variadic );
182
							}
183
						}
184
185
						$this->add( $declaration );
186
187
						break;
188
				}
189
				$row++;
190
			}
191
			fclose( $handle );
192
		}
193
	}
194
195
	public function find_differences( $prev_declarations ) {
196
197
		$differences = new Declaration_Differences();
198
		$total = 0;
199
		// for each declaration, see if it exists in the current analyzer's declarations
200
		// if not, add it to the list of differences - either as missing or different
201
		foreach( $prev_declarations->get() as $prev_declaration ) {
202
			$matched = false;
203
			foreach( $this->declarations as $declaration ) {
204
				if ( $prev_declaration->match( $declaration ) ) {
205
					$matched = true;
206
					break;
207
				}
208
			}
209
			if ( ! $matched ) {
210
				$differences->add( new Difference_Missing( $prev_declaration ) );
211
			}
212
			$total += 1;
213
		}
214
215
		echo "Total: $total\n";
216
		echo "Missing: " . count( $differences->get() ) . "\n";
217
		return $differences;
218
	}
219
}
220
221
class Declaration_Differences {
222
	private $differences;
223
	// private $parser;
224
225
	function __construct() {
226
		// $this->parser       = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 );
227
		$this->differences = array();
228
	}
229
230
	public function get() {
231
		return $this->differences;
232
	}
233
234
	public function add( $difference ) {
235
		$this->differences[] = $difference;
236
	}
237
}
238
239
class Declaration_Visitor extends NodeVisitorAbstract {
240
	private $current_class;
241
	private $declarations;
242
	private $current_relative_path;
243
244
	public function __construct( $current_relative_path, $declarations ) {
245
		$this->current_relative_path = $current_relative_path;
246
		$this->declarations = $declarations;
247
	}
248
249
	public function enterNode( Node $node ) {
250
		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...
251
			// $this->current_class = $node->name->name;
252
			$this->current_class = implode( '\\', $node->namespacedName->parts );
253
254
			$this->declarations->add( new Class_Declaration( $this->current_relative_path, $node->getLine(), $node->name->name ) );
255
		}
256
		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...
257
			$this->declarations->add( new Class_Property_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->props[0]->name->name, $node->isStatic() ) );
258
		}
259
		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...
260
			// ClassMethods are also listed inside interfaces, which means current_class is null
261
			// so we ignore these
262
			if ( ! $this->current_class ) {
263
				return;
264
			}
265
			$method = new Class_Method_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->name->name, $node->isStatic() );
266 View Code Duplication
			foreach ( $node->getParams() as $param ) {
267
				$method->add_param( $param->var->name, $param->default, $param->type, $param->byRef, $param->variadic );
268
			}
269
			$this->declarations->add( $method );
270
		}
271
		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...
272
			$function = new Function_Declaration( $this->current_relative_path, $node->getLine(), $node->name->name );
273 View Code Duplication
			foreach ( $node->getParams() as $param ) {
274
				$function->add_param( $param->var->name, $param->default, $param->type, $param->byRef, $param->variadic );
275
			}
276
			$this->declarations->add( $function  );
277
		}
278
	}
279
280
	public function leaveNode( Node $node ) {
281
		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...
282
			$this->current_class = null;
283
		}
284
	}
285
}
286
287
class Invocation_Visitor extends NodeVisitorAbstract {
288
	public $analyzer;
289
290
	public function __construct( $analyzer ) {
291
		$this->analyzer = $analyzer;
292
	}
293
294
	public function enterNode( Node $node ) {
295
296
		// if ( $node instanceof Node\Stmt\Class_ ) {
297
		// 	$this->current_class = $node->name->name;
298
		// 	$this->add( new Class_Declaration( $this->current_relative_path, $node->getLine(), $node->name->name ) );
299
		// }
300
		// if ( $node instanceof Node\Stmt\Property && $node->isPublic() ) {
301
		// 	$this->add( new Class_Property_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->props[0]->name->name, $node->isStatic() ) );
302
		// }
303
		// if ( $node instanceof Node\Stmt\ClassMethod && $node->isPublic() ) {
304
		// 	// ClassMethods are also listed inside interfaces, which means current_class is null
305
		// 	// so we ignore these
306
		// 	if ( ! $this->current_class ) {
307
		// 		return;
308
		// 	}
309
		// 	$method = new Class_Method_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->name->name, $node->isStatic() );
310
		// 	foreach ( $node->getParams() as $param ) {
311
		// 		$method->add_param( $param->var->name, $param->default, $param->type, $param->byRef, $param->variadic );
312
		// 	}
313
		// 	$this->add( $method );
314
		// }
315
	}
316
317
	public function leaveNode( Node $node ) {
318
		// if ( $node instanceof Node\Stmt\Class_ ) {
319
		// 	$this->current_class = null;
320
		// }
321
	}
322
}
323
324
class Difference_Missing {
325
	public $declaration;
326
327
	function __construct( $declaration ) {
328
		$this->declaration = $declaration;
329
	}
330
331
	public function to_csv() {
332
		return 'missing,' . $this->declaration->path . ',' . $this->declaration->type() . ',' . $this->declaration->display_name();
333
	}
334
}
335
336
/*
337
class Difference_Params {
338
	public $declaration;
339
340
	function __construct( $declaration ) {
341
		$this->declaration = $declaration;
342
	}
343
344
	public function to_csv() {
345
		return 'params,' . implode( ',', $this->declaration->to_csv_array() );
346
	}
347
}
348
*/
349
350
abstract class Declaration {
351
	public $path;
352
	public $line;
353
354
	function __construct( $path, $line ) {
355
		$this->path = $path;
356
		$this->line = $line;
357
	}
358
359
	function match( $other ) {
360
		return get_class( $other ) === get_class( $this )
361
			&& $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...
362
			&& $other->path === $this->path;
363
	}
364
365
	// a simple name, like 'method'
366
	abstract function type();
367
368
	// e.g. Jetpack::get_file_url_for_environment()
369
	abstract function display_name();
370
}
371
372
class Class_Declaration extends Declaration {
373
	public $class_name;
374
375
	function __construct( $path, $line, $class_name ) {
376
		$this->class_name = $class_name;
377
		parent::__construct( $path, $line );
378
	}
379
380
	function to_csv_array() {
381
		return array(
382
			$this->type(),
383
			$this->path,
384
			$this->line,
385
			$this->class_name
386
		);
387
	}
388
389
	function type() {
390
		return 'class';
391
	}
392
393
	function display_name() {
394
		return $this->class_name;
395
	}
396
}
397
398
/**
399
 * We only log public class methods, whether they are static, and their parameters
400
 */
401
class Class_Method_Declaration extends Declaration {
402
	public $class_name;
403
	public $name;
404
	public $params;
405
	public $static;
406
407 View Code Duplication
	function __construct( $path, $line, $class_name, $name, $static ) {
408
		$this->class_name = $class_name;
409
		$this->name = $name;
410
		$this->params = array();
411
		$this->static = $static;
412
		parent::__construct( $path, $line );
413
	}
414
415
	// TODO: parse "default" into comparable string form?
416
	function add_param( $name, $default, $type, $byRef, $variadic ) {
417
		$this->params[] = (object) compact( 'name', 'default', 'type', 'byRef', 'variadic' );
418
	}
419
420 View Code Duplication
	function to_csv_array() {
421
		return array(
422
			$this->type(),
423
			$this->path,
424
			$this->line,
425
			$this->class_name,
426
			$this->name,
427
			$this->static,
428
			json_encode( $this->params )
429
		);
430
	}
431
432
	function type() {
433
		return 'method';
434
	}
435
436
	function display_name() {
437
		$sep = $this->static ? '::' : '->';
438
		return $this->class_name . $sep . $this->name . '(' . implode( ', ', array_map( function( $param ) { return '$' . $param->name; }, $this->params ) ) . ')';
439
	}
440
}
441
442
/**
443
 * We only log public class variables
444
 */
445
class Class_Property_Declaration extends Declaration {
446
	public $class_name;
447
	public $name;
448
	public $static;
449
450 View Code Duplication
	function __construct( $path, $line, $class_name, $name, $static ) {
451
		$this->class_name = $class_name;
452
		$this->name = $name;
453
		$this->static = $static;
454
		parent::__construct( $path, $line );
455
	}
456
457 View Code Duplication
	function to_csv_array() {
458
		return array(
459
			$this->type(),
460
			$this->path,
461
			$this->line,
462
			$this->class_name,
463
			$this->name,
464
			$this->static,
465
			''
466
		);
467
	}
468
469
	function type() {
470
		return 'property';
471
	}
472
473
	function display_name() {
474
		$sep = $this->static ? '::$' : '->';
475
		return $this->class_name . $sep . $this->name;
476
	}
477
}
478
479
/**
480
 * We only log public class methods, whether they are static, and their parameters
481
 */
482
class Function_Declaration extends Declaration {
483
	public $name;
484
	public $params;
485
486
	function __construct( $path, $line, $name ) {
487
		$this->name = $name;
488
		$this->params = array();
489
		parent::__construct( $path, $line );
490
	}
491
492
	// TODO: parse "default" into comparable string form?
493
	function add_param( $name, $default, $type, $byRef, $variadic ) {
494
		$this->params[] = (object) compact( 'name', 'default', 'type', 'byRef', 'variadic' );
495
	}
496
497
	function to_csv_array() {
498
		return array(
499
			$this->type(),
500
			$this->path,
501
			$this->line,
502
			'',
503
			$this->name,
504
			'',
505
			json_encode( $this->params )
506
		);
507
	}
508
509
	function type() {
510
		return 'function';
511
	}
512
513
	function display_name() {
514
		return $this->name . '(' . implode( ', ', array_map( function( $param ) { return '$' . $param->name; }, $this->params ) ) . ')';
515
	}
516
}