Skip to content

Commit 2b66515

Browse files
authored
Add recursion limiter for TypeToString (#3832)
1 parent 2b9bd9c commit 2b66515

10 files changed

Lines changed: 113 additions & 49 deletions

File tree

internal/checker/checker.go

Lines changed: 49 additions & 32 deletions
Large diffs are not rendered by default.

internal/checker/flow.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1570,7 +1570,7 @@ func (c *Checker) reportFlowControlError(node *ast.Node) {
15701570
block := ast.FindAncestor(node, ast.IsFunctionOrModuleBlock)
15711571
sourceFile := ast.GetSourceFileOfNode(node)
15721572
span := scanner.GetRangeOfTokenAtPosition(sourceFile, block.StatementList().Pos())
1573-
c.diagnostics.Add(ast.NewDiagnostic(sourceFile, span, diagnostics.The_containing_function_or_module_body_is_too_large_for_control_flow_analysis))
1573+
c.addDiagnostic(ast.NewDiagnostic(sourceFile, span, diagnostics.The_containing_function_or_module_body_is_too_large_for_control_flow_analysis))
15741574
}
15751575

15761576
func (c *Checker) isMatchingReference(source *ast.Node, target *ast.Node) bool {

internal/checker/grammarchecks.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func (c *Checker) grammarErrorOnFirstToken(node *ast.Node, message *diagnostics.
1919
sourceFile := ast.GetSourceFileOfNode(node)
2020
if !c.hasParseDiagnostics(sourceFile) {
2121
span := scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos())
22-
c.diagnostics.Add(ast.NewDiagnostic(sourceFile, span, message, args...))
22+
c.addDiagnostic(ast.NewDiagnostic(sourceFile, span, message, args...))
2323
return true
2424
}
2525
return false
@@ -28,7 +28,7 @@ func (c *Checker) grammarErrorOnFirstToken(node *ast.Node, message *diagnostics.
2828
func (c *Checker) grammarErrorAtPos(nodeForSourceFile *ast.Node, start int, length int, message *diagnostics.Message, args ...any) bool {
2929
sourceFile := ast.GetSourceFileOfNode(nodeForSourceFile)
3030
if !c.hasParseDiagnostics(sourceFile) {
31-
c.diagnostics.Add(ast.NewDiagnostic(sourceFile, core.NewTextRange(start, start+length), message, args...))
31+
c.addDiagnostic(ast.NewDiagnostic(sourceFile, core.NewTextRange(start, start+length), message, args...))
3232
return true
3333
}
3434
return false
@@ -48,7 +48,7 @@ func (c *Checker) grammarErrorOnNodeSkippedOnNoEmit(node *ast.Node, message *dia
4848
if !c.hasParseDiagnostics(sourceFile) {
4949
d := NewDiagnosticForNode(node, message, args...)
5050
d.SetSkippedOnNoEmit()
51-
c.diagnostics.Add(d)
51+
c.addDiagnostic(d)
5252
return true
5353
}
5454
return false
@@ -81,7 +81,7 @@ func (c *Checker) checkGrammarRegularExpressionLiteral(node *ast.RegularExpressi
8181
lastError.AddRelatedInfo(err)
8282
} else if lastError == nil || start != lastError.Pos() {
8383
lastError = ast.NewDiagnostic(sourceFile, core.NewTextRange(start, start+length), message, args...)
84-
c.diagnostics.Add(lastError)
84+
c.addDiagnostic(lastError)
8585
}
8686
})
8787
c.regExpScanner.SetText(sourceFile.Text())
@@ -1208,13 +1208,13 @@ func (c *Checker) checkGrammarForInOrForOfStatement(forInOrOfStatement *ast.ForI
12081208
if ast.IsInTopLevelContext(asNode) {
12091209
if !c.hasParseDiagnostics(sourceFile) {
12101210
if !ast.IsEffectiveExternalModule(sourceFile, c.compilerOptions) {
1211-
c.diagnostics.Add(createDiagnosticForNode(forInOrOfStatement.AwaitModifier, diagnostics.X_for_await_loops_are_only_allowed_at_the_top_level_of_a_file_when_that_file_is_a_module_but_this_file_has_no_imports_or_exports_Consider_adding_an_empty_export_to_make_this_file_a_module))
1211+
c.addDiagnostic(createDiagnosticForNode(forInOrOfStatement.AwaitModifier, diagnostics.X_for_await_loops_are_only_allowed_at_the_top_level_of_a_file_when_that_file_is_a_module_but_this_file_has_no_imports_or_exports_Consider_adding_an_empty_export_to_make_this_file_a_module))
12121212
}
12131213
switch c.moduleKind {
12141214
case core.ModuleKindNode16, core.ModuleKindNode18, core.ModuleKindNode20, core.ModuleKindNodeNext:
12151215
sourceFileMetaData := c.program.GetSourceFileMetaData(sourceFile.Path())
12161216
if sourceFileMetaData.ImpliedNodeFormat == core.ModuleKindCommonJS {
1217-
c.diagnostics.Add(createDiagnosticForNode(forInOrOfStatement.AwaitModifier, diagnostics.The_current_file_is_a_CommonJS_module_and_cannot_use_await_at_the_top_level))
1217+
c.addDiagnostic(createDiagnosticForNode(forInOrOfStatement.AwaitModifier, diagnostics.The_current_file_is_a_CommonJS_module_and_cannot_use_await_at_the_top_level))
12181218
break
12191219
}
12201220
fallthrough
@@ -1227,7 +1227,7 @@ func (c *Checker) checkGrammarForInOrForOfStatement(forInOrOfStatement *ast.ForI
12271227
}
12281228
fallthrough
12291229
default:
1230-
c.diagnostics.Add(createDiagnosticForNode(forInOrOfStatement.AwaitModifier, diagnostics.Top_level_for_await_loops_are_only_allowed_when_the_module_option_is_set_to_es2022_esnext_system_node16_node18_node20_nodenext_or_preserve_and_the_target_option_is_set_to_es2017_or_higher))
1230+
c.addDiagnostic(createDiagnosticForNode(forInOrOfStatement.AwaitModifier, diagnostics.Top_level_for_await_loops_are_only_allowed_when_the_module_option_is_set_to_es2022_esnext_system_node16_node18_node20_nodenext_or_preserve_and_the_target_option_is_set_to_es2017_or_higher))
12311231
}
12321232
}
12331233
} else {
@@ -1240,7 +1240,7 @@ func (c *Checker) checkGrammarForInOrForOfStatement(forInOrOfStatement *ast.ForI
12401240
relatedInfo := createDiagnosticForNode(containingFunc, diagnostics.Did_you_mean_to_mark_this_function_as_async)
12411241
diagnostic.AddRelatedInfo(relatedInfo)
12421242
}
1243-
c.diagnostics.Add(diagnostic)
1243+
c.addDiagnostic(diagnostic)
12441244
return true
12451245
}
12461246
}
@@ -1702,7 +1702,7 @@ func (c *Checker) checkGrammarAwaitOrAwaitUsing(node *ast.Node) bool {
17021702
message = diagnostics.X_await_using_statements_are_only_allowed_at_the_top_level_of_a_file_when_that_file_is_a_module_but_this_file_has_no_imports_or_exports_Consider_adding_an_empty_export_to_make_this_file_a_module
17031703
}
17041704
diagnostic := ast.NewDiagnostic(sourceFile, span, message)
1705-
c.diagnostics.Add(diagnostic)
1705+
c.addDiagnostic(diagnostic)
17061706
hasError = true
17071707
}
17081708
switch c.moduleKind {
@@ -1715,7 +1715,7 @@ func (c *Checker) checkGrammarAwaitOrAwaitUsing(node *ast.Node) bool {
17151715
if !spanCalculated {
17161716
span = scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos())
17171717
}
1718-
c.diagnostics.Add(ast.NewDiagnostic(sourceFile, span, diagnostics.The_current_file_is_a_CommonJS_module_and_cannot_use_await_at_the_top_level))
1718+
c.addDiagnostic(ast.NewDiagnostic(sourceFile, span, diagnostics.The_current_file_is_a_CommonJS_module_and_cannot_use_await_at_the_top_level))
17191719
hasError = true
17201720
break
17211721
}
@@ -1738,7 +1738,7 @@ func (c *Checker) checkGrammarAwaitOrAwaitUsing(node *ast.Node) bool {
17381738
} else {
17391739
message = diagnostics.Top_level_await_using_statements_are_only_allowed_when_the_module_option_is_set_to_es2022_esnext_system_node16_node18_node20_nodenext_or_preserve_and_the_target_option_is_set_to_es2017_or_higher
17401740
}
1741-
c.diagnostics.Add(ast.NewDiagnostic(sourceFile, span, message))
1741+
c.addDiagnostic(ast.NewDiagnostic(sourceFile, span, message))
17421742
hasError = true
17431743
}
17441744
}
@@ -1758,7 +1758,7 @@ func (c *Checker) checkGrammarAwaitOrAwaitUsing(node *ast.Node) bool {
17581758
relatedInfo := NewDiagnosticForNode(container, diagnostics.Did_you_mean_to_mark_this_function_as_async)
17591759
diagnostic.AddRelatedInfo(relatedInfo)
17601760
}
1761-
c.diagnostics.Add(diagnostic)
1761+
c.addDiagnostic(diagnostic)
17621762
hasError = true
17631763
}
17641764
}

