diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index d1101c68a..c63f45da2 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -319,7 +319,9 @@ public static function visit(object $root, array $visitor, ?array $keyMap = null ]; $inList = $node instanceof NodeList; - $keys = ($inList ? $node : $visitorKeys[$node->kind]) ?? []; + // Nodes with a kind not present in $visitorKeys (e.g. a custom + // Node subclass) are treated as leaves, i.e. no children. + $keys = $inList ? $node : ($visitorKeys[$node->kind] ?? []); $index = -1; $edits = []; if ($parent !== null) { diff --git a/tests/Language/VisitorTest.php b/tests/Language/VisitorTest.php index 12acd43e0..70788a281 100644 --- a/tests/Language/VisitorTest.php +++ b/tests/Language/VisitorTest.php @@ -1905,4 +1905,70 @@ public function testThrowsExceptionWhenAddingIntegerToNodeList(): void ] ); } + + /** + * visit() must correctly invoke generic enter/leave visitors for a Node + * whose kind is not one of the built-in NodeKind constants (e.g. a + * custom Node subclass defined by a consumer). + */ + public function testVisitCallsGenericVisitorsForCustomKind(): void + { + $customNode = new class([]) extends Node { + public string $kind = 'CustomKind'; + }; + + $visited = []; + + Visitor::visit( + $customNode, + [ + 'enter' => static function (Node $node) use (&$visited): void { + $visited[] = ['enter', $node->kind]; + }, + 'leave' => static function (Node $node) use (&$visited): void { + $visited[] = ['leave', $node->kind]; + }, + ] + ); + + self::assertSame( + [ + ['enter', 'CustomKind'], + ['leave', 'CustomKind'], + ], + $visited + ); + } + + /** Same as testVisitCallsGenericVisitorsForCustomKind, but for a visitor keyed specifically by the custom kind. */ + public function testVisitCallsKindSpecificVisitorForCustomKind(): void + { + $customNode = new class([]) extends Node { + public string $kind = 'CustomKind'; + }; + + $visited = []; + + Visitor::visit( + $customNode, + [ + 'CustomKind' => [ + 'enter' => static function (Node $node) use (&$visited): void { + $visited[] = ['enter', $node->kind]; + }, + 'leave' => static function (Node $node) use (&$visited): void { + $visited[] = ['leave', $node->kind]; + }, + ], + ] + ); + + self::assertSame( + [ + ['enter', 'CustomKind'], + ['leave', 'CustomKind'], + ], + $visited + ); + } }