Skip to content

Commit 618a5c2

Browse files
committed
Unify recursive type naming during serialization
Recursive values can have valid declaration types without annotations. Separate function and alias fallbacks leave equivalent constructs with different results, and broader value annotations can silently weaken the recursive type. Keep ordinary type names independent of the definitions being emitted. Share definition context across syntax reuse and structural expansion so recursive references and cycle detection agree on what is in scope. A value self-reference must cross a lazy resolution boundary, and names usable in local displays are not necessarily usable in declarations. Starting from a selected signature must not add an extra recursive unfolding merely because its containing type was not traversed. Preserve isolated declaration inference restrictions and prevent cached output from crossing incompatible recursion contexts. Captured generic substitutions must not escape into parameters bound by a re-entered signature. Otherwise declaration consumers lose valid calls even when the original source infers the correct result. Fixes 55832
1 parent aca4412 commit 618a5c2

64 files changed

Lines changed: 2250 additions & 2400 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎tsc/internal/checker/checker_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ func TestTypeToStringRecursivePrototype(t *testing.T) {
183183
display := c.TypeToStringEx(typ, node.Parent, checker.TypeFormatFlagsNoTruncation|checker.TypeFormatFlagsGenerateNamesForShadowedTypeParams, nil)
184184
t.Logf("methods=%d, bytes=%d, first method occurrences=%d", count, len(display), strings.Count(display, "m000:"))
185185
for i := range count {
186-
assert.Equal(t, strings.Count(display, fmt.Sprintf("m%03d:", i)), 2)
186+
assert.Equal(t, strings.Count(display, fmt.Sprintf("m%03d:", i)), 1)
187187
}
188188
})
189189
}

‎tsc/internal/checker/nodebuilderimpl.go‎

Lines changed: 121 additions & 135 deletions
Large diffs are not rendered by default.

‎tsc/internal/checker/nodebuilderscopes.go‎

Lines changed: 259 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package checker
22

33
import (
4+
"slices"
5+
46
"github.com/microsoft/TypeScript/tsc/internal/ast"
57
"github.com/microsoft/TypeScript/tsc/internal/core"
68
"github.com/microsoft/TypeScript/tsc/internal/debug"
9+
"github.com/microsoft/TypeScript/tsc/internal/jsnum"
710
"github.com/microsoft/TypeScript/tsc/internal/nodebuilder"
11+
"github.com/microsoft/TypeScript/tsc/internal/scanner"
812
)
913

10-
func cloneNodeBuilderContext(context *NodeBuilderContext) func() {
14+
func enterTypeParameterNameScope(context *NodeBuilderContext) func() {
1115
// Make type parameters created within this context not consume the name outside this context
1216
// The symbol serializer ends up creating many sibling scopes that all need "separate" contexts when
1317
// it comes to naming things - within a normal `typeToTypeNode` call, the node builder only ever descends
@@ -50,14 +54,256 @@ func (b *NodeBuilderImpl) addSymbolTypeToContext(symbol *ast.Symbol, t *Type) fu
5054
}
5155
}
5256

