diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 6c1eb480123e6..bf26d90f9027e 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"; @@ -189,7 +190,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, SymbolOwnerKind, TypeFlags, TypeFormatFlags, TypePredicateKind }; +export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitFlags, EmitOnly, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, SymbolOwnerKind, TypeFlags, TypeFormatFlags, TypePredicateKind }; export type { APIImportAdderAction as ImportAdderAction, APIOptions, @@ -3119,6 +3120,36 @@ export interface PrintNodeOptions { preserveSourceNewlines?: boolean | undefined; neverAsciiEscape?: boolean | undefined; terminateUnterminatedLiterals?: boolean | undefined; + emitContext?: EmitContext | undefined; +} +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 { @@ -3129,25 +3160,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 01e2fa27b12c2..228e295fae1b8 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -1049,13 +1049,17 @@ export interface DiagnosticResponse { relatedInformation?: DiagnosticResponse[] | undefined; } -/** PrintNodeParams are the parameters for the printNode method. */ +/** + * PrintEmitNode carries the EmitFlags and synthetic comments a client attaches to a + * specific node (by index) via EmitContext before printing. + */ export interface PrintNodeParams { /** base64-encoded binary AST data */ data: string; preserveSourceNewlines?: boolean | undefined; neverAsciiEscape?: boolean | undefined; terminateUnterminatedLiterals?: boolean | undefined; + emitNodes?: Record | undefined; } export interface EmitParams { @@ -1696,6 +1700,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: RootedFilePath; text: string; @@ -1770,6 +1781,14 @@ export interface CompletionEntryLabelDetailsResponse { description?: string | undefined; } +/** PrintSynthesizedComment is a synthetic comment attached to a node via EmitContext */ +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 a2960224381ff..e42ae7b46e22e 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"; @@ -206,7 +207,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, SymbolOwnerKind, TypeFlags, TypeFormatFlags, TypePredicateKind }; +export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitFlags, EmitOnly, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, SymbolOwnerKind, TypeFlags, TypeFormatFlags, TypePredicateKind }; export type { APIImportAdderAction as ImportAdderAction, APIOptions, @@ -6466,6 +6467,36 @@ export interface PrintNodeOptions { preserveSourceNewlines?: boolean | undefined; neverAsciiEscape?: boolean | undefined; terminateUnterminatedLiterals?: boolean | undefined; + emitContext?: EmitContext | undefined; +} +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 { @@ -6484,23 +6515,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, }); }, ); @@ -6515,27 +6558,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 NodeHandle { diff --git a/packages/typescript/src/enums/emitFlags.enum.ts b/packages/typescript/src/enums/emitFlags.enum.ts new file mode 100644 index 0000000000000..480892fcfce14 --- /dev/null +++ b/packages/typescript/src/enums/emitFlags.enum.ts @@ -0,0 +1,33 @@ +// 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, + 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 new file mode 100644 index 0000000000000..df44634e5387f --- /dev/null +++ b/packages/typescript/src/enums/emitFlags.ts @@ -0,0 +1,33 @@ +// 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["None"] = 0] = "None"; + EmitFlags[EmitFlags["NoSourceMap"] = 12] = "NoSourceMap"; + EmitFlags[EmitFlags["NoTokenSourceMaps"] = 96] = "NoTokenSourceMaps"; + EmitFlags[EmitFlags["NoComments"] = 384] = "NoComments"; +})(EmitFlags || (EmitFlags = {})); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index b9ef5842d4489..422e5fae51d3e 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -49,10 +49,14 @@ import { createIdentifier, createKeywordTypeNode, createNumericLiteral, + createObjectLiteralExpression, createParameterDeclaration, + createPropertyAssignment, + createPropertySignatureDeclaration, createSourceFile, createToken, createTypeAliasDeclaration, + createTypeLiteralNode, createTypeReferenceNode, createUnionTypeNode, createVariableDeclaration, @@ -69,6 +73,7 @@ import { type ConfiguredProjectId, DiagnosticCategory, type DocumentIdentifier, + EmitFlags, EmitOnly, type FormatDiagnosticsHost, type FreshableType, @@ -8089,6 +8094,173 @@ 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("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); + + 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({ openProjects: ["/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 dbe34fb1b2d4b..ecfb393eec69f 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -57,10 +57,14 @@ import { createIdentifier, createKeywordTypeNode, createNumericLiteral, + createObjectLiteralExpression, createParameterDeclaration, + createPropertyAssignment, + createPropertySignatureDeclaration, createSourceFile, createToken, createTypeAliasDeclaration, + createTypeLiteralNode, createTypeReferenceNode, createUnionTypeNode, createVariableDeclaration, @@ -90,6 +94,7 @@ import { type ConfiguredProjectId, DiagnosticCategory, type DocumentIdentifier, + EmitFlags, EmitOnly, type FormatDiagnosticsHost, type FreshableType, @@ -7890,6 +7895,173 @@ 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("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); + + 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({ openProjects: ["/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 757f598fa88c6..79d27e1ea2d61 100644 --- a/tools/scripts/tsc/generate-enums.ts +++ b/tools/scripts/tsc/generate-enums.ts @@ -74,6 +74,7 @@ export 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 9deb4abe5f152..24f61edae8c69 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 f90d2892104c0..42a694cd0e4af 100644 --- a/tsc/internal/api/enum_values_generated.go +++ b/tsc/internal/api/enum_values_generated.go @@ -20,6 +20,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" tspath "github.com/microsoft/TypeScript/tsc/internal/tspath" ) @@ -1034,6 +1035,37 @@ 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), + "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 { panic(err) diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 8072445202b44..52fb9531e024a 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -1772,12 +1772,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 93d6ab53c303d..9f89dae0981ee 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -3944,7 +3944,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 } @@ -3953,28 +3953,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 || emitNode == nil { + 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/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) +} 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. ) diff --git a/tsc/internal/printer/printer.go b/tsc/internal/printer/printer.go index a27452d51472d..b931d3300d38f 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) }