From 40c610acb3016d2e1253e0434e86e32b672f4ec8 Mon Sep 17 00:00:00 2001 From: OpaqueRock <305760598+OpaqueRock@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:26:30 +0300 Subject: [PATCH] Fix undefined-array-key warning in visit() for custom Node kinds Node::kind is not required to be a NodeKind constant (custom Node subclasses are supported), so $visitorKeys[$node->kind] can be undefined. Previously the ?? [] fallback was applied to the whole ternary result, so it never triggered for the $inList branch and left the non-list branch's array access unguarded. Move the fallback onto just that array access instead. Add regression tests covering generic and kind-specific visitors for a custom Node kind. --- src/Language/Visitor.php | 4 ++- tests/Language/VisitorTest.php | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) 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 + ); + } }