57+
func (b *NodeBuilderImpl) serializationTypeId(t *Type) TypeId {
58+
if b.ch.isArrayOrTupleType(t) {
59+
// Deferred and regular references share a serialization identity.
60+
return b.ch.createTypeReference(t.Target(), b.ch.getTypeArguments(t)).id
61+
}
62+
return t.id
63+
}
64+
65+
func (b *NodeBuilderImpl) enterTypeDefinition(t *Type, symbol *ast.Symbol, expanding bool) func() {
66+
return b.enterDefinition(serializationDefinition{t: t, typeId: b.serializationTypeId(t), symbol: symbol, expanding: expanding})
67+
}
68+
69+
func (b *NodeBuilderImpl) enterDefinition(definition serializationDefinition) func() {
70+
definitions := b.ctx.definitions
71+
definition.depth = b.ctx.deferredTypeDepth
72+
b.ctx.definitions = append(definitions, definition)
73+
return func() { b.ctx.definitions = definitions }
74+
}
75+
76+
// Lexical scopes alone do not guard back-references; only lazy member boundaries do.
77+
func (b *NodeBuilderImpl) enterDeferredTypeScope() func() {
78+
depth := b.ctx.deferredTypeDepth
79+
b.ctx.deferredTypeDepth++
80+
return func() { b.ctx.deferredTypeDepth = depth }
81+
}
82+
83+
func (b *NodeBuilderImpl) enterInferTypeParameterScope(typeParameters []*Type) func() {
84+
inferTypeParameters := b.ctx.inferTypeParameters
85+
b.ctx.inferTypeParameters = typeParameters
86+
return func() { b.ctx.inferTypeParameters = inferTypeParameters }
87+
}
88+
89+
func (b *NodeBuilderImpl) isSameSerializationType(t *Type, typeId TypeId, definition serializationDefinition) bool {
90+
if typeId == definition.typeId {
91+
return true
92+
}
93+
other := definition.t
94+
// Regular and widened objects may have different IDs without changing shape.
95+
// Do not collapse distinct generic instantiations.
96+
return t.objectFlags&(ObjectFlagsAnonymous|ObjectFlagsInstantiated) == ObjectFlagsAnonymous &&
97+
other.objectFlags&(ObjectFlagsAnonymous|ObjectFlagsInstantiated) == ObjectFlagsAnonymous &&
98+
t.symbol != nil && t.symbol == other.symbol && b.ch.isTypeIdenticalTo(t, other)
99+
}
100+
101+
func (b *NodeBuilderImpl) getTypeDefinitionReference(t *Type) serializationTypeName {
102+
var typeId TypeId
103+
for _, definition := range b.ctx.definitions {
104+
if b.ctx.deferredTypeDepth <= definition.depth {
105+
continue
106+
}
107+
if signature := definition.signature; signature != nil {
108+
if t.objectFlags&ObjectFlagsAnonymous == 0 {
109+
continue
110+
}
111+
kind := core.IfElse(signature.flags&SignatureFlagsConstruct != 0, SignatureKindConstruct, SignatureKindCall)
112+
if !slices.Contains(b.ch.getSignaturesOfType(t, kind), signature) {
113+
continue
114+
}
115+
} else {
116+
if typeId == 0 {
117+
typeId = b.serializationTypeId(t)
118+
}
119+
if !b.isSameSerializationType(t, typeId, definition) {
120+
continue
121+
}
122+
}
123+
var name serializationTypeName
124+
if symbol := definition.symbol; symbol != nil {
125+
if symbol.Flags()&(ast.SymbolFlagsProperty|ast.SymbolFlagsMethod|ast.SymbolFlagsAccessor) != 0 {
126+
name = b.getSerializationTypeNameFromDeclaration(t, symbol.ValueDeclaration())
127+
} else {
128+
name = b.getValueTypeName(t, symbol, nil)
129+
}
130+
} else {
131+
name = b.getSerializationValueName(t)
132+
}
133+
if name.symbol != nil && b.isSerializationTypeNameAccessible(name) {
134+
return name
135+
}
136+
}
137+
return serializationTypeName{}
138+
}
139+
140+
func (b *NodeBuilderImpl) getValueTypeName(t *Type, symbol *ast.Symbol, path []*Type) serializationTypeName {
141+
if symbol == nil || symbol.Flags()&ast.SymbolFlagsValue == 0 || t.flags&TypeFlagsStructuredOrInstantiable == 0 ||
142+
IsPrivateIdentifierSymbol(symbol) || b.ctx.flags&nodebuilder.FlagsUseStructuralFallback != 0 && getDeclarationModifierFlagsFromSymbol(symbol)&(ast.ModifierFlagsPrivate|ast.ModifierFlagsProtected) != 0 ||
143+
!scanner.IsIdentifierText(symbol.Name(), core.LanguageVariantStandard) {
144+
return serializationTypeName{}
145+
}
146+
// Formatting a diagnostic must not restart inference for a declaration
147+
// whose type is still being resolved.
148+
namedType := b.ch.valueSymbolLinks.Get(symbol).resolvedType
149+
if namedType == nil {
150+
return serializationTypeName{}
151+
}
152+
for _, index := range path {
153+
property := b.ch.getPropertyOfType(namedType, getPropertyNameFromType(index))
154+
if property != nil {
155+
if IsPrivateIdentifierSymbol(property) || b.ctx.flags&nodebuilder.FlagsUseStructuralFallback != 0 && getDeclarationModifierFlagsFromSymbol(property)&(ast.ModifierFlagsPrivate|ast.ModifierFlagsProtected) != 0 ||
156+
b.ch.findResolutionCycleStartIndex(property, TypeSystemPropertyNameType) >= 0 {
157+
return serializationTypeName{}
158+
}
159+
}
160+
namedType = b.ch.getIndexedAccessType(namedType, index)
161+
}
162+
if b.serializationTypeId(namedType) != b.serializationTypeId(t) {
163+
return serializationTypeName{}
164+
}
165+
return serializationTypeName{symbol: symbol, meaning: ast.SymbolFlagsValue, path: path}
166+
}
167+
168+
func (b *NodeBuilderImpl) getSerializationValueName(t *Type) serializationTypeName {
169+
if t.objectFlags&ObjectFlagsAnonymous == 0 || t.symbol == nil {
170+
return serializationTypeName{}
171+
}
172+
node := t.symbol.ValueDeclaration()
173+
if node != nil && (ast.IsExpression(node) || ast.IsClassElement(node) || ast.IsPropertyAssignment(node)) {
174+
if name := b.getSerializationTypeNameFromDeclaration(t, node); name.symbol != nil {
175+
return name
176+
}
177+
}
178+
return b.getValueTypeName(t, t.symbol, nil)
179+
}
180+
181+
func (b *NodeBuilderImpl) isSerializationTypeNameAccessible(name serializationTypeName) bool {
182+
if b.ctx.flags&nodebuilder.FlagsForbidIndexedAccessSymbolReferences != 0 &&
183+
(len(name.path) != 0 || name.symbol.Flags()&(ast.SymbolFlagsProperty|ast.SymbolFlagsMethod|ast.SymbolFlagsAccessor) != 0) {
184+
return false
185+
}
186+
// Displays may use lexical names whose declarations cannot be exposed in a .d.ts.
187+
if b.ctx.flags&nodebuilder.FlagsUseStructuralFallback == 0 && b.ctx.enclosingDeclaration != nil &&
188+
b.ch.resolveName(b.ctx.enclosingDeclaration, name.symbol.Name(), name.meaning, nil /*nameNotFoundMessage*/, false /*isUse*/, false /*excludeGlobals*/) == name.symbol {
189+
return true
190+
}
191+
return b.ch.IsValueSymbolAccessible(name.symbol, b.ctx.enclosingDeclaration)
192+
}
193+
194+
func (b *NodeBuilderImpl) serializationTypeNameToNode(name serializationTypeName) *ast.TypeNode {
195+
typeArguments := b.mapToTypeNodes(name.typeArguments, false /*isBareList*/)
196+
if name.meaning == ast.SymbolFlagsType {
197+
if isReservedMemberName(name.symbol.Name()) && name.symbol.Flags()&ast.SymbolFlagsClass == 0 {
198+
return b.f.NewTypeReferenceNode(b.f.NewIdentifier(""), typeArguments)
199+
}
200+
if typeArguments != nil && len(typeArguments.Nodes) == 1 && name.symbol == b.ch.globalArrayType.symbol {
201+
return b.f.NewArrayTypeNode(typeArguments.Nodes[0])
202+
}
203+
}
204+
node := b.symbolToTypeNode(name.symbol, name.meaning, typeArguments)
205+
restoreFlags := b.saveRestoreFlags()
206+
b.ctx.flags &^= nodebuilder.FlagsAllowUniqueESSymbolType
207+
for _, index := range name.path {
208+
b.ctx.approximateLength += 2
209+
node = b.f.NewIndexedAccessTypeNode(node, b.typeToTypeNode(index))
210+
}
211+
restoreFlags()
212+
return node
213+
}
214+
215+
func (b *NodeBuilderImpl) prefersTypeOfFunction(t *Type, name serializationTypeName) bool {
216+
if name.symbol == nil || name.meaning != ast.SymbolFlagsValue || t.symbol == nil {
217+
return false
218+
}
219+
symbol := t.symbol
220+
if symbol.Flags()&ast.SymbolFlagsMethod != 0 {
221+
return core.Some(symbol.Declarations(), ast.IsStatic)
222+
}
223+
if symbol.Flags()&ast.SymbolFlagsFunction == 0 || len(name.path) != 0 {
224+
return false
225+
}
226+
declaration := name.symbol.ValueDeclaration()
227+
if declaration == nil || ast.IsVariableDeclaration(declaration) && declaration == b.ctx.enclosingDeclaration {
228+
return false
229+
}
230+
if ast.IsVariableDeclaration(declaration) {
231+
if declaration.Parent == nil || !ast.IsVariableDeclarationList(declaration.Parent) || declaration.Parent.Parent == nil {
232+
return false
233+
}
234+
declaration = declaration.Parent.Parent
235+
}
236+
return symbol.Parent() != nil || declaration.Parent != nil &&
237+
(declaration.Parent.Kind == ast.KindSourceFile || declaration.Parent.Kind == ast.KindModuleBlock)
238+
}
239+
240+
func (b *NodeBuilderImpl) getSerializationTypeNameFromDeclaration(t *Type, node *ast.Node) serializationTypeName {
241+
var path []*Type
242+
for node != nil {
243+
if declaration := getAssignedValueDeclaration(node); declaration != nil && !ast.IsStatic(declaration) {
244+
slices.Reverse(path)
245+
return b.getValueTypeName(t, b.ch.getSymbolOfDeclaration(declaration), path)
246+
}
247+
parent := walkUpOuterExpressions(node)
248+
if parent == nil {
249+
return serializationTypeName{}
250+
}
251+
switch {
252+
case ast.IsPropertyDeclaration(parent):
253+
node = parent
254+
case ast.IsPropertyAssignment(parent):
255+
index := b.ch.getLiteralTypeFromPropertyName(parent.Name())
256+
if !isTypeUsableAsPropertyName(index) {
257+
return serializationTypeName{}
258+
}
259+
path = append(path, index)
260+
node = parent.Parent
261+
case ast.IsObjectLiteralExpression(parent), ast.IsClassLike(parent):
262+
if node.Name() == nil {
263+
return serializationTypeName{}
264+
}
265+
index := b.ch.getLiteralTypeFromPropertyName(node.Name())
266+
if !isTypeUsableAsPropertyName(index) {
267+
return serializationTypeName{}
268+
}
269+
path = append(path, index)
270+
if ast.IsClassDeclaration(parent) {
271+
slices.Reverse(path)
272+
return b.getValueTypeName(t, b.ch.getSymbolOfDeclaration(parent), path)
273+
}
274+
node = parent
275+
case ast.IsArrayLiteralExpression(parent):
276+
elements := parent.AsArrayLiteralExpression().Elements.Nodes
277+
index := slices.IndexFunc(elements, func(element *ast.Node) bool {
278+
return ast.SkipOuterExpressions(element, ast.OEKAll) == node
279+
})
280+
if index < 0 || core.Some(elements[:index], ast.IsSpreadElement) {
281+
return serializationTypeName{}
282+
}
283+
path = append(path, b.ch.getNumberLiteralType(jsnum.Number(index)))
284+
node = parent
285+
default:
286+
return serializationTypeName{}
287+
}
288+
}
289+
return serializationTypeName{}
290+
}
291+
53292
func (b *NodeBuilderImpl) enterSignatureScope(signature *Signature) (expandedParams []*ast.Symbol, cleanup func()) {
293+
restoreDefinition := b.enterDefinition(serializationDefinition{signature: signature})
294+
restoreDeferredTypeScope := b.enterDeferredTypeScope()
54295
expandedParams = b.ch.getExpandedParameters(signature, true /*skipUnionExpanding*/)[0]
55-
cleanup = b.enterNewScope(signature.declaration, expandedParams, signature.typeParameters, signature.parameters, signature.mapper)
296+
restoreScope := b.enterNewScope(signature.declaration, expandedParams, signature.typeParameters, signature.parameters, signature.mapper)
297+
cleanup = func() {
298+
restoreScope()
299+
restoreDeferredTypeScope()
300+
restoreDefinition()
301+
}
56302
return expandedParams, cleanup
57303
}
58304

