From 7fa14948c3feeafe892b2b8ad690a7aab63ccc42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:14:00 +0000 Subject: [PATCH 1/2] Initial plan From 4d19708e1ff38cc568dadf37aa0b1b3621958e41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:21:25 +0000 Subject: [PATCH 2/2] Fix synthesized node ranges across native API encoding (#64859) Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- packages/typescript/src/api/node/encoder.ts | 12 +++++------ .../typescript/src/api/node/node.generated.ts | 4 ++-- packages/typescript/test/async/api.test.ts | 19 +++++++++++++++++ packages/typescript/test/encoder.test.ts | 11 ++++++++++ packages/typescript/test/sync/api.test.ts | 19 +++++++++++++++++ tools/scripts/tsc/generate-encoder.ts | 4 ++-- tsc/internal/api/encoder/decoder.go | 4 ++-- tsc/internal/api/encoder/decoder_test.go | 21 +++++++++++++++++++ 8 files changed, 82 insertions(+), 12 deletions(-) diff --git a/packages/typescript/src/api/node/encoder.ts b/packages/typescript/src/api/node/encoder.ts index 0bfdcc2ff02e4..2d7504ecde280 100644 --- a/packages/typescript/src/api/node/encoder.ts +++ b/packages/typescript/src/api/node/encoder.ts @@ -226,8 +226,8 @@ export function encodeNode(node: Node): Uint8Array { const data = getNodeData(node, strs, extendedDataValues, structuredWriter); nodeValues.push( node.kind, - node.pos >= 0 ? node.pos : 0, - node.end >= 0 ? node.end : 0, + node.pos, + node.end, 0, // next (filled in later) parentIndex, data, @@ -259,8 +259,8 @@ export function encodeNode(node: Node): Uint8Array { nodeValues.push( KIND_NODE_LIST, - list.pos >= 0 ? list.pos : 0, - list.end >= 0 ? list.end : 0, + list.pos, + list.end, 0, // next parentIndex, list.length, // data for NodeList is its length @@ -304,8 +304,8 @@ export function encodeNode(node: Node): Uint8Array { const rootData = getNodeData(node, strs, extendedDataValues, structuredWriter); nodeValues.push( node.kind, - node.pos >= 0 ? node.pos : 0, - node.end >= 0 ? node.end : 0, + node.pos, + node.end, 0, 0, rootData, diff --git a/packages/typescript/src/api/node/node.generated.ts b/packages/typescript/src/api/node/node.generated.ts index 0f69dbc63abd3..f223ecf27c2e7 100644 --- a/packages/typescript/src/api/node/node.generated.ts +++ b/packages/typescript/src/api/node/node.generated.ts @@ -58,11 +58,11 @@ export class RemoteNodeList extends Array implements NodeArray { + await using api = spawnAPI(); + await using lease = await api.createSourceFile("/test.ts", "/** leading comment */\nfoo();\n/* @__PURE__ */ baz();\n"); + const { sourceFile } = lease; + const added = createExpressionStatement( + createCallExpression(createIdentifier("bar"), undefined, undefined, [createStringLiteral("x", TokenFlags.None)], NodeFlags.None), + ); + const updated = updateSourceFile(sourceFile, [added, ...sourceFile.statements], sourceFile.endOfFileToken); + const expected = 'bar("x");\n/** leading comment */\nfoo();\n/* @__PURE__ */ baz();\n'; + + assert.strictEqual(await api.printer.printNode(updated), expected); + assert.strictEqual(await api.printer.printFile(updated), expected); + }); + test("printFile", async () => { await using api = spawnAPI(emitterFiles); diff --git a/packages/typescript/test/encoder.test.ts b/packages/typescript/test/encoder.test.ts index a3d09ad64ea22..d8df798619225 100644 --- a/packages/typescript/test/encoder.test.ts +++ b/packages/typescript/test/encoder.test.ts @@ -74,6 +74,17 @@ function decode(data: Uint8Array): RemoteSourceFile { } describe("Encoder", { concurrency }, () => { + test("preserves synthesized node and list ranges", () => { + const statement = createExpressionStatement(createIdentifier("x")); + const sf = makeSF("", "/test.ts", [statement]); + const decoded = decode(encodeSourceFile(sf)); + + for (const range of [decoded, decoded.statements!, decoded.statements!.at(0)!, decoded.endOfFileToken!]) { + assert.strictEqual(range.pos, -1); + assert.strictEqual(range.end, -1); + } + }); + test("encodes empty source file", () => { const sf = makeSF("", "/test.ts", []); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index f048e8ede87e6..548684c661c16 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -47,18 +47,22 @@ import { type RootedFilePath, type SourceFile, SyntaxKind, + TokenFlags, tryGetAmbientModuleNameFromSymbolName, unescapeLeadingUnderscores, } from "@typescript/typescript/ast"; import { cloneNode, createArrayTypeNode, + createCallExpression, + createExpressionStatement, createFunctionTypeNode, createIdentifier, createKeywordTypeNode, createNumericLiteral, createParameterDeclaration, createSourceFile, + createStringLiteral, createToken, createTypeAliasDeclaration, createTypeReferenceNode, @@ -66,6 +70,7 @@ import { createVariableDeclaration, createVariableDeclarationList, createVariableStatement, + updateSourceFile, } from "@typescript/typescript/ast/factory"; import { visitEachChild } from "@typescript/typescript/ast/visitor"; import { @@ -7765,6 +7770,20 @@ export const obj = { m: 1, s: "hi", b: true }; assert.strictEqual(text, "string"); }); + test("synthesized nodes do not acquire source comments", () => { + using api = spawnAPI(); + using lease = api.createSourceFile("/test.ts", "/** leading comment */\nfoo();\n/* @__PURE__ */ baz();\n"); + const { sourceFile } = lease; + const added = createExpressionStatement( + createCallExpression(createIdentifier("bar"), undefined, undefined, [createStringLiteral("x", TokenFlags.None)], NodeFlags.None), + ); + const updated = updateSourceFile(sourceFile, [added, ...sourceFile.statements], sourceFile.endOfFileToken); + const expected = 'bar("x");\n/** leading comment */\nfoo();\n/* @__PURE__ */ baz();\n'; + + assert.strictEqual(api.printer.printNode(updated), expected); + assert.strictEqual(api.printer.printFile(updated), expected); + }); + test("printFile", () => { using api = spawnAPI(emitterFiles); diff --git a/tools/scripts/tsc/generate-encoder.ts b/tools/scripts/tsc/generate-encoder.ts index 1b106bb8ff555..1bcccda2a62ba 100644 --- a/tools/scripts/tsc/generate-encoder.ts +++ b/tools/scripts/tsc/generate-encoder.ts @@ -1535,11 +1535,11 @@ function emitRemoteNodeList(w: CodeWriter) { w.write(` private _cursorNodeIndex: number = 0;`); w.write(``); w.write(` get pos(): number {`); - w.write(` return this.view.getUint32(this._byteIndex + NODE_OFFSET_POS, true);`); + w.write(` return this.view.getInt32(this._byteIndex + NODE_OFFSET_POS, true);`); w.write(` }`); w.write(``); w.write(` get end(): number {`); - w.write(` return this.view.getUint32(this._byteIndex + NODE_OFFSET_END, true);`); + w.write(` return this.view.getInt32(this._byteIndex + NODE_OFFSET_END, true);`); w.write(` }`); w.write(``); w.write(` get next(): number {`); diff --git a/tsc/internal/api/encoder/decoder.go b/tsc/internal/api/encoder/decoder.go index 9deb4abe5f152..6450f5b2a3114 100644 --- a/tsc/internal/api/encoder/decoder.go +++ b/tsc/internal/api/encoder/decoder.go @@ -149,8 +149,8 @@ func (d *astDecoder) decode() (*ast.Node, error) { // Process bottom-up so children exist before parents. for i := d.nodeCount - 1; i >= 1; i-- { kind := d.nodeField(i, NodeOffsetKind) - pos := d.nodeField(i, NodeOffsetPos) - end := d.nodeField(i, NodeOffsetEnd) + pos := int32(d.nodeField(i, NodeOffsetPos)) + end := int32(d.nodeField(i, NodeOffsetEnd)) data := d.nodeField(i, NodeOffsetData) childIndices := d.collectChildren(i) diff --git a/tsc/internal/api/encoder/decoder_test.go b/tsc/internal/api/encoder/decoder_test.go index def2821cb6a25..dafb8e99c64e5 100644 --- a/tsc/internal/api/encoder/decoder_test.go +++ b/tsc/internal/api/encoder/decoder_test.go @@ -2,6 +2,7 @@ package encoder_test import ( "bytes" + "encoding/binary" "os" "path/filepath" "testing" @@ -37,6 +38,26 @@ func TestDecodeSourceFile_Basic(t *testing.T) { assert.Assert(t, decoded.EndOfFileToken != nil) } +func TestDecodeSourceFile_SynthesizedRanges(t *testing.T) { + t.Parallel() + sf := parseSourceFile("foo();") + synthesized := core.NewTextRange(-1, -1) + buf, _, err := encoder.EncodeSourceFile(sf) + assert.NilError(t, err) + nodesOffset := int(binary.LittleEndian.Uint32(buf[encoder.HeaderOffsetNodes:])) + for offset := nodesOffset + encoder.NodeSize; offset < len(buf); offset += encoder.NodeSize { + binary.LittleEndian.PutUint32(buf[offset+encoder.NodeOffsetPos:], ^uint32(0)) + binary.LittleEndian.PutUint32(buf[offset+encoder.NodeOffsetEnd:], ^uint32(0)) + } + decoded, err := encoder.DecodeSourceFile(buf) + assert.NilError(t, err) + + assert.Equal(t, decoded.AsNode().Loc, synthesized) + assert.Equal(t, decoded.Statements.Loc, synthesized) + assert.Equal(t, decoded.Statements.Nodes[0].Loc, synthesized) + assert.Equal(t, decoded.EndOfFileToken.Loc, synthesized) +} + func TestDecodeSourceFile_Metadata(t *testing.T) { t.Parallel()