Completed
Push — try/code-signature-diff ( 5a9acd...c1d5fe )
by
unknown
07:54
created

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