From 7f15b1efbc5fbf9d0b16e9465035d7c16f34c6c0 Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Chauhan Date: Fri, 30 Oct 2026 23:25:32 +0530 Subject: [PATCH 1/2] Preserve single-line emit hints for nodes returned by the API Nodes built by the node builder (e.g. via typeToTypeNode) mark tuple and type literal nodes as single-line using emit flags kept in an emit context. The API encoder never sent those, so printNode on the client side printed such nodes across several lines, unlike typeToString and the 6.0 API. Carry EFSingleLine and EFNoAsciiEscaping across the API in two reserved NodeFlags bits when encoding the results of typeToTypeNode and signatureToSignatureDeclaration, and restore them as emit flags in printNode. --- packages/typescript/enums/nodeFlags.d.ts | 2 + packages/typescript/enums/nodeFlags.js | 2 + packages/typescript/test/async/api.test.ts | 29 ++++++++++++++ packages/typescript/test/sync/api.test.ts | 29 ++++++++++++++ tsc/internal/api/encoder/encoder.go | 34 +++++++++++++--- tsc/internal/api/enum_values_generated.go | 2 + tsc/internal/api/printnode_emitflags_test.go | 42 ++++++++++++++++++++ tsc/internal/api/session.go | 37 +++++++++++++---- tsc/internal/ast/nodeflags.go | 6 +++ tsc/internal/checker/printer.go | 14 +++++++ 10 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 tsc/internal/api/printnode_emitflags_test.go diff --git a/packages/typescript/enums/nodeFlags.d.ts b/packages/typescript/enums/nodeFlags.d.ts index 405ad7d48183e..2461c2a82e1ff 100644 --- a/packages/typescript/enums/nodeFlags.d.ts +++ b/packages/typescript/enums/nodeFlags.d.ts @@ -31,6 +31,8 @@ export declare enum NodeFlags { PossiblyContainsDeprecatedTag = 1 << 26, Unreachable = 1 << 27, ReparserTransformedLiteral = 1 << 28, + EmitSingleLine = 1 << 29, + EmitNoAsciiEscaping = 1 << 30, BlockScoped = Let | Const | Using, Constant = Const | Using, AwaitUsing = Const | Using, diff --git a/packages/typescript/enums/nodeFlags.js b/packages/typescript/enums/nodeFlags.js index 05e7c99f21190..bc3260321afb9 100644 --- a/packages/typescript/enums/nodeFlags.js +++ b/packages/typescript/enums/nodeFlags.js @@ -31,6 +31,8 @@ export var NodeFlags; NodeFlags[NodeFlags["PossiblyContainsDeprecatedTag"] = 67108864] = "PossiblyContainsDeprecatedTag"; NodeFlags[NodeFlags["Unreachable"] = 134217728] = "Unreachable"; NodeFlags[NodeFlags["ReparserTransformedLiteral"] = 268435456] = "ReparserTransformedLiteral"; + NodeFlags[NodeFlags["EmitSingleLine"] = 536870912] = "EmitSingleLine"; + NodeFlags[NodeFlags["EmitNoAsciiEscaping"] = 1073741824] = "EmitNoAsciiEscaping"; NodeFlags[NodeFlags["BlockScoped"] = 7] = "BlockScoped"; NodeFlags[NodeFlags["Constant"] = 6] = "Constant"; NodeFlags[NodeFlags["AwaitUsing"] = 6] = "AwaitUsing"; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 3d00791be523d..06482f925f213 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -8712,6 +8712,35 @@ export const obj = { m: 1, s: "hi", b: true }; assert.strictEqual(text, "number[]"); }); + test("typeToTypeNode + printNode keeps tuples and type literals on a single line", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "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/src/main.ts": `export type Shape = { tuple: [Date, number]; obj: { a: string } };`, + }); + + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const { checker, program } = snapshot.getConfiguredProject("/tsconfig.json")!; + const sourceFile = await program.getSourceFile("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/src/main.ts"); + assert.ok(sourceFile); + const alias = sourceFile.statements.find(s => (s as any).name?.text === "Shape") as any; + assert.ok(alias); + const symbol = await checker.getSymbolAtLocation(alias.name); + assert.ok(symbol); + const shape = await checker.getDeclaredTypeOfSymbol(symbol); + assert.ok(shape); + + const printed: Record = {}; + for (const prop of await checker.getPropertiesOfType(shape)) { + const type = await checker.getTypeOfSymbol(prop); + assert.ok(type); + const node = await checker.typeToTypeNode(type); + assert.ok(node); + printed[prop.name] = await api.printer.printNode(node); + assert.strictEqual(printed[prop.name], await checker.typeToString(type)); + } + assert.deepStrictEqual(printed, { tuple: "[Date, number]", obj: "{ a: string; }" }); + }); + test("typeToTypeNode + printNode round-trip", 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 7f7e2e31c6e0e..a684562d2a8f0 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -8512,6 +8512,35 @@ export const obj = { m: 1, s: "hi", b: true }; assert.strictEqual(text, "number[]"); }); + test("typeToTypeNode + printNode keeps tuples and type literals on a single line", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "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/src/main.ts": `export type Shape = { tuple: [Date, number]; obj: { a: string } };`, + }); + + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const { checker, program } = snapshot.getConfiguredProject("/tsconfig.json")!; + const sourceFile = program.getSourceFile("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/src/main.ts"); + assert.ok(sourceFile); + const alias = sourceFile.statements.find(s => (s as any).name?.text === "Shape") as any; + assert.ok(alias); + const symbol = checker.getSymbolAtLocation(alias.name); + assert.ok(symbol); + const shape = checker.getDeclaredTypeOfSymbol(symbol); + assert.ok(shape); + + const printed: Record = {}; + for (const prop of checker.getPropertiesOfType(shape)) { + const type = checker.getTypeOfSymbol(prop); + assert.ok(type); + const node = checker.typeToTypeNode(type); + assert.ok(node); + printed[prop.name] = api.printer.printNode(node); + assert.strictEqual(printed[prop.name], checker.typeToString(type)); + } + assert.deepStrictEqual(printed, { tuple: "[Date, number]", obj: "{ a: string; }" }); + }); + test("typeToTypeNode + printNode round-trip", () => { using api = spawnAPI(emitterFiles); diff --git a/tsc/internal/api/encoder/encoder.go b/tsc/internal/api/encoder/encoder.go index 39367ebc9fae8..e779078683928 100644 --- a/tsc/internal/api/encoder/encoder.go +++ b/tsc/internal/api/encoder/encoder.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/printer" "github.com/microsoft/TypeScript/tsc/internal/spanmap" "github.com/zeebo/xxh3" ) @@ -420,7 +421,7 @@ func GetNodeIndexTable(sourceFile *ast.SourceFile) *NodeIndexTable { // EncodeSourceFile encodes an entire source file AST into the binary format. // Returns the encoded bytes and a NodeIndexTable mapping encoder indices to AST nodes. func EncodeSourceFile(sourceFile *ast.SourceFile) ([]byte, *NodeIndexTable, error) { - data, nodeTable, err := encodeTree(sourceFile.AsNode(), sourceFile) + data, nodeTable, err := encodeTree(sourceFile.AsNode(), sourceFile, nil) if err != nil { return nil, nil, err } @@ -445,10 +446,33 @@ func SetSourceFileID(data []byte, id uint64) { // When encoding a non-SourceFile node, the header hash and parse options fields will be zero. // Returns the encoded bytes and a NodeIndexTable mapping encoder indices to AST nodes. func EncodeNode(node *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIndexTable, error) { - return encodeTree(node, sourceFile) + return encodeTree(node, sourceFile, nil) } -func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIndexTable, error) { +// EncodeNodeWithEmitContext is like EncodeNode, but also transmits printer hints recorded for synthesized nodes in +// emitContext (currently single-line and no-ASCII-escaping), as ast.NodeFlagsEmitSingleLine and +// ast.NodeFlagsEmitNoAsciiEscaping in each node's flags. This allows nodes built by the node builder to print +// the same way after a round trip through the API as they would in-process. +func EncodeNodeWithEmitContext(node *ast.Node, sourceFile *ast.SourceFile, emitContext *printer.EmitContext) ([]byte, *NodeIndexTable, error) { + return encodeTree(node, sourceFile, emitContext) +} + +// encodedNodeFlags returns the flags to encode for node, including transported emit flags. +func encodedNodeFlags(node *ast.Node, emitContext *printer.EmitContext) uint32 { + flags := node.Flags + if emitContext != nil { + emitFlags := emitContext.EmitFlags(node) + if emitFlags&printer.EFSingleLine != 0 { + flags |= ast.NodeFlagsEmitSingleLine + } + if emitFlags&printer.EFNoAsciiEscaping != 0 { + flags |= ast.NodeFlagsEmitNoAsciiEscaping + } + } + return uint32(flags) +} + +func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile, emitContext *printer.EmitContext) ([]byte, *NodeIndexTable, error) { var parentIndex, nodeCount, prevIndex uint32 var extendedData []byte var structuredData []byte @@ -554,7 +578,7 @@ func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIn nodes[prevIndex*NodeSize+NodeOffsetNext+3] = b3 } - nodes = appendUint32s(nodes, uint32(node.Kind), utf16(node.Pos()), utf16(node.End()), 0, parentIndex, getNodeData(node, strs, positionMap, &extendedData, &structuredData), uint32(node.Flags)) + nodes = appendUint32s(nodes, uint32(node.Kind), utf16(node.Pos()), utf16(node.End()), 0, parentIndex, getNodeData(node, strs, positionMap, &extendedData, &structuredData), encodedNodeFlags(node, emitContext)) if nodeIndexMap != nil { if _, ok := nodeIndexMap[node]; ok { @@ -585,7 +609,7 @@ func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIn nodeTable = append(nodeTable, rootNode) // index 1 = root node sfExtendedDataOffset = len(extendedData) - nodes = appendUint32s(nodes, uint32(rootNode.Kind), utf16(rootNode.Pos()), utf16(rootNode.End()), 0, 0, getNodeData(rootNode, strs, positionMap, &extendedData, &structuredData), uint32(rootNode.Flags)) + nodes = appendUint32s(nodes, uint32(rootNode.Kind), utf16(rootNode.Pos()), utf16(rootNode.End()), 0, 0, getNodeData(rootNode, strs, positionMap, &extendedData, &structuredData), encodedNodeFlags(rootNode, emitContext)) visitor.VisitEachChild(rootNode) if sourceFile != nil { diff --git a/tsc/internal/api/enum_values_generated.go b/tsc/internal/api/enum_values_generated.go index 1697a479539a5..3f807773de78a 100644 --- a/tsc/internal/api/enum_values_generated.go +++ b/tsc/internal/api/enum_values_generated.go @@ -751,6 +751,8 @@ func main() { "PossiblyContainsDeprecatedTag": toInt32(ast.NodeFlagsPossiblyContainsDeprecatedTag), "Unreachable": toInt32(ast.NodeFlagsUnreachable), "ReparserTransformedLiteral": toInt32(ast.NodeFlagsReparserTransformedLiteral), + "EmitSingleLine": toInt32(ast.NodeFlagsEmitSingleLine), + "EmitNoAsciiEscaping": toInt32(ast.NodeFlagsEmitNoAsciiEscaping), "BlockScoped": toInt32(ast.NodeFlagsBlockScoped), "Constant": toInt32(ast.NodeFlagsConstant), "AwaitUsing": toInt32(ast.NodeFlagsAwaitUsing), diff --git a/tsc/internal/api/printnode_emitflags_test.go b/tsc/internal/api/printnode_emitflags_test.go new file mode 100644 index 0000000000000..f9d8c9bc53a32 --- /dev/null +++ b/tsc/internal/api/printnode_emitflags_test.go @@ -0,0 +1,42 @@ +package api + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/api/encoder" + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/printer" + "gotest.tools/v3/assert" +) + +// Nodes built by the node builder carry EFSingleLine in an emit context. That must survive an encode/decode round +// trip so that printNode matches in-process printing. +func TestPrintNodeRestoresEmitFlags(t *testing.T) { + t.Parallel() + + build := func(ec *printer.EmitContext, singleLine bool) *ast.Node { + f := ec.Factory.AsNodeFactory() + str := func() *ast.Node { return f.NewKeywordTypeNode(ast.KindStringKeyword) } + member := f.NewPropertySignatureDeclaration(nil, f.NewIdentifier("a"), nil, str(), nil) + lit := f.NewTypeLiteralNode(f.NewNodeList([]*ast.Node{member})) + tuple := f.NewTupleTypeNode(f.NewNodeList([]*ast.Node{f.NewTypeReferenceNode(f.NewIdentifier("Date"), nil), str(), lit})) + if singleLine { + ec.SetEmitFlags(lit, printer.EFSingleLine) + ec.SetEmitFlags(tuple, printer.EFSingleLine) + } + return tuple + } + + roundTrip := func(singleLine bool) string { + ec := printer.NewEmitContext() + node := build(ec, singleLine) + data, _, err := encoder.EncodeNodeWithEmitContext(node, nil, ec) + assert.NilError(t, err) + decoded, err := encoder.DecodeNodes(data) + assert.NilError(t, err) + return newPrinter(&PrintNodeParams{}, decoded).Emit(decoded, nil) + } + + assert.Equal(t, roundTrip(true), "[Date, string, { a: string; }]") + assert.Assert(t, roundTrip(false) != "[Date, string, { a: string; }]") // no hint, default multi-line layout +} diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index c29058ece0df7..f963fde6da469 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -4035,7 +4035,7 @@ func (s *Session) handleTypeToTypeNode(ctx context.Context, params *TypeToTypeNo } } - typeNode := setup.checker.TypeToTypeNode(t, enclosingDeclaration, nodebuilder.Flags(params.Flags), nil) + typeNode, emitContext := setup.checker.TypeToTypeNodeWithEmitContext(t, enclosingDeclaration, nodebuilder.Flags(params.Flags), nil) if typeNode == nil { if s.useBinaryResponses { return RawBinary(nil), nil @@ -4043,7 +4043,7 @@ func (s *Session) handleTypeToTypeNode(ctx context.Context, params *TypeToTypeNo return nil, nil } - data, _, err := encoder.EncodeNode(typeNode.AsNode(), nil) + data, _, err := encoder.EncodeNodeWithEmitContext(typeNode.AsNode(), nil, emitContext) if err != nil { return nil, fmt.Errorf("failed to encode type node: %w", err) } @@ -4078,7 +4078,7 @@ func (s *Session) handleSignatureToSignatureDeclaration(ctx context.Context, par } } - node := setup.checker.SignatureToSignatureDeclaration(sig, ast.Kind(params.Kind), enclosingDeclaration, nodebuilder.Flags(params.Flags)) + node, emitContext := setup.checker.SignatureToSignatureDeclarationWithEmitContext(sig, ast.Kind(params.Kind), enclosingDeclaration, nodebuilder.Flags(params.Flags)) if node == nil { if s.useBinaryResponses { return RawBinary(nil), nil @@ -4086,7 +4086,7 @@ func (s *Session) handleSignatureToSignatureDeclaration(ctx context.Context, par return nil, nil } - data, _, err := encoder.EncodeNode(node.AsNode(), nil) + data, _, err := encoder.EncodeNodeWithEmitContext(node.AsNode(), nil, emitContext) if err != nil { return nil, fmt.Errorf("failed to encode signature declaration: %w", err) } @@ -4137,7 +4137,7 @@ 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, node).Emit(node, sourceFile), nil } func decodePrintNode(encoded string) (*ast.Node, error) { @@ -4153,12 +4153,35 @@ func decodePrintNode(encoded string) (*ast.Node, error) { return node, nil } -func newPrinter(params *PrintNodeParams) *printer.Printer { +func newPrinter(params *PrintNodeParams, root *ast.Node) *printer.Printer { + emitContext := printer.NewEmitContext() + restoreEmitFlags(emitContext, root) return printer.NewPrinter(printer.PrinterOptions{ PreserveSourceNewlines: params.PreserveSourceNewlines, NeverAsciiEscape: params.NeverAsciiEscape, TerminateUnterminatedLiterals: params.TerminateUnterminatedLiterals, - }, printer.PrintHandlers{}, nil) + }, printer.PrintHandlers{}, emitContext) +} + +// restoreEmitFlags converts the printer hints that the encoder transported in node flags (see +// encoder.EncodeNodeWithEmitContext) back into emit flags on emitContext, for every node in the tree. +func restoreEmitFlags(emitContext *printer.EmitContext, root *ast.Node) { + const transported = ast.NodeFlagsEmitSingleLine | ast.NodeFlagsEmitNoAsciiEscaping + var visit func(n *ast.Node) bool + visit = func(n *ast.Node) bool { + if n.Flags&transported != 0 { + var flags printer.EmitFlags + if n.Flags&ast.NodeFlagsEmitSingleLine != 0 { + flags |= printer.EFSingleLine + } + if n.Flags&ast.NodeFlagsEmitNoAsciiEscaping != 0 { + flags |= printer.EFNoAsciiEscaping + } + emitContext.AddEmitFlags(n, flags) + } + return n.ForEachChild(visit) + } + visit(root) } func (s *Session) handleEmit(ctx context.Context, params *EmitParams) (*EmitResponse, error) { diff --git a/tsc/internal/ast/nodeflags.go b/tsc/internal/ast/nodeflags.go index 2c808601a2260..f568e1ece2b1c 100644 --- a/tsc/internal/ast/nodeflags.go +++ b/tsc/internal/ast/nodeflags.go @@ -46,6 +46,12 @@ const ( NodeFlagsUnreachable NodeFlags = 1 << 27 // If node is unreachable according to the binder NodeFlagsReparserTransformedLiteral NodeFlags = 1 << 28 // If node was transformed during parsing, making its' naive text source not match the AST + // The following flags are only used to carry printer hints (see printer.EmitFlags) across the API boundary, + // where the emit context that normally holds them is not available. They are set by the API encoder on nodes + // that were given the corresponding emit flag (e.g. by the node builder), and are never set by the parser or binder. + NodeFlagsEmitSingleLine NodeFlags = 1 << 29 // Node should be printed on a single line (printer.EFSingleLine) + NodeFlagsEmitNoAsciiEscaping NodeFlags = 1 << 30 // Node's text should not be ASCII-escaped when printed (printer.EFNoAsciiEscaping) + NodeFlagsBlockScoped = NodeFlagsLet | NodeFlagsConst | NodeFlagsUsing NodeFlagsConstant = NodeFlagsConst | NodeFlagsUsing NodeFlagsAwaitUsing = NodeFlagsConst | NodeFlagsUsing // Variable declaration (NOTE: on a single node these flags would otherwise be mutually exclusive) diff --git a/tsc/internal/checker/printer.go b/tsc/internal/checker/printer.go index b84c967883d88..5d7173d9677d0 100644 --- a/tsc/internal/checker/printer.go +++ b/tsc/internal/checker/printer.go @@ -290,6 +290,20 @@ func (c *Checker) TypeToTypeNode(t *Type, enclosingDeclaration *ast.Node, flags return nodeBuilder.TypeToTypeNode(t, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil) } +// TypeToTypeNodeWithEmitContext is like TypeToTypeNode, but also returns the emit context holding the emit flags +// (such as single-line) that the node builder recorded on the resulting nodes. +func (c *Checker) TypeToTypeNodeWithEmitContext(t *Type, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) (*ast.TypeNode, *printer.EmitContext) { + nodeBuilder := c.getNodeBuilderEx(idToSymbol) + return nodeBuilder.TypeToTypeNode(t, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil), nodeBuilder.EmitContext() +} + +// SignatureToSignatureDeclarationWithEmitContext is like SignatureToSignatureDeclaration, but also returns the emit +// context holding the emit flags that the node builder recorded on the resulting nodes. +func (c *Checker) SignatureToSignatureDeclarationWithEmitContext(signature *Signature, kind ast.Kind, enclosingDeclaration *ast.Node, flags nodebuilder.Flags) (*ast.Node, *printer.EmitContext) { + nodeBuilder := c.getNodeBuilderEx(nil /*idToSymbol*/) + return nodeBuilder.SignatureToSignatureDeclaration(signature, kind, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil), nodeBuilder.EmitContext() +} + func (c *Checker) SignatureToSignatureDeclaration(signature *Signature, kind ast.Kind, enclosingDeclaration *ast.Node, flags nodebuilder.Flags) *ast.Node { nodeBuilder, release := c.getNodeBuilder() defer release() From 4df5f5d8253fd13bec36e138b0b811719c05bda5 Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Chauhan Date: Fri, 30 Oct 2026 23:54:11 +0530 Subject: [PATCH 2/2] Add round-trip test for transported EFNoAsciiEscaping hint --- tsc/internal/api/printnode_emitflags_test.go | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tsc/internal/api/printnode_emitflags_test.go b/tsc/internal/api/printnode_emitflags_test.go index f9d8c9bc53a32..01a6c31574e42 100644 --- a/tsc/internal/api/printnode_emitflags_test.go +++ b/tsc/internal/api/printnode_emitflags_test.go @@ -40,3 +40,26 @@ func TestPrintNodeRestoresEmitFlags(t *testing.T) { assert.Equal(t, roundTrip(true), "[Date, string, { a: string; }]") assert.Assert(t, roundTrip(false) != "[Date, string, { a: string; }]") // no hint, default multi-line layout } + +// EFNoAsciiEscaping must also survive the round trip: a synthesized non-ASCII string literal is escaped by default +// and printed verbatim when the node builder marked it. +func TestPrintNodeRestoresNoAsciiEscaping(t *testing.T) { + t.Parallel() + + roundTrip := func(noEscape bool) string { + ec := printer.NewEmitContext() + f := ec.Factory.AsNodeFactory() + node := f.NewLiteralTypeNode(f.NewStringLiteral("é", ast.TokenFlagsNone)) + if noEscape { + ec.SetEmitFlags(node.AsLiteralTypeNode().Literal, printer.EFNoAsciiEscaping) + } + data, _, err := encoder.EncodeNodeWithEmitContext(node, nil, ec) + assert.NilError(t, err) + decoded, err := encoder.DecodeNodes(data) + assert.NilError(t, err) + return newPrinter(&PrintNodeParams{}, decoded).Emit(decoded, nil) + } + + assert.Equal(t, roundTrip(true), `"é"`) + assert.Equal(t, roundTrip(false), `"\u00E9"`) +}