Skip to content
Closed
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
4 changes: 4 additions & 0 deletions tsc/internal/diagnostics/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,10 @@
"category": "Error",
"code": 1110
},
"Expression or type is too deeply nested. Simplify or split it into smaller parts.": {
"category": "Error",
"code": 1700
},
"Private field '{0}' must be declared in an enclosing class.": {
"category": "Error",
"code": 1111
Expand Down
4 changes: 4 additions & 0 deletions tsc/internal/diagnostics/diagnostics_generated.go

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

88 changes: 88 additions & 0 deletions tsc/internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ type Parser struct {
statementHasAwaitIdentifier bool
hasDeprecatedTag bool
hasParseError bool
nestingDepth int
nestingLimitHit bool

identifierCount int
notParenthesizedArrow collections.Set[int]
Expand All @@ -109,6 +111,61 @@ func newParser() *Parser {

var viableKeywordSuggestions = scanner.GetViableKeywordSuggestions()

// maxNestingDepth bounds recursive-descent nesting (expressions and types) so that
// pathologically nested input (e.g. hundreds of thousands of nested "(", "[", or "A<")
// is reported as a diagnostic instead of overflowing the goroutine stack with a fatal,
// unrecoverable runtime error. The limit is far above any human-authored or realistic
// machine-generated source, yet well below the depth that exhausts the stack.
const maxNestingDepth = 40000

// enterNesting increments the recursion-depth counter and reports whether the caller is
// still allowed to descend. Callers that receive false must not recurse further and
// should return a missing node so parsing can unwind cleanly. Every successful
// enterNesting (returning true) must be paired with a leaveNesting.
//
// The guard is applied at the recursive-descent entry points that can otherwise grow the
// stack without bound: parseType and parseTypeOperatorOrHigher for types, and
// parseAssignmentExpressionOrHigher, parseSimpleUnaryExpression and parsePrimaryExpression
// for expressions (covering e.g. "(((", "[[[", "!!!", "typeof typeof", "keyof keyof" and
// "new new" chains).
//
// The first time the limit is reached it records a single diagnostic, then fast-forwards
// the scanner to end-of-file via skipToEndOfFile. Positioning at EOF makes every enclosing
// construct terminate (isListTerminator and parseExpected both stop at EOF), so the parser
// unwinds and finishes in time linear in the nesting depth. This deliberately abandons the
// remainder of the file: it avoids pathological O(n^2) error recovery that would otherwise
// occur, for example, when a long chain of unterminated "A<" is re-scanned as a generic
// call in expression position.
//
// nestingDepth and nestingLimitHit are part of ParserState (see mark/rewind), so a limit
// reached inside a discarded speculative lookahead is rolled back together with the scanner
// and diagnostics; the committed parse then re-hits the limit and reports it exactly once.
// The parser is also fully reset in putParser, so neither field leaks across source files.
func (p *Parser) enterNesting() bool {
if p.nestingDepth >= maxNestingDepth {
if !p.nestingLimitHit {
p.parseErrorAtCurrentToken(diagnostics.Expression_or_type_is_too_deeply_nested_Simplify_or_split_it_into_smaller_parts)
p.nestingLimitHit = true
p.skipToEndOfFile()
}
Comment on lines +144 to +150
return false
}
p.nestingDepth++
return true
}

func (p *Parser) leaveNesting() {
p.nestingDepth--
}

// skipToEndOfFile advances the scanner to the end of the source and sets the current token
// to EOF. It is used only for error recovery after maxNestingDepth is exceeded, to unwind
// the recursive descent quickly instead of re-parsing the pathological remainder.
func (p *Parser) skipToEndOfFile() {
p.scanner.ResetPos(len(p.sourceText))
p.token = p.scanner.Scan()
}

// missingListNodes is a sentinel backing array used to distinguish "missing" node lists
// (where the expected opening token was not found) from ordinary empty node lists.
var missingListNodes = make([]*ast.Node, 0, 1)
Expand Down Expand Up @@ -347,6 +404,8 @@ type ParserState struct {
reparsedClonesLen int
statementHasAwaitIdentifier bool
hasParseError bool
nestingDepth int
nestingLimitHit bool
}

func (p *Parser) mark() ParserState {
Expand All @@ -359,6 +418,8 @@ func (p *Parser) mark() ParserState {
reparsedClonesLen: len(p.reparsedClones),
statementHasAwaitIdentifier: p.statementHasAwaitIdentifier,
hasParseError: p.hasParseError,
nestingDepth: p.nestingDepth,
nestingLimitHit: p.nestingLimitHit,
}
}

Expand All @@ -372,6 +433,8 @@ func (p *Parser) rewind(state ParserState) {
p.reparsedClones = p.reparsedClones[0:state.reparsedClonesLen]
p.statementHasAwaitIdentifier = state.statementHasAwaitIdentifier
p.hasParseError = state.hasParseError
p.nestingDepth = state.nestingDepth
p.nestingLimitHit = state.nestingLimitHit
}

func (p *Parser) lookAhead(callback func(p *Parser) bool) bool {
Expand Down Expand Up @@ -2657,6 +2720,10 @@ func (p *Parser) parseExportSpecifier() *ast.Node {
// TYPES

func (p *Parser) parseType() *ast.TypeNode {
if !p.enterNesting() {
return p.createMissingTypeNode()
}
defer p.leaveNesting()
Comment on lines +2723 to +2726
saveContextFlags := p.contextFlags
p.setContextFlags(ast.NodeFlagsTypeExcludesFlags, false)
var typeNode *ast.TypeNode
Expand Down Expand Up @@ -2723,6 +2790,10 @@ func (p *Parser) createUnionOrIntersectionTypeNode(operator ast.Kind, types *ast
}

func (p *Parser) parseTypeOperatorOrHigher() *ast.TypeNode {
if !p.enterNesting() {
return p.createMissingTypeNode()
}
defer p.leaveNesting()
operator := p.token
switch operator {
case ast.KindKeyOfKeyword, ast.KindUniqueKeyword, ast.KindReadonlyKeyword:
Expand Down Expand Up @@ -3030,6 +3101,11 @@ func (p *Parser) createMissingIdentifier() *ast.Node {
return p.finishNode(p.newIdentifier(""), p.nodePos())
}

func (p *Parser) createMissingTypeNode() *ast.TypeNode {
pos := p.nodePos()
return p.finishNode(p.factory.NewTypeReferenceNode(p.createMissingIdentifier(), nil), pos)
}

func (p *Parser) parsePrivateIdentifier() *ast.Node {
pos := p.nodePos()
text := p.scanner.TokenValue()
Expand Down Expand Up @@ -4128,6 +4204,10 @@ func (p *Parser) parseExpressionAllowIn() *ast.Expression {
}

func (p *Parser) parseAssignmentExpressionOrHigher() *ast.Expression {
if !p.enterNesting() {
return p.createMissingIdentifier()
}
defer p.leaveNesting()
return p.parseAssignmentExpressionOrHigherWorker(true /*allowReturnTypeInArrowFunction*/)
Comment on lines +4207 to 4211
}

Expand Down Expand Up @@ -5115,6 +5195,10 @@ func (p *Parser) parseJsxClosingFragment(inExpressionContext bool) *ast.Node {
}

func (p *Parser) parseSimpleUnaryExpression() *ast.Expression {
if !p.enterNesting() {
return p.createMissingIdentifier()
}
defer p.leaveNesting()
switch p.token {
case ast.KindPlusToken, ast.KindMinusToken, ast.KindTildeToken, ast.KindExclamationToken:
return p.parsePrefixUnaryExpression()
Expand Down Expand Up @@ -5604,6 +5688,10 @@ func (p *Parser) parseTemplateSpan(isTaggedTemplate bool) *ast.Node {
}

func (p *Parser) parsePrimaryExpression() *ast.Expression {
if !p.enterNesting() {
return p.createMissingIdentifier()
}
defer p.leaveNesting()
switch p.token {
case ast.KindNoSubstitutionTemplateLiteral:
if p.scanner.TokenFlags()&ast.TokenFlagsIsInvalid != 0 {
Expand Down
65 changes: 65 additions & 0 deletions tsc/internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,71 @@ func FuzzParser(f *testing.F) {
})
}

// TestDeeplyNestedInputDoesNotOverflow verifies that pathologically nested input is
// reported as a recoverable diagnostic (TS1700) instead of aborting the process with a
// fatal "stack overflow" runtime error. Reaching the end of ParseSourceFile at all is the
// core assertion: a stack overflow is a fatal, unrecoverable runtime error that would
// terminate the test binary rather than fail this test. See microsoft/TypeScript#64370.
func TestDeeplyNestedInputDoesNotOverflow(t *testing.T) {
t.Parallel()

// A depth comfortably beyond the parser's nesting cap so recovery is exercised, but
// small enough that the test stays fast.
const depth = 60000

tests := []struct {
name string
source string
}{
{name: "parenthesized expressions", source: strings.Repeat("(", depth)},
{name: "array literals", source: strings.Repeat("[", depth)},
{name: "type arguments", source: "type T = " + strings.Repeat("A<", depth)},
{name: "type operators", source: "type T = " + strings.Repeat("keyof ", depth) + "X"},
{name: "prefix unary operators", source: "const x = " + strings.Repeat("!", depth) + "y"},
{name: "typeof operators", source: "const x = " + strings.Repeat("typeof ", depth) + "y"},
{name: "new expressions", source: "const x = " + strings.Repeat("new ", depth) + "C"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
opts := ast.SourceFileParseOptions{
FileName: "/index.ts",
Path: "/index.ts",
}
file := parser.ParseSourceFile(opts, test.source, core.ScriptKindTS)

const tooDeeplyNestedCode = 1700
found := false
for _, d := range file.Diagnostics() {
if d.Code() == tooDeeplyNestedCode {
found = true
break
}
}
assert.Assert(t, found, "expected a 'too deeply nested' (TS1700) diagnostic")
})
}
}

// TestModeratelyNestedInputIsAccepted guards against false positives: nesting well under
// the cap must parse without emitting the "too deeply nested" diagnostic.
func TestModeratelyNestedInputIsAccepted(t *testing.T) {
t.Parallel()

const depth = 1000
sourceText := "const x = " + strings.Repeat("(", depth) + "1" + strings.Repeat(")", depth) + ";"
opts := ast.SourceFileParseOptions{
FileName: "/index.ts",
Path: "/index.ts",
}
file := parser.ParseSourceFile(opts, sourceText, core.ScriptKindTS)

for _, d := range file.Diagnostics() {
assert.Assert(t, d.Code() != 1700, "unexpected 'too deeply nested' diagnostic on valid input")
}
}

func TestHeritageClauseElementKinds(t *testing.T) {
t.Parallel()
sourceText := `
Expand Down