diff --git a/packages/typescript/src/api/node/encoder.ts b/packages/typescript/src/api/node/encoder.ts index 8f1acc5e43963..0071f7970a579 100644 --- a/packages/typescript/src/api/node/encoder.ts +++ b/packages/typescript/src/api/node/encoder.ts @@ -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; diff --git a/packages/typescript/src/api/node/node.generated.ts b/packages/typescript/src/api/node/node.generated.ts index d9a8092261627..035c8d308e737 100644 --- a/packages/typescript/src/api/node/node.generated.ts +++ b/packages/typescript/src/api/node/node.generated.ts @@ -43,7 +43,6 @@ export class RemoteNodeList extends Array implements NodeArray implements NodeArray { }); }); +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": "{}", diff --git a/packages/typescript/test/encoder.test.ts b/packages/typescript/test/encoder.test.ts index 9e1318bdc770f..c214e0675f56e 100644 --- a/packages/typescript/test/encoder.test.ts +++ b/packages/typescript/test/encoder.test.ts @@ -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", () => { diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 20160658071d5..e7baefefd9cc2 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -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": "{}", diff --git a/tools/scripts/tsc/generate-encoder.ts b/tools/scripts/tsc/generate-encoder.ts index 6bf04ddbcb2d2..fc1a67022a39d 100644 --- a/tools/scripts/tsc/generate-encoder.ts +++ b/tools/scripts/tsc/generate-encoder.ts @@ -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) {`); diff --git a/tsc/internal/api/encoder/encoder.go b/tsc/internal/api/encoder/encoder.go index f318a27c44547..19cee9a951512 100644 --- a/tsc/internal/api/encoder/encoder.go +++ b/tsc/internal/api/encoder/encoder.go @@ -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 diff --git a/tsc/internal/api/encoder/encoder_test.go b/tsc/internal/api/encoder/encoder_test.go index cc2dc00118cc0..4ac3e5ba036a6 100644 --- a/tsc/internal/api/encoder/encoder_test.go +++ b/tsc/internal/api/encoder/encoder_test.go @@ -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",