Skip to content

Commit c9d0b6c

Browse files
authored
Support WeakReferences in NodeConnectingVisitor (nikic#1057)
Add a constructor argument which enables the use of WeakReferences. The attributes are called weak_parent, weak_next and weak_previous in that case.
1 parent 14f9c9d commit c9d0b6c

File tree

6 files changed

+101
-7
lines changed

6 files changed

+101
-7
lines changed

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ phpstan: tools/vendor
88

99
php-cs-fixer: tools/vendor
1010
php tools/vendor/bin/php-cs-fixer fix
11+
12+
tests:
13+
php vendor/bin/phpunit

doc/component/FAQ.markdown

+14
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,17 @@ obtained through `$node->getAttribute('next')`.
5151

5252
`ParentConnectingVisitor` and `NodeConnectingVisitor` should not be used at the same time. The latter
5353
includes the functionality of the former.
54+
55+
56+
How can I limit the impact of cyclic references in the AST?
57+
-----
58+
59+
NodeConnectingVisitor adds a parent reference, which introduces a cycle. This means that the AST can now only be collected by the cycle garbage collector.
60+
This in turn can lead to performance and/or memory issues.
61+
62+
To break the cyclic references between AST nodes `NodeConnectingVisitor` supports a boolean `$weakReferences` constructor parameter.
63+
When set to `true`, all attributes added by `NodeConnectingVisitor` will be wrapped into a `WeakReference` object.
64+
65+
After enabling this parameter, the parent node can be obtained through `$node->getAttribute('weak_parent')`,
66+
the previous node can be obtained through `$node->getAttribute('weak_previous')`, and the next node can be
67+
obtained through `$node->getAttribute('weak_next')`.

lib/PhpParser/NodeVisitor/NodeConnectingVisitor.php

+27-5
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
* Visitor that connects a child node to its parent node
1010
* as well as its sibling nodes.
1111
*
12-
* On the child node, the parent node can be accessed through
12+
* With <code>$weakReferences=false</code> on the child node, the parent node can be accessed through
1313
* <code>$node->getAttribute('parent')</code>, the previous
1414
* node can be accessed through <code>$node->getAttribute('previous')</code>,
1515
* and the next node can be accessed through <code>$node->getAttribute('next')</code>.
16+
*
17+
* With <code>$weakReferences=true</code> attribute names are prefixed by "weak_", e.g. "weak_parent".
1618
*/
1719
final class NodeConnectingVisitor extends NodeVisitorAbstract {
1820
/**
@@ -25,19 +27,39 @@ final class NodeConnectingVisitor extends NodeVisitorAbstract {
2527
*/
2628
private $previous;
2729

30+
private bool $weakReferences;
31+
32+
public function __construct(bool $weakReferences = false) {
33+
$this->weakReferences = $weakReferences;
34+
}
35+
2836
public function beforeTraverse(array $nodes) {
2937
$this->stack = [];
3038
$this->previous = null;
3139
}
3240

3341
public function enterNode(Node $node) {
3442
if (!empty($this->stack)) {
35-
$node->setAttribute('parent', $this->stack[count($this->stack) - 1]);
43+
$parent = $this->stack[count($this->stack) - 1];
44+
if ($this->weakReferences) {
45+
$node->setAttribute('weak_parent', \WeakReference::create($parent));
46+
} else {
47+
$node->setAttribute('parent', $parent);
48+
}
3649
}
3750

38-
if ($this->previous !== null && $this->previous->getAttribute('parent') === $node->getAttribute('parent')) {
39-
$node->setAttribute('previous', $this->previous);
40-
$this->previous->setAttribute('next', $node);
51+
if ($this->previous !== null) {
52+
if (
53+
$this->weakReferences
54+
) {
55+
if ($this->previous->getAttribute('weak_parent') === $node->getAttribute('weak_parent')) {
56+
$node->setAttribute('weak_previous', \WeakReference::create($this->previous));
57+
$this->previous->setAttribute('weak_next', \WeakReference::create($node));
58+
}
59+
} elseif ($this->previous->getAttribute('parent') === $node->getAttribute('parent')) {
60+
$node->setAttribute('previous', $this->previous);
61+
$this->previous->setAttribute('next', $node);
62+
}
4163
}
4264

4365
$this->stack[] = $node;

lib/PhpParser/NodeVisitor/ParentConnectingVisitor.php

+15-2
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,35 @@
1111
/**
1212
* Visitor that connects a child node to its parent node.
1313
*
14-
* On the child node, the parent node can be accessed through
14+
* With <code>$weakReferences=false</code> on the child node, the parent node can be accessed through
1515
* <code>$node->getAttribute('parent')</code>.
16+
*
17+
* With <code>$weakReferences=true</code> the attribute name is "weak_parent" instead.
1618
*/
1719
final class ParentConnectingVisitor extends NodeVisitorAbstract {
1820
/**
1921
* @var Node[]
2022
*/
2123
private array $stack = [];
2224

25+
private bool $weakReferences;
26+
27+
public function __construct(bool $weakReferences = false) {
28+
$this->weakReferences = $weakReferences;
29+
}
30+
2331
public function beforeTraverse(array $nodes) {
2432
$this->stack = [];
2533
}
2634

2735
public function enterNode(Node $node) {
2836
if (!empty($this->stack)) {
29-
$node->setAttribute('parent', $this->stack[count($this->stack) - 1]);
37+
$parent = $this->stack[count($this->stack) - 1];
38+
if ($this->weakReferences) {
39+
$node->setAttribute('weak_parent', \WeakReference::create($parent));
40+
} else {
41+
$node->setAttribute('parent', $parent);
42+
}
3043
}
3144

3245
$this->stack[] = $node;

test/PhpParser/NodeVisitor/NodeConnectingVisitorTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,28 @@ public function testConnectsNodeToItsParentNodeAndItsSiblingNodes(): void {
3030

3131
$this->assertSame(Else_::class, get_class($node->getAttribute('next')));
3232
}
33+
34+
public function testWeakReferences(): void {
35+
$ast = (new ParserFactory())->createForNewestSupportedVersion()->parse(
36+
'<?php if (true) {} else {}'
37+
);
38+
39+
$traverser = new NodeTraverser();
40+
41+
$traverser->addVisitor(new NodeConnectingVisitor(true));
42+
43+
$ast = $traverser->traverse($ast);
44+
45+
$node = (new NodeFinder())->findFirstInstanceof($ast, Else_::class);
46+
47+
$this->assertInstanceOf(\WeakReference::class, $node->getAttribute('weak_parent'));
48+
$this->assertSame(If_::class, get_class($node->getAttribute('weak_parent')->get()));
49+
$this->assertInstanceOf(\WeakReference::class, $node->getAttribute('weak_previous'));
50+
$this->assertSame(ConstFetch::class, get_class($node->getAttribute('weak_previous')->get()));
51+
52+
$node = (new NodeFinder())->findFirstInstanceof($ast, ConstFetch::class);
53+
54+
$this->assertInstanceOf(\WeakReference::class, $node->getAttribute('weak_next'));
55+
$this->assertSame(Else_::class, get_class($node->getAttribute('weak_next')->get()));
56+
}
3357
}

test/PhpParser/NodeVisitor/ParentConnectingVisitorTest.php

+18
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,22 @@ public function testConnectsChildNodeToParentNode(): void {
2323

2424
$this->assertSame('C', $node->getAttribute('parent')->name->toString());
2525
}
26+
27+
public function testWeakReferences(): void {
28+
$ast = (new ParserFactory())->createForNewestSupportedVersion()->parse(
29+
'<?php class C { public function m() {} }'
30+
);
31+
32+
$traverser = new NodeTraverser();
33+
34+
$traverser->addVisitor(new ParentConnectingVisitor(true));
35+
36+
$ast = $traverser->traverse($ast);
37+
38+
$node = (new NodeFinder())->findFirstInstanceof($ast, ClassMethod::class);
39+
40+
$weakReference = $node->getAttribute('weak_parent');
41+
$this->assertInstanceOf(\WeakReference::class, $weakReference);
42+
$this->assertSame('C', $weakReference->get()->name->toString());
43+
}
2644
}

0 commit comments

Comments
 (0)