Completed
Push — try/code-signature-diff ( b0bfae...5a9acd )
by
unknown
08:04
created

Invocation_Finder::__construct()   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 1
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
					case 'method':
85
						$params = json_decode( $params_json );
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
				$row++;
98
			}
99
			fclose($handle);
100
		}
101
	}
102
103
	public function scan() {
104
		$exclude = array( '.git', 'vendor', 'tests', 'docker', 'bin', 'scss', 'images', 'docs', 'languages', 'node_modules' );
105
		$filter  = function ( $file, $key, $iterator ) use ( $exclude ) {
106
			if ( $iterator->hasChildren() && ! in_array( $file->getFilename(), $exclude ) ) {
107
				return true;
108
			}
109
			return $file->isFile();
110
		};
111
112
		$inner_iterator = new \RecursiveDirectoryIterator( $this->base_path, \RecursiveDirectoryIterator::SKIP_DOTS );
113
114
		$iterator = new \RecursiveIteratorIterator(
115
			new \RecursiveCallbackFilterIterator( $inner_iterator, $filter )
116
		);
117
118
		$display = array( 'php' );
119
		foreach ( $iterator as $file ) {
120
			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...
121
				$this->file( $file );
122
			}
123
		}
124
	}
125
126
	public function file( $file_path ) {
127
		$this->current_path = $file_path;
128
		$this->current_relative_path = str_replace( $this->base_path, '', $file_path );
129
130
		$source = file_get_contents( $file_path );
131
		try {
132
			$ast = $this->parser->parse( $source );
133
		} 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...
134
			echo "Parse error: {$error->getMessage()}\n";
135
			return;
136
		}
137
138
		// $dumper = new NodeDumper;
139
		// echo $dumper->dump($ast) . "\n";
140
141
		$traverser = new NodeTraverser();
142
		$nameResolver = new NameResolver();
143
		$traverser->addVisitor( $nameResolver );
144
145
		// Resolve names
146
		$ast = $traverser->traverse( $ast );
147
148
		// now scan for public methods etc
149
		$traverser = new NodeTraverser();
150
		$traverser->addVisitor( $this );
151
		$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...
152
	}
153
154 View Code Duplication
	public function enterNode( Node $node ) {
155
		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...
156
			// $this->current_class = $node->name->name;
157
			$this->current_class = implode( '\\', $node->namespacedName->parts );
158
159
			$this->add_declaration( new Class_Declaration( $this->current_relative_path, $node->getLine(), $node->name->name ) );
160
		}
161
		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...
162
			$this->add_declaration( new Class_Property_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->props[0]->name->name, $node->isStatic() ) );
163
		}
164
		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...
165
			// ClassMethods are also listed inside interfaces, which means current_class is null
166
			// so we ignore these
167
			if ( ! $this->current_class ) {
168
				return;
169
			}
170
			$method = new Class_Method_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->name->name, $node->isStatic() );
171
			foreach ( $node->getParams() as $param ) {
172
				$method->add_param( $param->var->name, $param->default, $param->type, $param->byRef, $param->variadic );
173
			}
174
			$this->add_declaration( $method );
175
		}
176
	}
177
178
	public function leaveNode( Node $node ) {
179
		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...
180
			$this->current_class = null;
181
		}
182
	}
183
184
	public function find_differences( $analyzer ) {
185
		// check the analyzers have been run
186
		if ( count( $analyzer->get_declarations() ) === 0 ) {
187
			$analyzer->scan();
188
		}
189
190
		if ( count( $this->get_declarations() ) === 0 ) {
191
			$this->scan();
192
		}
193
194
		$this->differences = array();
195
		$total = 0;
196
		// for each declaration, see if it exists in the current analyzer's declarations
197
		// if not, add it to the list of differences - either as missing or different
198
		foreach( $analyzer->get_declarations() as $prev_declaration ) {
199
			$matched = false;
200
			foreach( $this->declarations as $declaration ) {
201
				if ( $prev_declaration->match( $declaration ) ) {
202
					$matched = true;
203
					break;
204
				}
205
			}
206
			if ( ! $matched ) {
207
				$this->differences[] = new Difference_Missing( $prev_declaration );
208
			}
209
			$total += 1;
210
		}
211
212
		echo "Total: $total\n";
213
		echo "Missing: " . count( $differences ) . "\n";
0 ignored issues
show
Bug introduced by
The variable $differences does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
214
	}
215
216
	public function get_differences() {
217
		return $this->differences;
218
	}
219
220
	public function check_file_compatibility( $file_path ) {
221
		$source = file_get_contents( $file_path );
222
		try {
223
			$ast = $this->parser->parse( $source );
224
		} 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...
225
			echo "Parse error: {$error->getMessage()}\n";
226
			return;
227
		}
228
229
		// $dumper = new NodeDumper;
230
		// echo $dumper->dump($ast) . "\n";
231
232
		$traverser = new NodeTraverser();
233
		$invocation_finder = new Invocation_Finder( $this );
234
		$traverser->addVisitor( $invocation_finder );
235
		$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...
236
	}