59305
func (b *NodeBuilderImpl) enterNewScope(declaration *ast.Node, expandedParams []*ast.Symbol, typeParameters []*Type, originalParameters []*ast.Symbol, mapper *TypeMapper) func() {
60-
cleanupContext := cloneNodeBuilderContext(b.ctx)
306+
cleanupNames := enterTypeParameterNameScope(b.ctx)
61307
// For regular function/method declarations, the enclosing declaration will already be signature.declaration,
62308
// so this is a no-op, but for arrow functions and function expressions, the enclosing declaration will be
63309
// the declaration that the arrow function / function expression is assigned to.
@@ -77,6 +323,15 @@ func (b *NodeBuilderImpl) enterNewScope(declaration *ast.Node, expandedParams []
77323
if mapper != nil {
78324
b.ctx.mapper = mapper
79325
}
326+
if mapper := b.ctx.mapper; mapper != nil && len(typeParameters) != 0 {
327+
// Bound type parameters shadow inherited substitutions; captured parameters do not.
328+
b.ctx.mapper = newFunctionTypeMapper(func(t *Type) *Type {
329+
if slices.Contains(typeParameters, t) {
330+
return t
331+
}
332+
return mapper.Map(t)
333+
})
334+
}
80335
if b.ctx.enclosingDeclaration != nil && declaration != nil {
81336
// As a performance optimization, reuse the same fake scope within this chain.
82337
// This is especially needed when we are working on an excessively deep type;
@@ -244,7 +499,7 @@ func (b *NodeBuilderImpl) enterNewScope(declaration *ast.Node, expandedParams []
244499
if cleanupTypeParams != nil {
245500
cleanupTypeParams()
246501
}
247-
cleanupContext()
502+
cleanupNames()
248503
b.ctx.enclosingDeclaration = oldEnclosingDecl
249504
b.ctx.mapper = oldMapper
250505
}

‎tsc/internal/checker/nodecopy.go‎

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -830,23 +830,21 @@ func getExistingNodeTreeVisitor(b *NodeBuilderImpl, bound *recoveryBoundary) *as
830830
return node
831831
}
832832
recover_ := bound.startRecoveryScope()
833-
introducesNewScope := ast.IsFunctionLike(node) || ast.IsMappedTypeNode(node)
834833
var exit func()
835-
if introducesNewScope {
836-
var params []*ast.Symbol
837-
var typeParams []*Type
838-
if ast.IsFunctionLike(node) {
839-
sig := b.ch.getSignatureFromDeclaration(node)
840-
params = sig.parameters
841-
typeParams = sig.typeParameters
842-
} else if ast.IsConditionalTypeNode(node) { // !!! TODO: impossible in combination with the scope start check???
843-
typeParams = b.ch.getInferTypeParameters(node)
844-
} else if ast.IsMappedTypeNode(node) {
845-
typeParams = []*Type{b.ch.getDeclaredTypeOfTypeParameter(b.ch.getSymbolOfDeclaration(node.AsMappedTypeNode().TypeParameter))}
846-
}
847-
exit = b.enterNewScope(node, params, typeParams, nil, nil)
834+
if ast.IsFunctionLike(node) {
835+
_, exit = b.enterSignatureScope(b.ch.getSignatureFromDeclaration(node))
836+
} else if ast.IsMappedTypeNode(node) {
837+
typeParams := []*Type{b.ch.getDeclaredTypeOfTypeParameter(b.ch.getSymbolOfDeclaration(node.AsMappedTypeNode().TypeParameter))}
838+
exit = b.enterNewScope(node, nil, typeParams, nil, nil)
839+
}
840+
var exitDeferredTypeScope func()
841+
if ast.IsTypeLiteralNode(node) || node.Parent != nil && ast.IsMappedTypeNode(node.Parent) && node.Parent.Type() == node {
842+
exitDeferredTypeScope = b.enterDeferredTypeScope()
848843
}
849844
result := visitExistingNodeTreeSymbolsWorker(node)
845+
if exitDeferredTypeScope != nil {
846+
exitDeferredTypeScope()
847+
}
850848
if exit != nil {
851849
exit()
852850
}

‎tsc/internal/checker/pseudotypenodebuilder.go‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,21 @@ func (b *NodeBuilderImpl) pseudoTypeToNodeWithCheckerFallback(t *pseudochecker.P
4040
b.ctx.suppressReportInferenceFallback = oldSuppress
4141
return result
4242
}
43+
} else if (t.Kind == pseudochecker.PseudoTypeKindSingleCallSignature || t.Kind == pseudochecker.PseudoTypeKindObjectLiteral) &&
44+
b.getTypeDefinitionReference(checkerType).symbol != nil {
45+
// Reusing structural syntax must not unfold an already available back-reference.
46+
return b.typeToTypeNode(checkerType)
4347
}
4448
return b.pseudoTypeToNode(t)
4549
}
4650

4751
// Maps a pseudochecker's pseudotypes into ast nodes and reports any inference fallback errors the pseudotype structure implies
4852
func (b *NodeBuilderImpl) pseudoTypeToNode(t *pseudochecker.PseudoType) *ast.Node {
4953
debug.Assert(t != nil, "Attempted to serialize nil pseudotype")
54+
if t.Kind == pseudochecker.PseudoTypeKindObjectLiteral {
55+
restoreDeferredTypeScope := b.enterDeferredTypeScope()
56+
defer restoreDeferredTypeScope()
57+
}
5058
switch t.Kind {
5159
case pseudochecker.PseudoTypeKindDirect:
5260
return b.reuseTypeNode(t.AsPseudoTypeDirect().TypeNode)
@@ -177,8 +185,7 @@ func (b *NodeBuilderImpl) pseudoTypeToNode(t *pseudochecker.PseudoType) *ast.Nod
177185
case pseudochecker.PseudoTypeKindSingleCallSignature:
178186
d := t.AsPseudoTypeSingleCallSignature()
179187
signature := b.ch.getSignatureFromDeclaration(d.Signature)
180-
expandedParams := b.ch.getExpandedParameters(signature, true /*skipUnionExpanding*/)[0]
181-
cleanup := b.enterNewScope(d.Signature, expandedParams, signature.typeParameters, signature.parameters, signature.mapper)
188+
_, cleanup := b.enterSignatureScope(signature)
182189
defer cleanup()
183190
var typeParams *ast.NodeList
184191
if len(d.TypeParameters) > 0 {
@@ -232,8 +239,7 @@ func (b *NodeBuilderImpl) pseudoTypeToNode(t *pseudochecker.PseudoType) *ast.Nod
232239
var cleanup func()
233240
if e.Kind != pseudochecker.PseudoObjectElementKindPropertyAssignment {
234241
signature := b.ch.getSignatureFromDeclaration(e.Signature())
235-
expandedParams := b.ch.getExpandedParameters(signature, true /*skipUnionExpanding*/)[0]
236-
cleanup = b.enterNewScope(e.Signature(), expandedParams, signature.typeParameters, signature.parameters, signature.mapper)
242+
_, cleanup = b.enterSignatureScope(signature)
237243
}
238244
var newProp *ast.Node
239245
switch e.Kind {

0 commit comments

Comments
 (0)