From 59d909230bedeb85c877ee540f475ade2d9da9df Mon Sep 17 00:00:00 2001 From: sergioperezcheco Date: Mon, 19 Oct 2026 19:21:50 +0800 Subject: [PATCH] Clear stale node connections before reconnecting an AST Preserve non-connection attributes and the inactive reference namespace while rebuilding links after AST edits. Assisted-by: Hermes Agent (Nous Research) Signed-off-by: sergioperezcheco --- .../NodeVisitor/NodeConnectingVisitor.php | 9 +++ .../NodeVisitor/NodeConnectingVisitorTest.php | 60 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/lib/PhpParser/NodeVisitor/NodeConnectingVisitor.php b/lib/PhpParser/NodeVisitor/NodeConnectingVisitor.php index 70e051e2d9..b6b6a962ee 100644 --- a/lib/PhpParser/NodeVisitor/NodeConnectingVisitor.php +++ b/lib/PhpParser/NodeVisitor/NodeConnectingVisitor.php @@ -39,6 +39,15 @@ public function beforeTraverse(array $nodes) { } public function enterNode(Node $node) { + // Reconnecting a modified tree must not retain links from its old topology. + $attributes = $node->getAttributes(); + if ($this->weakReferences) { + unset($attributes['weak_parent'], $attributes['weak_previous'], $attributes['weak_next']); + } else { + unset($attributes['parent'], $attributes['previous'], $attributes['next']); + } + $node->setAttributes($attributes); + if (!empty($this->stack)) { $parent = $this->stack[count($this->stack) - 1]; if ($this->weakReferences) { diff --git a/test/PhpParser/NodeVisitor/NodeConnectingVisitorTest.php b/test/PhpParser/NodeVisitor/NodeConnectingVisitorTest.php index c9ac5e898a..c7c31a8377 100644 --- a/test/PhpParser/NodeVisitor/NodeConnectingVisitorTest.php +++ b/test/PhpParser/NodeVisitor/NodeConnectingVisitorTest.php @@ -31,6 +31,66 @@ public function testConnectsNodeToItsParentNodeAndItsSiblingNodes(): void { $this->assertSame(Else_::class, get_class($node->getAttribute('next'))); } + /** @dataProvider provideReferenceModes */ + public function testReconnectsReorderedNodes(bool $weakReferences): void { + $ast = (new ParserFactory())->createForNewestSupportedVersion()->parse( + 'traverse($ast); + $ast = [$ast[2], $ast[1], $ast[0]]; + $traverser->traverse($ast); + $prefix = $weakReferences ? 'weak_' : ''; + $this->assertNull($ast[0]->getAttribute($prefix . 'previous')); + $this->assertNull($ast[2]->getAttribute($prefix . 'next')); + $previous = $ast[1]->getAttribute($prefix . 'previous'); + $next = $ast[1]->getAttribute($prefix . 'next'); + $this->assertSame($ast[0], $weakReferences ? $previous->get() : $previous); + $this->assertSame($ast[2], $weakReferences ? $next->get() : $next); + } + + /** @dataProvider provideReferenceModes */ + public function testReconnectsPromotedNode(bool $weakReferences): void { + $ast = (new ParserFactory())->createForNewestSupportedVersion()->parse( + 'traverse($ast); + $this->assertInstanceOf(If_::class, $ast[0]); + $node = $ast[0]->stmts[0]; + $attributes = $node->getAttributes(); + $traverser->traverse([$node]); + $prefix = $weakReferences ? 'weak_' : ''; + $this->assertNull($node->getAttribute($prefix . 'parent')); + $this->assertNull($node->getAttribute($prefix . 'previous')); + $this->assertNull($node->getAttribute($prefix . 'next')); + $this->assertSame($attributes['startLine'], $node->getStartLine()); + } + + /** @dataProvider provideReferenceModes */ + public function testReconnectsAfterRemovingSiblings(bool $weakReferences): void { + $ast = (new ParserFactory())->createForNewestSupportedVersion()->parse( + 'traverse($ast); + $node = $ast[1]; + $node->setAttribute('custom', 'preserved'); + $otherAttribute = $weakReferences ? 'next' : 'weak_next'; + $node->setAttribute($otherAttribute, 'independent-mode'); + $traverser->traverse([$node]); + $prefix = $weakReferences ? 'weak_' : ''; + $this->assertFalse($node->hasAttribute($prefix . 'previous')); + $this->assertFalse($node->hasAttribute($prefix . 'next')); + $this->assertSame('preserved', $node->getAttribute('custom')); + $this->assertSame('independent-mode', $node->getAttribute($otherAttribute)); + } + + /** @return array */ + public function provideReferenceModes(): array { + return [[false], [true]]; + } + public function testWeakReferences(): void { $ast = (new ParserFactory())->createForNewestSupportedVersion()->parse( '