diff --git a/Makefile b/Makefile index 1473c9998c..a1057d052c 100644 --- a/Makefile +++ b/Makefile @@ -8,3 +8,6 @@ phpstan: tools/vendor php-cs-fixer: tools/vendor php tools/vendor/bin/php-cs-fixer fix + +tests: + php vendor/bin/phpunit \ No newline at end of file diff --git a/doc/component/FAQ.markdown b/doc/component/FAQ.markdown index 62c0970c31..2ef13f6707 100644 --- a/doc/component/FAQ.markdown +++ b/doc/component/FAQ.markdown @@ -51,3 +51,17 @@ obtained through `$node->getAttribute('next')`. `ParentConnectingVisitor` and `NodeConnectingVisitor` should not be used at the same time. The latter includes the functionality of the former. + + +How can I limit the impact of cyclic references in the AST? +----- + +NodeConnectingVisitor adds a parent reference, which introduces a cycle. This means that the AST can now only be collected by the cycle garbage collector. +This in turn can lead to performance and/or memory issues. + +To break the cyclic references between AST nodes `NodeConnectingVisitor` supports a boolean `$weakReferences` constructor parameter. +When set to `true`, all attributes added by `NodeConnectingVisitor` will be wrapped into a `WeakReference` object. + +After enabling this parameter, the parent node can be obtained through `$node->getAttribute('weak_parent')`, +the previous node can be obtained through `$node->getAttribute('weak_previous')`, and the next node can be +obtained through `$node->getAttribute('weak_next')`. \ No newline at end of file diff --git a/lib/PhpParser/NodeVisitor/NodeConnectingVisitor.php b/lib/PhpParser/NodeVisitor/NodeConnectingVisitor.php index 38fedfd506..70e051e2d9 100644 --- a/lib/PhpParser/NodeVisitor/NodeConnectingVisitor.php +++ b/lib/PhpParser/NodeVisitor/NodeConnectingVisitor.php @@ -9,10 +9,12 @@ * Visitor that connects a child node to its parent node * as well as its sibling nodes. * - * On the child node, the parent node can be accessed through + * With $weakReferences=false on the child node, the parent node can be accessed through * $node->getAttribute('parent'), the previous * node can be accessed through $node->getAttribute('previous'), * and the next node can be accessed through $node->getAttribute('next'). + * + * With $weakReferences=true attribute names are prefixed by "weak_", e.g. "weak_parent". */ final class NodeConnectingVisitor extends NodeVisitorAbstract { /** @@ -25,6 +27,12 @@ final class NodeConnectingVisitor extends NodeVisitorAbstract { */ private $previous; + private bool $weakReferences; + + public function __construct(bool $weakReferences = false) { + $this->weakReferences = $weakReferences; + } + public function beforeTraverse(array $nodes) { $this->stack = []; $this->previous = null; @@ -32,12 +40,26 @@ public function beforeTraverse(array $nodes) { public function enterNode(Node $node) { if (!empty($this->stack)) { - $node->setAttribute('parent', $this->stack[count($this->stack) - 1]); + $parent = $this->stack[count($this->stack) - 1]; + if ($this->weakReferences) { + $node->setAttribute('weak_parent', \WeakReference::create($parent)); + } else { + $node->setAttribute('parent', $parent); + } } - if ($this->previous !== null && $this->previous->getAttribute('parent') === $node->getAttribute('parent')) { - $node->setAttribute('previous', $this->previous); - $this->previous->setAttribute('next', $node); + if ($this->previous !== null) { + if ( + $this->weakReferences + ) { + if ($this->previous->getAttribute('weak_parent') === $node->getAttribute('weak_parent')) { + $node->setAttribute('weak_previous', \WeakReference::create($this->previous)); + $this->previous->setAttribute('weak_next', \WeakReference::create($node)); + } + } elseif ($this->previous->getAttribute('parent') === $node->getAttribute('parent')) { + $node->setAttribute('previous', $this->previous); + $this->previous->setAttribute('next', $node); + } } $this->stack[] = $node; diff --git a/lib/PhpParser/NodeVisitor/ParentConnectingVisitor.php b/lib/PhpParser/NodeVisitor/ParentConnectingVisitor.php index 1e7e9e8be5..abf6e37d2e 100644 --- a/lib/PhpParser/NodeVisitor/ParentConnectingVisitor.php +++ b/lib/PhpParser/NodeVisitor/ParentConnectingVisitor.php @@ -11,8 +11,10 @@ /** * Visitor that connects a child node to its parent node. * - * On the child node, the parent node can be accessed through + * With $weakReferences=false on the child node, the parent node can be accessed through * $node->getAttribute('parent'). + * + * With $weakReferences=true the attribute name is "weak_parent" instead. */ final class ParentConnectingVisitor extends NodeVisitorAbstract { /** @@ -20,13 +22,24 @@ final class ParentConnectingVisitor extends NodeVisitorAbstract { */ private array $stack = []; + private bool $weakReferences; + + public function __construct(bool $weakReferences = false) { + $this->weakReferences = $weakReferences; + } + public function beforeTraverse(array $nodes) { $this->stack = []; } public function enterNode(Node $node) { if (!empty($this->stack)) { - $node->setAttribute('parent', $this->stack[count($this->stack) - 1]); + $parent = $this->stack[count($this->stack) - 1]; + if ($this->weakReferences) { + $node->setAttribute('weak_parent', \WeakReference::create($parent)); + } else { + $node->setAttribute('parent', $parent); + } } $this->stack[] = $node; diff --git a/test/PhpParser/NodeVisitor/NodeConnectingVisitorTest.php b/test/PhpParser/NodeVisitor/NodeConnectingVisitorTest.php index eab58776cd..c9ac5e898a 100644 --- a/test/PhpParser/NodeVisitor/NodeConnectingVisitorTest.php +++ b/test/PhpParser/NodeVisitor/NodeConnectingVisitorTest.php @@ -30,4 +30,28 @@ public function testConnectsNodeToItsParentNodeAndItsSiblingNodes(): void { $this->assertSame(Else_::class, get_class($node->getAttribute('next'))); } + + public function testWeakReferences(): void { + $ast = (new ParserFactory())->createForNewestSupportedVersion()->parse( + 'addVisitor(new NodeConnectingVisitor(true)); + + $ast = $traverser->traverse($ast); + + $node = (new NodeFinder())->findFirstInstanceof($ast, Else_::class); + + $this->assertInstanceOf(\WeakReference::class, $node->getAttribute('weak_parent')); + $this->assertSame(If_::class, get_class($node->getAttribute('weak_parent')->get())); + $this->assertInstanceOf(\WeakReference::class, $node->getAttribute('weak_previous')); + $this->assertSame(ConstFetch::class, get_class($node->getAttribute('weak_previous')->get())); + + $node = (new NodeFinder())->findFirstInstanceof($ast, ConstFetch::class); + + $this->assertInstanceOf(\WeakReference::class, $node->getAttribute('weak_next')); + $this->assertSame(Else_::class, get_class($node->getAttribute('weak_next')->get())); + } } diff --git a/test/PhpParser/NodeVisitor/ParentConnectingVisitorTest.php b/test/PhpParser/NodeVisitor/ParentConnectingVisitorTest.php index 8f66a36479..059c2260ff 100644 --- a/test/PhpParser/NodeVisitor/ParentConnectingVisitorTest.php +++ b/test/PhpParser/NodeVisitor/ParentConnectingVisitorTest.php @@ -23,4 +23,22 @@ public function testConnectsChildNodeToParentNode(): void { $this->assertSame('C', $node->getAttribute('parent')->name->toString()); } + + public function testWeakReferences(): void { + $ast = (new ParserFactory())->createForNewestSupportedVersion()->parse( + 'addVisitor(new ParentConnectingVisitor(true)); + + $ast = $traverser->traverse($ast); + + $node = (new NodeFinder())->findFirstInstanceof($ast, ClassMethod::class); + + $weakReference = $node->getAttribute('weak_parent'); + $this->assertInstanceOf(\WeakReference::class, $weakReference); + $this->assertSame('C', $weakReference->get()->name->toString()); + } }