internal/checker/jsx.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func (c *Checker) checkJsxOpeningLikeElementOrOpeningFragment(node *ast.Node) {
147147
}
148148
var diags []*ast.Diagnostic
149149
if !c.checkTypeRelatedToEx(tagType, elementTypeConstraint, c.assignableRelation, tagName, diagnostics.Its_type_0_is_not_a_valid_JSX_element_type, &diags) {
150-
c.diagnostics.Add(ast.NewDiagnosticChain(diags[0], diagnostics.X_0_cannot_be_used_as_a_JSX_component, scanner.GetTextOfNode(tagName)))
150+
c.addDiagnostic(ast.NewDiagnosticChain(diags[0], diagnostics.X_0_cannot_be_used_as_a_JSX_component, scanner.GetTextOfNode(tagName)))
151151
}
152152
} else {
153153
c.checkJsxReturnAssignableToAppropriateBound(c.getJsxReferenceKind(node), c.getReturnTypeOfSignature(sig), node)
@@ -189,7 +189,7 @@ func (c *Checker) checkJsxReturnAssignableToAppropriateBound(refKind JsxReferenc
189189
c.checkTypeRelatedToEx(elemInstanceType, combined, c.assignableRelation, openingLikeElement.TagName(), diagnostics.Its_element_type_0_is_not_a_valid_JSX_element, &diags)
190190
}
191191
if len(diags) != 0 {
192-
c.diagnostics.Add(ast.NewDiagnosticChain(diags[0], diagnostics.X_0_cannot_be_used_as_a_JSX_component, scanner.GetTextOfNode(openingLikeElement.TagName())))
192+
c.addDiagnostic(ast.NewDiagnosticChain(diags[0], diagnostics.X_0_cannot_be_used_as_a_JSX_component, scanner.GetTextOfNode(openingLikeElement.TagName())))
193193
}
194194
}
195195

