From b4d45dfa5c6a899df710be401f33b21183866402 Mon Sep 17 00:00:00 2001 From: Shaurya Saria Date: Thu, 22 Oct 2026 01:21:47 +0530 Subject: [PATCH] Fix raw AST child list access --- .../typescript/src/api/node/node.generated.ts | 8 ++-- packages/typescript/test/sync/ast.test.ts | 40 +++++++++++++++++++ tools/scripts/tsc/generate-encoder.ts | 2 +- tsc/internal/api/encoder/encoder.go | 38 ++++++++++++++++-- 4 files changed, 79 insertions(+), 9 deletions(-) diff --git a/packages/typescript/src/api/node/node.generated.ts b/packages/typescript/src/api/node/node.generated.ts index 0f69dbc63abd3..144ccc86721f2 100644 --- a/packages/typescript/src/api/node/node.generated.ts +++ b/packages/typescript/src/api/node/node.generated.ts @@ -665,8 +665,8 @@ export class RemoteNode extends RemoteNodeBase implements Node { get checkType(): RemoteNode | undefined { return this.getNamedChild("checkType") as RemoteNode; } - get children(): RemoteNode | RemoteNodeList | undefined { - return this.getNamedChild("children") as RemoteNode | RemoteNodeList; + get children(): RemoteNodeList | undefined { + return this.getNamedChild("children") as RemoteNodeList; } get className(): RemoteNode | undefined { return this.getNamedChild("className") as RemoteNode; @@ -761,8 +761,8 @@ export class RemoteNode extends RemoteNodeBase implements Node { get initializer(): RemoteNode | undefined { return this.getNamedChild("initializer") as RemoteNode; } - get jsdocPropertyTags(): RemoteNode | undefined { - return this.getNamedChild("jsdocPropertyTags") as RemoteNode; + get jsdocPropertyTags(): RemoteNodeList | undefined { + return this.getNamedChild("jsdocPropertyTags") as RemoteNodeList; } get label(): RemoteNode | undefined { return this.getNamedChild("label") as RemoteNode; diff --git a/packages/typescript/test/sync/ast.test.ts b/packages/typescript/test/sync/ast.test.ts index 1c0cab8becb8c..9bbc2842a6d91 100644 --- a/packages/typescript/test/sync/ast.test.ts +++ b/packages/typescript/test/sync/ast.test.ts @@ -11,6 +11,9 @@ import type { Identifier, InterfaceDeclaration, JSDoc, + JSDocPropertyTag, + JSDocTypeLiteral, + JSDocTypedefTag, MethodDeclaration, Node, NodeArray, @@ -1359,6 +1362,43 @@ describe("RemoteNode + child/token getters", { concurrency }, () => { }); }); + test("jsdocPropertyTags exposes every raw list element", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { allowJs: true, noLib: true } }), + "/src/children.js": [ + "/**", + " * @typedef {Object} Events", + " * @property {string} a", + " * @property {string} b", + " * @property {string} c", + " */", + "export const x = 1;", + ].join("\n"), + }); + try { + const sf = getRemoteSourceFile(api, "/tsconfig.json", "/src/children.js"); + const typedefTag = sf.statements[0].jsDoc?.[0]?.tags?.[0] as JSDocTypedefTag | undefined; + assert.ok(typedefTag, "expected a JSDoc typedef tag"); + const typeLiteral = typedefTag.typeExpression as JSDocTypeLiteral | undefined; + assert.ok(typeLiteral?.kind === SyntaxKind.JSDocTypeLiteral, "expected a JSDoc type literal"); + const propertyTags = typeLiteral.jsdocPropertyTags; + assert.ok(Array.isArray(propertyTags), "jsdocPropertyTags should be a node list"); + assert.deepStrictEqual(propertyTags.map(tag => (tag as JSDocPropertyTag).name.getText(sf)), ["a", "b", "c"]); + + const visitedPropertyTagNames: string[] = []; + typeLiteral.forEachChild(child => { + if (child.kind === SyntaxKind.JSDocPropertyTag) { + visitedPropertyTagNames.push((child as JSDocPropertyTag).name.getText(sf)); + } + return undefined; + }); + assert.deepStrictEqual(visitedPropertyTagNames, ["a", "b", "c"]); + } + finally { + api.close(); + } + }); + test("getChildren throws on a synthesized node without a real position", () => { const synthesized = createBlock([]); // a non-token node with pos/end === -1 assert.throws(() => synthesized.getChildren(), /real position/); diff --git a/tools/scripts/tsc/generate-encoder.ts b/tools/scripts/tsc/generate-encoder.ts index 1b106bb8ff555..654c02803c9e3 100644 --- a/tools/scripts/tsc/generate-encoder.ts +++ b/tools/scripts/tsc/generate-encoder.ts @@ -1293,7 +1293,7 @@ function generateTSNodeGenerated(): string { for (const m of info.childProps) { const propName = api.uncapitalize(m.name); const ct = childType(m); - const isListType = ct === "nodeList" || ct === "modifierList"; + const isListType = ct === "nodeList" || ct === "rawNodeList" || ct === "modifierList"; const existing = childGetters.get(propName); if (!existing) { childGetters.set(propName, isListType ? "nodeList" : "node"); diff --git a/tsc/internal/api/encoder/encoder.go b/tsc/internal/api/encoder/encoder.go index 39367ebc9fae8..7ca4bea8df15b 100644 --- a/tsc/internal/api/encoder/encoder.go +++ b/tsc/internal/api/encoder/encoder.go @@ -335,6 +335,36 @@ type NodeIndexTable struct { var nodeIndexTableKey = ast.NewSourceFileDataKey[*NodeIndexTable]() +// rawNodeList returns the child list stored directly on raw-list AST members. +// The encoded tree represents lists with a synthetic NodeList node, so callers +// must visit this list through the same hook as ordinary NodeList members. +func rawNodeList(node *ast.Node) *ast.NodeList { + switch node.Kind { + case ast.KindSyntaxList: + children := node.AsSyntaxList().Children + if len(children) == 0 { + return nil + } + return &ast.NodeList{Loc: core.NewTextRange(node.Pos(), node.End()), Nodes: children} + case ast.KindJSDocTypeLiteral: + tags := node.AsJSDocTypeLiteral().JSDocPropertyTags + if len(tags) == 0 { + return nil + } + return &ast.NodeList{Loc: core.NewTextRange(tags[0].Pos(), tags[len(tags)-1].End()), Nodes: tags} + default: + return nil + } +} + +func visitEncodedChildren(node *ast.Node, visitor *ast.NodeVisitor) { + if list := rawNodeList(node); list != nil { + visitor.Hooks.VisitNodes(list, visitor) + return + } + visitor.VisitEachChild(node) +} + // GetIndex returns the encoder index for the given node. // On the first call the sortedIdx array is built (O(n log n) sort on a flat []uint32), // then subsequent calls use binary search (O(log n)). This turns out to be much faster than @@ -393,7 +423,7 @@ func BuildNodeIndexTable(sourceFile *ast.SourceFile) *NodeIndexTable { visitor.Visit = func(node *ast.Node) *ast.Node { nodeCount++ nodeTable = append(nodeTable, node) - visitor.VisitEachChild(node) + visitEncodedChildren(node, visitor) for _, jsdoc := range node.JSDoc(sourceFile) { visitor.Visit(jsdoc) } @@ -405,7 +435,7 @@ func BuildNodeIndexTable(sourceFile *ast.SourceFile) *NodeIndexTable { nodeCount++ nodeTable = append(nodeTable, rootNode) - visitor.VisitEachChild(rootNode) + visitEncodedChildren(rootNode, visitor) for _, jsdoc := range rootNode.JSDoc(sourceFile) { visitor.Visit(jsdoc) } @@ -567,7 +597,7 @@ func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIn currentIndex := nodeCount prevIndex = 0 parentIndex = currentIndex - visitor.VisitEachChild(node) + visitEncodedChildren(node, visitor) if sourceFile != nil { for _, jsdoc := range node.JSDoc(sourceFile) { visitor.Visit(jsdoc) @@ -587,7 +617,7 @@ func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIn sfExtendedDataOffset = len(extendedData) nodes = appendUint32s(nodes, uint32(rootNode.Kind), utf16(rootNode.Pos()), utf16(rootNode.End()), 0, 0, getNodeData(rootNode, strs, positionMap, &extendedData, &structuredData), uint32(rootNode.Flags)) - visitor.VisitEachChild(rootNode) + visitEncodedChildren(rootNode, visitor) if sourceFile != nil { for _, jsdoc := range rootNode.JSDoc(sourceFile) { visitor.Visit(jsdoc)