[api] Add hasTrailingComma getter in the RemoteNodeList class (#63957)

Signed-off-by: mrazauskas <tom@mrazauskas.de>
This commit is contained in:
Tom Mrazauskas authored and GitHub committed 2026-08-26 14:39:51 +00:00
1 parent 5739027c9a
commit 3276240332
9 files changed
+67 -12

No files matched your search

+1 -1
View File
@@ -267,7 +267,7 @@ export function encodeNode(node: Node): Uint8Array {
0, // next
parentIndex,
list.length, // data for NodeList is its length
0, // flags
list.hasTrailingComma ? 1 : 0, // NodeLists have no AST flags; this slot is reused for hasTrailingComma
);
const saveParentIndex = parentIndex;
@@ -43,7 +43,6 @@ export class RemoteNodeList extends Array<RemoteNode> implements NodeArray<Remot
}
parent: RemoteNode;
hasTrailingComma?: boolean;
transformFlags: number = 0;
protected view: DataView;
protected index: number;
@@ -71,6 +70,10 @@ export class RemoteNodeList extends Array<RemoteNode> implements NodeArray<Remot
return this.view.getUint32(this._byteIndex + NODE_OFFSET_DATA, true);
}
get hasTrailingComma(): boolean {
return (this.view.getUint32(this._byteIndex + NODE_OFFSET_FLAGS, true) & 1) !== 0;
}
private sourceFile: SourceFileInfo;
constructor(view: DataView, index: number, parent: RemoteNode, sourceFile: SourceFileInfo, offsetNodes: number) {
+1 -1
View File
@@ -1,4 +1,4 @@
export const PROTOCOL_VERSION = 7;
export const PROTOCOL_VERSION = 8;
export const HEADER_OFFSET_METADATA = 0;
export const HEADER_OFFSET_HASH_LO0 = 4;
@@ -7,6 +7,7 @@ import {
getSynthesizedDeepClone,
InternalSymbolName,
isCallExpression,
isExpressionStatement,
isFunctionDeclaration,
isIdentifier,
isImportDeclaration,
@@ -1236,6 +1237,29 @@ describe("SourceFile", () => {
});
});
describe("NodeArray", () => {
test("hasTrailingComma", async () => {
const api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
"/src/main.ts": `declare function foo(...args: any): void;\nfoo("a", "b",);\nfoo("a", "b");`,
});
try {
const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getProject("/tsconfig.json")!;
const sourceFile = await project.program.getSourceFile("/src/main.ts");
assert.ok(sourceFile);
const statements = sourceFile.statements.filter(isExpressionStatement);
assert.ok(isCallExpression(statements[0].expression));
assert.equal(statements[0].expression.arguments.hasTrailingComma, true);
assert.ok(isCallExpression(statements[1].expression));
assert.equal(statements[1].expression.arguments.hasTrailingComma, false);
}
finally {
await api.close();
}
});
});
test("unicode escapes", async () => {
const api = spawnAPI({
"/tsconfig.json": "{}",
+3 -3
View File
@@ -67,7 +67,7 @@ describe("Encoder", () => {
// Verify header
const view = new DataView(encoded.buffer, encoded.byteOffset, encoded.byteLength);
const metadata = view.getUint32(0, true);
assert.strictEqual(metadata >>> 24, 7, "protocol version should be 7");
assert.strictEqual(metadata >>> 24, 8, "protocol version should be 8");
// Verify we can decode it
const decoded = decode(encoded);
@@ -179,11 +179,11 @@ describe("Encoder", () => {
assert.strictEqual(rootKind, SyntaxKind.IfStatement);
});
test("protocol version is 7", () => {
test("protocol version is 8", () => {
const sf = makeSF("", "/test.ts", []);
const encoded = encodeSourceFile(sf);
const view = new DataView(encoded.buffer, encoded.byteOffset, encoded.byteLength);
assert.strictEqual(view.getUint32(0, true) >>> 24, 7);
assert.strictEqual(view.getUint32(0, true) >>> 24, 8);
});
test("encodes source files without content mapping metadata", () => {
+24
View File
@@ -15,6 +15,7 @@ import {
getSynthesizedDeepClone,
InternalSymbolName,
isCallExpression,
isExpressionStatement,
isFunctionDeclaration,
isIdentifier,
isImportDeclaration,
@@ -1152,6 +1153,29 @@ describe("SourceFile", () => {
});
});
describe("NodeArray", () => {
test("hasTrailingComma", () => {
const api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
"/src/main.ts": `declare function foo(...args: any): void;\nfoo("a", "b",);\nfoo("a", "b");`,
});
try {
const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getProject("/tsconfig.json")!;
const sourceFile = project.program.getSourceFile("/src/main.ts");
assert.ok(sourceFile);
const statements = sourceFile.statements.filter(isExpressionStatement);
assert.ok(isCallExpression(statements[0].expression));
assert.equal(statements[0].expression.arguments.hasTrailingComma, true);
assert.ok(isCallExpression(statements[1].expression));
assert.equal(statements[1].expression.arguments.hasTrailingComma, false);
}
finally {
api.close();
}
});
});
test("unicode escapes", () => {
const api = spawnAPI({
"/tsconfig.json": "{}",
+4 -1
View File
@@ -1518,7 +1518,6 @@ function emitRemoteNodeList(w: CodeWriter) {
w.write(` }`);
w.write(``);
w.write(` parent: RemoteNode;`);
w.write(` hasTrailingComma?: boolean;`);
w.write(` transformFlags: number = 0;`);
w.write(` protected view: DataView;`);
w.write(` protected index: number;`);
@@ -1546,6 +1545,10 @@ function emitRemoteNodeList(w: CodeWriter) {
w.write(` return this.view.getUint32(this._byteIndex + NODE_OFFSET_DATA, true);`);
w.write(` }`);
w.write(``);
w.write(` get hasTrailingComma(): boolean {`);
w.write(` return (this.view.getUint32(this._byteIndex + NODE_OFFSET_FLAGS, true) & 1) !== 0;`);
w.write(` }`);
w.write(``);
w.write(` private sourceFile: SourceFileInfo;`);
w.write(``);
w.write(` constructor(view: DataView, index: number, parent: RemoteNode, sourceFile: SourceFileInfo, offsetNodes: number) {`);
+4 -3
View File
@@ -63,7 +63,7 @@ const (
)
const (
ProtocolVersion uint8 = 7
ProtocolVersion uint8 = 8
)
// Source File Binary Format
@@ -202,7 +202,8 @@ const (
// NodeLists are represented as normal nodes with the special `kind` value `0xff_ff_ff_ff`. They are considered the parent
// of their contents in the encoded format. A client reconstructing an AST similar to TypeScript's internal representation
// should instead set the `parent` pointers of a NodeList's children to the NodeList's parent. A NodeList's `data` field
// is the uint32 length of the list, and does not use one of the data types described below.
// is the uint32 length of the list, and does not use one of the data types described below. A NodeList's `flags` field
// is not used for AST node flags (NodeLists have none); bit 0 instead encodes `HasTrailingComma`.
//
// For node types other than NodeList, the node data field encodes one of the following, determined by the first 2 bits of
// the field:
@@ -502,7 +503,7 @@ func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIn
nodes[prevIndex*NodeSize+NodeOffsetNext+3] = b3
}
nodes = appendUint32s(nodes, SyntaxKindNodeList, utf16(nodeList.Pos()), utf16(nodeList.End()), 0, parentIndex, uint32(len(nodeList.Nodes)), 0)
nodes = appendUint32s(nodes, SyntaxKindNodeList, utf16(nodeList.Pos()), utf16(nodeList.End()), 0, parentIndex, uint32(len(nodeList.Nodes)), uint32(boolToByte(nodeList.HasTrailingComma())))
saveParentIndex := parentIndex
+2 -2
View File
@@ -37,8 +37,8 @@ func TestEncodeSourceFile(t *testing.T) {
func TestEncodeContentMapperSourceFileMetadata(t *testing.T) {
t.Parallel()
if encoder.ProtocolVersion != 7 {
t.Fatalf("protocol version = %d, want 7", encoder.ProtocolVersion)
if encoder.ProtocolVersion != 8 {
t.Fatalf("protocol version = %d, want 8", encoder.ProtocolVersion)
}
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/component.vue",