@@ -554,7 +554,7 @@ func (c *Checker) resolveJsxOpeningLikeElement(node *ast.Node, candidatesOutArra
554554
sourceFile := ast.GetSourceFileOfNode(node)
555555
typeArgumentList := node.TypeArgumentList()
556556
loc := core.NewTextRange(scanner.SkipTrivia(sourceFile.Text(), typeArgumentList.Loc.Pos()), typeArgumentList.Loc.End())
557-
c.diagnostics.Add(ast.NewDiagnostic(sourceFile, loc, diagnostics.Expected_0_type_arguments_but_got_1, 0, len(typeArguments)))
557+
c.addDiagnostic(ast.NewDiagnostic(sourceFile, loc, diagnostics.Expected_0_type_arguments_but_got_1, 0, len(typeArguments)))
558558
}
559559
return fakeSignature
560560
}

internal/checker/printer.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ func (c *Checker) TypeToStringEx(t *Type, enclosingDeclaration *ast.Node, flags
187187
}
188188

189189
func (c *Checker) typeToStringEx(t *Type, enclosingDeclaration *ast.Node, flags TypeFormatFlags, vc *VerbosityContext) string {
190+
// Serialization of types can lead to (lazy) resolution of members, which can cause diagnostics that again require
191+
// serialization of types. This can potentially result in infinite recursion and stack overflows. To prevent that,
192+
// after a certain number of recursive invocations the function simply returns "?".
193+
if c.serializationLevel >= maxSerializationLevel {
194+
return "?"
195+
}
190196
newLine := ""
191197
if flags&TypeFormatFlagsMultilineObjectLiterals != 0 {
192198
newLine = "\n"
@@ -204,7 +210,9 @@ func (c *Checker) typeToStringEx(t *Type, enclosingDeclaration *ast.Node, flags
204210
defer func() {
205211
nodeBuilder.verbosity = oldVerbosity
206212
}()
213+
c.serializationLevel++
207214
typeNode := nodeBuilder.TypeToTypeNode(t, enclosingDeclaration, combinedFlags, nodebuilder.InternalFlagsNone, nil)
215+
c.serializationLevel--
208216
if typeNode == nil {
209217
panic("should always get typenode")
210218
}

internal/checker/relater.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ func (c *Checker) reportDiagnostic(diagnostic *ast.Diagnostic, diagnosticOutput
418418
if diagnosticOutput != nil {
419419
*diagnosticOutput = append(*diagnosticOutput, diagnostic)
420420
} else {
421-
c.diagnostics.Add(diagnostic)
421+
c.addDiagnostic(diagnostic)
422422
}
423423
}
424424
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
noTypeToStringRecursion.ts(3,7): error TS7023: 'f' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
2+
noTypeToStringRecursion.ts(3,20): error TS1360: Type 'number' does not satisfy the expected type '() => any'.
3+
4+
5+
==== noTypeToStringRecursion.ts (2 errors) ====
6+
// https://github.com/microsoft/typescript-go/issues/3805
7+
8+
const f = () => 42 satisfies typeof f;
9+
~
10+
!!! error TS7023: 'f' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
11+
~~~~~~~~~
12+
!!! error TS1360: Type 'number' does not satisfy the expected type '() => any'.
13+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//// [tests/cases/compiler/noTypeToStringRecursion.ts] ////
2+
3+
=== noTypeToStringRecursion.ts ===
4+
// https://github.com/microsoft/typescript-go/issues/3805
5+
6+
const f = () => 42 satisfies typeof f;
7+
>f : Symbol(f, Decl(noTypeToStringRecursion.ts, 2, 5))
8+
>f : Symbol(f, Decl(noTypeToStringRecursion.ts, 2, 5))
9+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//// [tests/cases/compiler/noTypeToStringRecursion.ts] ////
2+
3+
=== noTypeToStringRecursion.ts ===
4+
// https://github.com/microsoft/typescript-go/issues/3805
5+
6+
const f = () => 42 satisfies typeof f;
7+
>f : () => any
8+
>() => 42 satisfies typeof f : () => any
9+
>42 satisfies typeof f : 42
10+
>42 : 42
11+
>f : () => any
12+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// @noEmit: true
2+
3+
// https://github.com/microsoft/typescript-go/issues/3805
4+
5+
const f = () => 42 satisfies typeof f;

0 commit comments

Comments
 (0)