Files

2087 lines
63 KiB
Go

// This file contains code for parsing TypeScript syntax. The parser just skips
// over type expressions as if they are whitespace and doesn't bother generating
// an AST because nothing uses type information.
package js_parser
import (
"fmt"
"strings"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_lexer"
"github.com/evanw/esbuild/internal/logger"
)
func (p *parser) skipTypeScriptBinding() {
switch p.lexer.Token {
case js_lexer.TIdentifier, js_lexer.TThis:
p.lexer.Next()
case js_lexer.TOpenBracket:
p.lexer.Next()
// "[, , a]"
for p.lexer.Token == js_lexer.TComma {
p.lexer.Next()
}
// "[a, b]"
for p.lexer.Token != js_lexer.TCloseBracket {
// "[...a]"
if p.lexer.Token == js_lexer.TDotDotDot {
p.lexer.Next()
}
p.skipTypeScriptBinding()
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
p.lexer.Expect(js_lexer.TCloseBracket)
case js_lexer.TOpenBrace:
p.lexer.Next()
for p.lexer.Token != js_lexer.TCloseBrace {
foundIdentifier := false
switch p.lexer.Token {
case js_lexer.TDotDotDot:
p.lexer.Next()
if p.lexer.Token != js_lexer.TIdentifier {
p.lexer.Unexpected()
}
// "{...x}"
foundIdentifier = true
p.lexer.Next()
case js_lexer.TIdentifier:
// "{x}"
// "{x: y}"
foundIdentifier = true
p.lexer.Next()
// "{1: y}"
// "{'x': y}"
case js_lexer.TStringLiteral, js_lexer.TNumericLiteral:
p.lexer.Next()
default:
if p.lexer.IsIdentifierOrKeyword() {
// "{if: x}"
p.lexer.Next()
} else {
p.lexer.Unexpected()
}
}
if p.lexer.Token == js_lexer.TColon || !foundIdentifier {
p.lexer.Expect(js_lexer.TColon)
p.skipTypeScriptBinding()
}
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
p.lexer.Expect(js_lexer.TCloseBrace)
default:
p.lexer.Unexpected()
}
}
func (p *parser) skipTypeScriptFnArgs() {
p.lexer.Expect(js_lexer.TOpenParen)
for p.lexer.Token != js_lexer.TCloseParen {
// "(...a)"
if p.lexer.Token == js_lexer.TDotDotDot {
p.lexer.Next()
}
p.skipTypeScriptBinding()
// "(a?)"
if p.lexer.Token == js_lexer.TQuestion {
p.lexer.Next()
}
// "(a: any)"
if p.lexer.Token == js_lexer.TColon {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}
// "(a, b)"
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
p.lexer.Expect(js_lexer.TCloseParen)
}
// This is a spot where the TypeScript grammar is highly ambiguous. Here are
// some cases that are valid:
//
// let x = (y: any): (() => {}) => { };
// let x = (y: any): () => {} => { };
// let x = (y: any): (y) => {} => { };
// let x = (y: any): (y[]) => {};
// let x = (y: any): (a | b) => {};
//
// Here are some cases that aren't valid:
//
// let x = (y: any): (y) => {};
// let x = (y: any): (y) => {return 0};
// let x = (y: any): asserts y is (y) => {};
func (p *parser) skipTypeScriptParenOrFnType() {
if p.trySkipTypeScriptArrowArgsWithBacktracking() {
p.skipTypeScriptReturnType()
} else {
p.lexer.Expect(js_lexer.TOpenParen)
p.skipTypeScriptType(js_ast.LLowest)
p.lexer.Expect(js_lexer.TCloseParen)
}
}
func (p *parser) skipTypeScriptReturnType() {
p.skipTypeScriptTypeWithFlags(js_ast.LLowest, isReturnTypeFlag)
}
func (p *parser) skipTypeScriptType(level js_ast.L) {
p.skipTypeScriptTypeWithFlags(level, 0)
}
type skipTypeFlags uint8
const (
isReturnTypeFlag skipTypeFlags = 1 << iota
isIndexSignatureFlag
allowTupleLabelsFlag
disallowConditionalTypesFlag
)
func (flags skipTypeFlags) has(flag skipTypeFlags) bool {
return (flags & flag) != 0
}
type tsTypeIdentifierKind uint8
const (
tsTypeIdentifierNormal tsTypeIdentifierKind = iota
tsTypeIdentifierUnique
tsTypeIdentifierAbstract
tsTypeIdentifierAsserts
tsTypeIdentifierPrefix
tsTypeIdentifierPrimitive
tsTypeIdentifierInfer
)
// Use a map to improve lookup speed
var tsTypeIdentifierMap = map[string]tsTypeIdentifierKind{
"unique": tsTypeIdentifierUnique,
"abstract": tsTypeIdentifierAbstract,
"asserts": tsTypeIdentifierAsserts,
"keyof": tsTypeIdentifierPrefix,
"readonly": tsTypeIdentifierPrefix,
"any": tsTypeIdentifierPrimitive,
"never": tsTypeIdentifierPrimitive,
"unknown": tsTypeIdentifierPrimitive,
"undefined": tsTypeIdentifierPrimitive,
"object": tsTypeIdentifierPrimitive,
"number": tsTypeIdentifierPrimitive,
"string": tsTypeIdentifierPrimitive,
"boolean": tsTypeIdentifierPrimitive,
"bigint": tsTypeIdentifierPrimitive,
"symbol": tsTypeIdentifierPrimitive,
"infer": tsTypeIdentifierInfer,
}
func (p *parser) skipTypeScriptTypeWithFlags(level js_ast.L, flags skipTypeFlags) {
loop:
for {
switch p.lexer.Token {
case js_lexer.TNumericLiteral, js_lexer.TBigIntegerLiteral, js_lexer.TStringLiteral,
js_lexer.TNoSubstitutionTemplateLiteral, js_lexer.TTrue, js_lexer.TFalse,
js_lexer.TNull, js_lexer.TVoid:
p.lexer.Next()
case js_lexer.TConst:
r := p.lexer.Range()
p.lexer.Next()
// "[const: number]"
if flags.has(allowTupleLabelsFlag) && p.lexer.Token == js_lexer.TColon {
p.log.AddError(&p.tracker, r, "Unexpected \"const\"")
}
case js_lexer.TThis:
p.lexer.Next()
// "function check(): this is boolean"
if p.lexer.IsContextualKeyword("is") && !p.lexer.HasNewlineBefore {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
return
}
case js_lexer.TMinus:
// "-123"
// "-123n"
p.lexer.Next()
if p.lexer.Token == js_lexer.TBigIntegerLiteral {
p.lexer.Next()
} else {
p.lexer.Expect(js_lexer.TNumericLiteral)
}
case js_lexer.TAmpersand:
case js_lexer.TBar:
// Support things like "type Foo = | A | B" and "type Foo = & A & B"
p.lexer.Next()
continue
case js_lexer.TImport:
// "import('fs')"
p.lexer.Next()
// "[import: number]"
// "[import?: number]"
if flags.has(allowTupleLabelsFlag) && (p.lexer.Token == js_lexer.TColon || p.lexer.Token == js_lexer.TQuestion) {
return
}
p.lexer.Expect(js_lexer.TOpenParen)
p.lexer.Expect(js_lexer.TStringLiteral)
// "import('./foo.json', { assert: { type: 'json' } })"
if p.lexer.Token == js_lexer.TComma {
p.lexer.Next()
p.skipTypeScriptObjectType()
// "import('./foo.json', { assert: { type: 'json' } }, )"
if p.lexer.Token == js_lexer.TComma {
p.lexer.Next()
}
}
p.lexer.Expect(js_lexer.TCloseParen)
case js_lexer.TNew:
// "new () => Foo"
// "new <T>() => Foo<T>"
p.lexer.Next()
// "[new: number]"
// "[new?: number]"
if flags.has(allowTupleLabelsFlag) && (p.lexer.Token == js_lexer.TColon || p.lexer.Token == js_lexer.TQuestion) {
return
}
p.skipTypeScriptTypeParameters(allowConstModifier)
p.skipTypeScriptParenOrFnType()
case js_lexer.TLessThan:
// "<T>() => Foo<T>"
p.skipTypeScriptTypeParameters(allowConstModifier)
p.skipTypeScriptParenOrFnType()
case js_lexer.TOpenParen:
// "(number | string)"
p.skipTypeScriptParenOrFnType()
case js_lexer.TIdentifier:
kind := tsTypeIdentifierMap[p.lexer.Identifier.String]
checkTypeParameters := true
switch kind {
case tsTypeIdentifierPrefix:
p.lexer.Next()
// Valid:
// "[keyof: string]"
// "[keyof?: string]"
// "{[keyof: string]: number}"
// "{[keyof in string]: number}"
//
// Invalid:
// "A extends B ? keyof : string"
//
if (p.lexer.Token != js_lexer.TColon && p.lexer.Token != js_lexer.TQuestion && p.lexer.Token != js_lexer.TIn) ||
(!flags.has(isIndexSignatureFlag) && !flags.has(allowTupleLabelsFlag)) {
p.skipTypeScriptType(js_ast.LPrefix)
}
break loop
case tsTypeIdentifierInfer:
p.lexer.Next()
// "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"
// "type Foo = { [infer in Bar]: number }"
// "type Foo = [infer: number]"
// "type Foo = [infer?: number]"
if (p.lexer.Token != js_lexer.TColon && p.lexer.Token != js_lexer.TQuestion && p.lexer.Token != js_lexer.TIn) ||
(!flags.has(isIndexSignatureFlag) && !flags.has(allowTupleLabelsFlag)) {
p.lexer.Expect(js_lexer.TIdentifier)
if p.lexer.Token == js_lexer.TExtends {
p.trySkipTypeScriptConstraintOfInferTypeWithBacktracking(flags)
}
}
break loop
case tsTypeIdentifierUnique:
p.lexer.Next()
// "let foo: unique symbol"
if p.lexer.IsContextualKeyword("symbol") {
p.lexer.Next()
break loop
}
case tsTypeIdentifierAbstract:
p.lexer.Next()
// "let foo: abstract new () => {}" added in TypeScript 4.2
if p.lexer.Token == js_lexer.TNew {
continue
}
case tsTypeIdentifierAsserts:
p.lexer.Next()
// "function assert(x: boolean): asserts x"
// "function assert(x: boolean): asserts x is boolean"
if flags.has(isReturnTypeFlag) && !p.lexer.HasNewlineBefore && (p.lexer.Token == js_lexer.TIdentifier || p.lexer.Token == js_lexer.TThis) {
p.lexer.Next()
}
case tsTypeIdentifierPrimitive:
p.lexer.Next()
checkTypeParameters = false
default:
p.lexer.Next()
}
// "function assert(x: any): x is boolean"
if p.lexer.IsContextualKeyword("is") && !p.lexer.HasNewlineBefore {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
return
}
// "let foo: any \n <number>foo" must not become a single type
if checkTypeParameters && !p.lexer.HasNewlineBefore {
p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{})
}
case js_lexer.TTypeof:
p.lexer.Next()
// "[typeof: number]"
// "[typeof?: number]"
if flags.has(allowTupleLabelsFlag) && (p.lexer.Token == js_lexer.TColon || p.lexer.Token == js_lexer.TQuestion) {
return
}
if p.lexer.Token == js_lexer.TImport {
// "typeof import('fs')"
continue
} else {
// "typeof x"
if !p.lexer.IsIdentifierOrKeyword() {
p.lexer.Expected(js_lexer.TIdentifier)
}
p.lexer.Next()
// "typeof x.y"
// "typeof x.#y"
for p.lexer.Token == js_lexer.TDot {
p.lexer.Next()
if !p.lexer.IsIdentifierOrKeyword() && p.lexer.Token != js_lexer.TPrivateIdentifier {
p.lexer.Expected(js_lexer.TIdentifier)
}
p.lexer.Next()
}
if !p.lexer.HasNewlineBefore {
p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{})
}
}
case js_lexer.TOpenBracket:
// "[number, string]"
// "[first: number, second: string]"
p.lexer.Next()
for p.lexer.Token != js_lexer.TCloseBracket {
if p.lexer.Token == js_lexer.TDotDotDot {
p.lexer.Next()
}
p.skipTypeScriptTypeWithFlags(js_ast.LLowest, allowTupleLabelsFlag)
if p.lexer.Token == js_lexer.TQuestion {
p.lexer.Next()
}
if p.lexer.Token == js_lexer.TColon {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
p.lexer.Expect(js_lexer.TCloseBracket)
case js_lexer.TOpenBrace:
p.skipTypeScriptObjectType()
case js_lexer.TTemplateHead:
// "`${'a' | 'b'}-${'c' | 'd'}`"
for {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
p.lexer.RescanCloseBraceAsTemplateToken()
if p.lexer.Token == js_lexer.TTemplateTail {
p.lexer.Next()
break
}
}
default:
// "[function: number]"
// "[function?: number]"
if flags.has(allowTupleLabelsFlag) && p.lexer.IsIdentifierOrKeyword() {
if p.lexer.Token != js_lexer.TFunction {
p.log.AddError(&p.tracker, p.lexer.Range(), fmt.Sprintf("Unexpected %q", p.lexer.Raw()))
}
p.lexer.Next()
if p.lexer.Token != js_lexer.TColon && p.lexer.Token != js_lexer.TQuestion {
p.lexer.Expect(js_lexer.TColon)
}
return
}
p.lexer.Unexpected()
}
break
}
for {
switch p.lexer.Token {
case js_lexer.TBar:
if level >= js_ast.LBitwiseOr {
return
}
p.lexer.Next()
p.skipTypeScriptTypeWithFlags(js_ast.LBitwiseOr, flags)
case js_lexer.TAmpersand:
if level >= js_ast.LBitwiseAnd {
return
}
p.lexer.Next()
p.skipTypeScriptTypeWithFlags(js_ast.LBitwiseAnd, flags)
case js_lexer.TExclamation:
// A postfix "!" is allowed in JSDoc types in TypeScript, which are only
// present in comments. While it's not valid in a non-comment position,
// it's still parsed and turned into a soft error by the TypeScript
// compiler. It turns out parsing this is important for correctness for
// "as" casts because the "!" token must still be consumed.
if p.lexer.HasNewlineBefore {
return
}
p.lexer.Next()
case js_lexer.TDot:
p.lexer.Next()
if !p.lexer.IsIdentifierOrKeyword() {
p.lexer.Expect(js_lexer.TIdentifier)
}
p.lexer.Next()
// "{ <A extends B>(): c.d \n <E extends F>(): g.h }" must not become a single type
if !p.lexer.HasNewlineBefore {
p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{})
}
case js_lexer.TOpenBracket:
// "{ ['x']: string \n ['y']: string }" must not become a single type
if p.lexer.HasNewlineBefore {
return
}
p.lexer.Next()
if p.lexer.Token != js_lexer.TCloseBracket {
p.skipTypeScriptType(js_ast.LLowest)
}
p.lexer.Expect(js_lexer.TCloseBracket)
case js_lexer.TExtends:
// "{ x: number \n extends: boolean }" must not become a single type
if p.lexer.HasNewlineBefore || flags.has(disallowConditionalTypesFlag) {
return
}
p.lexer.Next()
// The type following "extends" is not permitted to be another conditional type
p.skipTypeScriptTypeWithFlags(js_ast.LLowest, disallowConditionalTypesFlag)
p.lexer.Expect(js_lexer.TQuestion)
p.skipTypeScriptType(js_ast.LLowest)
p.lexer.Expect(js_lexer.TColon)
p.skipTypeScriptType(js_ast.LLowest)
default:
return
}
}
}
func (p *parser) skipTypeScriptObjectType() {
p.lexer.Expect(js_lexer.TOpenBrace)
for p.lexer.Token != js_lexer.TCloseBrace {
// "{ -readonly [K in keyof T]: T[K] }"
// "{ +readonly [K in keyof T]: T[K] }"
if p.lexer.Token == js_lexer.TPlus || p.lexer.Token == js_lexer.TMinus {
p.lexer.Next()
}
// Skip over modifiers and the property identifier
foundKey := false
for p.lexer.IsIdentifierOrKeyword() ||
p.lexer.Token == js_lexer.TStringLiteral ||
p.lexer.Token == js_lexer.TNumericLiteral {
p.lexer.Next()
foundKey = true
}
if p.lexer.Token == js_lexer.TOpenBracket {
// Index signature or computed property
p.lexer.Next()
p.skipTypeScriptTypeWithFlags(js_ast.LLowest, isIndexSignatureFlag)
// "{ [key: string]: number }"
// "{ readonly [K in keyof T]: T[K] }"
if p.lexer.Token == js_lexer.TColon {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
} else if p.lexer.Token == js_lexer.TIn {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
if p.lexer.IsContextualKeyword("as") {
// "{ [K in keyof T as `get-${K}`]: T[K] }"
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}
}
p.lexer.Expect(js_lexer.TCloseBracket)
// "{ [K in keyof T]+?: T[K] }"
// "{ [K in keyof T]-?: T[K] }"
if p.lexer.Token == js_lexer.TPlus || p.lexer.Token == js_lexer.TMinus {
p.lexer.Next()
}
foundKey = true
}
// "?" indicates an optional property
// "!" indicates an initialization assertion
if foundKey && (p.lexer.Token == js_lexer.TQuestion || p.lexer.Token == js_lexer.TExclamation) {
p.lexer.Next()
}
// Type parameters come right after the optional mark
p.skipTypeScriptTypeParameters(allowConstModifier)
switch p.lexer.Token {
case js_lexer.TColon:
// Regular property
if !foundKey {
p.lexer.Expect(js_lexer.TIdentifier)
}
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
case js_lexer.TOpenParen:
// Method signature
p.skipTypeScriptFnArgs()
if p.lexer.Token == js_lexer.TColon {
p.lexer.Next()
p.skipTypeScriptReturnType()
}
default:
if !foundKey {
p.lexer.Unexpected()
}
}
switch p.lexer.Token {
case js_lexer.TCloseBrace:
case js_lexer.TComma, js_lexer.TSemicolon:
p.lexer.Next()
default:
if !p.lexer.HasNewlineBefore {
p.lexer.Unexpected()
}
}
}
p.lexer.Expect(js_lexer.TCloseBrace)
}
type typeParameterFlags uint8
const (
// TypeScript 4.7
allowInOutVarianceAnnotations typeParameterFlags = 1 << iota
// TypeScript 5.0
allowConstModifier
// Allow "<>" without any type parameters
allowEmptyTypeParameters
)
type skipTypeScriptTypeParametersResult uint8
const (
didNotSkipAnything skipTypeScriptTypeParametersResult = iota
couldBeTypeCast
definitelyTypeParameters
)
// This is the type parameter declarations that go with other symbol
// declarations (class, function, type, etc.)
func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags) skipTypeScriptTypeParametersResult {
if p.lexer.Token != js_lexer.TLessThan {
return didNotSkipAnything
}
p.lexer.Next()
result := couldBeTypeCast
if (flags&allowEmptyTypeParameters) != 0 && p.lexer.Token == js_lexer.TGreaterThan {
p.lexer.Next()
return definitelyTypeParameters
}
for {
hasIn := false
hasOut := false
expectIdentifier := true
invalidModifierRange := logger.Range{}
// Scan over a sequence of "in" and "out" modifiers (a.k.a. optional
// variance annotations) as well as "const" modifiers
for {
if p.lexer.Token == js_lexer.TConst {
if invalidModifierRange.Len == 0 && (flags&allowConstModifier) == 0 {
// Valid:
// "class Foo<const T> {}"
// Invalid:
// "interface Foo<const T> {}"
invalidModifierRange = p.lexer.Range()
}
result = definitelyTypeParameters
p.lexer.Next()
expectIdentifier = true
continue
}
if p.lexer.Token == js_lexer.TIn {
if invalidModifierRange.Len == 0 && ((flags&allowInOutVarianceAnnotations) == 0 || hasIn || hasOut) {
// Valid:
// "type Foo<in T> = T"
// Invalid:
// "type Foo<in in T> = T"
// "type Foo<out in T> = T"
invalidModifierRange = p.lexer.Range()
}
p.lexer.Next()
hasIn = true
expectIdentifier = true
continue
}
if p.lexer.IsContextualKeyword("out") {
r := p.lexer.Range()
if invalidModifierRange.Len == 0 && (flags&allowInOutVarianceAnnotations) == 0 {
invalidModifierRange = r
}
p.lexer.Next()
if invalidModifierRange.Len == 0 && hasOut && (p.lexer.Token == js_lexer.TIn || p.lexer.Token == js_lexer.TIdentifier) {
// Valid:
// "type Foo<out T> = T"
// "type Foo<out out> = T"
// "type Foo<out out, T> = T"
// "type Foo<out out = T> = T"
// "type Foo<out out extends T> = T"
// Invalid:
// "type Foo<out out in T> = T"
// "type Foo<out out T> = T"
invalidModifierRange = r
}
hasOut = true
expectIdentifier = false
continue
}
break
}
// Only report an error for the first invalid modifier
if invalidModifierRange.Len > 0 {
p.log.AddError(&p.tracker, invalidModifierRange, fmt.Sprintf(
"The modifier %q is not valid here:", p.source.TextForRange(invalidModifierRange)))
}
// expectIdentifier => Mandatory identifier (e.g. after "type Foo <in ___")
// !expectIdentifier => Optional identifier (e.g. after "type Foo <out ___" since "out" may be the identifier)
if expectIdentifier || p.lexer.Token == js_lexer.TIdentifier {
p.lexer.Expect(js_lexer.TIdentifier)
}
// "class Foo<T extends number> {}"
if p.lexer.Token == js_lexer.TExtends {
result = definitelyTypeParameters
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}
// "class Foo<T = void> {}"
if p.lexer.Token == js_lexer.TEquals {
result = definitelyTypeParameters
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
if p.lexer.Token == js_lexer.TGreaterThan {
result = definitelyTypeParameters
break
}
}
p.lexer.ExpectGreaterThan(false /* isInsideJSXElement */)
return result
}
type skipTypeScriptTypeArgumentsOpts struct {
isInsideJSXElement bool
isParseTypeArgumentsInExpression bool
}
func (p *parser) skipTypeScriptTypeArguments(opts skipTypeScriptTypeArgumentsOpts) bool {
switch p.lexer.Token {
case js_lexer.TLessThan, js_lexer.TLessThanEquals,
js_lexer.TLessThanLessThan, js_lexer.TLessThanLessThanEquals:
default:
return false
}
p.lexer.ExpectLessThan(false /* isInsideJSXElement */)
for {
p.skipTypeScriptType(js_ast.LLowest)
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
// This type argument list must end with a ">"
if !opts.isParseTypeArgumentsInExpression {
// Normally TypeScript allows any token starting with ">". For example,
// "Array<Array<number>>()" is a type argument list even though there's a
// ">>" token, because ">>" starts with ">".
p.lexer.ExpectGreaterThan(opts.isInsideJSXElement)
} else {
// However, if we're emulating the TypeScript compiler's function called
// "parseTypeArgumentsInExpression" function, then we must only allow the
// ">" token itself. For example, "x < y >= z" is not a type argument list.
//
// This doesn't detect ">>" in "Array<Array<number>>()" because the inner
// type argument list isn't a call to "parseTypeArgumentsInExpression"
// because it's within a type context, not an expression context. So the
// token that we see here is ">" in that case because the first ">" has
// already been stripped off of the ">>" by the inner call.
if opts.isInsideJSXElement {
p.lexer.ExpectInsideJSXElement(js_lexer.TGreaterThan)
} else {
p.lexer.Expect(js_lexer.TGreaterThan)
}
}
return true
}
func (p *parser) trySkipTypeArgumentsInExpressionWithBacktracking() 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)
}
}()
if p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{isParseTypeArgumentsInExpression: true}) {
// Check the token after the type argument list and backtrack if it's invalid
if !p.tsCanFollowTypeArgumentsInExpression() {
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
}
func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() skipTypeScriptTypeParametersResult {
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)
}
}()
result := p.skipTypeScriptTypeParameters(allowConstModifier)
if p.lexer.Token != js_lexer.TOpenParen {
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 result
}
func (p *parser) trySkipTypeScriptArrowReturnTypeWithBacktracking() 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.TColon)
p.skipTypeScriptReturnType()
// Check the token after this and backtrack if it's the wrong one
if p.lexer.Token != js_lexer.TEqualsGreaterThan {
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
}
// This is a very specific function that determines whether a colon token is a
// TypeScript arrow function return type in the case where the arrow function
// is the middle expression of a JavaScript ternary operator (i.e. is between
// the "?" and ":" tokens). It's separate from the other function above called
// "trySkipTypeScriptArrowReturnTypeWithBacktracking" because it's much more
// expensive, and likely not as robust.
func (originalParser *parser) isTypeScriptArrowReturnTypeAfterQuestionAndBeforeColon(await awaitOrYield) bool {
// Implement "backtracking" by swallowing lexer errors on a temporary parser
defer func() {
r := recover()
if _, isLexerPanic := r.(js_lexer.LexerPanic); isLexerPanic {
return // Swallow this error
} else if r != nil {
panic(r)
}
}()
// THIS IS A GROSS HACK. Some context:
//
// JavaScript is designed to not require a backtracking parser. Generally a
// backtracking parser is not regarded as a good thing and you try to avoid
// having one if it's not necessary.
//
// However, TypeScript's parser does do backtracking in an (admittedly noble)
// effort to retrofit nice type syntax onto JavaScript. Up until this edge
// case was discovered, this backtracking was limited to type syntax so
// esbuild could deal with it by using a backtracking lexer without needing a
// backtracking parser.
//
// This edge case requires a backtracking parser. The TypeScript compiler's
// algorithm for parsing this is to try to parse the entire arrow function
// body and then reset all the way back to the colon for the arrow function
// return type if the token following the arrow function body is not another
// colon. For example:
//
// x = a ? (b) : c => d;
// y = a ? (b) : c => d : e;
//
// The first colon of "x" pairs with the "?" because the arrow function
// "(b) : c => d" is not followed by a colon. However, the first colon of "y"
// starts a return type because the arrow function "(b) : c => d" is followed
// by a colon. In other words, the first ":" before the arrow function body
// must pair with the "?" unless there is another ":" to pair with it after
// the function body.
//
// I'm not going to rewrite esbuild's parser to support backtracking for this
// one edge case. So instead, esbuild tries to parse the arrow function body
// using a rough copy of the parser and then always throws the result away.
// So arrow function bodies will always be parsed twice for this edge case.
//
// This is a hack instead of a good solution because the parser isn't designed
// for this, and doing this is not going to have good test coverage given that
// it's an edge case. We can't prevent parser code (either currently or in the
// future) from accidentally depending on some parser state that isn't cloned
// here. That could result in a parser panic when parsing a more complex
// version of this edge case.
p := newParser(logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, nil), originalParser.source, originalParser.lexer, &originalParser.options)
// Clone all state that the parser needs to parse this arrow function body
p.allowIn = originalParser.allowIn
p.lexer.IsLogDisabled = true
p.pushScopeForParsePass(js_ast.ScopeEntry, logger.Loc{Start: 0})
p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, logger.Loc{Start: 1})
// Parse the return type
p.lexer.Expect(js_lexer.TColon)
p.skipTypeScriptReturnType()
// Parse the body and throw it out (with the side effect of maybe throwing an error)
_ = p.parseArrowBody([]js_ast.Arg{}, fnOrArrowDataParse{await: await})
// There must be a colon following the arrow function body to pair with the leading "?"
p.lexer.Expect(js_lexer.TColon)
// Parsing was successful if we get here
return true
}
func (p *parser) trySkipTypeScriptArrowArgsWithBacktracking() 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.skipTypeScriptFnArgs()
p.lexer.Expect(js_lexer.TEqualsGreaterThan)
// 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
}
func (p *parser) trySkipTypeScriptConstraintOfInferTypeWithBacktracking(flags skipTypeFlags) 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.skipTypeScriptTypeWithFlags(js_ast.LPrefix, disallowConditionalTypesFlag)
if !flags.has(disallowConditionalTypesFlag) && 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) {
oldLexer := p.lexer
p.lexer.Next()
// Look ahead to see if this should be an arrow function instead
if p.lexer.Token == js_lexer.TConst {
p.lexer.Next()
}
if p.lexer.Token == js_lexer.TIdentifier {
p.lexer.Next()
if p.lexer.Token == js_lexer.TComma || p.lexer.Token == js_lexer.TEquals {
isTSArrowFn = true
} else if p.lexer.Token == js_lexer.TExtends {
p.lexer.Next()
isTSArrowFn = p.lexer.Token != js_lexer.TEquals && p.lexer.Token != js_lexer.TGreaterThan && p.lexer.Token != js_lexer.TSlash
}
}
// Restore the lexer
p.lexer = oldLexer
return
}
// This function is taken from the official TypeScript compiler source code:
// https://github.com/microsoft/TypeScript/blob/master/src/compiler/parser.ts
//
// This function is pretty inefficient as written, and could be collapsed into
// a single switch statement. But that would make it harder to keep this in
// sync with the TypeScript compiler's source code, so we keep doing it the
// slow way.
func (p *parser) tsCanFollowTypeArgumentsInExpression() bool {
switch p.lexer.Token {
case
// These tokens can follow a type argument list in a call expression.
js_lexer.TOpenParen, // foo<x>(
js_lexer.TNoSubstitutionTemplateLiteral, // foo<T> `...`
js_lexer.TTemplateHead: // foo<T> `...${100}...`
return true
// A type argument list followed by `<` never makes sense, and a type argument list followed
// by `>` is ambiguous with a (re-scanned) `>>` operator, so we disqualify both. Also, in
// this context, `+` and `-` are unary operators, not binary operators.
case js_lexer.TLessThan,
js_lexer.TGreaterThan,
js_lexer.TPlus,
js_lexer.TMinus,
// TypeScript always sees "TGreaterThan" instead of these tokens since
// their scanner works a little differently than our lexer. So since
// "TGreaterThan" is forbidden above, we also forbid these too.
js_lexer.TGreaterThanEquals,
js_lexer.TGreaterThanGreaterThan,
js_lexer.TGreaterThanGreaterThanEquals,
js_lexer.TGreaterThanGreaterThanGreaterThan,
js_lexer.TGreaterThanGreaterThanGreaterThanEquals:
return false
}
// We favor the type argument list interpretation when it is immediately followed by
// a line break, a binary operator, or something that can't start an expression.
return p.lexer.HasNewlineBefore || p.tsIsBinaryOperator() || !p.tsIsStartOfExpression()
}
// This function is taken from the official TypeScript compiler source code:
// https://github.com/microsoft/TypeScript/blob/master/src/compiler/parser.ts
func (p *parser) tsIsBinaryOperator() bool {
switch p.lexer.Token {
case js_lexer.TIn:
return p.allowIn
case
js_lexer.TQuestionQuestion,
js_lexer.TBarBar,
js_lexer.TAmpersandAmpersand,
js_lexer.TBar,
js_lexer.TCaret,
js_lexer.TAmpersand,
js_lexer.TEqualsEquals,
js_lexer.TExclamationEquals,
js_lexer.TEqualsEqualsEquals,
js_lexer.TExclamationEqualsEquals,
js_lexer.TLessThan,
js_lexer.TGreaterThan,
js_lexer.TLessThanEquals,
js_lexer.TGreaterThanEquals,
js_lexer.TInstanceof,
js_lexer.TLessThanLessThan,
js_lexer.TGreaterThanGreaterThan,
js_lexer.TGreaterThanGreaterThanGreaterThan,
js_lexer.TPlus,
js_lexer.TMinus,
js_lexer.TAsterisk,
js_lexer.TSlash,
js_lexer.TPercent,
js_lexer.TAsteriskAsterisk:
return true
case js_lexer.TIdentifier:
if p.lexer.IsContextualKeyword("as") || p.lexer.IsContextualKeyword("satisfies") {
return true
}
}
return false
}
// This function is taken from the official TypeScript compiler source code:
// https://github.com/microsoft/TypeScript/blob/master/src/compiler/parser.ts
func (p *parser) tsIsStartOfExpression() bool {
if p.tsIsStartOfLeftHandSideExpression() {
return true
}
switch p.lexer.Token {
case
js_lexer.TPlus,
js_lexer.TMinus,
js_lexer.TTilde,
js_lexer.TExclamation,
js_lexer.TDelete,
js_lexer.TTypeof,
js_lexer.TVoid,
js_lexer.TPlusPlus,
js_lexer.TMinusMinus,
js_lexer.TLessThan,
js_lexer.TPrivateIdentifier,
js_lexer.TAt:
return true
default:
if p.lexer.Token == js_lexer.TIdentifier && (p.lexer.Identifier.String == "await" || p.lexer.Identifier.String == "yield") {
// Yield/await always starts an expression. Either it is an identifier (in which case
// it is definitely an expression). Or it's a keyword (either because we're in
// a generator or async function, or in strict mode (or both)) and it started a yield or await expression.
return true
}
// Error tolerance. If we see the start of some binary operator, we consider
// that the start of an expression. That way we'll parse out a missing identifier,
// give a good message about an identifier being missing, and then consume the
// rest of the binary expression.
if p.tsIsBinaryOperator() {
return true
}
return p.tsIsIdentifier()
}
}
// This function is taken from the official TypeScript compiler source code:
// https://github.com/microsoft/TypeScript/blob/master/src/compiler/parser.ts
func (p *parser) tsIsStartOfLeftHandSideExpression() bool {
switch p.lexer.Token {
case
js_lexer.TThis,
js_lexer.TSuper,
js_lexer.TNull,
js_lexer.TTrue,
js_lexer.TFalse,
js_lexer.TNumericLiteral,
js_lexer.TBigIntegerLiteral,
js_lexer.TStringLiteral,
js_lexer.TNoSubstitutionTemplateLiteral,
js_lexer.TTemplateHead,
js_lexer.TOpenParen,
js_lexer.TOpenBracket,
js_lexer.TOpenBrace,
js_lexer.TFunction,
js_lexer.TClass,
js_lexer.TNew,
js_lexer.TSlash,
js_lexer.TSlashEquals,
js_lexer.TIdentifier:
return true
case js_lexer.TImport:
return p.tsLookAheadNextTokenIsOpenParenOrLessThanOrDot()
default:
return p.tsIsIdentifier()
}
}
// This function is taken from the official TypeScript compiler source code:
// https://github.com/microsoft/TypeScript/blob/master/src/compiler/parser.ts
func (p *parser) tsLookAheadNextTokenIsOpenParenOrLessThanOrDot() (result bool) {
oldLexer := p.lexer
p.lexer.Next()
result = p.lexer.Token == js_lexer.TOpenParen ||
p.lexer.Token == js_lexer.TLessThan ||
p.lexer.Token == js_lexer.TDot
// Restore the lexer
p.lexer = oldLexer
return
}
// This function is taken from the official TypeScript compiler source code:
// https://github.com/microsoft/TypeScript/blob/master/src/compiler/parser.ts
func (p *parser) tsIsIdentifier() bool {
if p.lexer.Token == js_lexer.TIdentifier {
// If we have a 'yield' keyword, and we're in the [yield] context, then 'yield' is
// considered a keyword and is not an identifier.
if p.fnOrArrowDataParse.yield != allowIdent && p.lexer.Identifier.String == "yield" {
return false
}
// If we have a 'await' keyword, and we're in the [Await] context, then 'await' is
// considered a keyword and is not an identifier.
if p.fnOrArrowDataParse.await != allowIdent && p.lexer.Identifier.String == "await" {
return false
}
return true
}
return false
}
func (p *parser) skipTypeScriptInterfaceStmt(opts parseStmtOpts) {
name := p.lexer.Identifier.String
p.lexer.Expect(js_lexer.TIdentifier)
if opts.isModuleScope {
p.localTypeNames[name] = true
}
p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowEmptyTypeParameters)
if p.lexer.Token == js_lexer.TExtends {
p.lexer.Next()
for {
p.skipTypeScriptType(js_ast.LLowest)
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
}
if p.lexer.IsContextualKeyword("implements") {
p.lexer.Next()
for {
p.skipTypeScriptType(js_ast.LLowest)
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
}
p.skipTypeScriptObjectType()
}
func (p *parser) skipTypeScriptTypeStmt(opts parseStmtOpts) {
if opts.isExport {
switch p.lexer.Token {
case js_lexer.TOpenBrace:
// "export type {foo}"
// "export type {foo} from 'bar'"
p.parseExportClause()
if p.lexer.IsContextualKeyword("from") {
p.lexer.Next()
p.parsePath()
}
p.lexer.ExpectOrInsertSemicolon()
return
// This is invalid TypeScript, and is rejected by the TypeScript compiler:
//
// example.ts:1:1 - error TS1383: Only named exports may use 'export type'.
//
// 1 export type * from './types'
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// However, people may not know this and then blame esbuild for it not
// working. So we parse it anyway and then discard it (since we always
// discard all types). People who do this should be running the TypeScript
// type checker when using TypeScript, which will then report this error.
case js_lexer.TAsterisk:
// "export type * from 'path'"
p.lexer.Next()
if p.lexer.IsContextualKeyword("as") {
// "export type * as ns from 'path'"
p.lexer.Next()
p.parseClauseAlias("export")
p.lexer.Next()
}
p.lexer.ExpectContextualKeyword("from")
p.parsePath()
p.lexer.ExpectOrInsertSemicolon()
return
}
}
name := p.lexer.Identifier.String
p.lexer.Expect(js_lexer.TIdentifier)
if opts.isModuleScope {
p.localTypeNames[name] = true
}
p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowEmptyTypeParameters)
p.lexer.Expect(js_lexer.TEquals)
p.skipTypeScriptType(js_ast.LLowest)
p.lexer.ExpectOrInsertSemicolon()
}
func (p *parser) parseTypeScriptEnumStmt(loc logger.Loc, opts parseStmtOpts) js_ast.Stmt {
p.lexer.Expect(js_lexer.TEnum)
nameLoc := p.lexer.Loc()
nameText := p.lexer.Identifier.String
p.lexer.Expect(js_lexer.TIdentifier)
name := ast.LocRef{Loc: nameLoc, Ref: ast.InvalidRef}
// Generate the namespace object
exportedMembers := p.getOrCreateExportedNamespaceMembers(nameText, opts.isExport)
tsNamespace := &js_ast.TSNamespaceScope{
ExportedMembers: exportedMembers,
ArgRef: ast.InvalidRef,
IsEnumScope: true,
}
enumMemberData := &js_ast.TSNamespaceMemberNamespace{
ExportedMembers: exportedMembers,
}
// Declare the enum and create the scope
scopeIndex := len(p.scopesInOrder)
if !opts.isTypeScriptDeclare {
name.Ref = p.declareSymbol(ast.SymbolTSEnum, nameLoc, nameText)
p.pushScopeForParsePass(js_ast.ScopeEntry, loc)
p.currentScope.TSNamespace = tsNamespace
p.refToTSNamespaceMemberData[name.Ref] = enumMemberData
}
p.lexer.Expect(js_lexer.TOpenBrace)
values := []js_ast.EnumValue{}
oldFnOrArrowData := p.fnOrArrowDataParse
p.fnOrArrowDataParse = fnOrArrowDataParse{
isThisDisallowed: true,
needsAsyncLoc: logger.Loc{Start: -1},
}
// Parse the body
for p.lexer.Token != js_lexer.TCloseBrace {
nameRange := p.lexer.Range()
value := js_ast.EnumValue{
Loc: nameRange.Loc,
Ref: ast.InvalidRef,
}
// Parse the name
var nameText string
if p.lexer.Token == js_lexer.TStringLiteral {
value.Name = p.lexer.StringLiteral()
nameText = helpers.UTF16ToString(value.Name)
} else if p.lexer.IsIdentifierOrKeyword() {
nameText = p.lexer.Identifier.String
value.Name = helpers.StringToUTF16(nameText)
} else {
p.lexer.Expect(js_lexer.TIdentifier)
}
p.lexer.Next()
// Identifiers can be referenced by other values
if !opts.isTypeScriptDeclare && js_ast.IsIdentifierUTF16(value.Name) {
value.Ref = p.declareSymbol(ast.SymbolOther, value.Loc, helpers.UTF16ToString(value.Name))
}
// Parse the initializer
if p.lexer.Token == js_lexer.TEquals {
p.lexer.Next()
value.ValueOrNil = p.parseExpr(js_ast.LComma)
}
values = append(values, value)
// Add this enum value as a member of the enum's namespace
exportedMembers[nameText] = js_ast.TSNamespaceMember{
Loc: value.Loc,
Data: &js_ast.TSNamespaceMemberProperty{},
IsEnumValue: true,
}
if p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TSemicolon {
if p.lexer.IsIdentifierOrKeyword() || p.lexer.Token == js_lexer.TStringLiteral {
var errorLoc logger.Loc
var errorText string
if value.ValueOrNil.Data == nil {
errorLoc = logger.Loc{Start: nameRange.End()}
errorText = fmt.Sprintf("Expected \",\" after %q in enum", nameText)
} else {
var nextName string
if p.lexer.Token == js_lexer.TStringLiteral {
nextName = helpers.UTF16ToString(p.lexer.StringLiteral())
} else {
nextName = p.lexer.Identifier.String
}
errorLoc = p.lexer.Loc()
errorText = fmt.Sprintf("Expected \",\" before %q in enum", nextName)
}
data := p.tracker.MsgData(logger.Range{Loc: errorLoc}, errorText)
data.Location.Suggestion = ","
p.log.AddMsg(logger.Msg{Kind: logger.Error, Data: data})
panic(js_lexer.LexerPanic{})
}
break
}
p.lexer.Next()
}
p.fnOrArrowDataParse = oldFnOrArrowData
if !opts.isTypeScriptDeclare {
// Avoid a collision with the enum closure argument variable if the
// enum exports a symbol with the same name as the enum itself:
//
// enum foo {
// foo = 123,
// bar = foo,
// }
//
// TypeScript generates the following code in this case:
//
// var foo;
// (function (foo) {
// foo[foo["foo"] = 123] = "foo";
// foo[foo["bar"] = 123] = "bar";
// })(foo || (foo = {}));
//
// Whereas in this case:
//
// enum foo {
// bar = foo as any,
// }
//
// TypeScript generates the following code:
//
// var foo;
// (function (foo) {
// foo[foo["bar"] = foo] = "bar";
// })(foo || (foo = {}));
//
if _, ok := p.currentScope.Members[nameText]; ok {
// Add a "_" to make tests easier to read, since non-bundler tests don't
// run the renamer. For external-facing things the renamer will avoid
// collisions automatically so this isn't important for correctness.
tsNamespace.ArgRef = p.newSymbol(ast.SymbolHoisted, "_"+nameText)
p.currentScope.Generated = append(p.currentScope.Generated, tsNamespace.ArgRef)
} else {
tsNamespace.ArgRef = p.declareSymbol(ast.SymbolHoisted, nameLoc, nameText)
}
p.refToTSNamespaceMemberData[tsNamespace.ArgRef] = enumMemberData
p.popScope()
}
p.lexer.Expect(js_lexer.TCloseBrace)
if opts.isTypeScriptDeclare {
if opts.isNamespaceScope && opts.isExport {
p.hasNonLocalExportDeclareInsideNamespace = true
}
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
// Save these for when we do out-of-order enum visiting
if p.scopesInOrderForEnum == nil {
p.scopesInOrderForEnum = make(map[logger.Loc][]scopeOrder)
}
// Make a copy of "scopesInOrder" instead of a slice since the original
// array may be flattened in the future by "popAndFlattenScope"
p.scopesInOrderForEnum[loc] = append([]scopeOrder{}, p.scopesInOrder[scopeIndex:]...)
return js_ast.Stmt{Loc: loc, Data: &js_ast.SEnum{
Name: name,
Arg: tsNamespace.ArgRef,
Values: values,
IsExport: opts.isExport,
}}
}
// This assumes the caller has already parsed the "import" token
func (p *parser) parseTypeScriptImportEqualsStmt(loc logger.Loc, opts parseStmtOpts, defaultNameLoc logger.Loc, defaultName string) js_ast.Stmt {
p.lexer.Expect(js_lexer.TEquals)
kind := p.selectLocalKind(js_ast.LocalConst)
name := p.lexer.Identifier
value := js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EIdentifier{Ref: p.storeNameInRef(name)}}
p.lexer.Expect(js_lexer.TIdentifier)
if name.String == "require" && p.lexer.Token == js_lexer.TOpenParen {
// "import ns = require('x')"
p.lexer.Next()
path := js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EString{Value: p.lexer.StringLiteral()}}
p.lexer.Expect(js_lexer.TStringLiteral)
p.lexer.Expect(js_lexer.TCloseParen)
value.Data = &js_ast.ECall{
Target: value,
Args: []js_ast.Expr{path},
}
} else {
// "import Foo = Bar"
// "import Foo = Bar.Baz"
for p.lexer.Token == js_lexer.TDot {
p.lexer.Next()
value.Data = &js_ast.EDot{
Target: value,
Name: p.lexer.Identifier.String,
NameLoc: p.lexer.Loc(),
CanBeRemovedIfUnused: true,
}
p.lexer.Expect(js_lexer.TIdentifier)
}
}
p.lexer.ExpectOrInsertSemicolon()
if opts.isTypeScriptDeclare {
// "import type foo = require('bar');"
// "import type foo = bar.baz;"
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
ref := p.declareSymbol(ast.SymbolConst, defaultNameLoc, defaultName)
decls := []js_ast.Decl{{
Binding: js_ast.Binding{Loc: defaultNameLoc, Data: &js_ast.BIdentifier{Ref: ref}},
ValueOrNil: value,
}}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SLocal{
Kind: kind,
Decls: decls,
IsExport: opts.isExport,
WasTSImportEquals: true,
}}
}
// Generate a TypeScript namespace object for this namespace's scope. If this
// namespace is another block that is to be merged with an existing namespace,
// use that earlier namespace's object instead.
func (p *parser) getOrCreateExportedNamespaceMembers(name string, isExport bool) js_ast.TSNamespaceMembers {
// Merge with a sibling namespace from the same scope
if existingMember, ok := p.currentScope.Members[name]; ok {
if memberData, ok := p.refToTSNamespaceMemberData[existingMember.Ref]; ok {
if nsMemberData, ok := memberData.(*js_ast.TSNamespaceMemberNamespace); ok {
return nsMemberData.ExportedMembers
}
}
}
// Merge with a sibling namespace from a different scope
if isExport {
if parentNamespace := p.currentScope.TSNamespace; parentNamespace != nil {
if existing, ok := parentNamespace.ExportedMembers[name]; ok {
if existing, ok := existing.Data.(*js_ast.TSNamespaceMemberNamespace); ok {
return existing.ExportedMembers
}
}
}
}
// Otherwise, generate a new namespace object
return make(js_ast.TSNamespaceMembers)
}
func (p *parser) parseTypeScriptNamespaceStmt(loc logger.Loc, opts parseStmtOpts) js_ast.Stmt {
// "namespace Foo {}"
nameLoc := p.lexer.Loc()
nameText := p.lexer.Identifier.String
p.lexer.Next()
// Generate the namespace object
exportedMembers := p.getOrCreateExportedNamespaceMembers(nameText, opts.isExport)
tsNamespace := &js_ast.TSNamespaceScope{
ExportedMembers: exportedMembers,
ArgRef: ast.InvalidRef,
}
nsMemberData := &js_ast.TSNamespaceMemberNamespace{
ExportedMembers: exportedMembers,
}
// Declare the namespace and create the scope
name := ast.LocRef{Loc: nameLoc, Ref: ast.InvalidRef}
scopeIndex := p.pushScopeForParsePass(js_ast.ScopeEntry, loc)
p.currentScope.TSNamespace = tsNamespace
oldHasNonLocalExportDeclareInsideNamespace := p.hasNonLocalExportDeclareInsideNamespace
oldFnOrArrowData := p.fnOrArrowDataParse
p.hasNonLocalExportDeclareInsideNamespace = false
p.fnOrArrowDataParse = fnOrArrowDataParse{
isThisDisallowed: true,
isReturnDisallowed: true,
needsAsyncLoc: logger.Loc{Start: -1},
}
// Parse the statements inside the namespace
var stmts []js_ast.Stmt
if p.lexer.Token == js_lexer.TDot {
dotLoc := p.lexer.Loc()
p.lexer.Next()
stmts = []js_ast.Stmt{p.parseTypeScriptNamespaceStmt(dotLoc, parseStmtOpts{
isExport: true,
isNamespaceScope: true,
isTypeScriptDeclare: opts.isTypeScriptDeclare,
})}
} else if opts.isTypeScriptDeclare && p.lexer.Token != js_lexer.TOpenBrace {
p.lexer.ExpectOrInsertSemicolon()
} else {
p.lexer.Expect(js_lexer.TOpenBrace)
stmts = p.parseStmtsUpTo(js_lexer.TCloseBrace, parseStmtOpts{
isNamespaceScope: true,
isTypeScriptDeclare: opts.isTypeScriptDeclare,
})
p.lexer.Next()
}
hasNonLocalExportDeclareInsideNamespace := p.hasNonLocalExportDeclareInsideNamespace
p.hasNonLocalExportDeclareInsideNamespace = oldHasNonLocalExportDeclareInsideNamespace
p.fnOrArrowDataParse = oldFnOrArrowData
// Add any exported members from this namespace's body as members of the
// associated namespace object.
for _, stmt := range stmts {
switch s := stmt.Data.(type) {
case *js_ast.SFunction:
if s.IsExport {
name := p.symbols[s.Fn.Name.Ref.InnerIndex].OriginalName
member := js_ast.TSNamespaceMember{
Loc: s.Fn.Name.Loc,
Data: &js_ast.TSNamespaceMemberProperty{},
}
exportedMembers[name] = member
p.refToTSNamespaceMemberData[s.Fn.Name.Ref] = member.Data
}
case *js_ast.SClass:
if s.IsExport {
name := p.symbols[s.Class.Name.Ref.InnerIndex].OriginalName
member := js_ast.TSNamespaceMember{
Loc: s.Class.Name.Loc,
Data: &js_ast.TSNamespaceMemberProperty{},
}
exportedMembers[name] = member
p.refToTSNamespaceMemberData[s.Class.Name.Ref] = member.Data
}
case *js_ast.SNamespace:
if s.IsExport {
if memberData, ok := p.refToTSNamespaceMemberData[s.Name.Ref]; ok {
if nsMemberData, ok := memberData.(*js_ast.TSNamespaceMemberNamespace); ok {
member := js_ast.TSNamespaceMember{
Loc: s.Name.Loc,
Data: &js_ast.TSNamespaceMemberNamespace{
ExportedMembers: nsMemberData.ExportedMembers,
},
}
exportedMembers[p.symbols[s.Name.Ref.InnerIndex].OriginalName] = member
p.refToTSNamespaceMemberData[s.Name.Ref] = member.Data
}
}
}
case *js_ast.SEnum:
if s.IsExport {
if memberData, ok := p.refToTSNamespaceMemberData[s.Name.Ref]; ok {
if nsMemberData, ok := memberData.(*js_ast.TSNamespaceMemberNamespace); ok {
member := js_ast.TSNamespaceMember{
Loc: s.Name.Loc,
Data: &js_ast.TSNamespaceMemberNamespace{
ExportedMembers: nsMemberData.ExportedMembers,
},
}
exportedMembers[p.symbols[s.Name.Ref.InnerIndex].OriginalName] = member
p.refToTSNamespaceMemberData[s.Name.Ref] = member.Data
}
}
}
case *js_ast.SLocal:
if s.IsExport {
js_ast.ForEachIdentifierBindingInDecls(s.Decls, func(loc logger.Loc, b *js_ast.BIdentifier) {
name := p.symbols[b.Ref.InnerIndex].OriginalName
member := js_ast.TSNamespaceMember{
Loc: loc,
Data: &js_ast.TSNamespaceMemberProperty{},
}
exportedMembers[name] = member
p.refToTSNamespaceMemberData[b.Ref] = member.Data
})
}
}
}
// Import assignments may be only used in type expressions, not value
// expressions. If this is the case, the TypeScript compiler removes
// them entirely from the output. That can cause the namespace itself
// to be considered empty and thus be removed.
importEqualsCount := 0
for _, stmt := range stmts {
if local, ok := stmt.Data.(*js_ast.SLocal); ok && local.WasTSImportEquals && !local.IsExport {
importEqualsCount++
}
}
// TypeScript omits namespaces without values. These namespaces
// are only allowed to be used in type expressions. They are
// allowed to be exported, but can also only be used in type
// expressions when imported. So we shouldn't count them as a
// real export either.
//
// TypeScript also strangely counts namespaces containing only
// "export declare" statements as non-empty even though "declare"
// statements are only type annotations. We cannot omit the namespace
// in that case. See https://github.com/evanw/esbuild/issues/1158.
if (len(stmts) == importEqualsCount && !hasNonLocalExportDeclareInsideNamespace) || opts.isTypeScriptDeclare {
p.popAndDiscardScope(scopeIndex)
if opts.isModuleScope {
p.localTypeNames[nameText] = true
}
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
if !opts.isTypeScriptDeclare {
// Avoid a collision with the namespace closure argument variable if the
// namespace exports a symbol with the same name as the namespace itself:
//
// namespace foo {
// export let foo = 123
// console.log(foo)
// }
//
// TypeScript generates the following code in this case:
//
// var foo;
// (function (foo_1) {
// foo_1.foo = 123;
// console.log(foo_1.foo);
// })(foo || (foo = {}));
//
if _, ok := p.currentScope.Members[nameText]; ok {
// Add a "_" to make tests easier to read, since non-bundler tests don't
// run the renamer. For external-facing things the renamer will avoid
// collisions automatically so this isn't important for correctness.
tsNamespace.ArgRef = p.newSymbol(ast.SymbolHoisted, "_"+nameText)
p.currentScope.Generated = append(p.currentScope.Generated, tsNamespace.ArgRef)
} else {
tsNamespace.ArgRef = p.declareSymbol(ast.SymbolHoisted, nameLoc, nameText)
}
p.refToTSNamespaceMemberData[tsNamespace.ArgRef] = nsMemberData
}
p.popScope()
if !opts.isTypeScriptDeclare {
name.Ref = p.declareSymbol(ast.SymbolTSNamespace, nameLoc, nameText)
p.refToTSNamespaceMemberData[name.Ref] = nsMemberData
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SNamespace{
Name: name,
Arg: tsNamespace.ArgRef,
Stmts: stmts,
IsExport: opts.isExport,
}}
}
func (p *parser) generateClosureForTypeScriptNamespaceOrEnum(
stmts []js_ast.Stmt, stmtLoc logger.Loc, isExport bool, nameLoc logger.Loc,
nameRef ast.Ref, argRef ast.Ref, stmtsInsideClosure []js_ast.Stmt,
) []js_ast.Stmt {
// Follow the link chain in case symbols were merged
symbol := p.symbols[nameRef.InnerIndex]
for symbol.Link != ast.InvalidRef {
nameRef = symbol.Link
symbol = p.symbols[nameRef.InnerIndex]
}
// Make sure to only emit a variable once for a given namespace, since there
// can be multiple namespace blocks for the same namespace
if (symbol.Kind == ast.SymbolTSNamespace || symbol.Kind == ast.SymbolTSEnum) && !p.emittedNamespaceVars[nameRef] {
decls := []js_ast.Decl{{Binding: js_ast.Binding{Loc: nameLoc, Data: &js_ast.BIdentifier{Ref: nameRef}}}}
p.emittedNamespaceVars[nameRef] = true
if p.currentScope == p.moduleScope {
// Top-level namespace: "var"
stmts = append(stmts, js_ast.Stmt{Loc: stmtLoc, Data: &js_ast.SLocal{
Kind: js_ast.LocalVar,
Decls: decls,
IsExport: isExport,
}})
} else {
// Nested namespace: "let"
stmts = append(stmts, js_ast.Stmt{Loc: stmtLoc, Data: &js_ast.SLocal{
Kind: js_ast.LocalLet,
Decls: decls,
}})
}
}
var argExpr js_ast.Expr
if p.options.minifySyntax && !p.options.unsupportedJSFeatures.Has(compat.LogicalAssignment) {
// If the "||=" operator is supported, our minified output can be slightly smaller
if isExport && p.enclosingNamespaceArgRef != nil {
// "name = (enclosing.name ||= {})"
argExpr = js_ast.Assign(
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: nameRef}},
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EBinary{
Op: js_ast.BinOpLogicalOrAssign,
Left: js_ast.Expr{Loc: nameLoc, Data: p.dotOrMangledPropVisit(
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: *p.enclosingNamespaceArgRef}},
p.symbols[nameRef.InnerIndex].OriginalName,
nameLoc,
)},
Right: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EObject{}},
}},
)
p.recordUsage(*p.enclosingNamespaceArgRef)
p.recordUsage(nameRef)
} else {
// "name ||= {}"
argExpr = js_ast.Expr{Loc: nameLoc, Data: &js_ast.EBinary{
Op: js_ast.BinOpLogicalOrAssign,
Left: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: nameRef}},
Right: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EObject{}},
}}
p.recordUsage(nameRef)
}
} else {
if isExport && p.enclosingNamespaceArgRef != nil {
// "name = enclosing.name || (enclosing.name = {})"
name := p.symbols[nameRef.InnerIndex].OriginalName
argExpr = js_ast.Assign(
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: nameRef}},
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EBinary{
Op: js_ast.BinOpLogicalOr,
Left: js_ast.Expr{Loc: nameLoc, Data: p.dotOrMangledPropVisit(
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: *p.enclosingNamespaceArgRef}},
name,
nameLoc,
)},
Right: js_ast.Assign(
js_ast.Expr{Loc: nameLoc, Data: p.dotOrMangledPropVisit(
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: *p.enclosingNamespaceArgRef}},
name,
nameLoc,
)},
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EObject{}},
),
}},
)
p.recordUsage(*p.enclosingNamespaceArgRef)
p.recordUsage(*p.enclosingNamespaceArgRef)
p.recordUsage(nameRef)
} else {
// "name || (name = {})"
argExpr = js_ast.Expr{Loc: nameLoc, Data: &js_ast.EBinary{
Op: js_ast.BinOpLogicalOr,
Left: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: nameRef}},
Right: js_ast.Assign(
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: nameRef}},
js_ast.Expr{Loc: nameLoc, Data: &js_ast.EObject{}},
),
}}
p.recordUsage(nameRef)
p.recordUsage(nameRef)
}
}
// Try to use an arrow function if possible for compactness
var targetExpr js_ast.Expr
args := []js_ast.Arg{{Binding: js_ast.Binding{Loc: nameLoc, Data: &js_ast.BIdentifier{Ref: argRef}}}}
if p.options.unsupportedJSFeatures.Has(compat.Arrow) {
targetExpr = js_ast.Expr{Loc: stmtLoc, Data: &js_ast.EFunction{Fn: js_ast.Fn{
Args: args,
Body: js_ast.FnBody{Loc: stmtLoc, Block: js_ast.SBlock{Stmts: stmtsInsideClosure}},
}}}
} else {
// "(() => { foo() })()" => "(() => foo())()"
if p.options.minifySyntax && len(stmtsInsideClosure) == 1 {
if expr, ok := stmtsInsideClosure[0].Data.(*js_ast.SExpr); ok {
stmtsInsideClosure[0].Data = &js_ast.SReturn{ValueOrNil: expr.Value}
}
}
targetExpr = js_ast.Expr{Loc: stmtLoc, Data: &js_ast.EArrow{
Args: args,
Body: js_ast.FnBody{Loc: stmtLoc, Block: js_ast.SBlock{Stmts: stmtsInsideClosure}},
PreferExpr: true,
}}
}
// Call the closure with the name object
stmts = append(stmts, js_ast.Stmt{Loc: stmtLoc, Data: &js_ast.SExpr{Value: js_ast.Expr{Loc: stmtLoc, Data: &js_ast.ECall{
Target: targetExpr,
Args: []js_ast.Expr{argExpr},
}}}})
return stmts
}
func (p *parser) generateClosureForTypeScriptEnum(
stmts []js_ast.Stmt, stmtLoc logger.Loc, isExport bool, nameLoc logger.Loc,
nameRef ast.Ref, argRef ast.Ref, exprsInsideClosure []js_ast.Expr,
allValuesArePure bool,
) []js_ast.Stmt {
// Bail back to the namespace code for enums that aren't at the top level.
// Doing this for nested enums is problematic for two reasons. First of all
// enums inside of namespaces must be property accesses off the namespace
// object instead of variable declarations. Also we'd need to use "let"
// instead of "var" which doesn't allow sibling declarations to be merged.
if p.currentScope != p.moduleScope {
stmtsInsideClosure := []js_ast.Stmt{}
if len(exprsInsideClosure) > 0 {
if p.options.minifySyntax {
// "a; b; c;" => "a, b, c;"
joined := js_ast.JoinAllWithComma(exprsInsideClosure)
stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: joined.Loc, Data: &js_ast.SExpr{Value: joined}})
} else {
for _, expr := range exprsInsideClosure {
stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: expr.Loc, Data: &js_ast.SExpr{Value: expr}})
}
}
}
return p.generateClosureForTypeScriptNamespaceOrEnum(
stmts, stmtLoc, isExport, nameLoc, nameRef, argRef, stmtsInsideClosure)
}
// This uses an output format for enums that's different but equivalent to
// what TypeScript uses. Here is TypeScript's output:
//
// var x;
// (function (x) {
// x[x["y"] = 1] = "y";
// })(x || (x = {}));
//
// And here's our output:
//
// var x = /* @__PURE__ */ ((x) => {
// x[x["y"] = 1] = "y";
// return x;
// })(x || {});
//
// One benefit is that the minified output is smaller:
//
// // Old output minified
// var x;(function(n){n[n.y=1]="y"})(x||(x={}));
//
// // New output minified
// var x=(r=>(r[r.y=1]="y",r))(x||{});
//
// Another benefit is that the @__PURE__ annotation means it automatically
// works with tree-shaking, even with more advanced features such as sibling
// enum declarations and enum/namespace merges. Ideally all uses of the enum
// are just direct references to enum members (and are therefore inlined as
// long as the enum value is a constant) and the enum definition itself is
// unused and can be removed as dead code.
// Follow the link chain in case symbols were merged
symbol := p.symbols[nameRef.InnerIndex]
for symbol.Link != ast.InvalidRef {
nameRef = symbol.Link
symbol = p.symbols[nameRef.InnerIndex]
}
// Generate the body of the closure, including a return statement at the end
stmtsInsideClosure := []js_ast.Stmt{}
argExpr := js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: argRef}}
if p.options.minifySyntax {
// "a; b; return c;" => "return a, b, c;"
joined := js_ast.JoinAllWithComma(exprsInsideClosure)
joined = js_ast.JoinWithComma(joined, argExpr)
stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: joined.Loc, Data: &js_ast.SReturn{ValueOrNil: joined}})
} else {
for _, expr := range exprsInsideClosure {
stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: expr.Loc, Data: &js_ast.SExpr{Value: expr}})
}
stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: argExpr.Loc, Data: &js_ast.SReturn{ValueOrNil: argExpr}})
}
// Try to use an arrow function if possible for compactness
var targetExpr js_ast.Expr
args := []js_ast.Arg{{Binding: js_ast.Binding{Loc: nameLoc, Data: &js_ast.BIdentifier{Ref: argRef}}}}
if p.options.unsupportedJSFeatures.Has(compat.Arrow) {
targetExpr = js_ast.Expr{Loc: stmtLoc, Data: &js_ast.EFunction{Fn: js_ast.Fn{
Args: args,
Body: js_ast.FnBody{Loc: stmtLoc, Block: js_ast.SBlock{Stmts: stmtsInsideClosure}},
}}}
} else {
targetExpr = js_ast.Expr{Loc: stmtLoc, Data: &js_ast.EArrow{
Args: args,
Body: js_ast.FnBody{Loc: stmtLoc, Block: js_ast.SBlock{Stmts: stmtsInsideClosure}},
PreferExpr: p.options.minifySyntax,
}}
}
// Call the closure with the name object and store it to the variable
decls := []js_ast.Decl{{
Binding: js_ast.Binding{Loc: nameLoc, Data: &js_ast.BIdentifier{Ref: nameRef}},
ValueOrNil: js_ast.Expr{Loc: stmtLoc, Data: &js_ast.ECall{
Target: targetExpr,
Args: []js_ast.Expr{{Loc: nameLoc, Data: &js_ast.EBinary{
Op: js_ast.BinOpLogicalOr,
Left: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: nameRef}},
Right: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EObject{}},
}}},
CanBeUnwrappedIfUnused: allValuesArePure,
}},
}}
p.recordUsage(nameRef)
// Use a "var" statement since this is a top-level enum, but only use "export" once
stmts = append(stmts, js_ast.Stmt{Loc: stmtLoc, Data: &js_ast.SLocal{
Kind: js_ast.LocalVar,
Decls: decls,
IsExport: isExport && !p.emittedNamespaceVars[nameRef],
}})
p.emittedNamespaceVars[nameRef] = true
return stmts
}
func (p *parser) wrapInlinedEnum(value js_ast.Expr, comment string) js_ast.Expr {
if strings.Contains(comment, "*/") {
// Don't wrap with a comment
return value
}
// Wrap with a comment
return js_ast.Expr{Loc: value.Loc, Data: &js_ast.EInlinedEnum{
Value: value,
Comment: comment,
}}
}