mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
2087 lines
63 KiB
Go
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,
|
|
}}
|
|
}
|