Skip to content

Commit

Permalink
fix #2675: parsing of infer inside extends
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Nov 15, 2022
1 parent c6e880a commit 9dee65c
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 2 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,20 @@

## Unreleased

* Fix parsing of TypeScript `infer` inside a conditional `extends` ([#2675](https://github.com/evanw/esbuild/issues/2675))

Unlike JavaScript, parsing TypeScript sometimes requires backtracking. The `infer A` type operator can take an optional constraint of the form `infer A extends B`. However, this syntax conflicts with the similar conditional type operator `A extends B ? C : D` in cases where the syntax is combined, such as `infer A extends B ? C : D`. This is supposed to be parsed as `(infer A) extends B ? C : D`. Previously esbuild incorrectly parsed this as `(infer A extends B) ? C : D` instead, which is a parse error since the `?:` conditional operator requires the `extends` keyword as part of the conditional type. TypeScript disambiguates by speculatively parsing the `extends` after the `infer`, but backtracking if a `?` token is encountered afterward. With this release, esbuild should now do the same thing, so esbuild should now correctly parse these types. Here's a real-world example of such a type:

```ts
type Normalized<T> = T extends Array<infer A extends object ? infer A : never>
? Dictionary<Normalized<A>>
: {
[P in keyof T]: T[P] extends Array<infer A extends object ? infer A : never>
? Dictionary<Normalized<A>>
: Normalized<T[P]>
}
```
* Avoid unnecessary watch mode rebuilds when debug logging is enabled ([#2661](https://github.com/evanw/esbuild/issues/2661))
When debug-level logs are enabled (such as with `--log-level=debug`), esbuild's path resolution subsystem generates debug log messages that say something like "Read 20 entries for directory /home/user" to help you debug what esbuild's path resolution is doing. This caused esbuild's watch mode subsystem to add a dependency on the full list of entries in that directory since if that changes, the generated log message would also have to be updated. However, meant that on systems where a parent directory undergoes constant directory entry churn, esbuild's watch mode would continue to rebuild if `--log-level=debug` was passed.
Expand Down
30 changes: 28 additions & 2 deletions internal/js_parser/ts_parser.go
Expand Up @@ -316,11 +316,11 @@ loop:

// "type Foo = Bar extends [infer T] ? T : null"
// "type Foo = Bar extends [infer T extends string] ? T : null"
// "type Foo = Bar extends [infer T extends string ? infer T : never] ? T : null"
if p.lexer.Token != js_lexer.TColon || (!opts.isIndexSignature && !opts.allowTupleLabels) {
p.lexer.Expect(js_lexer.TIdentifier)
if p.lexer.Token == js_lexer.TExtends {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LPrefix)
p.trySkipTypeScriptConstraintOfInferTypeWithBacktracking()
}
}
break loop
Expand Down Expand Up @@ -853,6 +853,32 @@ func (p *parser) trySkipTypeScriptArrowArgsWithBacktracking() bool {
return true
}

func (p *parser) trySkipTypeScriptConstraintOfInferTypeWithBacktracking() bool {
oldLexer := p.lexer
p.lexer.IsLogDisabled = true

// Implement backtracking by restoring the lexer's memory to its original state
defer func() {
r := recover()
if _, isLexerPanic := r.(js_lexer.LexerPanic); isLexerPanic {
p.lexer = oldLexer
} else if r != nil {
panic(r)
}
}()

p.lexer.Expect(js_lexer.TExtends)
p.skipTypeScriptType(js_ast.LPrefix)
if p.lexer.Token == js_lexer.TQuestion {
p.lexer.Unexpected()
}

// Restore the log disabled flag. Note that we can't just set it back to false
// because it may have been true to start with.
p.lexer.IsLogDisabled = oldLexer.IsLogDisabled
return true
}

// Returns true if the current less-than token is considered to be an arrow
// function under TypeScript's rules for files containing JSX syntax
func (p *parser) isTSArrowFnJSX() (isTSArrowFn bool) {
Expand Down
1 change: 1 addition & 0 deletions internal/js_parser/ts_parser_test.go
Expand Up @@ -180,6 +180,7 @@ func TestTSTypes(t *testing.T) {
expectPrintedTS(t, "type Foo = Bar extends [infer T] ? T : null", "")
expectPrintedTS(t, "type Foo = Bar extends [infer T extends string] ? T : null", "")
expectPrintedTS(t, "let x: A extends B<infer C extends D> ? D : never", "let x;\n")
expectPrintedTS(t, "let x: A extends B<infer C extends D ? infer C : never> ? D : never", "let x;\n")

expectPrintedTS(t, "let x: A.B<X.Y>", "let x;\n")
expectPrintedTS(t, "let x: A.B<X.Y>=2", "let x = 2;\n")
Expand Down

0 comments on commit 9dee65c

Please sign in to comment.