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());
+ }
}