PluginProbe
Plugin Check (PCP) / trunk
Plugin Check (PCP) vtrunk
2.1.0 trunk 0.1 0.2.0 0.2.1 0.2.2 0.2.3 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.3.0 1.3.1 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 1.9.0 2.0.0 ci-artifacts
plugin-check / includes / Scanner / PHP_Parser.php

PHP_Parser.php in Plugin Check (PCP) trunk, at includes/Scanner/PHP_Parser.php

1,348 lines 43.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class WordPress\Plugin_Check\Scanner\PHP_Parser
4 *
5 * @package plugin-check
6 */
7
8 namespace WordPress\Plugin_Check\Scanner;
9
10 use PhpParser\Error;
11 use PhpParser\Node;
12 use PhpParser\Node\Const_;
13 use PhpParser\Node\Expr\AssignOp;
14 use PhpParser\Node\Stmt\ClassConst;
15 use PhpParser\Node\Stmt\PropertyProperty;
16 use PhpParser\NodeFinder;
17 use PhpParser\NodeTraverser;
18 use PhpParser\NodeVisitor\NodeConnectingVisitor;
19 use PhpParser\NodeVisitor\ParentConnectingVisitor;
20 use PhpParser\ParserFactory;
21 use PhpParser\PrettyPrinter\Standard;
22
23 /**
24 * Abstract class responsible for parsing files, logging, and processing Abstract Syntax Tree (AST) data.
25 *
26 * @since 1.7.0
27 *
28 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
29 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
30 * @SuppressWarnings(PHPMD.TooManyFields)
31 */
32 abstract class PHP_Parser {
33
34 /**
35 * A collection of all file paths to be processed.
36 *
37 * @since 1.7.0
38 * @var string[]
39 */
40 public array $files = array();
41
42 /**
43 * A collection of PHP file paths to be processed.
44 *
45 * @since 1.7.0
46 * @var string[]
47 */
48 public array $files_php = array();
49
50 /**
51 * The full path of the file currently being processed.
52 *
53 * @since 1.7.0
54 * @var string
55 */
56 public string $file = '';
57
58 /**
59 * The relative path of the file currently being processed.
60 *
61 * @since 1.7.0
62 * @var string
63 */
64 public string $file_relative = '';
65
66 /**
67 * Flag to indicate if parent nodes need to be fetched during AST traversal.
68 *
69 * @since 1.7.0
70 * @var bool
71 */
72 public bool $needs_get_parents = false;
73
74 /**
75 * Flag to indicate if sibling nodes need to be fetched during AST traversal.
76 *
77 * @since 1.7.0
78 * @var bool
79 */
80 public bool $needs_get_siblings = false;
81
82 /**
83 * Flag to indicate if the service is initialized and ready.
84 *
85 * @since 1.7.0
86 * @var bool
87 */
88 private bool $ready = false;
89
90 /**
91 * The PHP-Parser NodeFinder instance.
92 *
93 * @since 1.7.0
94 * @var \PhpParser\NodeFinder
95 */
96 public $node_finder;
97
98 /**
99 * The Abstract Syntax Tree (AST) of the current file.
100 *
101 * @since 1.7.0
102 * @var \PhpParser\Node[]|null
103 */
104 public $stmts;
105
106 /**
107 * The logging object instance.
108 *
109 * @since 1.7.0
110 * @var Log
111 */
112 private Log $log_object;
113
114 /**
115 * The PHP-Parser PrettyPrinter instance.
116 *
117 * @since 1.7.0
118 * @var \PhpParser\PrettyPrinter\Standard
119 */
120 public $pretty_printer;
121
122 /**
123 * List of known sanitization functions.
124 *
125 * @since 1.7.0
126 * @var string[]
127 */
128 public array $sanitize_functions;
129
130 /**
131 * List of known escaping functions.
132 *
133 * @since 1.7.0
134 * @var string[]
135 */
136 public array $escaping_functions;
137
138 /**
139 * Cache for `define()` statement objects found during parsing.
140 *
141 * @since 1.7.0
142 * @var array
143 */
144 private array $defines_objects = array();
145
146 /**
147 * Flag to indicate if the `define()` statement objects have been loaded.
148 *
149 * @since 1.7.0
150 * @var bool
151 */
152 private bool $defines_objects_loaded = false;
153
154 /**
155 * Cache for variable assignment expressions to avoid re-parsing.
156 *
157 * @since 1.7.0
158 * @var array
159 */
160 private array $cache_assignments_expressions_for_variable = array();
161
162 /**
163 * Constructor.
164 *
165 * @since 1.7.0
166 */
167 public function __construct() {
168 $this->log_object = new Log( $this );
169 $this->sanitize_functions = include dirname( __DIR__ ) . '/Vars/sanitize-functions.php';
170 $this->escaping_functions = include dirname( __DIR__ ) . '/Vars/escaping-functions.php';
171 }
172
173 /**
174 * Loads files.
175 *
176 * @since 1.7.0
177 *
178 * @param array $files Array of files.
179 * @return void
180 */
181 public function load_files( $files ) {
182 $this->files = $files;
183
184 $this->files_php = array_filter(
185 $files,
186 function ( $file ) {
187 return pathinfo( $file, PATHINFO_EXTENSION ) === 'php';
188 }
189 );
190 }
191
192 /**
193 * Returns relative path.
194 *
195 * @since 1.7.0
196 *
197 * @param string $file File path.
198 * @return string Relative path.
199 */
200 public function get_relative_path( $file ) {
201 $relative = explode( 'current_plugin/', $file );
202 $relative = end( $relative );
203 $relative = explode( 'prt_phpunit/', $relative );
204 return end( $relative );
205 }
206
207 /**
208 * Abstract method to process each file.
209 *
210 * @return mixed The return type and value are determined by the concrete implementation of this method.
211 */
212 abstract public function find();
213
214 /**
215 * Loads a file, initializes it, parses its content, and processes further operations if the file is ready.
216 *
217 * @param string $file The path to the file that needs to be loaded.
218 *
219 * @return null Always returns null after attempting to load and process the file.
220 */
221 public function load( $file ) {
222 if ( $this->init_file( $file ) ) {
223 $this->parse_file( $this->file );
224 $this->pretty_printer = new Standard();
225 if ( $this->is_ready() ) {
226 $this->find();
227 }
228 }
229
230 return null;
231 }
232
233 /**
234 * Retrieves the log object.
235 *
236 * @return mixed Returns the log object associated with the instance.
237 */
238 public function log() {
239 return $this->log_object;
240 }
241
242 /**
243 * Initializes a file, setting the file's path and its relative path.
244 * Checks if the file exists before proceeding.
245 *
246 * @param string $file The path to the file to be initialized.
247 *
248 * @return bool Returns true if the file exists and is successfully initialized, otherwise false.
249 */
250 public function init_file( $file ) {
251 $this->stmts = null;
252 if ( ! file_exists( $file ) ) {
253 return false;
254 }
255 $this->file = $file;
256 $this->file_relative = $this->get_relative_path( $this->file );
257 return true;
258 }
259
260 /**
261 * Initializes the node finder instance for searching specific nodes in the parsed Abstract Syntax Tree (AST).
262 *
263 * @return void
264 */
265 public function initialize_node_finder() {
266 if ( null === $this->node_finder ) {
267 $this->node_finder = new NodeFinder();
268 }
269 }
270
271 /**
272 * Parses a PHP file and processes its abstract syntax tree (AST).
273 * The method can enhance the AST with additional attributes such as parent and sibling relationships if requested.
274 *
275 * @param string $file The path to the PHP file to be parsed.
276 *
277 * @return void This method does not return a value, but it processes the file and initializes necessary attributes for further usage.
278 */
279 private function parse_file( $file ) {
280 // Check if this is a PHP file.
281 $ext = pathinfo( $file, PATHINFO_EXTENSION );
282 if ( in_array( $ext, array( 'php' ), true ) ) {
283 // Options.
284 // Activate ability to get parents. Performance will be degraded.
285 // Get parents using $node->getAttribute('parent').
286 $traverser = null;
287 if ( $this->needs_get_parents ) {
288 $traverser = new NodeTraverser();
289 $traverser->addVisitor( new ParentConnectingVisitor() );
290 }
291 if ( $this->needs_get_siblings ) {
292 if ( null === $traverser ) {
293 $traverser = new NodeTraverser();
294 }
295 $traverser->addVisitor( new NodeConnectingVisitor() );
296 }
297
298 // Parse file.
299 $parser = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 );
300 try {
301 $code = file_get_contents( $file );
302 $this->stmts = $parser->parse( $code );
303 if ( ( $this->needs_get_parents || $this->needs_get_siblings ) && null !== $traverser && is_array( $this->stmts ) ) {
304 $this->stmts = $traverser->traverse( $this->stmts );
305 }
306 } catch ( Error $error ) {
307 return;
308 }
309 }
310 $this->initialize_node_finder();
311 $this->ready = true;
312 }
313
314 /**
315 * Parses the provided PHP code and optionally applies traversal for attaching
316 * parent or sibling node relations, based on configuration flags.
317 *
318 * @param string $code The PHP code to be parsed.
319 *
320 * @return array|null Returns an array of statements parsed from the PHP code,
321 * or null if an error occurs or the code is empty.
322 */
323 public function parse_code( $code ) {
324 $stmts = null;
325 if ( ! empty( $code ) ) {
326 // Activate ability to get parents. Performance will be degraded.
327 // Get parents using $node->getAttribute('parent').
328 $traverser = null;
329 if ( $this->needs_get_parents ) {
330 $traverser = new NodeTraverser();
331 $traverser->addVisitor( new ParentConnectingVisitor() );
332 }
333
334 if ( $this->needs_get_siblings ) {
335 if ( null === $traverser ) {
336 $traverser = new NodeTraverser();
337 }
338 $traverser->addVisitor( new NodeConnectingVisitor() );
339 }
340
341 $parser = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 );
342 try {
343 $stmts = $parser->parse( $code );
344 if ( ( $this->needs_get_parents || $this->needs_get_siblings ) && null !== $traverser && is_array( $stmts ) ) {
345 $stmts = $traverser->traverse( $stmts );
346 }
347 } catch ( Error $error ) {
348 return null;
349 }
350 }
351 return $stmts;
352 }
353
354 /**
355 * Checks the readiness state of the current instance.
356 *
357 * @return bool Returns true if the instance is ready, otherwise false.
358 */
359 public function is_ready() {
360 return $this->ready;
361 }
362
363 /**
364 * Checks if the given object is of one of the specified classes.
365 *
366 * @since 1.7.0
367 *
368 * @param object $object_name The object to check.
369 * @param array $classes An array of class names to check against.
370 *
371 * @return bool Returns true if the object's class is in the given array of classes, false otherwise.
372 */
373 public function is_object_of_type( $object_name, array $classes ) {
374 return in_array( get_class( $object_name ), $classes, true );
375 }
376
377 /**
378 * Retrieves the call name from the provided expression.
379 *
380 * This method examines an expression and attempts to extract the associated
381 * call name, handling static calls, fully qualified names, and other
382 * cases based on the given expression type.
383 *
384 * @param mixed $expr The expression to evaluate, typically an instance of a
385 * `PhpParser\Node` type like `StaticCall` or `New_`.
386 * @param bool &$found_in_same_line A reference parameter indicating whether the
387 * call name is found on the same line as the expression.
388 * Defaults to true.
389 *
390 * @return string The extracted call name, or an empty string if no name can
391 * be determined.
392 */
393 public function get_call_name( $expr, &$found_in_same_line = true ) {
394 $name = '';
395
396 // Determine the object to evaluate.
397 $name_object = null;
398
399 if ( $this->is_object_of_type( $expr, array( 'PhpParser\Node\Expr\StaticCall', 'PhpParser\Node\Expr\New_' ) ) ) {
400 $name_object = $expr->class;
401 } elseif ( isset( $expr->name ) ) {
402 $name_object = $expr->name;
403 }
404
405 // Return early if no name object is found.
406 if ( empty( $name_object ) ) {
407 return $name;
408 }
409
410 // Handle PhpParser\Node\Name class.
411 if ( $this->is_object_of_type( $name_object, array( 'PhpParser\Node\Name' ) ) ) {
412 $name = $name_object->__toString();
413 } elseif ( $this->is_object_of_type( $name_object, array( 'PhpParser\Node\Name\FullyQualified' ) ) ) { // Handle PhpParser\Node\Name\FullyQualified class.
414 if ( ! empty( $expr->name->parts ) ) {
415 $name = implode( '\\', $expr->name->parts );
416 } elseif ( ! empty( $expr->class ) && ! empty( $expr->class->parts ) ) {
417 $name = implode( '\\', $expr->class->parts );
418 }
419 } else { // Fallback case for other objects.
420 $name = $this->get_possible_string_for_element( $name_object, $found_in_same_line );
421
422 if ( empty( $name ) ) {
423 $name = get_class( $name_object );
424 }
425 }
426
427 return $name;
428 }
429
430 /**
431 * Extracts concatenated elements.
432 *
433 * @param mixed $expr The concatenated expression to process.
434 * @param array $elements An array to accumulate the extracted elements (optional).
435 *
436 * @return array An array containing the extracted and concatenated elements.
437 */
438 public function extract_concat_elements( $expr, $elements = array() ) {
439 if ( $this->is_object_of_type( $expr, array( 'PhpParser\Node\Expr\BinaryOp\Concat' ) ) ) {
440 $elements = $this->extract_concat_elements( $expr->left, $elements );
441 if ( ! empty( $expr->right ) ) {
442 $elements[] = $expr->right;
443 }
444 } elseif ( $this->is_object_of_type( $expr, array( 'PhpParser\Node\Scalar\Encapsed' ) ) ) {
445 if ( ! empty( $expr->parts ) ) {
446 $parts = $expr->parts;
447 foreach ( $parts as $part ) {
448 $elements = $this->extract_concat_elements( $part, $elements );
449 }
450 }
451 } else {
452 $elements[] = $expr;
453 }
454
455 return $elements;
456 }
457
458 /**
459 * Determines if the given expression is a name.
460 *
461 * @param mixed $name_expr The expression to check, potentially representing a name.
462 *
463 * @return bool Returns true if the expression is of the type 'PhpParser\Node\Name' or 'PhpParser\Node\Name\FullyQualified', false otherwise.
464 */
465 private function has_name( $name_expr ) {
466 if ( empty( $name_expr ) ) {
467 return false;
468 }
469 return $this->is_object_of_type( $name_expr, array( 'PhpParser\Node\Name', 'PhpParser\Node\Name\FullyQualified' ) );
470 }
471
472 /**
473 * Determines if the given function call has a recognized name.
474 *
475 * NOTE: $use_context false prevents infinite loop on init_defines, ideally this wouldn't be needed.
476 *
477 * @param object $func_call The function call object to check.
478 * @param bool $use_context Optional. Whether to use context to resolve the function name. Defaults to true.
479 *
480 * @return bool Returns true if the function call has a recognized name, false otherwise.
481 */
482 public function has_function_name( $func_call, $use_context = true ) {
483 if ( $this->has_name( $func_call->name ) ) {
484 return true;
485 }
486 if ( $use_context ) {
487 $find_name = $this->get_call_name( $func_call );
488 if ( ! empty( $find_name ) ) {
489 return true;
490 }
491 }
492
493 return false;
494 }
495
496 /**
497 * Retrieve the name of a variable from a node object.
498 *
499 * @param object $node The node object from which to retrieve the variable name.
500 *
501 * @return string The name of the variable.
502 */
503 public function get_variable_name( $node ) {
504 $name = '';
505 if ( 'PhpParser\Node\Arg' === get_class( $node ) ) {
506 $name = $this->get_variable_name( $node->value );
507 } elseif ( 'PhpParser\Node\Scalar\String_' === get_class( $node ) ) {
508 $name = $node->value;
509 }
510 if ( isset( $node->var ) && ( 'PhpParser\Node\Expr\Variable' === get_class( $node->var ) || 'PhpParser\Node\Expr\ArrayDimFetch' === get_class( $node->var ) ) ) {
511 $name = $this->get_variable_name( $node->var );
512 }
513 if ( isset( $node->name ) ) {
514 if ( 'PhpParser\Node\Expr\Variable' === get_class( $node ) ) {
515 $name = $node->name;
516 } elseif ( 'PhpParser\Node\Scalar\String_' === get_class( $node->name ) ) {
517 $name = $node->name->value;
518 } elseif ( 'PhpParser\Node\Identifier' === get_class( $node->name ) ) {
519 $name = $node->name->name;
520 } elseif ( 'PhpParser\Node\VarLikeIdentifier' === get_class( $node->name ) ) {
521 $name = $node->name->name;
522 } elseif ( 'PhpParser\Node\Name' === get_class( $node->name ) ) {
523 $name = $node->name->__toString();
524 }
525 }
526 if ( is_object( $name ) ) {
527 $name = $this->get_variable_name( $name );
528 }
529 return $name;
530 }
531
532 /**
533 * Retrieve the dim of a ArrayDimFetch variable from a node object.
534 *
535 * @param object $node The node object from which to retrieve the variable dimension.
536 *
537 * @return array<int, string> The dimensions of the variable.
538 */
539 public function extract_dims_values( $node ) {
540 $dims = array();
541 if ( ! empty( $node->var->dim ) ) {
542 $dims = array_merge( $dims, (array) $this->extract_dims_values( $node->var ) );
543 }
544 if ( ! empty( $node->dim ) ) {
545 if ( 'PhpParser\Node\Scalar\String_' === get_class( $node->dim ) ) {
546 $dims[] = $node->dim->value;
547 }
548 }
549 return $dims;
550 }
551
552 /**
553 * Extracts dimension objects from the given node.
554 *
555 * @param mixed $node The node from which to extract dimension objects.
556 *
557 * @return array An array of dimension objects extracted from the node.
558 */
559 public function extract_dims_objects( $node ) {
560 $dims = array();
561 if ( ! empty( $node->var->dim ) ) {
562 $dims = array_merge( $dims, (array) $this->extract_dims_objects( $node->var ) );
563 }
564 if ( ! empty( $node->dim ) ) {
565 $dims[] = $node->dim;
566 }
567 return $dims;
568 }
569
570 /**
571 * Retrieves a STMTS limited to the context (scope) of the given element. As for example, the function where the element is.
572 *
573 * @param object $element The element (such as a node) for which the context is being retrieved.
574 * @param string|null $is_inside_element_type Will be set to the type of element found, such as `Node\Stmt\Class_`, `Node\Stmt\ClassMethod`, etc., or null.
575 * @param string $file The file in which the element resides. If provided and differs from the current file, the method will parse the specified file.
576 *
577 * @return array An associative array containing:
578 * - 'context': The relevant statements (if found) for the element.
579 * - 'file': The file being analyzed, which may differ if a specific file is passed as an argument.
580 * - 'class': The class statements (if applicable) for the element.
581 * - 'contextWrapper': The wrapper node for the context (if available).
582 */
583 public function get_contextual_stmts_for_element( $element, &$is_inside_element_type = null, $file = '' ) {
584 $return = array(
585 'context' => '',
586 'file' => $this->file,
587 'class' => '',
588 );
589
590 $element_start_line = method_exists( $element, 'getStartLine' ) ? $element->getStartLine() : 0;
591 $element_end_line = method_exists( $element, 'getEndLine' ) ? $element->getEndLine() : 0;
592
593 $classes = array(
594 Node\Stmt\ClassMethod::class,
595 Node\Stmt\Class_::class,
596 Node\Stmt\Function_::class,
597 Node\Stmt\Interface_::class,
598 );
599
600 if ( ! empty( $file ) && $file !== $this->file ) {
601 $original_file = $this->file;
602 $return['file'] = $file;
603 $this->parse_file( $file );
604 }
605
606 foreach ( $classes as $class ) {
607 $functions = $this->node_finder->findInstanceOf( $this->stmts, $class );
608 if ( ! empty( $functions ) ) {
609 foreach ( $functions as $function ) {
610 if (
611 method_exists( $function, 'getStartLine' ) &&
612 method_exists( $function, 'getEndLine' ) &&
613 $function->getStartLine() <= $element_start_line &&
614 $function->getEndLine() >= $element_end_line
615 ) {
616 if ( empty( $return['context'] ) ) {
617 $is_inside_element_type = $class;
618 if ( property_exists( $function, 'stmts' ) ) {
619 $return['context'] = $function->stmts;
620 }
621 $return['contextWrapper'] = $function;
622 }
623 if ( empty( $return['class'] ) && Node\Stmt\Class_::class === $class ) {
624 if ( property_exists( $function, 'stmts' ) ) {
625 $return['class'] = $function->stmts;
626 }
627 }
628 }
629 }
630 }
631 }
632
633 if ( empty( $return['context'] ) ) {
634 $return['context'] = $this->stmts;
635 }
636
637 if ( ! empty( $original_file ) ) {
638 $this->parse_file( $original_file );
639 }
640
641 return $return;
642 }
643
644 /**
645 * Retrieves the assignment expressions that can affect the value of a given variable.
646 *
647 * @param mixed $element The variable element to process for finding assignments.
648 * @param string $file The file path to analyze, or an empty string to use the default file context.
649 *
650 * @return array|false An array containing details of the identified assignments, or false if the element is a skippable constant.
651 */
652 public function get_assignments_expressions_for_variable( $element, $file = '' ) {
653 // Skip known PHP constants. Constant known PHP elements that does not worth the while.
654 if ( $this->is_skippable_constant_for_variable_assignments( $element ) ) {
655 return false;
656 }
657
658 $file = empty( $file ) ? $this->file : $file;
659
660 $cached = $this->get_cache_assignments_expressions_for_variable( $element, $file );
661
662 if ( -1 !== $cached ) {
663 return $cached;
664 }
665
666 $this->init_defines();
667
668 $final_assigns = array();
669 $possible_assigns = array();
670 $concat_assigns = array();
671 $define_assigns = array();
672 $define_consts = array();
673 $define_class_property = array();
674 $define_class_consts = array();
675
676 $assignments = array(
677 'standard' => array(),
678 'concat' => array(),
679 'const' => array(),
680 'classProperty' => array(),
681 'classConst' => array(),
682 );
683
684 $context = $this->get_contextual_stmts_for_element( $element, $is_inside_element_type, $file );
685 $stmts = $context['context'];
686 $stmts_class = $context['class'];
687 if ( ! empty( $stmts ) ) {
688 $assignments['standard'] = $this->node_finder->findInstanceOf( $stmts, Node\Expr\Assign::class );
689 $assignments['concat'] = $this->node_finder->findInstanceOf( $stmts, Node\Expr\AssignOp\Concat::class );
690 $assignments['const'] = $this->node_finder->findInstanceOf( $stmts, Const_::class );
691 $assignments['classProperty'] = $this->node_finder->findInstanceOf( $stmts_class, PropertyProperty::class );
692 $assignments['classConst'] = $this->node_finder->findInstanceOf( $stmts_class, ClassConst::class );
693 }
694
695 // Process all found assignments.
696 $assigns = array_merge(
697 $this->defines_objects,
698 $assignments['standard'],
699 $assignments['concat'],
700 $assignments['const'],
701 $assignments['classProperty'],
702 $assignments['classConst']
703 );
704
705 if ( ! empty( $assigns ) ) {
706 foreach ( $assigns as $assign ) {
707 if ( $this->is_a_define_call( $assign ) ) { // Defines aren't limited by context.
708 if ( is_a( $element, 'PhpParser\Node\Expr\ConstFetch' ) ) {
709 $element_name = $this->get_variable_name( $element );
710 $assign_name = $this->get_variable_name( $assign->args[0] );
711 if ( $element_name === $assign_name ) {
712 $define_assigns[] = $assign;
713 }
714 }
715 } elseif ( method_exists( $assign, 'getEndLine' ) && method_exists( $element, 'getEndLine' ) && $assign->getEndLine() < $element->getEndLine() ) { // Only assigns before the $element.
716 if ( is_a( $assign, Const_::class ) ) {
717 if ( is_a( $element, 'PhpParser\Node\Expr\ConstFetch' ) ) {
718 $element_name = $this->get_variable_name( $element );
719 $assign_name = $this->get_variable_name( $assign );
720 if ( $element_name === $assign_name ) {
721 $define_consts[] = $assign;
722 }
723 }
724 } elseif ( is_a( $assign, PropertyProperty::class ) ) {
725 if ( is_a( $element, 'PhpParser\Node\Expr\PropertyFetch' ) ) {
726 $element_name = $this->get_variable_name( $element );
727 $assign_name = $this->get_variable_name( $assign );
728 if ( $element_name === $assign_name ) {
729 $define_class_property[] = $assign;
730 }
731 }
732 } elseif ( is_a( $assign, ClassConst::class ) ) {
733 // For now is only able to find ClassConsts that are in the same class.
734 if ( isset( $element->class ) ) {
735 if ( 'PhpParser\Node\Name' === get_class( $element->class ) ) {
736 if ( 'self' === $element->class->parts[0] ) {
737 $element_name = $this->get_variable_name( $element );
738 $consts = $assign->consts;
739 foreach ( $consts as $const ) {
740 $assign_name = $this->get_variable_name( $const );
741 if ( $element_name === $assign_name ) {
742 $define_class_consts[] = $const;
743 }
744 }
745 }
746 }
747 }
748 } elseif ( is_object( $assign ) && isset( $assign->var ) && get_class( $assign->var ) === get_class( $element ) ) {
749 $element_name = $this->get_variable_name( $element );
750 $assign_name = $this->get_variable_name( $assign->var );
751
752 if ( ! empty( $element_name ) ) {
753 if ( 'PhpParser\Node\Expr\Variable' === get_class( $element ) ) {
754 if ( $element_name === $assign_name ) {
755 if ( 'PhpParser\Node\Expr\AssignOp\Concat' === get_class( $assign ) ) {
756 $concat_assigns[] = $assign;
757 } else {
758 $possible_assigns[] = $assign;
759 }
760 }
761 }
762 if ( 'PhpParser\Node\Expr\PropertyFetch' === get_class( $element ) ) {
763 if ( $element_name === $assign_name ) {
764 if ( 'PhpParser\Node\Expr\AssignOp\Concat' === get_class( $assign ) ) {
765 $concat_assigns[] = $assign;
766 } else {
767 $possible_assigns[] = $assign;
768 }
769 }
770 }
771 if ( 'PhpParser\Node\Expr\ArrayDimFetch' === get_class( $element ) ) {
772 if ( $element_name === $assign_name ) {
773 if ( $this->extract_dims_values( $element ) === $this->extract_dims_values( is_object( $assign ) && isset( $assign->var ) ? $assign->var : null ) ) {
774 if ( 'PhpParser\Node\Expr\AssignOp\Concat' === get_class( $assign ) ) {
775 $concat_assigns[] = $assign;
776 } else {
777 $possible_assigns[] = $assign;
778 }
779 }
780 }
781 }
782 }
783 }
784 }
785 }
786 }
787
788 if ( ! empty( $define_assigns ) ) {
789 foreach ( $define_assigns as $define_assign ) {
790 $final_assigns[] = array(
791 'expr' => $define_assign,
792 'value' => $define_assign->args[1],
793 'sameContext' => true,
794 'type' => 'define',
795 'file' => $define_assign->getAttribute( 'file' ),
796 );
797 }
798 }
799
800 if ( ! empty( $define_consts ) ) {
801 foreach ( $define_consts as $define_const ) {
802 $final_assigns[] = array(
803 'expr' => $define_const,
804 'value' => $define_const->value,
805 'sameContext' => true,
806 'type' => 'const',
807 'file' => '',
808 );
809 }
810 }
811
812 if ( ! empty( $define_class_consts ) ) {
813 foreach ( $define_class_consts as $define_class_const ) {
814 $final_assigns[] = array(
815 'expr' => $define_class_const,
816 'value' => $define_class_const->value,
817 'sameContext' => true,
818 'type' => 'const',
819 'file' => '',
820 );
821 }
822 }
823
824 // Incorporate class properties but only if there are not already assigns in the function.
825 if ( ! empty( $define_class_property ) ) {
826 foreach ( $define_class_property as $define_class_property ) {
827 if ( ! empty( $define_class_property->default ) ) {
828 $skip = false;
829 if ( ! empty( $possible_assigns ) ) {
830 foreach ( $possible_assigns as $possible_assign ) {
831 if ( ! empty( $possible_assign->var ) && is_object( $possible_assign ) && is_object( $possible_assign->var ) && 'PhpParser\Node\Expr\PropertyFetch' === get_class( $possible_assign->var ) ) {
832 if ( $this->get_variable_name( $define_class_property ) && $this->get_variable_name( $possible_assign->var ) ) {
833 $skip = true;
834 break;
835 }
836 }
837 }
838 }
839 if ( ! $skip ) {
840 $final_assigns[] = array(
841 'expr' => $define_class_property,
842 'value' => $define_class_property->default,
843 'sameContext' => '',
844 'type' => 'assign',
845 'file' => '',
846 );
847 }
848 }
849 }
850 }
851
852 if ( ! empty( $possible_assigns ) ) {
853 $last_assign_same_execution_context = '';
854 $assign_others = array();
855
856 // Find assigns in the same execution context and remove all concats that are before them.
857 $same_execution_context_lines = $this->get_same_execution_context_lines( $stmts, $element );
858
859 foreach ( $possible_assigns as $possible_assign ) {
860 $same_execution_context = false;
861 if ( ! empty( $same_execution_context_lines ) ) {
862 foreach ( $same_execution_context_lines as $same_execution_context_line ) {
863 if ( method_exists( $possible_assign, 'getStartLine' ) && method_exists( $possible_assign, 'getEndLine' ) && $same_execution_context_line['startLine'] === $possible_assign->getStartLine() && $same_execution_context_line['endLine'] === $possible_assign->getEndLine() ) {
864 $same_execution_context = true;
865 $concat_assigns = array_filter(
866 $concat_assigns,
867 function ( $assign ) use ( $possible_assign ) {
868 return method_exists( $assign, 'getEndLine' ) && method_exists( $possible_assign, 'getEndLine' ) ? $assign->getEndLine() > $possible_assign->getEndLine() : false;
869 }
870 );
871 }
872 }
873 }
874 if ( $same_execution_context ) {
875 $last_assign_same_execution_context = $possible_assign;
876 $assign_others = array();
877 } else {
878 $assign_others[] = $possible_assign;
879 }
880 }
881
882 if ( ! empty( $concat_assigns ) ) {
883 foreach ( $concat_assigns as $concat_assign ) {
884 $final_assigns[] = array(
885 'expr' => $concat_assign,
886 'value' => ( is_object( $concat_assign ) && $concat_assign instanceof AssignOp ) ? $concat_assign->expr : null,
887 'sameContext' => '',
888 'type' => 'concat',
889 'file' => '',
890 );
891 }
892 }
893
894 // Return the closer to the $element.
895 if ( ! empty( $assign_others ) ) {
896 foreach ( $assign_others as $assign_other ) {
897 $final_assigns[] = array(
898 'expr' => $assign_other,
899 'value' => ( is_object( $assign_other ) && $assign_other instanceof AssignOp ) ? $assign_other->expr : null,
900 'sameContext' => false,
901 'type' => 'assign',
902 'file' => '',
903 );
904 }
905 }
906
907 if ( ! empty( $last_assign_same_execution_context ) ) {
908 $final_assigns[] = array(
909 'expr' => $last_assign_same_execution_context,
910 'value' => ( is_object( $last_assign_same_execution_context ) && $last_assign_same_execution_context instanceof AssignOp ) ? $last_assign_same_execution_context->expr : null,
911 'sameContext' => true,
912 'type' => 'assign',
913 'file' => '',
914 );
915 }
916 } elseif ( ! empty( $concat_assigns ) ) {
917 foreach ( $concat_assigns as $concat_assign ) {
918 $final_assigns[] = array(
919 'expr' => $concat_assign,
920 'value' => ( is_object( $concat_assign ) && $concat_assign instanceof AssignOp ) ? $concat_assign->expr : null,
921 'sameContext' => '',
922 'type' => 'concat',
923 'file' => '',
924 );
925 }
926 }
927
928 if ( ! empty( $final_assigns ) ) {
929 $this->set_cache_assignments_expressions_for_variable( $element, $file, $final_assigns );
930 return $final_assigns;
931 }
932
933 $this->set_cache_assignments_expressions_for_variable( $element, $file, false );
934 return false;
935 }
936
937 /**
938 * Determines if the given element is a skippable constant for variable assignments.
939 * For example, it makes no sense to further check a true value.
940 *
941 * @param mixed $element The element to inspect.
942 *
943 * @return bool Returns true if the element is a constant and matches one of the predefined skippable constants, otherwise false.
944 */
945 private function is_skippable_constant_for_variable_assignments( $element ) {
946 if ( 'PhpParser\Node\Expr\ConstFetch' !== get_class( $element ) ) {
947 return false;
948 }
949
950 $skip_constants = array(
951 'true',
952 'false',
953 'null',
954 'php_eol',
955 'day_in_seconds',
956 'hour_in_seconds',
957 'minute_in_seconds',
958 'doing_ajax',
959 'doing_cron',
960 );
961
962 $name = strtolower( $this->get_variable_name( $element ) );
963 return in_array( $name, $skip_constants, true );
964 }
965
966
967 /**
968 * Retrieves cached assignment expressions for a specific variable.
969 *
970 * @param mixed $element The element representing the variable to look up.
971 * @param mixed $file The file context in which the lookup is performed.
972 *
973 * @return mixed Returns the cached assignment expressions for the variable if available,
974 * or -1 if no cached data is found.
975 */
976 private function get_cache_assignments_expressions_for_variable( $element, $file ) {
977 $element_id = $this->get_cache_element_id( $element, $file );
978 if ( isset( $this->cache_assignments_expressions_for_variable[ $element_id ] ) ) {
979 return $this->cache_assignments_expressions_for_variable[ $element_id ];
980 }
981 return -1;
982 }
983
984 /**
985 * Sets the cache for assignments and expressions associated with a variable.
986 *
987 * @param mixed $element The variable or element to process.
988 * @param mixed $file The file context for the variable or element.
989 * @param mixed $data The data to be cached for the variable or element.
990 *
991 * @return void
992 */
993 private function set_cache_assignments_expressions_for_variable( $element, $file, $data ) {
994 $element_id = $this->get_cache_element_id( $element, $file );
995 $this->cache_assignments_expressions_for_variable[ $element_id ] = $data;
996 }
997
998 /**
999 * Generates a cache element ID based on the provided element and file.
1000 *
1001 * @param mixed $element The element object to derive properties from.
1002 * @param string $file The filename associated with the element.
1003 *
1004 * @return string Returns a hashed string (MD5) representing the cache element ID.
1005 */
1006 private function get_cache_element_id( $element, $file ) {
1007 $line_id = $file . '_' . ( method_exists( $element, 'getStartLine' ) ? $element->getStartLine() : 0 ) . '_' . ( method_exists( $element, 'getEndLine' ) ? $element->getEndLine() : 0 ) . '_' . $this->get_variable_name( $element );
1008 return md5( $line_id );
1009 }
1010
1011 /**
1012 * Look for a string for that element having in mind the context.
1013 *
1014 * NOTE: If is not able to reconstruct the string in a reliable way, and is set to $accurate, will return false.
1015 *
1016 * @param object $element The PHP Parser element to analyze.
1017 * @param bool &$found_in_same_line Reference variable indicating if the string was found
1018 * in the same line of context. Defaults to true.
1019 * @param bool $accurate Whether to use accurate context checking. Defaults to true.
1020 * @param string $file The file path being analyzed, if applicable. Defaults to an empty string.
1021 *
1022 * @return string|bool Returns the resolved string if possible, false if accurate context checking fails,
1023 * or an empty string for non-accurate processing when no string is found.
1024 */
1025 public function get_possible_string_for_element( $element, &$found_in_same_line = true, $accurate = true, $file = '' ) {
1026 if ( ! is_object( $element ) ) {
1027 if ( $accurate ) {
1028 return false;
1029 } else {
1030 return '';
1031 }
1032 }
1033
1034 $class = get_class( $element );
1035
1036 switch ( $class ) {
1037 case 'PhpParser\Node\Arg':
1038 if ( isset( $element->value ) ) {
1039 return $this->get_possible_string_for_element( $element->value, $found_in_same_line, $accurate, $file );
1040 }
1041 break;
1042
1043 case 'PhpParser\Node\Expr\FuncCall':
1044 if ( $this->has_function_name( $element ) ) {
1045 $function_name = $this->get_call_name( $element );
1046 // Check inside a escaping function.
1047 $functions = array_merge( array( 'trailingslashit', 'untrailingslashit' ), $this->escaping_functions );
1048 if ( in_array( $function_name, $functions, true ) ) {
1049 if ( ! empty( $element->args ) && ! empty( $element->args[0] ) && ! empty( $element->args[0]->value ) ) {
1050 return $this->get_possible_string_for_element( $element->args[0], $found_in_same_line, $accurate, $file );
1051 }
1052 }
1053 }
1054 break;
1055
1056 case 'PhpParser\Node\Scalar\String_':
1057 case 'PhpParser\Node\Scalar\EncapsedStringPart':
1058 if ( ! empty( $element->value ) ) {
1059 return $element->value;
1060 }
1061 break;
1062
1063 case 'PhpParser\Node\Identifier':
1064 if ( ! empty( $element->name ) ) {
1065 return $element->name;
1066 }
1067 break;
1068
1069 case 'PhpParser\Node\Expr\BinaryOp\Concat':
1070 case 'PhpParser\Node\Scalar\Encapsed':
1071 $concat = $this->extract_concat_elements( $element );
1072 if ( ! empty( $concat ) ) {
1073 $concat_string = '';
1074 foreach ( $concat as $c ) {
1075 $string = $this->get_possible_string_for_element( $c, $found_in_same_line, $accurate, $file );
1076 if ( false === $string ) {
1077 return false;
1078 } else {
1079 $concat_string .= $string;
1080 }
1081 }
1082 return $concat_string;
1083 }
1084 break;
1085 case 'PhpParser\Node\Expr\Variable':
1086 case 'PhpParser\Node\Expr\ArrayDimFetch':
1087 case 'PhpParser\Node\Expr\PropertyFetch':
1088 case 'PhpParser\Node\Expr\ConstFetch':
1089 case 'PhpParser\Node\Expr\ClassConstFetch':
1090 $assigns = $this->get_assignments_expressions_for_variable( $element, $file );
1091 if ( ! empty( $assigns ) ) {
1092 $concat_string = '';
1093 foreach ( $assigns as $assign ) {
1094 if ( ! $accurate || $assign['sameContext'] ) {
1095 $string = $this->get_possible_string_for_element( $assign['value'], $found_in_same_line, $accurate, $assign['file'] );
1096 if ( ! empty( $string ) ) {
1097 $found_in_same_line = false;
1098 }
1099 if ( false === $string ) {
1100 return false;
1101 } else {
1102 $concat_string .= $string;
1103 }
1104 }
1105 }
1106 return $concat_string;
1107 }
1108 break;
1109 }
1110 if ( $accurate ) {
1111 return false;
1112 } else {
1113 return '';
1114 }
1115 }
1116
1117 /**
1118 * Retrieves the lines of code that share the same execution context as the specified element.
1119 *
1120 * @param mixed $stmts The statements to process.
1121 * @param mixed $element The element to find the matching execution context lines for.
1122 *
1123 * @return array An array of lines sharing the same execution context as the specified element.
1124 */
1125 private function get_same_execution_context_lines( $stmts, $element ) {
1126 $same_execution_context_lines = array();
1127 $lines_array = array();
1128 if ( $this->process_same_execution_context_lines( $stmts, $element, $lines_array ) ) {
1129 $same_execution_context_lines = $lines_array;
1130 }
1131
1132 return $same_execution_context_lines;
1133 }
1134
1135 /**
1136 * Processes statements to determine if they share the same execution context
1137 * lines with a given element and populates an array with their line ranges.
1138 *
1139 * @param array $stmts The list of statements to process.
1140 * @param object $element The element to compare the statements against.
1141 * @param array &$lines_array The array to store lines that match within the same execution context.
1142 *
1143 * @return bool Returns true if the element's line range is completely within the range of any processed statement,
1144 * otherwise false.
1145 */
1146 private function process_same_execution_context_lines( $stmts, $element, &$lines_array ) {
1147 foreach ( $stmts as $stmt ) {
1148 $class = get_class( $stmt );
1149
1150 switch ( $class ) :
1151 case 'PhpParser\Node\Stmt\If_':
1152 case 'PhpParser\Node\Stmt\Else_':
1153 case 'PhpParser\Node\Stmt\ElseIf_':
1154 case 'PhpParser\Node\Stmt\Foreach_':
1155 case 'PhpParser\Node\Stmt\For_':
1156 case 'PhpParser\Node\Stmt\While_':
1157 case 'PhpParser\Node\Stmt\Do_':
1158 case 'PhpParser\Node\Stmt\Switch_':
1159 case 'PhpParser\Node\Stmt\TryCatch':
1160 $available_stmts = array();
1161 if ( ! empty( $stmt->stmts ) ) {
1162 $available_stmts[] = $stmt->stmts;
1163 }
1164 if ( ! empty( $stmt->elseifs ) ) {
1165 $elseifs = $stmt->elseifs;
1166 foreach ( $elseifs as $elseif ) {
1167 if ( ! empty( $elseif->stmts ) ) {
1168 $available_stmts[] = $elseif->stmts;
1169 }
1170 }
1171 }
1172 if ( ! empty( $stmt->else ) ) {
1173 if ( ! empty( $stmt->else->stmts ) ) {
1174 $available_stmts[] = $stmt->else->stmts;
1175 }
1176 }
1177 if ( ! empty( $stmt->cases ) ) {
1178 $cases = $stmt->cases;
1179 foreach ( $cases as $case ) {
1180 if ( ! empty( $case->stmts ) ) {
1181 $available_stmts[] = $case->stmts;
1182 }
1183 }
1184 }
1185
1186 foreach ( $available_stmts as $check_stmts ) {
1187 $possible_array = array();
1188 if ( $this->process_same_execution_context_lines( $check_stmts, $element, $possible_array ) ) {
1189 $lines_array = array_merge( $lines_array, $possible_array );
1190 return true;
1191 }
1192 }
1193
1194 break;
1195
1196 default:
1197 if ( method_exists( $stmt, 'getStartLine' ) && method_exists( $element, 'getStartLine' ) && method_exists( $stmt, 'getEndLine' ) && method_exists( $element, 'getEndLine' ) && $stmt->getStartLine() <= $element->getStartLine() && $stmt->getEndLine() >= $element->getEndLine() ) {
1198 return true;
1199 }
1200 $lines_array[] = array(
1201 'startLine' => method_exists( $stmt, 'getStartLine' ) ? $stmt->getStartLine() : 0,
1202 'endLine' => method_exists( $stmt, 'getEndLine' ) ? $stmt->getEndLine() : 0,
1203 );
1204
1205 endswitch;
1206 }
1207
1208 return false;
1209 }
1210
1211 /**
1212 * Determines whether a specific line number is being logged.
1213 *
1214 * @param int $line_number The line number to check.
1215 *
1216 * @return bool
1217 */
1218 public function is_logged_line( $line_number ) {
1219 // Intended to be extended by the specific class.
1220 return false;
1221 }
1222
1223 /**
1224 * Initializes the defines by processing PHP files within a specified folder.
1225 * This method ensures that defines are only initialized once per instance.
1226 *
1227 * @return void
1228 */
1229 private function init_defines() {
1230 if ( $this->defines_objects_loaded ) {
1231 return;
1232 }
1233 $this->defines_objects_loaded = true;
1234
1235 $files = $this->files_php;
1236 if ( empty( $files ) ) {
1237 return;
1238 }
1239 $this->initialize_node_finder();
1240
1241 foreach ( $files as $file ) {
1242 $this->init_defines_for_file( $file );
1243 }
1244 }
1245
1246 /**
1247 * Initializes constants defined within a specific file.
1248 *
1249 * @param string $file The file path to analyze for constants.
1250 *
1251 * @return void
1252 */
1253 private function init_defines_for_file( string $file ) {
1254 $code = file_get_contents( $file );
1255 $stmts = $this->parse_code( $code );
1256
1257 if ( empty( $stmts ) ) {
1258 return;
1259 }
1260
1261 $function_calls = $this->node_finder->findInstanceOf( $stmts, Node\Expr\FuncCall::class );
1262
1263 foreach ( $function_calls as $function_call ) {
1264 $this->init_define_for_function( $function_call, $file );
1265 }
1266 }
1267
1268 /**
1269 * Processes a function call and initializes it as a define call if valid.
1270 *
1271 * @param mixed $function_call The function call to be processed.
1272 * @param string $file The file where the function call is located.
1273 *
1274 * @return void
1275 */
1276 private function init_define_for_function( $function_call, string $file ) {
1277 if ( ! $this->is_a_define_call( $function_call ) || ! $this->init_define_is_valid_define_call( $function_call, $file ) ) {
1278 return;
1279 }
1280
1281 $function_call->setAttribute( 'file', $file );
1282 $this->defines_objects[] = $function_call;
1283 }
1284
1285 /**
1286 * Validates whether a given function call can be initialized as a define call.
1287 *
1288 * @param mixed $function_call The function call to validate.
1289 * @param string $file The file where the function call resides, used for error reporting.
1290 *
1291 * @return bool True if the function call is a valid define call, false otherwise.
1292 */
1293 private function init_define_is_valid_define_call( $function_call, $file ) {
1294 if ( ! isset( $function_call->args[0], $function_call->args[1] ) ) {
1295 return false;
1296 }
1297
1298 $define_name = $this->get_define_name( $function_call );
1299 if ( null === $define_name ) {
1300 return false;
1301 }
1302
1303 // I know this is weird, but some people define a define using the value of the same define they are defining and that creates an infinite loop when trying to get the value.
1304 $elements = $this->extract_concat_elements( $function_call->args[1]->value );
1305 if ( ! empty( $elements ) && is_array( $elements ) ) {
1306 foreach ( $elements as $element ) {
1307 if ( get_class( $element ) === 'PhpParser\Node\Expr\ConstFetch' ) {
1308 $included_const_fetch_name = $element->name->__toString();
1309 if ( $define_name === $included_const_fetch_name ) {
1310 var_dump( 'IS ERROR: Infinite loop detected. Define ' . $define_name . ' at ' . $file . ':' . ( method_exists( $function_call, 'getStartLine' ) ? $function_call->getStartLine() : 0 ) . ' is defined using the value of the same define. Ignoring this define.' );
1311 return false;
1312 }
1313 }
1314 }
1315 }
1316
1317 return true;
1318 }
1319
1320 /**
1321 * Determines if the given element represents a call to the `define` function.
1322 *
1323 * @param mixed $element The element to inspect, expected to be a function call node.
1324 *
1325 * @return bool Returns true if the element is a function call to `define`, otherwise false.
1326 */
1327 private function is_a_define_call( $element ) {
1328 if ( is_a( $element, 'PhpParser\Node\Expr\FuncCall' ) && $this->has_function_name( $element ) && 'define' === $this->get_call_name( $element ) ) {
1329 return true;
1330 }
1331 return false;
1332 }
1333
1334 /**
1335 * Retrieves the name defined within a function call, specifically when the argument is a string.
1336 *
1337 * @param object $function_call The function call object, which is expected to contain arguments to be evaluated.
1338 *
1339 * @return string|null Returns the string value of the define name if the argument is a string, otherwise null.
1340 */
1341 private function get_define_name( $function_call ) {
1342 if ( get_class( $function_call->args[0]->value ) === 'PhpParser\Node\Scalar\String_' ) {
1343 return $function_call->args[0]->value->value;
1344 }
1345 return null;
1346 }
1347 }
1348