From 9a9cc1ece367154909637125f0618b47a47e8072 Mon Sep 17 00:00:00 2001 From: tcernicovad1 Date: Mon, 28 Sep 2026 16:26:00 +0100 Subject: [PATCH 1/3] Add support for setting emit flags and synthetic comments --- packages/typescript/src/api/async/api.ts | 100 ++++++++++-- packages/typescript/src/api/node/encoder.ts | 5 +- .../typescript/src/api/proto.generated.ts | 16 ++ packages/typescript/src/api/sync/api.ts | 117 +++++++++++--- .../typescript/src/enums/emitFlags.enum.ts | 29 ++++ packages/typescript/src/enums/emitFlags.ts | 29 ++++ packages/typescript/test/async/api.test.ts | 149 ++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 149 ++++++++++++++++++ tools/scripts/tsc/generate-enums.ts | 1 + tsc/internal/api/encoder/decoder.go | 29 ++-- tsc/internal/api/enum_values_generated.go | 28 ++++ tsc/internal/api/proto.go | 27 +++- tsc/internal/api/session.go | 51 ++++-- tsc/internal/printer/printer.go | 1 - 14 files changed, 674 insertions(+), 57 deletions(-) create mode 100644 packages/typescript/src/enums/emitFlags.enum.ts create mode 100644 packages/typescript/src/enums/emitFlags.ts diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index d767720351e95..e9d2a23ccbe80 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -2,6 +2,7 @@ import { CheckFlags } from "#enums/checkFlags"; import { CompletionItemKind } from "#enums/completionItemKind"; import { DiagnosticCategory } from "#enums/diagnosticCategory"; import { ElementFlags } from "#enums/elementFlags"; +import { EmitFlags } from "#enums/emitFlags"; import { EmitOnly } from "#enums/emitOnly"; import { IndexKind } from "#enums/indexKind"; import { JsxEmit } from "#enums/jsxEmit"; @@ -170,7 +171,7 @@ import type { export { formatDiagnostics, formatDiagnosticsWithColorAndContext } from "../diagnosticFormatter.ts"; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; -export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; +export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitFlags, EmitOnly, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; export type { APIImportAdderAction as ImportAdderAction, APIOptions, @@ -2937,6 +2938,36 @@ export interface PrintNodeOptions { preserveSourceNewlines?: boolean | undefined; neverAsciiEscape?: boolean | undefined; terminateUnterminatedLiterals?: boolean | undefined; + emitContext?: EmitContext; +} +type CommentKind = SyntaxKind.MultiLineCommentTrivia | SyntaxKind.SingleLineCommentTrivia; +export interface SynthesizedComment { + kind: CommentKind; + text: string; + hasTrailingNewLine: boolean; + hasLeadingNewLine: boolean; +} + +/** @internal */ +interface EmitNode { + emitFlags: EmitFlags; + leadingComments: SynthesizedComment[] | undefined; + trailingComments: SynthesizedComment[] | undefined; +} + +export interface EmitContext { + /** @internal */ + getEmitNode(node: Node): EmitNode | undefined; + setEmitFlags(node: T, emitFlags: EmitFlags): T; + getEmitFlags(node: Node): EmitFlags; + + addSyntheticLeadingComment(node: Node, kind: CommentKind, text: string, hasTrailingNewLine: boolean, hasLeadingNewLine: boolean): Node; + setSyntheticLeadingComments(node: T, comments: SynthesizedComment[]): T; + getSyntheticLeadingComments(node: Node): readonly SynthesizedComment[] | undefined; + + addSyntheticTrailingComment(node: Node, kind: CommentKind, text: string, hasTrailingNewLine: boolean, hasLeadingNewLine: boolean): Node; + setSyntheticTrailingComments(node: T, comments: SynthesizedComment[]): T; + getSyntheticTrailingComments(node: Node): readonly SynthesizedComment[] | undefined; } export class Printer { @@ -2947,25 +2978,74 @@ export class Printer { } async printNode(node: Node, options: PrintNodeOptions = {}): Promise { - const encoded = encodeNode(node); + const emitNodes: Record = {}; + const encoded = encodeNode(node, (node, index) => { + const emitNode = options.emitContext?.getEmitNode(node); + if (emitNode === undefined) return; + emitNodes[index] = emitNode; + }); const base64 = uint8ArrayToBase64(encoded); return this.client.apiRequest("printNode", { data: base64, preserveSourceNewlines: options.preserveSourceNewlines, neverAsciiEscape: options.neverAsciiEscape, terminateUnterminatedLiterals: options.terminateUnterminatedLiterals, + emitNodes, }); } async printFile(sourceFile: SourceFile, options: PrintNodeOptions = {}): Promise { - const encoded = encodeNode(sourceFile); - const base64 = uint8ArrayToBase64(encoded); - return this.client.apiRequest("printNode", { - data: base64, - preserveSourceNewlines: options.preserveSourceNewlines, - neverAsciiEscape: options.neverAsciiEscape, - terminateUnterminatedLiterals: options.terminateUnterminatedLiterals, - }); + return this.printNode(sourceFile, options); + } + + createEmitContext(): EmitContext { + const emitNodes = new WeakMap(); + + function getOrCreateEmitNode(node: Node): EmitNode { + let emitNode = emitNodes.get(node); + if (!emitNode) { + emitNode = { emitFlags: 0 as EmitFlags, leadingComments: undefined, trailingComments: undefined }; + emitNodes.set(node, emitNode); + } + return emitNode; + } + + return { + getEmitNode(node) { + return emitNodes.get(node); + }, + setEmitFlags(node, emitFlags) { + getOrCreateEmitNode(node).emitFlags = emitFlags; + return node; + }, + getEmitFlags(node) { + return emitNodes.get(node)?.emitFlags ?? 0 as EmitFlags; + }, + + addSyntheticLeadingComment(node, kind, text, hasTrailingNewLine, hasLeadingNewLine) { + (getOrCreateEmitNode(node).leadingComments ??= []).push({ kind, text, hasTrailingNewLine, hasLeadingNewLine }); + return node; + }, + setSyntheticLeadingComments(node, comments) { + getOrCreateEmitNode(node).leadingComments = comments; + return node; + }, + getSyntheticLeadingComments(node) { + return emitNodes.get(node)?.leadingComments; + }, + + addSyntheticTrailingComment(node, kind, text, hasTrailingNewLine, hasLeadingNewLine) { + (getOrCreateEmitNode(node).trailingComments ??= []).push({ kind, text, hasTrailingNewLine, hasLeadingNewLine }); + return node; + }, + setSyntheticTrailingComments(node, comments) { + getOrCreateEmitNode(node).trailingComments = comments; + return node; + }, + getSyntheticTrailingComments(node) { + return emitNodes.get(node)?.trailingComments; + }, + }; } } diff --git a/packages/typescript/src/api/node/encoder.ts b/packages/typescript/src/api/node/encoder.ts index 0bfdcc2ff02e4..81db701855134 100644 --- a/packages/typescript/src/api/node/encoder.ts +++ b/packages/typescript/src/api/node/encoder.ts @@ -199,7 +199,7 @@ export function encodeSourceFile(sourceFile: SourceFile): Uint8Array { * Encode an arbitrary AST node into the binary format. * When encoding a non-SourceFile node, the header hash and parse options fields will be zero. */ -export function encodeNode(node: Node): Uint8Array { +export function encodeNode(node: Node, onNodeEncoded?: (node: Node, index: number) => void): Uint8Array { const strs = new StringTable(); const extendedDataValues: number[] = []; const structuredWriter = new MsgpackWriter(); @@ -233,7 +233,7 @@ export function encodeNode(node: Node): Uint8Array { data, node.flags, ); - + onNodeEncoded?.(node, currentIndex); const saveParentIndex = parentIndex; const savePrevIndex = prevIndex; parentIndex = currentIndex; @@ -315,6 +315,7 @@ export function encodeNode(node: Node): Uint8Array { const saveParent = parentIndex; prevIndex = 0; parentIndex = 1; // root is at index 1 + onNodeEncoded?.(node, 1); visitChildren(node); parentIndex = saveParent; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 4dccbe902202c..232f6205091c8 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -1005,6 +1005,7 @@ export interface PrintNodeParams { preserveSourceNewlines?: boolean | undefined; neverAsciiEscape?: boolean | undefined; terminateUnterminatedLiterals?: boolean | undefined; + emitNodes?: Record | undefined; } /** FormatNodeForInsertionParams are the parameters for the formatNodeForInsertion method. */ @@ -1727,6 +1728,13 @@ export interface DiagnosticSourceLineResponse { text: string; } +/** Extra information for a node used during node printing */ +export interface PrintEmitNode { + emitFlags?: number | undefined; + leadingComments?: PrintSynthesizedComment[] | undefined; + trailingComments?: PrintSynthesizedComment[] | undefined; +} + export interface EmitOutputFile { fileName: string; text: string; @@ -1798,6 +1806,14 @@ export interface CompletionEntryLabelDetailsResponse { description?: string | undefined; } +/** Synthetic comment added during node printing */ +export interface PrintSynthesizedComment { + kind: number; + text: string; + hasTrailingNewLine?: boolean | undefined; + hasLeadingNewLine?: boolean | undefined; +} + export interface CreateProgramOptions { projectReferences?: ProjectReference[] | undefined; configFileParsingDiagnostics?: DiagnosticResponse[] | undefined; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index dcd00e40ee58d..fa6bfd22b72b6 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -19,6 +19,7 @@ import { CheckFlags } from "#enums/checkFlags"; import { CompletionItemKind } from "#enums/completionItemKind"; import { DiagnosticCategory } from "#enums/diagnosticCategory"; import { ElementFlags } from "#enums/elementFlags"; +import { EmitFlags } from "#enums/emitFlags"; import { EmitOnly } from "#enums/emitOnly"; import { IndexKind } from "#enums/indexKind"; import { JsxEmit } from "#enums/jsxEmit"; @@ -187,7 +188,7 @@ import type { export { formatDiagnostics, formatDiagnosticsWithColorAndContext } from "../diagnosticFormatter.ts"; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; -export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; +export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitFlags, EmitOnly, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; export type { APIImportAdderAction as ImportAdderAction, APIOptions, @@ -6191,6 +6192,36 @@ export interface PrintNodeOptions { preserveSourceNewlines?: boolean | undefined; neverAsciiEscape?: boolean | undefined; terminateUnterminatedLiterals?: boolean | undefined; + emitContext?: EmitContext; +} +type CommentKind = SyntaxKind.MultiLineCommentTrivia | SyntaxKind.SingleLineCommentTrivia; +export interface SynthesizedComment { + kind: CommentKind; + text: string; + hasTrailingNewLine: boolean; + hasLeadingNewLine: boolean; +} + +/** @internal */ +interface EmitNode { + emitFlags: EmitFlags; + leadingComments: SynthesizedComment[] | undefined; + trailingComments: SynthesizedComment[] | undefined; +} + +export interface EmitContext { + /** @internal */ + getEmitNode(node: Node): EmitNode | undefined; + setEmitFlags(node: T, emitFlags: EmitFlags): T; + getEmitFlags(node: Node): EmitFlags; + + addSyntheticLeadingComment(node: Node, kind: CommentKind, text: string, hasTrailingNewLine: boolean, hasLeadingNewLine: boolean): Node; + setSyntheticLeadingComments(node: T, comments: SynthesizedComment[]): T; + getSyntheticLeadingComments(node: Node): readonly SynthesizedComment[] | undefined; + + addSyntheticTrailingComment(node: Node, kind: CommentKind, text: string, hasTrailingNewLine: boolean, hasLeadingNewLine: boolean): Node; + setSyntheticTrailingComments(node: T, comments: SynthesizedComment[]): T; + getSyntheticTrailingComments(node: Node): readonly SynthesizedComment[] | undefined; } export class Printer { @@ -6209,23 +6240,35 @@ export class Printer { owner, "printNode", function (node: Node, options: PrintNodeOptions = {}): string { - const encoded = encodeNode(node); + const emitNodes: Record = {}; + const encoded = encodeNode(node, (node, index) => { + const emitNode = options.emitContext?.getEmitNode(node); + if (emitNode === undefined) return; + emitNodes[index] = emitNode; + }); const base64 = uint8ArrayToBase64(encoded); return owner.client.apiRequest("printNode", { data: base64, preserveSourceNewlines: options.preserveSourceNewlines, neverAsciiEscape: options.neverAsciiEscape, terminateUnterminatedLiterals: options.terminateUnterminatedLiterals, + emitNodes, }); }, function* (node: Node, options: PrintNodeOptions = {}): Generator { - const encoded = encodeNode(node); + const emitNodes: Record = {}; + const encoded = encodeNode(node, (node, index) => { + const emitNode = options.emitContext?.getEmitNode(node); + if (emitNode === undefined) return; + emitNodes[index] = emitNode; + }); const base64 = uint8ArrayToBase64(encoded); return yield* apiRequest("printNode", { data: base64, preserveSourceNewlines: options.preserveSourceNewlines, neverAsciiEscape: options.neverAsciiEscape, terminateUnterminatedLiterals: options.terminateUnterminatedLiterals, + emitNodes, }); }, ); @@ -6240,27 +6283,63 @@ export class Printer { owner, "printFile", function (sourceFile: SourceFile, options: PrintNodeOptions = {}): string { - const encoded = encodeNode(sourceFile); - const base64 = uint8ArrayToBase64(encoded); - return owner.client.apiRequest("printNode", { - data: base64, - preserveSourceNewlines: options.preserveSourceNewlines, - neverAsciiEscape: options.neverAsciiEscape, - terminateUnterminatedLiterals: options.terminateUnterminatedLiterals, - }); + return owner.printNode(sourceFile, options); }, function* (sourceFile: SourceFile, options: PrintNodeOptions = {}): Generator { - const encoded = encodeNode(sourceFile); - const base64 = uint8ArrayToBase64(encoded); - return yield* apiRequest("printNode", { - data: base64, - preserveSourceNewlines: options.preserveSourceNewlines, - neverAsciiEscape: options.neverAsciiEscape, - terminateUnterminatedLiterals: options.terminateUnterminatedLiterals, - }); + return yield* owner.printNode.gen(sourceFile, options); }, ); } + + createEmitContext(): EmitContext { + const emitNodes = new WeakMap(); + + function getOrCreateEmitNode(node: Node): EmitNode { + let emitNode = emitNodes.get(node); + if (!emitNode) { + emitNode = { emitFlags: 0 as EmitFlags, leadingComments: undefined, trailingComments: undefined }; + emitNodes.set(node, emitNode); + } + return emitNode; + } + + return { + getEmitNode(node) { + return emitNodes.get(node); + }, + setEmitFlags(node, emitFlags) { + getOrCreateEmitNode(node).emitFlags = emitFlags; + return node; + }, + getEmitFlags(node) { + return emitNodes.get(node)?.emitFlags ?? 0 as EmitFlags; + }, + + addSyntheticLeadingComment(node, kind, text, hasTrailingNewLine, hasLeadingNewLine) { + (getOrCreateEmitNode(node).leadingComments ??= []).push({ kind, text, hasTrailingNewLine, hasLeadingNewLine }); + return node; + }, + setSyntheticLeadingComments(node, comments) { + getOrCreateEmitNode(node).leadingComments = comments; + return node; + }, + getSyntheticLeadingComments(node) { + return emitNodes.get(node)?.leadingComments; + }, + + addSyntheticTrailingComment(node, kind, text, hasTrailingNewLine, hasLeadingNewLine) { + (getOrCreateEmitNode(node).trailingComments ??= []).push({ kind, text, hasTrailingNewLine, hasLeadingNewLine }); + return node; + }, + setSyntheticTrailingComments(node, comments) { + getOrCreateEmitNode(node).trailingComments = comments; + return node; + }, + getSyntheticTrailingComments(node) { + return emitNodes.get(node)?.trailingComments; + }, + }; + } } export class SnapshotInternalAPI { diff --git a/packages/typescript/src/enums/emitFlags.enum.ts b/packages/typescript/src/enums/emitFlags.enum.ts new file mode 100644 index 0000000000000..2f179a816ce04 --- /dev/null +++ b/packages/typescript/src/enums/emitFlags.enum.ts @@ -0,0 +1,29 @@ +// Code generated by tools/scripts/tsc/generate-enums.ts from tsc/internal/printer/emitflags.go. DO NOT EDIT. + +export enum EmitFlags { + SingleLine = 1 << 0, + MultiLine = 1 << 1, + NoLeadingSourceMap = 1 << 2, + NoTrailingSourceMap = 1 << 3, + NoNestedSourceMaps = 1 << 4, + NoTokenLeadingSourceMaps = 1 << 5, + NoTokenTrailingSourceMaps = 1 << 6, + NoLeadingComments = 1 << 7, + NoTrailingComments = 1 << 8, + NoNestedComments = 1 << 9, + HelperName = 1 << 10, + ExportName = 1 << 11, + LocalName = 1 << 12, + Indented = 1 << 13, + NoIndentation = 1 << 14, + ReuseTempVariableScope = 1 << 15, + CustomPrologue = 1 << 16, + NoAsciiEscaping = 1 << 17, + ExternalHelpers = 1 << 18, + StartOnNewLine = 1 << 19, + IndirectCall = 1 << 20, + AsyncFunctionBody = 1 << 21, + NoLexicalArguments = 1 << 22, + TransformPrivateStaticElements = 1 << 23, + NoLexicalThis = 1 << 24, +} diff --git a/packages/typescript/src/enums/emitFlags.ts b/packages/typescript/src/enums/emitFlags.ts new file mode 100644 index 0000000000000..0529718c10c2c --- /dev/null +++ b/packages/typescript/src/enums/emitFlags.ts @@ -0,0 +1,29 @@ +// Code generated by tools/scripts/tsc/generate-enums.ts from tsc/internal/printer/emitflags.go. DO NOT EDIT. +export var EmitFlags: any; +(function (EmitFlags) { + EmitFlags[EmitFlags["SingleLine"] = 1] = "SingleLine"; + EmitFlags[EmitFlags["MultiLine"] = 2] = "MultiLine"; + EmitFlags[EmitFlags["NoLeadingSourceMap"] = 4] = "NoLeadingSourceMap"; + EmitFlags[EmitFlags["NoTrailingSourceMap"] = 8] = "NoTrailingSourceMap"; + EmitFlags[EmitFlags["NoNestedSourceMaps"] = 16] = "NoNestedSourceMaps"; + EmitFlags[EmitFlags["NoTokenLeadingSourceMaps"] = 32] = "NoTokenLeadingSourceMaps"; + EmitFlags[EmitFlags["NoTokenTrailingSourceMaps"] = 64] = "NoTokenTrailingSourceMaps"; + EmitFlags[EmitFlags["NoLeadingComments"] = 128] = "NoLeadingComments"; + EmitFlags[EmitFlags["NoTrailingComments"] = 256] = "NoTrailingComments"; + EmitFlags[EmitFlags["NoNestedComments"] = 512] = "NoNestedComments"; + EmitFlags[EmitFlags["HelperName"] = 1024] = "HelperName"; + EmitFlags[EmitFlags["ExportName"] = 2048] = "ExportName"; + EmitFlags[EmitFlags["LocalName"] = 4096] = "LocalName"; + EmitFlags[EmitFlags["Indented"] = 8192] = "Indented"; + EmitFlags[EmitFlags["NoIndentation"] = 16384] = "NoIndentation"; + EmitFlags[EmitFlags["ReuseTempVariableScope"] = 32768] = "ReuseTempVariableScope"; + EmitFlags[EmitFlags["CustomPrologue"] = 65536] = "CustomPrologue"; + EmitFlags[EmitFlags["NoAsciiEscaping"] = 131072] = "NoAsciiEscaping"; + EmitFlags[EmitFlags["ExternalHelpers"] = 262144] = "ExternalHelpers"; + EmitFlags[EmitFlags["StartOnNewLine"] = 524288] = "StartOnNewLine"; + EmitFlags[EmitFlags["IndirectCall"] = 1048576] = "IndirectCall"; + EmitFlags[EmitFlags["AsyncFunctionBody"] = 2097152] = "AsyncFunctionBody"; + EmitFlags[EmitFlags["NoLexicalArguments"] = 4194304] = "NoLexicalArguments"; + EmitFlags[EmitFlags["TransformPrivateStaticElements"] = 8388608] = "TransformPrivateStaticElements"; + EmitFlags[EmitFlags["NoLexicalThis"] = 16777216] = "NoLexicalThis"; +})(EmitFlags || (EmitFlags = {})); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 3226d409e17a8..2bc7ce74e133a 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -39,9 +39,13 @@ import { createIdentifier, createKeywordTypeNode, createNumericLiteral, + createObjectLiteralExpression, createParameterDeclaration, + createPropertyAssignment, + createPropertySignatureDeclaration, createToken, createTypeAliasDeclaration, + createTypeLiteralNode, createTypeReferenceNode, createUnionTypeNode, createVariableDeclaration, @@ -57,6 +61,7 @@ import { type ConfiguredProjectId, DiagnosticCategory, type DocumentIdentifier, + EmitFlags, EmitOnly, type FreshableType, type ImportAdderAction, @@ -7110,6 +7115,150 @@ export const obj = { m: 1, s: "hi", b: true }; const textWith = await api.printer.printNode(regexNode, { terminateUnterminatedLiterals: true }); assert.strictEqual(textWith, "/asdfasf/"); }); + + test("printNode with emitContext synthetic comments on the root node and on nested members", async () => { + await using api = spawnAPI(emitterFiles); + + const emitContext = api.printer.createEmitContext(); + const stringProp = createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!); + const numberProp = createPropertySignatureDeclaration(undefined, createIdentifier("b"), undefined, createKeywordTypeNode(SyntaxKind.NumberKeyword), undefined!); + const typeLiteral = createTypeLiteralNode([stringProp, numberProp]); + + emitContext.addSyntheticLeadingComment(typeLiteral, SyntaxKind.SingleLineCommentTrivia, " top", true, false); + emitContext.addSyntheticLeadingComment(stringProp, SyntaxKind.SingleLineCommentTrivia, " nested a", true, false); + emitContext.addSyntheticLeadingComment(stringProp, SyntaxKind.SingleLineCommentTrivia, " nested a again", true, false); + emitContext.addSyntheticTrailingComment(numberProp, SyntaxKind.MultiLineCommentTrivia, " nested b ", false, false); + + assert.deepStrictEqual( + emitContext.getSyntheticLeadingComments(stringProp)?.map(c => c.text), + [" nested a", " nested a again"], + ); + assert.deepStrictEqual(emitContext.getSyntheticTrailingComments(numberProp)?.map(c => c.text), [" nested b "]); + + assert.deepStrictEqual(emitContext.getSyntheticLeadingComments(numberProp), undefined); + assert.deepStrictEqual(emitContext.getSyntheticTrailingComments(stringProp), undefined); + assert.strictEqual(emitContext.getSyntheticLeadingComments(createIdentifier("untouched")), undefined); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), 0); + + const text = await api.printer.printNode(typeLiteral, { emitContext }); + assert.strictEqual( + text, + `// top +{ + // nested a + // nested a again + a: string; + b: number; /* nested b */ +}`, + ); + }); + + test("printNode with emitContext bulk-set synthetic leading and trailing comments", async () => { + await using api = spawnAPI(emitterFiles); + + const emitContext = api.printer.createEmitContext(); + const stringProp = createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!); + const typeLiteral = createTypeLiteralNode([stringProp]); + + emitContext.addSyntheticLeadingComment(stringProp, SyntaxKind.SingleLineCommentTrivia, " will be replaced", true, false); + emitContext.setSyntheticLeadingComments(stringProp, [ + { kind: SyntaxKind.SingleLineCommentTrivia, text: " bulk leading 1", hasTrailingNewLine: true, hasLeadingNewLine: false }, + { kind: SyntaxKind.SingleLineCommentTrivia, text: " bulk leading 2", hasTrailingNewLine: true, hasLeadingNewLine: false }, + ]); + emitContext.setSyntheticTrailingComments(stringProp, [ + { kind: SyntaxKind.SingleLineCommentTrivia, text: " bulk trailing", hasTrailingNewLine: false, hasLeadingNewLine: false }, + ]); + + assert.deepStrictEqual( + emitContext.getSyntheticLeadingComments(stringProp)?.map(c => c.text), + [" bulk leading 1", " bulk leading 2"], + ); + assert.deepStrictEqual(emitContext.getSyntheticTrailingComments(stringProp)?.map(c => c.text), [" bulk trailing"]); + + const text = await api.printer.printNode(typeLiteral, { emitContext }); + assert.strictEqual( + text, + `{ + // bulk leading 1 + // bulk leading 2 + a: string; // bulk trailing +}`, + ); + }); + + test("printNode with emitContext SingleLine flag collapses a multi-member type literal", async () => { + await using api = spawnAPI(emitterFiles); + + const typeLiteral = createTypeLiteralNode([ + createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!), + createPropertySignatureDeclaration(undefined, createIdentifier("b"), undefined, createKeywordTypeNode(SyntaxKind.NumberKeyword), undefined!), + ]); + + assert.strictEqual(await api.printer.printNode(typeLiteral), "{\n a: string;\n b: number;\n}"); + const emitContext = api.printer.createEmitContext(); + emitContext.setEmitFlags(typeLiteral, EmitFlags.SingleLine); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), EmitFlags.SingleLine); + + assert.strictEqual(await api.printer.printNode(typeLiteral, { emitContext }), "{ a: string; b: number; }"); + + emitContext.setEmitFlags(typeLiteral, EmitFlags.MultiLine); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), EmitFlags.MultiLine); + assert.strictEqual(await api.printer.printNode(typeLiteral, { emitContext }), "{\n a: string;\n b: number;\n}"); + }); + + test("printNode with emitContext MultiLine flag expands a single-line object literal", async () => { + await using api = spawnAPI(emitterFiles); + + const multiLineObject = createObjectLiteralExpression([ + createPropertyAssignment(undefined, createIdentifier("a"), undefined, undefined!, createNumericLiteral("1", 0)), + ]); + assert.strictEqual(await api.printer.printNode(multiLineObject), "{ a: 1 }"); + + const emitContext = api.printer.createEmitContext(); + emitContext.setEmitFlags(multiLineObject, EmitFlags.MultiLine); + assert.strictEqual(emitContext.getEmitFlags(multiLineObject), EmitFlags.MultiLine); + + const text = await api.printer.printNode(multiLineObject, { emitContext }); + assert.strictEqual(text, "{\n a: 1\n}"); + }); + + test("printNode with emitContext only affects the node it was attached to", async () => { + await using api = spawnAPI(emitterFiles); + + const emitContext = api.printer.createEmitContext(); + const commentedProp = createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!); + const typeLiteral = createTypeLiteralNode([ + commentedProp, + createPropertySignatureDeclaration(undefined, createIdentifier("b"), undefined, createKeywordTypeNode(SyntaxKind.NumberKeyword), undefined!), + ]); + emitContext.addSyntheticLeadingComment(commentedProp, SyntaxKind.SingleLineCommentTrivia, " only on a", true, false); + + // Both members are part of the same printed tree. The comment attached to + // `commentedProp` should not leak onto its sibling. + const text = await api.printer.printNode(typeLiteral, { emitContext }); + assert.strictEqual( + text, + `{ + // only on a + a: string; + b: number; +}`, + ); + }); + + test("printFile applies emitContext synthetic leading comment on a top-level statement", async () => { + await using api = spawnAPI(emitterFiles); + + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const sourceFile = await snapshot.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/main.ts"); + assert(sourceFile); + + const emitContext = api.printer.createEmitContext(); + emitContext.addSyntheticLeadingComment(sourceFile.statements[0], SyntaxKind.SingleLineCommentTrivia, " printFile honors emitContext", true, false); + + const text = await api.printer.printFile(sourceFile, { emitContext }); + assert.match(text, /printFile honors emitContext/); + }); }); describe("Program - selected file emit", { concurrency }, () => { diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index aaa550e6ba456..1f1e7d9cf4476 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -47,9 +47,13 @@ import { createIdentifier, createKeywordTypeNode, createNumericLiteral, + createObjectLiteralExpression, createParameterDeclaration, + createPropertyAssignment, + createPropertySignatureDeclaration, createToken, createTypeAliasDeclaration, + createTypeLiteralNode, createTypeReferenceNode, createUnionTypeNode, createVariableDeclaration, @@ -72,6 +76,7 @@ import { type ConfiguredProjectId, DiagnosticCategory, type DocumentIdentifier, + EmitFlags, EmitOnly, type FreshableType, type ImportAdderAction, @@ -6944,6 +6949,150 @@ export const obj = { m: 1, s: "hi", b: true }; const textWith = api.printer.printNode(regexNode, { terminateUnterminatedLiterals: true }); assert.strictEqual(textWith, "/asdfasf/"); }); + + test("printNode with emitContext synthetic comments on the root node and on nested members", () => { + using api = spawnAPI(emitterFiles); + + const emitContext = api.printer.createEmitContext(); + const stringProp = createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!); + const numberProp = createPropertySignatureDeclaration(undefined, createIdentifier("b"), undefined, createKeywordTypeNode(SyntaxKind.NumberKeyword), undefined!); + const typeLiteral = createTypeLiteralNode([stringProp, numberProp]); + + emitContext.addSyntheticLeadingComment(typeLiteral, SyntaxKind.SingleLineCommentTrivia, " top", true, false); + emitContext.addSyntheticLeadingComment(stringProp, SyntaxKind.SingleLineCommentTrivia, " nested a", true, false); + emitContext.addSyntheticLeadingComment(stringProp, SyntaxKind.SingleLineCommentTrivia, " nested a again", true, false); + emitContext.addSyntheticTrailingComment(numberProp, SyntaxKind.MultiLineCommentTrivia, " nested b ", false, false); + + assert.deepStrictEqual( + emitContext.getSyntheticLeadingComments(stringProp)?.map(c => c.text), + [" nested a", " nested a again"], + ); + assert.deepStrictEqual(emitContext.getSyntheticTrailingComments(numberProp)?.map(c => c.text), [" nested b "]); + + assert.deepStrictEqual(emitContext.getSyntheticLeadingComments(numberProp), undefined); + assert.deepStrictEqual(emitContext.getSyntheticTrailingComments(stringProp), undefined); + assert.strictEqual(emitContext.getSyntheticLeadingComments(createIdentifier("untouched")), undefined); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), 0); + + const text = api.printer.printNode(typeLiteral, { emitContext }); + assert.strictEqual( + text, + `// top +{ + // nested a + // nested a again + a: string; + b: number; /* nested b */ +}`, + ); + }); + + test("printNode with emitContext bulk-set synthetic leading and trailing comments", () => { + using api = spawnAPI(emitterFiles); + + const emitContext = api.printer.createEmitContext(); + const stringProp = createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!); + const typeLiteral = createTypeLiteralNode([stringProp]); + + emitContext.addSyntheticLeadingComment(stringProp, SyntaxKind.SingleLineCommentTrivia, " will be replaced", true, false); + emitContext.setSyntheticLeadingComments(stringProp, [ + { kind: SyntaxKind.SingleLineCommentTrivia, text: " bulk leading 1", hasTrailingNewLine: true, hasLeadingNewLine: false }, + { kind: SyntaxKind.SingleLineCommentTrivia, text: " bulk leading 2", hasTrailingNewLine: true, hasLeadingNewLine: false }, + ]); + emitContext.setSyntheticTrailingComments(stringProp, [ + { kind: SyntaxKind.SingleLineCommentTrivia, text: " bulk trailing", hasTrailingNewLine: false, hasLeadingNewLine: false }, + ]); + + assert.deepStrictEqual( + emitContext.getSyntheticLeadingComments(stringProp)?.map(c => c.text), + [" bulk leading 1", " bulk leading 2"], + ); + assert.deepStrictEqual(emitContext.getSyntheticTrailingComments(stringProp)?.map(c => c.text), [" bulk trailing"]); + + const text = api.printer.printNode(typeLiteral, { emitContext }); + assert.strictEqual( + text, + `{ + // bulk leading 1 + // bulk leading 2 + a: string; // bulk trailing +}`, + ); + }); + + test("printNode with emitContext SingleLine flag collapses a multi-member type literal", () => { + using api = spawnAPI(emitterFiles); + + const typeLiteral = createTypeLiteralNode([ + createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!), + createPropertySignatureDeclaration(undefined, createIdentifier("b"), undefined, createKeywordTypeNode(SyntaxKind.NumberKeyword), undefined!), + ]); + + assert.strictEqual(api.printer.printNode(typeLiteral), "{\n a: string;\n b: number;\n}"); + const emitContext = api.printer.createEmitContext(); + emitContext.setEmitFlags(typeLiteral, EmitFlags.SingleLine); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), EmitFlags.SingleLine); + + assert.strictEqual(api.printer.printNode(typeLiteral, { emitContext }), "{ a: string; b: number; }"); + + emitContext.setEmitFlags(typeLiteral, EmitFlags.MultiLine); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), EmitFlags.MultiLine); + assert.strictEqual(api.printer.printNode(typeLiteral, { emitContext }), "{\n a: string;\n b: number;\n}"); + }); + + test("printNode with emitContext MultiLine flag expands a single-line object literal", () => { + using api = spawnAPI(emitterFiles); + + const multiLineObject = createObjectLiteralExpression([ + createPropertyAssignment(undefined, createIdentifier("a"), undefined, undefined!, createNumericLiteral("1", 0)), + ]); + assert.strictEqual(api.printer.printNode(multiLineObject), "{ a: 1 }"); + + const emitContext = api.printer.createEmitContext(); + emitContext.setEmitFlags(multiLineObject, EmitFlags.MultiLine); + assert.strictEqual(emitContext.getEmitFlags(multiLineObject), EmitFlags.MultiLine); + + const text = api.printer.printNode(multiLineObject, { emitContext }); + assert.strictEqual(text, "{\n a: 1\n}"); + }); + + test("printNode with emitContext only affects the node it was attached to", () => { + using api = spawnAPI(emitterFiles); + + const emitContext = api.printer.createEmitContext(); + const commentedProp = createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!); + const typeLiteral = createTypeLiteralNode([ + commentedProp, + createPropertySignatureDeclaration(undefined, createIdentifier("b"), undefined, createKeywordTypeNode(SyntaxKind.NumberKeyword), undefined!), + ]); + emitContext.addSyntheticLeadingComment(commentedProp, SyntaxKind.SingleLineCommentTrivia, " only on a", true, false); + + // Both members are part of the same printed tree. The comment attached to + // `commentedProp` should not leak onto its sibling. + const text = api.printer.printNode(typeLiteral, { emitContext }); + assert.strictEqual( + text, + `{ + // only on a + a: string; + b: number; +}`, + ); + }); + + test("printFile applies emitContext synthetic leading comment on a top-level statement", () => { + using api = spawnAPI(emitterFiles); + + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const sourceFile = snapshot.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/main.ts"); + assert(sourceFile); + + const emitContext = api.printer.createEmitContext(); + emitContext.addSyntheticLeadingComment(sourceFile.statements[0], SyntaxKind.SingleLineCommentTrivia, " printFile honors emitContext", true, false); + + const text = api.printer.printFile(sourceFile, { emitContext }); + assert.match(text, /printFile honors emitContext/); + }); }); describe("Program - selected file emit", { concurrency }, () => { diff --git a/tools/scripts/tsc/generate-enums.ts b/tools/scripts/tsc/generate-enums.ts index e79cfc8c571a8..a3f7a163cc325 100644 --- a/tools/scripts/tsc/generate-enums.ts +++ b/tools/scripts/tsc/generate-enums.ts @@ -57,6 +57,7 @@ const enumDefs = [ { name: "NodeBuilderFlags", goPrefix: "Flags", goFile: "tsc/internal/nodebuilder/types.go", outDir: "packages/typescript/src/enums" }, { name: "CompletionItemKind", goPrefix: "CompletionItemKind", goFile: "tsc/internal/lsp/lsproto/lsp_generated.go", outDir: "packages/typescript/src/enums" }, { name: "EmitOnly", goPrefix: "Emit", goFile: "tsc/internal/compiler/emitter.go", outDir: "packages/typescript/src/enums", excludeMembers: ["OnlyBuilderSignature"] }, + { name: "EmitFlags", goPrefix: "EF", goFile: "tsc/internal/printer/emitflags.go", outDir: "packages/typescript/src/enums" }, // String enum: Go stores internal names with a "\xFE" sentinel prefix, but the escaped // form sent over the wire uses "__" (see EscapeSymbolName), so map the sentinel accordingly. { name: "InternalSymbolName", goPrefix: "InternalSymbolName", goFile: "tsc/internal/ast/symbol.go", outDir: "packages/typescript/src/enums", stringEnum: true, valueReplacements: { InternalSymbolNamePrefix: "__" } }, diff --git a/tsc/internal/api/encoder/decoder.go b/tsc/internal/api/encoder/decoder.go index 14d5a9250968b..f34fec53f28aa 100644 --- a/tsc/internal/api/encoder/decoder.go +++ b/tsc/internal/api/encoder/decoder.go @@ -25,8 +25,9 @@ type astDecoder struct { // Arena for batch-allocating []*ast.Node slices used by NodeLists. nodeArena []*ast.Node // Results - nodes []*ast.Node - nodeLists []*ast.NodeList + nodes []*ast.Node + nodeLists []*ast.NodeList + onNodeDecoded func(node *ast.Node, index int) } // DecodeSourceFile decodes binary-encoded data into an *ast.SourceFile. @@ -43,14 +44,18 @@ func DecodeSourceFile(data []byte) (*ast.SourceFile, error) { // DecodeNodes decodes binary-encoded AST data into a tree of *ast.Node objects. func DecodeNodes(data []byte) (*ast.Node, error) { - d, err := newASTDecoder(data) + return DecodeNodesWithCallback(data, nil) +} + +func DecodeNodesWithCallback(data []byte, onNodeDecoded func(node *ast.Node, index int)) (*ast.Node, error) { + d, err := newASTDecoder(data, onNodeDecoded) if err != nil { return nil, err } return d.decode() } -func newASTDecoder(data []byte) (*astDecoder, error) { +func newASTDecoder(data []byte, onNodeDecoded func(node *ast.Node, index int)) (*astDecoder, error) { if len(data) < HeaderSize { return nil, fmt.Errorf("data too short for header: %d bytes", len(data)) } @@ -77,12 +82,13 @@ func newASTDecoder(data []byte) (*astDecoder, error) { } d := &astDecoder{ - raw: data, - strTable: strTable, - strData: strData, - extData: extData, - nodeOff: nodeOff, - factory: ast.NewNodeFactory(ast.NodeFactoryHooks{}), + raw: data, + strTable: strTable, + strData: strData, + extData: extData, + nodeOff: nodeOff, + factory: ast.NewNodeFactory(ast.NodeFactoryHooks{}), + onNodeDecoded: onNodeDecoded, } d.nodeCount = (len(data) - int(d.nodeOff)) / NodeSize @@ -176,6 +182,9 @@ func (d *astDecoder) decode() (*ast.Node, error) { if kind == uint32(ast.KindSourceFile) { node.AsSourceFile().IsDeclarationFile = node.Flags&ast.NodeFlagsAmbient != 0 } + if d.onNodeDecoded != nil { + d.onNodeDecoded(node, i) + } d.nodes[i] = node } diff --git a/tsc/internal/api/enum_values_generated.go b/tsc/internal/api/enum_values_generated.go index cd38442117003..9882933182fb9 100644 --- a/tsc/internal/api/enum_values_generated.go +++ b/tsc/internal/api/enum_values_generated.go @@ -19,6 +19,7 @@ import ( diagnostics "github.com/microsoft/TypeScript/tsc/internal/diagnostics" lsproto "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" nodebuilder "github.com/microsoft/TypeScript/tsc/internal/nodebuilder" + printer "github.com/microsoft/TypeScript/tsc/internal/printer" spanmap "github.com/microsoft/TypeScript/tsc/internal/spanmap" ) @@ -1007,6 +1008,33 @@ func main() { "OnlyJs": toInt32(compiler.EmitOnlyJs), "OnlyDts": toInt32(compiler.EmitOnlyDts), }, + "EmitFlags": { + "SingleLine": toInt32(printer.EFSingleLine), + "MultiLine": toInt32(printer.EFMultiLine), + "NoLeadingSourceMap": toInt32(printer.EFNoLeadingSourceMap), + "NoTrailingSourceMap": toInt32(printer.EFNoTrailingSourceMap), + "NoNestedSourceMaps": toInt32(printer.EFNoNestedSourceMaps), + "NoTokenLeadingSourceMaps": toInt32(printer.EFNoTokenLeadingSourceMaps), + "NoTokenTrailingSourceMaps": toInt32(printer.EFNoTokenTrailingSourceMaps), + "NoLeadingComments": toInt32(printer.EFNoLeadingComments), + "NoTrailingComments": toInt32(printer.EFNoTrailingComments), + "NoNestedComments": toInt32(printer.EFNoNestedComments), + "HelperName": toInt32(printer.EFHelperName), + "ExportName": toInt32(printer.EFExportName), + "LocalName": toInt32(printer.EFLocalName), + "Indented": toInt32(printer.EFIndented), + "NoIndentation": toInt32(printer.EFNoIndentation), + "ReuseTempVariableScope": toInt32(printer.EFReuseTempVariableScope), + "CustomPrologue": toInt32(printer.EFCustomPrologue), + "NoAsciiEscaping": toInt32(printer.EFNoAsciiEscaping), + "ExternalHelpers": toInt32(printer.EFExternalHelpers), + "StartOnNewLine": toInt32(printer.EFStartOnNewLine), + "IndirectCall": toInt32(printer.EFIndirectCall), + "AsyncFunctionBody": toInt32(printer.EFAsyncFunctionBody), + "NoLexicalArguments": toInt32(printer.EFNoLexicalArguments), + "TransformPrivateStaticElements": toInt32(printer.EFTransformPrivateStaticElements), + "NoLexicalThis": toInt32(printer.EFNoLexicalThis), + }, } if err := json.NewEncoder(os.Stdout).Encode(values); err != nil { panic(err) diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 818ce96e35816..e9af672a86316 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -1726,12 +1726,29 @@ type SignatureToSignatureDeclarationParams struct { Flags int32 `json:"flags,omitempty"` } -// PrintNodeParams are the parameters for the printNode method. +// Extra information for a node used during node printing +type PrintEmitNode struct { + EmitFlags uint32 `json:"emitFlags,omitempty"` + LeadingComments []PrintSynthesizedComment `json:"leadingComments,omitempty"` + TrailingComments []PrintSynthesizedComment `json:"trailingComments,omitempty"` +} + +// PrintSynthesizedComment is a synthetic comment attached to a node via EmitContext +type PrintSynthesizedComment struct { + Kind int32 `json:"kind"` + Text string `json:"text"` + HasTrailingNewLine bool `json:"hasTrailingNewLine,omitempty"` + HasLeadingNewLine bool `json:"hasLeadingNewLine,omitempty"` +} + +// PrintEmitNode carries the EmitFlags and synthetic comments a client attaches to a +// specific node (by index) via EmitContext before printing. type PrintNodeParams struct { - Data string `json:"data"` // base64-encoded binary AST data - PreserveSourceNewlines bool `json:"preserveSourceNewlines,omitempty"` - NeverAsciiEscape bool `json:"neverAsciiEscape,omitempty"` - TerminateUnterminatedLiterals bool `json:"terminateUnterminatedLiterals,omitempty"` + Data string `json:"data"` // base64-encoded binary AST data + PreserveSourceNewlines bool `json:"preserveSourceNewlines,omitempty"` + NeverAsciiEscape bool `json:"neverAsciiEscape,omitempty"` + TerminateUnterminatedLiterals bool `json:"terminateUnterminatedLiterals,omitempty"` + EmitNodes map[int]*PrintEmitNode `json:"emitNodes,omitempty"` } type EmitParams struct { diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d6f39a7df04f1..1cfeece24990b 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -3528,7 +3528,7 @@ func (s *Session) handleTypeToString(ctx context.Context, params *TypeToTypeNode // handlePrintNode decodes a binary-encoded AST node and prints it to text. func (s *Session) handlePrintNode(_ context.Context, params *PrintNodeParams) (string, error) { - node, err := decodePrintNode(params.Data) + node, emitContext, err := decodePrintNode(params.Data, params.EmitNodes) if err != nil { return "", err } @@ -3537,28 +3537,59 @@ func (s *Session) handlePrintNode(_ context.Context, params *PrintNodeParams) (s if ast.IsSourceFile(node) { sourceFile = node.AsSourceFile() } - return newPrinter(params).Emit(node, sourceFile), nil + return newPrinter(params, emitContext).Emit(node, sourceFile), nil } -func decodePrintNode(encoded string) (*ast.Node, error) { +func mapComments(comments []PrintSynthesizedComment) []printer.SynthesizedComment { + return core.Map(comments, func(c PrintSynthesizedComment) printer.SynthesizedComment { + return printer.SynthesizedComment{ + Kind: ast.Kind(c.Kind), + Loc: core.NewTextRange(-1, -1), + HasLeadingNewLine: c.HasLeadingNewLine, + HasTrailingNewLine: c.HasTrailingNewLine, + Text: c.Text, + } + }) +} + +func decodePrintNode(encoded string, emitNodes map[int]*PrintEmitNode) (*ast.Node, *printer.EmitContext, error) { + emitContext := printer.NewEmitContext() data, err := base64.StdEncoding.DecodeString(encoded) if err != nil { - return nil, fmt.Errorf("%w: invalid base64 data: %w", ErrClientError, err) + return nil, nil, fmt.Errorf("%w: invalid base64 data: %w", ErrClientError, err) } - - node, err := encoder.DecodeNodes(data) + usedEmitNodes := 0 + node, err := encoder.DecodeNodesWithCallback(data, func(node *ast.Node, index int) { + emitNode, ok := emitNodes[index] + if !ok { + return + } + usedEmitNodes++ + emitContext.SetEmitFlags(node, printer.EmitFlags(emitNode.EmitFlags)) + if len(emitNode.LeadingComments) != 0 { + leadingComments := mapComments(emitNode.LeadingComments) + emitContext.SetSyntheticLeadingComments(node, leadingComments) + } + if len(emitNode.TrailingComments) != 0 { + trailingComments := mapComments(emitNode.TrailingComments) + emitContext.SetSyntheticTrailingComments(node, trailingComments) + } + }) if err != nil { - return nil, fmt.Errorf("%w: failed to decode AST: %w", ErrClientError, err) + return nil, nil, fmt.Errorf("%w: failed to decode AST: %w", ErrClientError, err) + } + if usedEmitNodes != len(emitNodes) { + return nil, nil, fmt.Errorf("%w: emitNodes had indices outside of the actual parsed nodes", ErrClientError) } - return node, nil + return node, emitContext, nil } -func newPrinter(params *PrintNodeParams) *printer.Printer { +func newPrinter(params *PrintNodeParams, emitContext *printer.EmitContext) *printer.Printer { return printer.NewPrinter(printer.PrinterOptions{ PreserveSourceNewlines: params.PreserveSourceNewlines, NeverAsciiEscape: params.NeverAsciiEscape, TerminateUnterminatedLiterals: params.TerminateUnterminatedLiterals, - }, printer.PrintHandlers{}, nil) + }, printer.PrintHandlers{}, emitContext) } func (s *Session) handleEmit(ctx context.Context, params *EmitParams) (*EmitResponse, error) { diff --git a/tsc/internal/printer/printer.go b/tsc/internal/printer/printer.go index 51673e4afd02e..51f20a6f48420 100644 --- a/tsc/internal/printer/printer.go +++ b/tsc/internal/printer/printer.go @@ -741,7 +741,6 @@ func (p *Printer) writeCommentRangeWorker(text string, lineMap []core.TextPos, k func (p *Printer) shouldEmitComments(node *ast.Node) bool { return !p.commentsDisabled && - p.currentSourceFile != nil && !ast.IsSourceFile(node) } From aca7a1ba026dbeb895d4e1e91bfd6c99b3e9fbf7 Mon Sep 17 00:00:00 2001 From: tcernicovad1 Date: Mon, 28 Sep 2026 17:36:53 +0100 Subject: [PATCH 2/3] Address code review --- packages/typescript/src/api/async/api.ts | 2 +- packages/typescript/src/api/sync/api.ts | 2 +- packages/typescript/src/enums/emitFlags.enum.ts | 4 ++++ packages/typescript/src/enums/emitFlags.ts | 4 ++++ tsc/internal/api/enum_values_generated.go | 4 ++++ tsc/internal/api/session.go | 2 +- tsc/internal/printer/emitflags.go | 11 ++++------- 7 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index e9d2a23ccbe80..0624e91cdbfb8 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -2938,7 +2938,7 @@ export interface PrintNodeOptions { preserveSourceNewlines?: boolean | undefined; neverAsciiEscape?: boolean | undefined; terminateUnterminatedLiterals?: boolean | undefined; - emitContext?: EmitContext; + emitContext?: EmitContext | undefined; } type CommentKind = SyntaxKind.MultiLineCommentTrivia | SyntaxKind.SingleLineCommentTrivia; export interface SynthesizedComment { diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index fa6bfd22b72b6..b072f50ea9f4b 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -6192,7 +6192,7 @@ export interface PrintNodeOptions { preserveSourceNewlines?: boolean | undefined; neverAsciiEscape?: boolean | undefined; terminateUnterminatedLiterals?: boolean | undefined; - emitContext?: EmitContext; + emitContext?: EmitContext | undefined; } type CommentKind = SyntaxKind.MultiLineCommentTrivia | SyntaxKind.SingleLineCommentTrivia; export interface SynthesizedComment { diff --git a/packages/typescript/src/enums/emitFlags.enum.ts b/packages/typescript/src/enums/emitFlags.enum.ts index 2f179a816ce04..480892fcfce14 100644 --- a/packages/typescript/src/enums/emitFlags.enum.ts +++ b/packages/typescript/src/enums/emitFlags.enum.ts @@ -26,4 +26,8 @@ export enum EmitFlags { NoLexicalArguments = 1 << 22, TransformPrivateStaticElements = 1 << 23, NoLexicalThis = 1 << 24, + None = 0, + NoSourceMap = NoLeadingSourceMap | NoTrailingSourceMap, + NoTokenSourceMaps = NoTokenLeadingSourceMaps | NoTokenTrailingSourceMaps, + NoComments = NoLeadingComments | NoTrailingComments, } diff --git a/packages/typescript/src/enums/emitFlags.ts b/packages/typescript/src/enums/emitFlags.ts index 0529718c10c2c..df44634e5387f 100644 --- a/packages/typescript/src/enums/emitFlags.ts +++ b/packages/typescript/src/enums/emitFlags.ts @@ -26,4 +26,8 @@ export var EmitFlags: any; EmitFlags[EmitFlags["NoLexicalArguments"] = 4194304] = "NoLexicalArguments"; EmitFlags[EmitFlags["TransformPrivateStaticElements"] = 8388608] = "TransformPrivateStaticElements"; EmitFlags[EmitFlags["NoLexicalThis"] = 16777216] = "NoLexicalThis"; + EmitFlags[EmitFlags["None"] = 0] = "None"; + EmitFlags[EmitFlags["NoSourceMap"] = 12] = "NoSourceMap"; + EmitFlags[EmitFlags["NoTokenSourceMaps"] = 96] = "NoTokenSourceMaps"; + EmitFlags[EmitFlags["NoComments"] = 384] = "NoComments"; })(EmitFlags || (EmitFlags = {})); diff --git a/tsc/internal/api/enum_values_generated.go b/tsc/internal/api/enum_values_generated.go index 9882933182fb9..34f34d0828e56 100644 --- a/tsc/internal/api/enum_values_generated.go +++ b/tsc/internal/api/enum_values_generated.go @@ -1034,6 +1034,10 @@ func main() { "NoLexicalArguments": toInt32(printer.EFNoLexicalArguments), "TransformPrivateStaticElements": toInt32(printer.EFTransformPrivateStaticElements), "NoLexicalThis": toInt32(printer.EFNoLexicalThis), + "None": toInt32(printer.EFNone), + "NoSourceMap": toInt32(printer.EFNoSourceMap), + "NoTokenSourceMaps": toInt32(printer.EFNoTokenSourceMaps), + "NoComments": toInt32(printer.EFNoComments), }, } if err := json.NewEncoder(os.Stdout).Encode(values); err != nil { diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 1cfeece24990b..e7640a7fac55a 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -3561,7 +3561,7 @@ func decodePrintNode(encoded string, emitNodes map[int]*PrintEmitNode) (*ast.Nod usedEmitNodes := 0 node, err := encoder.DecodeNodesWithCallback(data, func(node *ast.Node, index int) { emitNode, ok := emitNodes[index] - if !ok { + if !ok || emitNode == nil { return } usedEmitNodes++ diff --git a/tsc/internal/printer/emitflags.go b/tsc/internal/printer/emitflags.go index bdcea5f817753..eed50b4dfb6cf 100644 --- a/tsc/internal/printer/emitflags.go +++ b/tsc/internal/printer/emitflags.go @@ -28,11 +28,8 @@ const ( EFNoLexicalArguments // Do not capture `arguments` for this arrow function. Set on arrows lowered from class static blocks, where `arguments` is an error; preserves Strada's emit behavior. EFTransformPrivateStaticElements // Indicates static private elements in a file or class should be transformed regardless of --target (used by esDecorators transform). EFNoLexicalThis // Do not capture `this` for this node's subtree. Set on relocated static initializers, where `this` is handled by the class fields transform. -) - -const ( - EFNone EmitFlags = 0 - EFNoSourceMap = EFNoLeadingSourceMap | EFNoTrailingSourceMap // Do not emit a source map location for this node. - EFNoTokenSourceMaps = EFNoTokenLeadingSourceMaps | EFNoTokenTrailingSourceMaps // Do not emit source map locations for tokens of this node. - EFNoComments = EFNoLeadingComments | EFNoTrailingComments // Do not emit comments for this node. + EFNone EmitFlags = 0 + EFNoSourceMap = EFNoLeadingSourceMap | EFNoTrailingSourceMap // Do not emit a source map location for this node. + EFNoTokenSourceMaps = EFNoTokenLeadingSourceMaps | EFNoTokenTrailingSourceMaps // Do not emit source map locations for tokens of this node. + EFNoComments = EFNoLeadingComments | EFNoTrailingComments // Do not emit comments for this node. ) From 73a61dba57d532f86a64ff12a58eb69949be5d49 Mon Sep 17 00:00:00 2001 From: tcernicovad1 Date: Thu, 15 Oct 2026 17:47:17 +0100 Subject: [PATCH 3/3] Add regression tests for code review fixes - Null and unknown emitNodes entries are rejected as client errors instead of panicking. - EmitFlags exposes None and the composed flags, and None resets a node. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/typescript/test/async/api.test.ts | 23 ++++++++++ packages/typescript/test/sync/api.test.ts | 23 ++++++++++ tsc/internal/api/session_printnode_test.go | 51 ++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 tsc/internal/api/session_printnode_test.go diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 7664a9e6aea1c..422e5fae51d3e 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -8201,6 +8201,29 @@ export const obj = { m: 1, s: "hi", b: true }; assert.strictEqual(text, "{\n a: 1\n}"); }); + test("EmitFlags includes None and the composed flags", async () => { + await using api = spawnAPI(emitterFiles); + + assert.strictEqual(EmitFlags.None, 0); + assert.strictEqual(EmitFlags.NoComments, EmitFlags.NoLeadingComments | EmitFlags.NoTrailingComments); + assert.strictEqual(EmitFlags.NoSourceMap, EmitFlags.NoLeadingSourceMap | EmitFlags.NoTrailingSourceMap); + assert.strictEqual(EmitFlags.NoTokenSourceMaps, EmitFlags.NoTokenLeadingSourceMaps | EmitFlags.NoTokenTrailingSourceMaps); + + const typeLiteral = createTypeLiteralNode([ + createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!), + createPropertySignatureDeclaration(undefined, createIdentifier("b"), undefined, createKeywordTypeNode(SyntaxKind.NumberKeyword), undefined!), + ]); + const emitContext = api.printer.createEmitContext(); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), EmitFlags.None); + + emitContext.setEmitFlags(typeLiteral, EmitFlags.SingleLine); + assert.strictEqual(await api.printer.printNode(typeLiteral, { emitContext }), "{ a: string; b: number; }"); + + emitContext.setEmitFlags(typeLiteral, EmitFlags.None); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), EmitFlags.None); + assert.strictEqual(await api.printer.printNode(typeLiteral, { emitContext }), "{\n a: string;\n b: number;\n}"); + }); + test("printNode with emitContext only affects the node it was attached to", async () => { await using api = spawnAPI(emitterFiles); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index b7c87304f63b9..ecfb393eec69f 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -8002,6 +8002,29 @@ export const obj = { m: 1, s: "hi", b: true }; assert.strictEqual(text, "{\n a: 1\n}"); }); + test("EmitFlags includes None and the composed flags", () => { + using api = spawnAPI(emitterFiles); + + assert.strictEqual(EmitFlags.None, 0); + assert.strictEqual(EmitFlags.NoComments, EmitFlags.NoLeadingComments | EmitFlags.NoTrailingComments); + assert.strictEqual(EmitFlags.NoSourceMap, EmitFlags.NoLeadingSourceMap | EmitFlags.NoTrailingSourceMap); + assert.strictEqual(EmitFlags.NoTokenSourceMaps, EmitFlags.NoTokenLeadingSourceMaps | EmitFlags.NoTokenTrailingSourceMaps); + + const typeLiteral = createTypeLiteralNode([ + createPropertySignatureDeclaration(undefined, createIdentifier("a"), undefined, createKeywordTypeNode(SyntaxKind.StringKeyword), undefined!), + createPropertySignatureDeclaration(undefined, createIdentifier("b"), undefined, createKeywordTypeNode(SyntaxKind.NumberKeyword), undefined!), + ]); + const emitContext = api.printer.createEmitContext(); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), EmitFlags.None); + + emitContext.setEmitFlags(typeLiteral, EmitFlags.SingleLine); + assert.strictEqual(api.printer.printNode(typeLiteral, { emitContext }), "{ a: string; b: number; }"); + + emitContext.setEmitFlags(typeLiteral, EmitFlags.None); + assert.strictEqual(emitContext.getEmitFlags(typeLiteral), EmitFlags.None); + assert.strictEqual(api.printer.printNode(typeLiteral, { emitContext }), "{\n a: string;\n b: number;\n}"); + }); + test("printNode with emitContext only affects the node it was attached to", () => { using api = spawnAPI(emitterFiles); diff --git a/tsc/internal/api/session_printnode_test.go b/tsc/internal/api/session_printnode_test.go new file mode 100644 index 0000000000000..f1e5668923ec1 --- /dev/null +++ b/tsc/internal/api/session_printnode_test.go @@ -0,0 +1,51 @@ +package api + +import ( + "encoding/base64" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/api/encoder" + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/parser" + "github.com/microsoft/TypeScript/tsc/internal/printer" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "gotest.tools/v3/assert" +) + +func encodePrintNodeForTest(t *testing.T) string { + t.Helper() + sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/test.ts", + PathKey: tspath.PathKey("/test.ts"), + }, "type T = { a: string; b: number };", core.ScriptKindTS) + data, _, err := encoder.EncodeNode(sourceFile.Statements.Nodes[0], sourceFile) + assert.NilError(t, err) + return base64.StdEncoding.EncodeToString(data) +} + +func TestDecodePrintNodeAppliesEmitNodes(t *testing.T) { + t.Parallel() + + // The encoded root node is at index 1. + node, emitContext, err := decodePrintNode(encodePrintNodeForTest(t), map[int]*PrintEmitNode{ + 1: {EmitFlags: uint32(printer.EFSingleLine)}, + }) + assert.NilError(t, err) + assert.Equal(t, node.Kind, ast.KindTypeAliasDeclaration) + assert.Equal(t, emitContext.EmitFlags(node), printer.EFSingleLine) +} + +func TestDecodePrintNodeRejectsNullEmitNode(t *testing.T) { + t.Parallel() + + _, _, err := decodePrintNode(encodePrintNodeForTest(t), map[int]*PrintEmitNode{1: nil}) + assert.ErrorIs(t, err, ErrClientError) +} + +func TestDecodePrintNodeRejectsUnknownEmitNodeIndex(t *testing.T) { + t.Parallel() + + _, _, err := decodePrintNode(encodePrintNodeForTest(t), map[int]*PrintEmitNode{1 << 20: {}}) + assert.ErrorIs(t, err, ErrClientError) +}