diff --git a/packages/typescript/src/api/node/encoder.ts b/packages/typescript/src/api/node/encoder.ts index 8f1acc5e43963..08dcdb9c87ae3 100644 --- a/packages/typescript/src/api/node/encoder.ts +++ b/packages/typescript/src/api/node/encoder.ts @@ -207,11 +207,11 @@ export function encodeNode(node: Node): Uint8Array { const extendedDataValues: number[] = []; const structuredWriter = new MsgpackWriter(); - // We'll build an array of uint32 values for the nodes section, 7 per node + // We'll build an array of uint32 values for the nodes section, NODE_FIELDS per node const nodeValues: number[] = []; // Nil node (index 0) - nodeValues.push(0, 0, 0, 0, 0, 0, 0); + nodeValues.push(0, 0, 0, 0, 0, 0, 0, 0); let nodeCount = 0; let parentIndex = 0; @@ -235,6 +235,7 @@ export function encodeNode(node: Node): Uint8Array { parentIndex, data, node.flags, + 0, // hasTrailingComma — not applicable to ordinary nodes ); const saveParentIndex = parentIndex; @@ -267,7 +268,8 @@ export function encodeNode(node: Node): Uint8Array { 0, // next parentIndex, list.length, // data for NodeList is its length - 0, // flags + 0, // flags — not applicable to NodeLists + list.hasTrailingComma ? 1 : 0, ); const saveParentIndex = parentIndex; @@ -313,6 +315,7 @@ export function encodeNode(node: Node): Uint8Array { 0, rootData, node.flags, + 0, // hasTrailingComma — not applicable to ordinary nodes ); const saveParent = parentIndex; diff --git a/packages/typescript/src/api/node/node.generated.ts b/packages/typescript/src/api/node/node.generated.ts index d9a8092261627..5293baac436ea 100644 --- a/packages/typescript/src/api/node/node.generated.ts +++ b/packages/typescript/src/api/node/node.generated.ts @@ -29,6 +29,7 @@ import { NODE_OFFSET_DATA, NODE_OFFSET_END, NODE_OFFSET_FLAGS, + NODE_OFFSET_HAS_TRAILING_COMMA, NODE_OFFSET_KIND, NODE_OFFSET_NEXT, NODE_OFFSET_PARENT, @@ -43,7 +44,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 6974061a4a6fb..2ba00f75e1227 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, @@ -1091,6 +1092,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..64ef63c198408 100644 --- a/tools/scripts/tsc/generate-encoder.ts +++ b/tools/scripts/tsc/generate-encoder.ts @@ -1491,6 +1491,7 @@ function emitNodeGeneratedImports(w: CodeWriter) { w.write(` NODE_OFFSET_DATA,`); w.write(` NODE_OFFSET_END,`); w.write(` NODE_OFFSET_FLAGS,`); + w.write(` NODE_OFFSET_HAS_TRAILING_COMMA,`); w.write(` NODE_OFFSET_KIND,`); w.write(` NODE_OFFSET_NEXT,`); w.write(` NODE_OFFSET_PARENT,`); @@ -1518,7 +1519,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 +1546,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_HAS_TRAILING_COMMA, true) !== 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..34a2a5c3902be 100644 --- a/tsc/internal/api/encoder/encoder.go +++ b/tsc/internal/api/encoder/encoder.go @@ -27,6 +27,7 @@ const ( NodeOffsetParent NodeOffsetData NodeOffsetFlags + NodeOffsetHasTrailingComma // NodeSize is the number of bytes that represents a single node in the encoded format. NodeSize ) @@ -63,7 +64,7 @@ const ( ) const ( - ProtocolVersion uint8 = 7 + ProtocolVersion uint8 = 8 ) // Source File Binary Format @@ -84,7 +85,7 @@ const ( // | String data | variable | UTF-8 encoded string data. | // | Extended node data | variable | Extra data for some kinds of nodes. | // | Structured data | variable | Msgpack-encoded metadata blobs (e.g. file references). | -// | Nodes | 28 bytes per node | Defines the AST structure of the file, with references to strings and extended data. | +// | Nodes | 32 bytes per node | Defines the AST structure of the file, with references to strings and extended data. | // // Header (44 bytes) // ----------------- @@ -179,7 +180,7 @@ const ( // // An offset of 0xFFFFFFFF indicates no data (empty array). // -// Nodes (28 bytes per node) +// Nodes (32 bytes per node) // ------------------------- // // The nodes section contains the AST structure of the file. Nodes are represented in a flat array in source order, @@ -195,8 +196,9 @@ const ( // | 16-20 | uint32 | Node index of parent | // | 20-24 | | Node data | // | 24-28 | uint32 | Node flags | +// | 28-32 | uint32 | HasTrailingComma (NodeList only; reserved/0 otherwise) | // -// The first 28 bytes of the nodes section are zeros representing a nil node, such that nodes without a parent or next +// The first 32 bytes of the nodes section are zeros representing a nil node, such that nodes without a parent or next // sibling can unambiuously use `0` for those indices. // // NodeLists are represented as normal nodes with the special `kind` value `0xff_ff_ff_ff`. They are considered the parent @@ -502,7 +504,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)), 0, uint32(boolToByte(nodeList.HasTrailingComma()))) saveParentIndex := parentIndex @@ -535,7 +537,7 @@ func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIn nodes[prevIndex*NodeSize+NodeOffsetNext+3] = b3 } - nodes = appendUint32s(nodes, uint32(node.Kind), utf16(node.Pos()), utf16(node.End()), 0, parentIndex, getNodeData(node, strs, positionMap, &extendedData, &structuredData), uint32(node.Flags)) + nodes = appendUint32s(nodes, uint32(node.Kind), utf16(node.Pos()), utf16(node.End()), 0, parentIndex, getNodeData(node, strs, positionMap, &extendedData, &structuredData), uint32(node.Flags), 0) if nodeIndexMap != nil { if _, ok := nodeIndexMap[node]; ok { @@ -559,14 +561,14 @@ func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIn return node } - nodes = appendUint32s(nodes, 0, 0, 0, 0, 0, 0, 0) + nodes = appendUint32s(nodes, 0, 0, 0, 0, 0, 0, 0, 0) nodeCount++ parentIndex++ nodeTable = append(nodeTable, rootNode) // index 1 = root node 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)) + nodes = appendUint32s(nodes, uint32(rootNode.Kind), utf16(rootNode.Pos()), utf16(rootNode.End()), 0, 0, getNodeData(rootNode, strs, positionMap, &extendedData, &structuredData), uint32(rootNode.Flags), 0) visitor.VisitEachChild(rootNode) if sourceFile != nil { 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",