Fix synthesized node ranges across native API encoding (#64859)

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]andRyanCavanaugh authored and GitHub committed 2026-10-21 17:21:25 +00:00
1 parent 7fa14948c3
commit 4d19708e1f
8 files changed
+82 -12

No files matched your search

+6 -6
View File
@@ -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,
@@ -58,11 +58,11 @@ export class RemoteNodeList extends Array<RemoteNode> implements NodeArray<Remot
private _cursorNodeIndex: number = 0;
get pos(): number {
return this.view.getUint32(this._byteIndex + NODE_OFFSET_POS, true);
return this.view.getInt32(this._byteIndex + NODE_OFFSET_POS, true);
}
get end(): number {
return this.view.getUint32(this._byteIndex + NODE_OFFSET_END, true);
return this.view.getInt32(this._byteIndex + NODE_OFFSET_END, true);
}
get next(): number {
@@ -39,18 +39,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,
@@ -58,6 +62,7 @@ import {
createVariableDeclaration,
createVariableDeclarationList,
createVariableStatement,
updateSourceFile,
} from "@typescript/typescript/ast/factory";
import { visitEachChild } from "@typescript/typescript/ast/visitor";
import {
@@ -7964,6 +7969,20 @@ export const obj = { m: 1, s: "hi", b: true };
assert.strictEqual(text, "string");
});
test("synthesized nodes do not acquire source comments", async () => {
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);
+11
View File
@@ -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", []);
+19
View File
@@ -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);
+2 -2
View File
@@ -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 {`);
+2 -2
View File
@@ -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)
+21
View File
@@ -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()