Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/typescript/enums/nodeFlags.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/typescript/enums/nodeFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
29 changes: 29 additions & 0 deletions packages/typescript/test/async/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }),
"/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("/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<string, string> = {};
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);

Expand Down
29 changes: 29 additions & 0 deletions packages/typescript/test/sync/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }),
"/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("/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<string, string> = {};
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);

Expand Down
34 changes: 29 additions & 5 deletions tsc/internal/api/encoder/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Comment on lines +468 to +469

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks. Added TestPrintNodeRestoresNoAsciiEscaping: a synthesized "é" literal round-trips verbatim with EFNoAsciiEscaping and as "\u00E9" without it. I also confirmed it fails if the encoder stops setting 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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions tsc/internal/api/enum_values_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions tsc/internal/api/printnode_emitflags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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
}

// 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"`)
}
37 changes: 30 additions & 7 deletions tsc/internal/api/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -4035,15 +4035,15 @@ 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
}
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)
}
Expand Down Expand Up @@ -4078,15 +4078,15 @@ 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
}
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)
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions tsc/internal/ast/nodeflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions tsc/internal/checker/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down