237
}
238
239
class Invocation_Finder extends NodeVisitorAbstract {
240
	public $analyzer;
241
242
	public function __construct( $analyzer ) {
243
		$this->analyzer = $analyzer;
244
	}
245
246 View Code Duplication
	public function enterNode( Node $node ) {
247
		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...
248
			$this->current_class = $node->name->name;
249
			$this->add_declaration( new Class_Declaration( $this->current_relative_path, $node->getLine(), $node->name->name ) );
250
		}
251
		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...
252
			$this->add_declaration( new Class_Property_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->props[0]->name->name, $node->isStatic() ) );
253
		}
254
		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...
255
			// ClassMethods are also listed inside interfaces, which means current_class is null
256
			// so we ignore these
257
			if ( ! $this->current_class ) {
258
				return;
259
			}
260
			$method = new Class_Method_Declaration( $this->current_relative_path, $node->getLine(), $this->current_class, $node->name->name, $node->isStatic() );
261
			foreach ( $node->getParams() as $param ) {
262
				$method->add_param( $param->var->name, $param->default, $param->type, $param->byRef, $param->variadic );
263
			}
264
			$this->add_declaration( $method );
265
		}
266
	}
267
268
	public function leaveNode( Node $node ) {
269
		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...
270
			$this->current_class = null;
271
		}
272
	}
273
}
274
275
class Difference_Missing {
276
	public $declaration;
277
278
	function __construct( $declaration ) {
279
		$this->declaration = $declaration;
280
	}
281
282
	public function to_csv() {
283
		return 'missing,' . $this->declaration->path . ',' . $this->declaration->type() . ',' . $this->declaration->display_name();
284
	}
285
}
286
287
/*
288
class Difference_Params {
289
	public $declaration;
290
291
	function __construct( $declaration ) {
292
		$this->declaration = $declaration;
293
	}
294
295
	public function to_csv() {
296
		return 'params,' . implode( ',', $this->declaration->to_csv_array() );
297
	}
298
}
299
*/
300
301
abstract class Declaration {
302
	public $path;
303
	public $line;
304
305
	function __construct( $path, $line ) {
306
		$this->path = $path;
307
		$this->line = $line;
308
	}
309
310
	function match( $other ) {
311
		return get_class( $other ) === get_class( $this )
312
			&& $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...
313
			&& $other->path === $this->path;
314
	}
315
316
	abstract function type();
317
	// e.g. Jetpack::get_file_url_for_environment()
318
	abstract function display_name();
319
}
320
321
class Class_Declaration extends Declaration {
322
	public $class_name;
323
324
	function __construct( $path, $line, $class_name ) {
325
		$this->class_name = $class_name;
326
		parent::__construct( $path, $line );
327
	}
328
329
	function to_csv_array() {
330
		return array(
331
			$this->type(),
332
			$this->path,
333
			$this->line,
334
			$this->class_name
335
		);
336
	}
337
338
	function type() {
339
		return 'class';
340
	}
341
342
	function display_name() {
343
		return $this->class_name;
344
	}
345
}
346
347
/**
348
 * We only log public class methods, whether they are static, and their parameters
349
 */
350
class Class_Method_Declaration extends Declaration {
351
	public $class_name;
352
	public $name;
353
	public $params;
354
	public $static;
355
356 View Code Duplication
	function __construct( $path, $line, $class_name, $name, $static ) {
357
		$this->class_name = $class_name;
358
		$this->name = $name;
359
		$this->params = array();
360
		$this->static = $static;
361
		parent::__construct( $path, $line );
362
	}
363
364
	// TODO: parse "default" into comparable string form?
365
	function add_param( $name, $default, $type, $byRef, $variadic ) {
366
		$this->params[] = (object) compact( 'name', 'default', 'type', 'byRef', 'variadic' );
367
	}
368
369 View Code Duplication
	function to_csv_array() {
370
		return array(
371
			$this->type(),
372
			$this->path,
373
			$this->line,
374
			$this->class_name,
375
			$this->name,
376
			$this->static,
377
			json_encode( $this->params )
378
		);
379
	}
380
381
	function type() {
382
		return 'method';
383
	}
384
385
	function display_name() {
386
		$sep = $this->static ? '::' : '->';
387
		return $this->class_name . $sep . $this->name;
388
	}
389
}
390
391
/**
392
 * We only log public class variables
393
 */
394
class Class_Property_Declaration extends Declaration {
395
	public $class_name;
396
	public $name;
397
	public $static;
398
399 View Code Duplication
	function __construct( $path, $line, $class_name, $name, $static ) {
400
		$this->class_name = $class_name;
401
		$this->name = $name;
402
		$this->static = $static;
403
		parent::__construct( $path, $line );
404
	}
405
406 View Code Duplication
	function to_csv_array() {
407
		return array(
408
			$this->type(),
409
			$this->path,
410
			$this->line,
411
			$this->class_name,
412
			$this->name,
413
			$this->static,
414
			''
415
		);
416
	}
417
418
	function type() {
419
		return 'property';
420
	}
421
422
	function display_name() {
423
		$sep = $this->static ? '::$' : '->';
424
		return $this->class_name . $sep . $this->name;
425
	}
426
}
427