// 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 () => Foo" 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: // "() => Foo" 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 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() // "{ (): c.d \n (): 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 {}" // Invalid: // "interface Foo {}" 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 = T" // Invalid: // "type Foo = T" // "type Foo = 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 = T" // "type Foo = T" // "type Foo = T" // "type Foo = T" // "type Foo = T" // Invalid: // "type Foo = T" // "type Foo = 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 Optional identifier (e.g. after "type Foo {}" if p.lexer.Token == js_lexer.TExtends { result = definitelyTypeParameters p.lexer.Next() p.skipTypeScriptType(js_ast.LLowest) } // "class Foo {}" 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>()" 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>()" 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( js_lexer.TNoSubstitutionTemplateLiteral, // foo `...` js_lexer.TTemplateHead: // foo `...${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, }} }