Files
esbuild/internal/js_parser/js_parser.go
T

18847 lines
644 KiB
Go

package js_parser
import (
"fmt"
"math"
"math/big"
"regexp"
"sort"
"strings"
"unicode/utf8"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/config"
"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"
"github.com/evanw/esbuild/internal/renamer"
"github.com/evanw/esbuild/internal/runtime"
)
// This parser does two passes:
//
// 1. Parse the source into an AST, create the scope tree, and declare symbols.
//
// 2. Visit each node in the AST, bind identifiers to declared symbols, do
// constant folding, substitute compile-time variable definitions, and
// lower certain syntactic constructs as appropriate given the language
// target.
//
// So many things have been put in so few passes because we want to minimize
// the number of full-tree passes to improve performance. However, we need
// to have at least two separate passes to handle variable hoisting. See the
// comment about scopesInOrder below for more information.
type parser struct {
options Options
log logger.Log
source logger.Source
tracker logger.LineColumnTracker
fnOrArrowDataParse fnOrArrowDataParse
fnOnlyDataVisit fnOnlyDataVisit
allocatedNames []string
currentScope *js_ast.Scope
currentPart *js_ast.Part
symbols []ast.Symbol
astHelpers js_ast.HelperContext
tsUseCounts []uint32
injectedDefineSymbols []ast.Ref
injectedSymbolSources map[ast.Ref]injectedSymbolSource
injectedDotNames map[string][]injectedDotName
dropLabelsMap map[string]struct{}
exprComments map[logger.Loc][]string
mangledProps map[string]ast.Ref
reservedProps map[string]bool
globPatternImports []globPatternImport
runtimeImports map[string]ast.LocRef
duplicateCaseChecker duplicateCaseChecker
unrepresentableIdentifiers map[string]bool
legacyOctalLiterals map[js_ast.E]logger.Range
scopesInOrderForEnum map[logger.Loc][]scopeOrder
binaryExprStack []binaryExprVisitor
// For strict mode handling
hoistedRefForSloppyModeBlockFn map[ast.Ref]ast.Ref
// For lowering private methods
privateGetters map[ast.Ref]ast.Ref
privateSetters map[ast.Ref]ast.Ref
// These are for TypeScript
//
// We build up enough information about the TypeScript namespace hierarchy to
// be able to resolve scope lookups and property accesses for TypeScript enum
// and namespace features. Each JavaScript scope object inside a namespace
// has a reference to a map of exported namespace members from sibling scopes.
//
// In addition, there is a map from each relevant symbol reference to the data
// associated with that namespace or namespace member: "refToTSNamespaceMemberData".
// This gives enough info to be able to resolve queries into the namespace.
//
// When visiting expressions, namespace metadata is associated with the most
// recently visited node. If namespace metadata is present, "tsNamespaceTarget"
// will be set to the most recently visited node (as a way to mark that this
// node has metadata) and "tsNamespaceMemberData" will be set to the metadata.
refToTSNamespaceMemberData map[ast.Ref]js_ast.TSNamespaceMemberData
tsNamespaceTarget js_ast.E
tsNamespaceMemberData js_ast.TSNamespaceMemberData
emittedNamespaceVars map[ast.Ref]bool
isExportedInsideNamespace map[ast.Ref]ast.Ref
localTypeNames map[string]bool
tsEnums map[ast.Ref]map[string]js_ast.TSEnumValue
constValues map[ast.Ref]js_ast.ConstValue
propDerivedCtorValue js_ast.E
propMethodDecoratorScope *js_ast.Scope
// This is the reference to the generated function argument for the namespace,
// which is different than the reference to the namespace itself:
//
// namespace ns {
// }
//
// The code above is transformed into something like this:
//
// var ns1;
// (function(ns2) {
// })(ns1 || (ns1 = {}));
//
// This variable is "ns2" not "ns1". It is only used during the second
// "visit" pass.
enclosingNamespaceArgRef *ast.Ref
// Imports (both ES6 and CommonJS) are tracked at the top level
importRecords []ast.ImportRecord
exportStarImportRecords []uint32
// These are for handling ES6 imports and exports
importItemsForNamespace map[ast.Ref]namespaceImportItems
isImportItem map[ast.Ref]bool
namedImports map[ast.Ref]js_ast.NamedImport
namedExports map[string]js_ast.NamedExport
topLevelSymbolToParts map[ast.Ref][]uint32
importNamespaceCCMap map[importNamespaceCall]bool
// The parser does two passes and we need to pass the scope tree information
// from the first pass to the second pass. That's done by tracking the calls
// to pushScopeForParsePass() and popScope() during the first pass in
// scopesInOrder.
//
// Then, when the second pass calls pushScopeForVisitPass() and popScope(),
// we consume entries from scopesInOrder and make sure they are in the same
// order. This way the second pass can efficiently use the same scope tree
// as the first pass without having to attach the scope tree to the AST.
//
// We need to split this into two passes because the pass that declares the
// symbols must be separate from the pass that binds identifiers to declared
// symbols to handle declaring a hoisted "var" symbol in a nested scope and
// binding a name to it in a parent or sibling scope.
scopesInOrder []scopeOrder
// These propagate the name from the parent context into an anonymous child
// expression. For example:
//
// let foo = function() {}
// assert.strictEqual(foo.name, 'foo')
//
nameToKeep string
nameToKeepIsFor js_ast.E
// These properties are for the visit pass, which runs after the parse pass.
// The visit pass binds identifiers to declared symbols, does constant
// folding, substitutes compile-time variable definitions, and lowers certain
// syntactic constructs as appropriate.
stmtExprValue js_ast.E
callTarget js_ast.E
dotOrIndexTarget js_ast.E
templateTag js_ast.E
deleteTarget js_ast.E
loopBody js_ast.S
suspiciousLogicalOperatorInsideArrow js_ast.E
moduleScope *js_ast.Scope
// This is internal-only data used for the implementation of Yarn PnP
manifestForYarnPnP js_ast.Expr
stringLocalsForYarnPnP map[ast.Ref]stringLocalForYarnPnP
// This helps recognize the "await import()" pattern. When this is present,
// warnings about non-string import paths will be omitted inside try blocks.
awaitTarget js_ast.E
// This helps recognize the "import().catch()" pattern. We also try to avoid
// warning about this just like the "try { await import() }" pattern.
thenCatchChain thenCatchChain
// When bundling, hoisted top-level local variables declared with "var" in
// nested scopes are moved up to be declared in the top-level scope instead.
// The old "var" statements are turned into regular assignments instead. This
// makes it easier to quickly scan the top-level statements for "var" locals
// with the guarantee that all will be found.
relocatedTopLevelVars []ast.LocRef
// We need to lower private names such as "#foo" if they are used in a brand
// check such as "#foo in x" even if the private name syntax would otherwise
// be supported. This is because private names are a newly-added feature.
//
// However, this parser operates in only two passes for speed. The first pass
// parses things and declares variables, and the second pass lowers things and
// resolves references to declared variables. So the existence of a "#foo in x"
// expression for a specific "#foo" cannot be used to decide to lower "#foo"
// because it's too late by that point. There may be another expression such
// as "x.#foo" before that point and that must be lowered as well even though
// it has already been visited.
//
// Instead what we do is track just the names of fields used in private brand
// checks during the first pass. This tracks the names themselves, not symbol
// references. Then, during the second pass when we are about to enter into
// a class, we conservatively decide to lower all private names in that class
// which are used in a brand check anywhere in the file.
lowerAllOfThesePrivateNames map[string]bool
// Temporary variables used for lowering
tempLetsToDeclare []ast.Ref
tempRefsToDeclare []tempRef
topLevelTempRefsToDeclare []tempRef
lexer js_lexer.Lexer
// Private field access in a decorator lowers all private fields in that class
parseExperimentalDecoratorNesting int
// Temporary variables used for lowering
tempRefCount int
topLevelTempRefCount int
// We need to scan over the source contents to recover the line and column offsets
jsxSourceLoc int
jsxSourceLine int
jsxSourceColumn int
exportsRef ast.Ref
requireRef ast.Ref
moduleRef ast.Ref
importMetaRef ast.Ref
promiseRef ast.Ref
regExpRef ast.Ref
bigIntRef ast.Ref
superCtorRef ast.Ref
// Imports from "react/jsx-runtime" and "react", respectively.
// (Or whatever was specified in the "importSource" option)
jsxRuntimeImports map[string]ast.LocRef
jsxLegacyImports map[string]ast.LocRef
// For lowering private methods
weakMapRef ast.Ref
weakSetRef ast.Ref
esmImportStatementKeyword logger.Range
esmImportMeta logger.Range
esmExportKeyword logger.Range
enclosingClassKeyword logger.Range
topLevelAwaitKeyword logger.Range
liveTopLevelAwaitKeyword logger.Range
latestArrowArgLoc logger.Loc
forbidSuffixAfterAsLoc logger.Loc
firstJSXElementLoc logger.Loc
fnOrArrowDataVisit fnOrArrowDataVisit
singleStmtDepth int
// ArrowFunction is a special case in the grammar. Although it appears to be
// a PrimaryExpression, it's actually an AssignmentExpression. This means if
// a AssignmentExpression ends up producing an ArrowFunction then nothing can
// come after it other than the comma operator, since the comma operator is
// the only thing above AssignmentExpression under the Expression rule:
//
// AssignmentExpression:
// ArrowFunction
// ConditionalExpression
// LeftHandSideExpression = AssignmentExpression
// LeftHandSideExpression AssignmentOperator AssignmentExpression
//
// Expression:
// AssignmentExpression
// Expression , AssignmentExpression
//
afterArrowBodyLoc logger.Loc
// Setting this to true disables warnings about code that is very likely to
// be a bug. This is used to ignore issues inside "node_modules" directories.
// This has caught real issues in the past. However, it's not esbuild's job
// to find bugs in other libraries, and these warnings are problematic for
// people using these libraries with esbuild. The only fix is to either
// disable all esbuild warnings and not get warnings about your own code, or
// to try to get the warning fixed in the affected library. This is
// especially annoying if the warning is a false positive as was the case in
// https://github.com/firebase/firebase-js-sdk/issues/3814. So these warnings
// are now disabled for code inside "node_modules" directories.
suppressWarningsAboutWeirdCode bool
// A file is considered to be an ECMAScript module if it has any of the
// features of one (e.g. the "export" keyword), otherwise it's considered
// a CommonJS module.
//
// However, we have a single exception: a file where the only ESM feature
// is the "import" keyword is allowed to have CommonJS exports. This feature
// is necessary to be able to synchronously import ESM code into CommonJS,
// which we need to enable in a few important cases. Some examples are:
// our runtime code, injected files (the "inject" feature is ESM-only),
// and certain automatically-generated virtual modules from plugins.
isFileConsideredToHaveESMExports bool // Use only for export-related stuff
isFileConsideredESM bool // Use for all other stuff
// Inside a TypeScript namespace, an "export declare" statement can be used
// to cause a namespace to be emitted even though it has no other observable
// effect. This flag is used to implement this feature.
//
// Specifically, namespaces should be generated for all of the following
// namespaces below except for "f", which should not be generated:
//
// namespace a { export declare const a }
// namespace b { export declare let [[b]] }
// namespace c { export declare function c() }
// namespace d { export declare class d {} }
// namespace e { export declare enum e {} }
// namespace f { export declare namespace f {} }
//
// The TypeScript compiler compiles this into the following code (notice "f"
// is missing):
//
// var a; (function (a_1) {})(a || (a = {}));
// var b; (function (b_1) {})(b || (b = {}));
// var c; (function (c_1) {})(c || (c = {}));
// var d; (function (d_1) {})(d || (d = {}));
// var e; (function (e_1) {})(e || (e = {}));
//
// Note that this should not be implemented by declaring symbols for "export
// declare" statements because the TypeScript compiler doesn't generate any
// code for these statements, so these statements are actually references to
// global variables. There is one exception, which is that local variables
// *should* be declared as symbols because they are replaced with. This seems
// like very arbitrary behavior but it's what the TypeScript compiler does,
// so we try to match it.
//
// Specifically, in the following code below "a" and "b" should be declared
// and should be substituted with "ns.a" and "ns.b" but the other symbols
// shouldn't. References to the other symbols actually refer to global
// variables instead of to symbols that are exported from the namespace.
// This is the case as of TypeScript 4.3. I assume this is a TypeScript bug:
//
// namespace ns {
// export declare const a
// export declare let [[b]]
// export declare function c()
// export declare class d { }
// export declare enum e { }
// console.log(a, b, c, d, e)
// }
//
// The TypeScript compiler compiles this into the following code:
//
// var ns;
// (function (ns) {
// console.log(ns.a, ns.b, c, d, e);
// })(ns || (ns = {}));
//
// Relevant issue: https://github.com/evanw/esbuild/issues/1158
hasNonLocalExportDeclareInsideNamespace bool
// When this flag is enabled, we attempt to fold all expressions that
// TypeScript would consider to be "constant expressions". This flag is
// enabled inside each enum body block since TypeScript requires numeric
// constant folding in enum definitions.
//
// We also enable this flag in certain cases in JavaScript files such as when
// parsing "const" declarations at the top of a non-ESM file, but we still
// reuse TypeScript's notion of "constant expressions" for our own convenience.
//
// As of TypeScript 5.0, a "constant expression" is defined as follows:
//
// An expression is considered a constant expression if it is
//
// * a number or string literal,
// * a unary +, -, or ~ applied to a numeric constant expression,
// * a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions,
// * a binary + applied to two constant expressions whereof at least one is a string,
// * a template expression where each substitution expression is a constant expression,
// * a parenthesized constant expression,
// * a dotted name (e.g. x.y.z) that references a const variable with a constant expression initializer and no type annotation,
// * a dotted name that references an enum member with an enum literal type, or
// * a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type.
//
// More detail: https://github.com/microsoft/TypeScript/pull/50528. Note that
// we don't implement certain items in this list. For example, we don't do all
// number-to-string conversions since ours might differ from how JavaScript
// would do it, which would be a correctness issue.
shouldFoldTypeScriptConstantExpressions bool
allowIn bool
hasTopLevelReturn bool
latestReturnHadSemicolon bool
messageAboutThisIsUndefined bool
isControlFlowDead bool
shouldAddKeyComment bool
// If this is true, then all top-level statements are wrapped in a try/catch
willWrapModuleInTryCatchForUsing bool
}
type globPatternImport struct {
assertOrWith *ast.ImportAssertOrWith
parts []helpers.GlobPart
name string
approximateRange logger.Range
ref ast.Ref
kind ast.ImportKind
phase ast.ImportPhase
}
type namespaceImportItems struct {
entries map[string]ast.LocRef
importRecordIndex uint32
}
type stringLocalForYarnPnP struct {
value []uint16
loc logger.Loc
}
type injectedSymbolSource struct {
source logger.Source
loc logger.Loc
}
type injectedDotName struct {
parts []string
injectedDefineIndex uint32
}
type importNamespaceCallKind uint8
const (
exprKindCall importNamespaceCallKind = iota
exprKindNew
exprKindJSXTag
)
type importNamespaceCall struct {
ref ast.Ref
kind importNamespaceCallKind
}
type thenCatchChain struct {
nextTarget js_ast.E
catchLoc logger.Loc
hasMultipleArgs bool
hasCatch bool
}
// This is used as part of an incremental build cache key. Some of these values
// can potentially change between builds if they are derived from nearby
// "package.json" or "tsconfig.json" files that were changed since the last
// build.
type Options struct {
injectedFiles []config.InjectedFile
jsx config.JSXOptions
tsAlwaysStrict *config.TSAlwaysStrict
mangleProps *regexp.Regexp
reserveProps *regexp.Regexp
dropLabels []string
// This pointer will always be different for each build but the contents
// shouldn't ever behave different semantically. We ignore this field for the
// equality comparison.
defines *config.ProcessedDefines
// This is an embedded struct. Always access these directly instead of off
// the name "optionsThatSupportStructuralEquality". This is only grouped like
// this to make the equality comparison easier and safer (and hopefully faster).
optionsThatSupportStructuralEquality
}
type optionsThatSupportStructuralEquality struct {
originalTargetEnv string
moduleTypeData js_ast.ModuleTypeData
unsupportedJSFeatures compat.JSFeature
unsupportedJSFeatureOverrides compat.JSFeature
unsupportedJSFeatureOverridesMask compat.JSFeature
// Byte-sized values go here (gathered together here to keep this object compact)
ts config.TSOptions
mode config.Mode
platform config.Platform
outputFormat config.Format
logPathStyle logger.PathStyle
codePathStyle logger.PathStyle
asciiOnly bool
keepNames bool
minifySyntax bool
minifyIdentifiers bool
minifyWhitespace bool
omitRuntimeForTests bool
omitJSXRuntimeForTests bool
ignoreDCEAnnotations bool
treeShaking bool
dropDebugger bool
mangleQuoted bool
// This is an internal-only option used for the implementation of Yarn PnP
decodeHydrateRuntimeStateYarnPnP bool
}
func OptionsForYarnPnP() Options {
return Options{
optionsThatSupportStructuralEquality: optionsThatSupportStructuralEquality{
decodeHydrateRuntimeStateYarnPnP: true,
},
}
}
func OptionsFromConfig(options *config.Options) Options {
return Options{
injectedFiles: options.InjectedFiles,
jsx: options.JSX,
defines: options.Defines,
tsAlwaysStrict: options.TSAlwaysStrict,
mangleProps: options.MangleProps,
reserveProps: options.ReserveProps,
dropLabels: options.DropLabels,
optionsThatSupportStructuralEquality: optionsThatSupportStructuralEquality{
unsupportedJSFeatures: options.UnsupportedJSFeatures,
unsupportedJSFeatureOverrides: options.UnsupportedJSFeatureOverrides,
unsupportedJSFeatureOverridesMask: options.UnsupportedJSFeatureOverridesMask,
originalTargetEnv: options.OriginalTargetEnv,
ts: options.TS,
mode: options.Mode,
platform: options.Platform,
outputFormat: options.OutputFormat,
moduleTypeData: options.ModuleTypeData,
asciiOnly: options.ASCIIOnly,
keepNames: options.KeepNames,
minifySyntax: options.MinifySyntax,
minifyIdentifiers: options.MinifyIdentifiers,
minifyWhitespace: options.MinifyWhitespace,
omitRuntimeForTests: options.OmitRuntimeForTests,
omitJSXRuntimeForTests: options.OmitJSXRuntimeForTests,
ignoreDCEAnnotations: options.IgnoreDCEAnnotations,
treeShaking: options.TreeShaking,
dropDebugger: options.DropDebugger,
mangleQuoted: options.MangleQuoted,
logPathStyle: options.LogPathStyle,
codePathStyle: options.CodePathStyle,
},
}
}
func (a *Options) Equal(b *Options) bool {
// Compare "optionsThatSupportStructuralEquality"
if a.optionsThatSupportStructuralEquality != b.optionsThatSupportStructuralEquality {
return false
}
// Compare "tsAlwaysStrict"
if (a.tsAlwaysStrict == nil && b.tsAlwaysStrict != nil) || (a.tsAlwaysStrict != nil && b.tsAlwaysStrict == nil) ||
(a.tsAlwaysStrict != nil && b.tsAlwaysStrict != nil && *a.tsAlwaysStrict != *b.tsAlwaysStrict) {
return false
}
// Compare "mangleProps" and "reserveProps"
if !isSameRegexp(a.mangleProps, b.mangleProps) || !isSameRegexp(a.reserveProps, b.reserveProps) {
return false
}
// Compare "dropLabels"
if !helpers.StringArraysEqual(a.dropLabels, b.dropLabels) {
return false
}
// Compare "injectedFiles"
if len(a.injectedFiles) != len(b.injectedFiles) {
return false
}
for i, x := range a.injectedFiles {
y := b.injectedFiles[i]
if x.Source != y.Source || x.DefineName != y.DefineName || len(x.Exports) != len(y.Exports) {
return false
}
for j := range x.Exports {
if x.Exports[j] != y.Exports[j] {
return false
}
}
}
// Compare "jsx"
if a.jsx.Parse != b.jsx.Parse || !jsxExprsEqual(a.jsx.Factory, b.jsx.Factory) || !jsxExprsEqual(a.jsx.Fragment, b.jsx.Fragment) {
return false
}
// Do a cheap assert that the defines object hasn't changed
if (a.defines != nil || b.defines != nil) && (a.defines == nil || b.defines == nil ||
len(a.defines.IdentifierDefines) != len(b.defines.IdentifierDefines) ||
len(a.defines.DotDefines) != len(b.defines.DotDefines)) {
panic("Internal error")
}
return true
}
func isSameRegexp(a *regexp.Regexp, b *regexp.Regexp) bool {
if a == nil {
return b == nil
} else {
return b != nil && a.String() == b.String()
}
}
func jsxExprsEqual(a config.DefineExpr, b config.DefineExpr) bool {
if !helpers.StringArraysEqual(a.Parts, b.Parts) {
return false
}
if a.Constant != nil {
if b.Constant == nil || !js_ast.ValuesLookTheSame(a.Constant, b.Constant) {
return false
}
} else if b.Constant != nil {
return false
}
return true
}
type tempRef struct {
valueOrNil js_ast.Expr
ref ast.Ref
}
const (
locModuleScope = -1
)
type scopeOrder struct {
scope *js_ast.Scope
loc logger.Loc
}
type awaitOrYield uint8
const (
// The keyword is used as an identifier, not a special expression
allowIdent awaitOrYield = iota
// Declaring the identifier is forbidden, and the keyword is used as a special expression
allowExpr
// Declaring the identifier is forbidden, and using the identifier is also forbidden
forbidAll
)
// This is function-specific information used during parsing. It is saved and
// restored on the call stack around code that parses nested functions and
// arrow expressions.
type fnOrArrowDataParse struct {
arrowArgErrors *deferredArrowArgErrors
decoratorScope *js_ast.Scope
asyncRange logger.Range
needsAsyncLoc logger.Loc
await awaitOrYield
yield awaitOrYield
allowSuperCall bool
allowSuperProperty bool
isTopLevel bool
isConstructor bool
isTypeScriptDeclare bool
isThisDisallowed bool
isReturnDisallowed bool
// In TypeScript, forward declarations of functions have no bodies
allowMissingBodyForTypeScript bool
}
// This is function-specific information used during visiting. It is saved and
// restored on the call stack around code that parses nested functions and
// arrow expressions.
type fnOrArrowDataVisit struct {
// This is used to silence unresolvable imports due to "require" calls inside
// a try/catch statement. The assumption is that the try/catch statement is
// there to handle the case where the reference to "require" crashes.
tryBodyCount int32
tryCatchLoc logger.Loc
isArrow bool
isAsync bool
isGenerator bool
isInsideLoop bool
isInsideSwitch bool
isDerivedClassCtor bool
isOutsideFnOrArrow bool
shouldLowerSuperPropertyAccess bool
}
// This is function-specific information used during visiting. It is saved and
// restored on the call stack around code that parses nested functions (but not
// nested arrow functions).
type fnOnlyDataVisit struct {
// This is a reference to the magic "arguments" variable that exists inside
// functions in JavaScript. It will be non-nil inside functions and nil
// otherwise.
argumentsRef *ast.Ref
// Arrow functions don't capture the value of "this" and "arguments". Instead,
// the values are inherited from the surrounding context. If arrow functions
// are turned into regular functions due to lowering, we will need to generate
// local variables to capture these values so they are preserved correctly.
thisCaptureRef *ast.Ref
argumentsCaptureRef *ast.Ref
// If true, we're inside a static class context where "this" expressions
// should be replaced with the class name.
shouldReplaceThisWithInnerClassNameRef bool
// This is true if "this" is equal to the class name. It's true if we're in a
// static class field initializer, a static class method, or a static class
// block.
isInStaticClassContext bool
// This is a reference to the enclosing class name if there is one. It's used
// to implement "this" and "super" references. A name is automatically generated
// if one is missing so this will always be present inside a class body.
innerClassNameRef *ast.Ref
// If we're inside an async arrow function and async functions are not
// supported, then we will have to convert that arrow function to a generator
// function. That means references to "arguments" inside the arrow function
// will have to reference a captured variable instead of the real variable.
isInsideAsyncArrowFn bool
// If false, disallow "new.target" expressions. We disallow all "new.target"
// expressions at the top-level of the file (i.e. not inside a function or
// a class field). Technically since CommonJS files are wrapped in a function
// you can use "new.target" in node as an alias for "undefined" but we don't
// support that.
isNewTargetAllowed bool
// If false, the value for "this" is the top-level module scope "this" value.
// That means it's "undefined" for ECMAScript modules and "exports" for
// CommonJS modules. We track this information so that we can substitute the
// correct value for these top-level "this" references at compile time instead
// of passing the "this" expression through to the output and leaving the
// interpretation up to the run-time behavior of the generated code.
//
// If true, the value for "this" is nested inside something (either a function
// or a class declaration). That means the top-level module scope "this" value
// has been shadowed and is now inaccessible.
isThisNested bool
// If true, "this" is used in current function scope.
hasThisUsage bool
// Do not warn about "this" being undefined for code that the TypeScript
// compiler generates that looks like this:
//
// var __rest = (this && this.__rest) || function (s, e) {
// ...
// };
//
silenceMessageAboutThisBeingUndefined bool
}
type livenessStatus int8
const (
alwaysDead livenessStatus = -1
livenessUnknown livenessStatus = 0
alwaysLive livenessStatus = 1
)
type switchCaseLiveness struct {
status livenessStatus
canFallThrough bool
}
func analyzeSwitchCasesForLiveness(s *js_ast.SSwitch) []switchCaseLiveness {
cases := make([]switchCaseLiveness, 0, len(s.Cases))
defaultIndex := -1
// Determine the status of the individual cases independently
maxStatus := alwaysDead
for i, c := range s.Cases {
if c.ValueOrNil.Data == nil {
defaultIndex = i
}
// Check the value for strict equality
var status livenessStatus
if maxStatus == alwaysLive {
status = alwaysDead // Everything after an always-live case is always dead
} else if c.ValueOrNil.Data == nil {
status = alwaysDead // This is the default case, and will be filled in later
} else if isEqualToTest, ok := js_ast.CheckEqualityIfNoSideEffects(s.Test.Data, c.ValueOrNil.Data, js_ast.StrictEquality); ok {
if isEqualToTest {
status = alwaysLive // This branch will always be matched, and will be taken unless an earlier branch was taken
} else {
status = alwaysDead // This branch will never be matched, and will not be taken unless there was fall-through
}
} else {
status = livenessUnknown // This branch depends on run-time values and may or may not be matched
}
if maxStatus < status {
maxStatus = status
}
cases = append(cases, switchCaseLiveness{
status: status,
canFallThrough: caseBodyCouldHaveFallThrough(c.Body),
})
}
// Set the liveness for the default case last based on the other cases
if defaultIndex != -1 {
// The negation here transposes "always live" with "always dead"
status := -maxStatus
if maxStatus < status {
maxStatus = status
}
cases[defaultIndex].status = status
}
// Then propagate fall-through information in linear fall-through order
for i, c := range cases {
// Propagate state forward if this isn't dead. Note that the "can fall
// through" flag does not imply "must fall through". The body may have
// an embedded "break" inside an if statement, for example.
if c.status != alwaysDead {
for j := i + 1; j < len(cases) && cases[j-1].canFallThrough; j++ {
cases[j].status = livenessUnknown
}
} else if maxStatus > alwaysDead && stmtsCareAboutScope(s.Cases[i].Body) {
// Since adjacent cases share a scope, dead cases can potentially still
// affect other cases that are live. Consider the following:
//
// globalThis.foo = true
// switch (1) {
// case 0:
// let foo
// case 1:
// return foo
// }
//
// This code is supposed to throw a ReferenceError. But if we treat the
// first case as dead code, then "let foo" will end up being removed and
// the code will incorrectly return true instead.
cases[i].status = livenessUnknown
}
}
return cases
}
// Check for potential fall-through by checking for a jump at the end of the body
func caseBodyCouldHaveFallThrough(stmts []js_ast.Stmt) bool {
for len(stmts) > 0 {
switch s := stmts[len(stmts)-1].Data.(type) {
case *js_ast.SBlock:
stmts = s.Stmts // If this ends with a block, check the block's body next
continue
case *js_ast.SBreak, *js_ast.SContinue, *js_ast.SReturn, *js_ast.SThrow:
return false
}
break
}
return true
}
const bloomFilterSize = 251
type duplicateCaseValue struct {
value js_ast.Expr
hash uint32
}
type duplicateCaseChecker struct {
cases []duplicateCaseValue
bloomFilter [(bloomFilterSize + 7) / 8]byte
}
func (dc *duplicateCaseChecker) reset() {
// Preserve capacity
dc.cases = dc.cases[:0]
// This should be optimized by the compiler. See this for more information:
// https://github.com/golang/go/issues/5373
bytes := dc.bloomFilter
for i := range bytes {
bytes[i] = 0
}
}
func (dc *duplicateCaseChecker) check(p *parser, expr js_ast.Expr) {
if hash, ok := duplicateCaseHash(expr); ok {
bucket := hash % bloomFilterSize
entry := &dc.bloomFilter[bucket/8]
mask := byte(1) << (bucket % 8)
// Check for collisions
if (*entry & mask) != 0 {
for _, c := range dc.cases {
if c.hash == hash {
if equals, couldBeIncorrect := duplicateCaseEquals(c.value, expr); equals {
var laterRange logger.Range
var earlierRange logger.Range
if _, ok := expr.Data.(*js_ast.EString); ok {
laterRange = p.source.RangeOfString(expr.Loc)
} else {
laterRange = p.source.RangeOfOperatorBefore(expr.Loc, "case")
}
if _, ok := c.value.Data.(*js_ast.EString); ok {
earlierRange = p.source.RangeOfString(c.value.Loc)
} else {
earlierRange = p.source.RangeOfOperatorBefore(c.value.Loc, "case")
}
text := "This case clause will never be evaluated because it duplicates an earlier case clause"
if couldBeIncorrect {
text = "This case clause may never be evaluated because it likely duplicates an earlier case clause"
}
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddIDWithNotes(logger.MsgID_JS_DuplicateCase, kind, &p.tracker, laterRange, text,
[]logger.MsgData{p.tracker.MsgData(earlierRange, "The earlier case clause is here:")})
}
return
}
}
}
*entry |= mask
dc.cases = append(dc.cases, duplicateCaseValue{hash: hash, value: expr})
}
}
func duplicateCaseHash(expr js_ast.Expr) (uint32, bool) {
switch e := expr.Data.(type) {
case *js_ast.EInlinedEnum:
return duplicateCaseHash(e.Value)
case *js_ast.ENull:
return 0, true
case *js_ast.EUndefined:
return 1, true
case *js_ast.EBoolean:
if e.Value {
return helpers.HashCombine(2, 1), true
}
return helpers.HashCombine(2, 0), true
case *js_ast.ENumber:
bits := math.Float64bits(e.Value)
return helpers.HashCombine(helpers.HashCombine(3, uint32(bits)), uint32(bits>>32)), true
case *js_ast.EString:
hash := uint32(4)
for _, c := range e.Value {
hash = helpers.HashCombine(hash, uint32(c))
}
return hash, true
case *js_ast.EBigInt:
hash := uint32(5)
for _, c := range e.Value {
hash = helpers.HashCombine(hash, uint32(c))
}
return hash, true
case *js_ast.EIdentifier:
return helpers.HashCombine(6, e.Ref.InnerIndex), true
case *js_ast.EDot:
if target, ok := duplicateCaseHash(e.Target); ok {
return helpers.HashCombineString(helpers.HashCombine(7, target), e.Name), true
}
case *js_ast.EIndex:
if target, ok := duplicateCaseHash(e.Target); ok {
if index, ok := duplicateCaseHash(e.Index); ok {
return helpers.HashCombine(helpers.HashCombine(8, target), index), true
}
}
}
return 0, false
}
func duplicateCaseEquals(left js_ast.Expr, right js_ast.Expr) (equals bool, couldBeIncorrect bool) {
if b, ok := right.Data.(*js_ast.EInlinedEnum); ok {
return duplicateCaseEquals(left, b.Value)
}
switch a := left.Data.(type) {
case *js_ast.EInlinedEnum:
return duplicateCaseEquals(a.Value, right)
case *js_ast.ENull:
_, ok := right.Data.(*js_ast.ENull)
return ok, false
case *js_ast.EUndefined:
_, ok := right.Data.(*js_ast.EUndefined)
return ok, false
case *js_ast.EBoolean:
b, ok := right.Data.(*js_ast.EBoolean)
return ok && a.Value == b.Value, false
case *js_ast.ENumber:
b, ok := right.Data.(*js_ast.ENumber)
return ok && a.Value == b.Value, false
case *js_ast.EString:
b, ok := right.Data.(*js_ast.EString)
return ok && helpers.UTF16EqualsUTF16(a.Value, b.Value), false
case *js_ast.EBigInt:
if b, ok := right.Data.(*js_ast.EBigInt); ok {
equal, ok := js_ast.CheckEqualityBigInt(a.Value, b.Value)
return ok && equal, false
}
case *js_ast.EIdentifier:
b, ok := right.Data.(*js_ast.EIdentifier)
return ok && a.Ref == b.Ref, false
case *js_ast.EDot:
if b, ok := right.Data.(*js_ast.EDot); ok && a.OptionalChain == b.OptionalChain && a.Name == b.Name {
equals, _ := duplicateCaseEquals(a.Target, b.Target)
return equals, true
}
case *js_ast.EIndex:
if b, ok := right.Data.(*js_ast.EIndex); ok && a.OptionalChain == b.OptionalChain {
if equals, _ := duplicateCaseEquals(a.Index, b.Index); equals {
equals, _ := duplicateCaseEquals(a.Target, b.Target)
return equals, true
}
}
}
return false, false
}
type duplicatePropertiesIn uint8
const (
duplicatePropertiesInObject duplicatePropertiesIn = iota
duplicatePropertiesInClass
)
func (p *parser) warnAboutDuplicateProperties(properties []js_ast.Property, in duplicatePropertiesIn) {
if len(properties) < 2 {
return
}
type keyKind uint8
type existingKey struct {
loc logger.Loc
kind keyKind
}
const (
keyMissing keyKind = iota
keyNormal
keyGet
keySet
keyGetAndSet
)
instanceKeys := make(map[string]existingKey)
staticKeys := make(map[string]existingKey)
for _, property := range properties {
if property.Kind != js_ast.PropertySpread {
if str, ok := property.Key.Data.(*js_ast.EString); ok {
var keys map[string]existingKey
if property.Flags.Has(js_ast.PropertyIsStatic) {
keys = staticKeys
} else {
keys = instanceKeys
}
key := helpers.UTF16ToString(str.Value)
prevKey := keys[key]
nextKey := existingKey{kind: keyNormal, loc: property.Key.Loc}
if property.Kind == js_ast.PropertyGetter {
nextKey.kind = keyGet
} else if property.Kind == js_ast.PropertySetter {
nextKey.kind = keySet
}
if prevKey.kind != keyMissing && (in != duplicatePropertiesInObject || key != "__proto__") && (in != duplicatePropertiesInClass || key != "constructor") {
if (prevKey.kind == keyGet && nextKey.kind == keySet) || (prevKey.kind == keySet && nextKey.kind == keyGet) {
nextKey.kind = keyGetAndSet
} else {
var id logger.MsgID
var what string
var where string
switch in {
case duplicatePropertiesInObject:
id = logger.MsgID_JS_DuplicateObjectKey
what = "key"
where = "object literal"
case duplicatePropertiesInClass:
id = logger.MsgID_JS_DuplicateClassMember
what = "member"
where = "class body"
}
r := js_lexer.RangeOfIdentifier(p.source, property.Key.Loc)
p.log.AddIDWithNotes(id, logger.Warning, &p.tracker, r,
fmt.Sprintf("Duplicate %s %q in %s", what, key, where),
[]logger.MsgData{p.tracker.MsgData(js_lexer.RangeOfIdentifier(p.source, prevKey.loc),
fmt.Sprintf("The original %s %q is here:", what, key))})
}
}
keys[key] = nextKey
}
}
}
}
func isJumpStatement(data js_ast.S) bool {
switch data.(type) {
case *js_ast.SBreak, *js_ast.SContinue, *js_ast.SReturn, *js_ast.SThrow:
return true
}
return false
}
func jumpStmtsLookTheSame(left js_ast.S, right js_ast.S) bool {
switch a := left.(type) {
case *js_ast.SBreak:
b, ok := right.(*js_ast.SBreak)
return ok && (a.Label == nil) == (b.Label == nil) && (a.Label == nil || a.Label.Ref == b.Label.Ref)
case *js_ast.SContinue:
b, ok := right.(*js_ast.SContinue)
return ok && (a.Label == nil) == (b.Label == nil) && (a.Label == nil || a.Label.Ref == b.Label.Ref)
case *js_ast.SReturn:
b, ok := right.(*js_ast.SReturn)
return ok && (a.ValueOrNil.Data == nil) == (b.ValueOrNil.Data == nil) &&
(a.ValueOrNil.Data == nil || js_ast.ValuesLookTheSame(a.ValueOrNil.Data, b.ValueOrNil.Data))
case *js_ast.SThrow:
b, ok := right.(*js_ast.SThrow)
return ok && js_ast.ValuesLookTheSame(a.Value.Data, b.Value.Data)
}
return false
}
func (p *parser) selectLocalKind(kind js_ast.LocalKind) js_ast.LocalKind {
// Use "var" instead of "let" and "const" if they aren't supported
if (kind == js_ast.LocalLet || kind == js_ast.LocalConst) && p.options.unsupportedJSFeatures.Has(compat.ConstAndLet) {
return js_ast.LocalVar
}
// Use "var" instead of "let" and "const" if the variable declaration may
// need to be separated from the initializer. This allows us to safely move
// this declaration into a nested scope.
if p.currentScope.Parent == nil && (kind == js_ast.LocalLet || kind == js_ast.LocalConst) &&
(p.options.mode == config.ModeBundle || p.willWrapModuleInTryCatchForUsing) {
return js_ast.LocalVar
}
// Optimization: use "let" instead of "const" because it's shorter. This is
// only done when bundling because assigning to "const" is only an error when
// bundling.
if p.options.mode == config.ModeBundle && kind == js_ast.LocalConst && p.options.minifySyntax {
return js_ast.LocalLet
}
return kind
}
func (p *parser) pushScopeForParsePass(kind js_ast.ScopeKind, loc logger.Loc) int {
parent := p.currentScope
scope := &js_ast.Scope{
Kind: kind,
Parent: parent,
Members: make(map[string]js_ast.ScopeMember),
Label: ast.LocRef{Ref: ast.InvalidRef},
}
if parent != nil {
parent.Children = append(parent.Children, scope)
scope.StrictMode = parent.StrictMode
scope.UseStrictLoc = parent.UseStrictLoc
}
p.currentScope = scope
// Enforce that scope locations are strictly increasing to help catch bugs
// where the pushed scopes are mismatched between the first and second passes
if len(p.scopesInOrder) > 0 {
prevStart := p.scopesInOrder[len(p.scopesInOrder)-1].loc.Start
if prevStart >= loc.Start {
panic(fmt.Sprintf("Scope location %d must be greater than %d", loc.Start, prevStart))
}
}
// Copy down function arguments into the function body scope. That way we get
// errors if a statement in the function body tries to re-declare any of the
// arguments.
if kind == js_ast.ScopeFunctionBody {
if scope.Parent.Kind != js_ast.ScopeFunctionArgs {
panic("Internal error")
}
for name, member := range scope.Parent.Members {
// Don't copy down the optional function expression name. Re-declaring
// the name of a function expression is allowed.
kind := p.symbols[member.Ref.InnerIndex].Kind
if kind != ast.SymbolHoistedFunction {
scope.Members[name] = member
}
}
}
// Remember the length in case we call popAndDiscardScope() later
scopeIndex := len(p.scopesInOrder)
p.scopesInOrder = append(p.scopesInOrder, scopeOrder{loc: loc, scope: scope})
return scopeIndex
}
func (p *parser) popScope() {
// We cannot rename anything inside a scope containing a direct eval() call
if p.currentScope.ContainsDirectEval {
for _, member := range p.currentScope.Members {
// Using direct eval when bundling is not a good idea in general because
// esbuild must assume that it can potentially reach anything in any of
// the containing scopes. We try to make it work but this isn't possible
// in some cases.
//
// For example, symbols imported using an ESM import are a live binding
// to the underlying symbol in another file. This is emulated during
// scope hoisting by erasing the ESM import and just referencing the
// underlying symbol in the flattened bundle directly. However, that
// symbol may have a different name which could break uses of direct
// eval:
//
// // Before bundling
// import { foo as bar } from './foo.js'
// console.log(eval('bar'))
//
// // After bundling
// let foo = 123 // The contents of "foo.js"
// console.log(eval('bar'))
//
// There really isn't any way to fix this. You can't just rename "foo" to
// "bar" in the example above because there may be a third bundled file
// that also contains direct eval and imports the same symbol with a
// different conflicting import alias. And there is no way to store a
// live binding to the underlying symbol in a variable with the import's
// name so that direct eval can access it:
//
// // After bundling
// let foo = 123 // The contents of "foo.js"
// const bar = /* cannot express a live binding to "foo" here */
// console.log(eval('bar'))
//
// Technically a "with" statement could potentially make this work (with
// a big hit to performance), but they are deprecated and are unavailable
// in strict mode. This is a non-starter since all ESM code is strict mode.
//
// So while we still try to obey the requirement that all symbol names are
// pinned when direct eval is present, we make an exception for top-level
// symbols in an ESM file when bundling is enabled. We make no guarantee
// that "eval" will be able to reach these symbols and we allow them to be
// renamed or removed by tree shaking.
if p.options.mode == config.ModeBundle && p.currentScope.Parent == nil && p.isFileConsideredESM {
continue
}
p.symbols[member.Ref.InnerIndex].Flags |= ast.MustNotBeRenamed
}
}
p.currentScope = p.currentScope.Parent
}
func (p *parser) popAndDiscardScope(scopeIndex int) {
// Unwind any newly-added scopes in reverse order
for i := len(p.scopesInOrder) - 1; i >= scopeIndex; i-- {
scope := p.scopesInOrder[i].scope
parent := scope.Parent
last := len(parent.Children) - 1
if parent.Children[last] != scope {
panic("Internal error")
}
parent.Children = parent.Children[:last]
}
// Move up to the parent scope
p.currentScope = p.currentScope.Parent
// Truncate the scope order where we started to pretend we never saw this scope
p.scopesInOrder = p.scopesInOrder[:scopeIndex]
}
func (p *parser) popAndFlattenScope(scopeIndex int) {
// Move up to the parent scope
toFlatten := p.currentScope
parent := toFlatten.Parent
p.currentScope = parent
// Erase this scope from the order. This will shift over the indices of all
// the scopes that were created after us. However, we shouldn't have to
// worry about other code with outstanding scope indices for these scopes.
// These scopes were all created in between this scope's push and pop
// operations, so they should all be child scopes and should all be popped
// by the time we get here.
copy(p.scopesInOrder[scopeIndex:], p.scopesInOrder[scopeIndex+1:])
p.scopesInOrder = p.scopesInOrder[:len(p.scopesInOrder)-1]
// Remove the last child from the parent scope
last := len(parent.Children) - 1
if parent.Children[last] != toFlatten {
panic("Internal error")
}
parent.Children = parent.Children[:last]
// Reparent our child scopes into our parent
for _, scope := range toFlatten.Children {
scope.Parent = parent
parent.Children = append(parent.Children, scope)
}
}
// Undo all scopes pushed and popped after this scope index. This assumes that
// the scope stack is at the same level now as it was at the given scope index.
func (p *parser) discardScopesUpTo(scopeIndex int) {
// Remove any direct children from their parent
children := p.currentScope.Children
for _, child := range p.scopesInOrder[scopeIndex:] {
if child.scope.Parent == p.currentScope {
for i := len(children) - 1; i >= 0; i-- {
if children[i] == child.scope {
children = append(children[:i], children[i+1:]...)
break
}
}
}
}
p.currentScope.Children = children
// Truncate the scope order where we started to pretend we never saw this scope
p.scopesInOrder = p.scopesInOrder[:scopeIndex]
}
func (p *parser) newSymbol(kind ast.SymbolKind, name string) ast.Ref {
ref := ast.Ref{SourceIndex: p.source.Index, InnerIndex: uint32(len(p.symbols))}
p.symbols = append(p.symbols, ast.Symbol{
Kind: kind,
OriginalName: name,
Link: ast.InvalidRef,
})
if p.options.ts.Parse {
p.tsUseCounts = append(p.tsUseCounts, 0)
}
return ref
}
// This is similar to "ast.MergeSymbols" but it works with this parser's
// one-level symbol map instead of the linker's two-level symbol map. It also
// doesn't handle cycles since they shouldn't come up due to the way this
// function is used.
func (p *parser) mergeSymbols(old ast.Ref, new ast.Ref) ast.Ref {
if old == new {
return new
}
oldSymbol := &p.symbols[old.InnerIndex]
if oldSymbol.Link != ast.InvalidRef {
oldSymbol.Link = p.mergeSymbols(oldSymbol.Link, new)
return oldSymbol.Link
}
newSymbol := &p.symbols[new.InnerIndex]
if newSymbol.Link != ast.InvalidRef {
newSymbol.Link = p.mergeSymbols(old, newSymbol.Link)
return newSymbol.Link
}
oldSymbol.Link = new
newSymbol.MergeContentsWith(oldSymbol)
return new
}
type mergeResult int
const (
mergeForbidden = iota
mergeReplaceWithNew
mergeOverwriteWithNew
mergeKeepExisting
mergeBecomePrivateGetSetPair
mergeBecomePrivateStaticGetSetPair
)
func (p *parser) canMergeSymbols(scope *js_ast.Scope, existing ast.SymbolKind, new ast.SymbolKind) mergeResult {
if existing == ast.SymbolUnbound {
return mergeReplaceWithNew
}
// In TypeScript, imports are allowed to silently collide with symbols within
// the module. Presumably this is because the imports may be type-only:
//
// import {Foo} from 'bar'
// class Foo {}
//
if p.options.ts.Parse && existing == ast.SymbolImport {
return mergeReplaceWithNew
}
// "enum Foo {} enum Foo {}"
if new == ast.SymbolTSEnum && existing == ast.SymbolTSEnum {
return mergeKeepExisting
}
// "namespace Foo { ... } enum Foo {}"
if new == ast.SymbolTSEnum && existing == ast.SymbolTSNamespace {
return mergeReplaceWithNew
}
// "namespace Foo { ... } namespace Foo { ... }"
// "function Foo() {} namespace Foo { ... }"
// "enum Foo {} namespace Foo { ... }"
if new == ast.SymbolTSNamespace {
switch existing {
case ast.SymbolTSNamespace, ast.SymbolHoistedFunction, ast.SymbolGeneratorOrAsyncFunction, ast.SymbolTSEnum, ast.SymbolClass:
return mergeKeepExisting
}
}
// "var foo; var foo;"
// "var foo; function foo() {}"
// "function foo() {} var foo;"
// "function *foo() {} function *foo() {}" but not "{ function *foo() {} function *foo() {} }"
if new.IsHoistedOrFunction() && existing.IsHoistedOrFunction() &&
(scope.Kind == js_ast.ScopeEntry ||
scope.Kind == js_ast.ScopeFunctionBody ||
scope.Kind == js_ast.ScopeFunctionArgs ||
(new == existing && new.IsHoisted())) {
return mergeReplaceWithNew
}
// "get #foo() {} set #foo() {}"
// "set #foo() {} get #foo() {}"
if (existing == ast.SymbolPrivateGet && new == ast.SymbolPrivateSet) ||
(existing == ast.SymbolPrivateSet && new == ast.SymbolPrivateGet) {
return mergeBecomePrivateGetSetPair
}
if (existing == ast.SymbolPrivateStaticGet && new == ast.SymbolPrivateStaticSet) ||
(existing == ast.SymbolPrivateStaticSet && new == ast.SymbolPrivateStaticGet) {
return mergeBecomePrivateStaticGetSetPair
}
// "try {} catch (e) { var e }"
if existing == ast.SymbolCatchIdentifier && new == ast.SymbolHoisted {
return mergeReplaceWithNew
}
// "function() { var arguments }"
if existing == ast.SymbolArguments && new == ast.SymbolHoisted {
return mergeKeepExisting
}
// "function() { let arguments }"
if existing == ast.SymbolArguments && new != ast.SymbolHoisted {
return mergeOverwriteWithNew
}
return mergeForbidden
}
func (p *parser) addSymbolAlreadyDeclaredError(name string, newLoc logger.Loc, oldLoc logger.Loc) {
p.log.AddErrorWithNotes(&p.tracker,
js_lexer.RangeOfIdentifier(p.source, newLoc),
fmt.Sprintf("The symbol %q has already been declared", name),
[]logger.MsgData{p.tracker.MsgData(
js_lexer.RangeOfIdentifier(p.source, oldLoc),
fmt.Sprintf("The symbol %q was originally declared here:", name),
)},
)
}
func (p *parser) declareSymbol(kind ast.SymbolKind, loc logger.Loc, name string) ast.Ref {
p.checkForUnrepresentableIdentifier(loc, name)
// Allocate a new symbol
ref := p.newSymbol(kind, name)
// Check for a collision in the declaring scope
if existing, ok := p.currentScope.Members[name]; ok {
symbol := &p.symbols[existing.Ref.InnerIndex]
switch p.canMergeSymbols(p.currentScope, symbol.Kind, kind) {
case mergeForbidden:
p.addSymbolAlreadyDeclaredError(name, loc, existing.Loc)
return existing.Ref
case mergeKeepExisting:
ref = existing.Ref
case mergeReplaceWithNew:
symbol.Link = ref
p.currentScope.Replaced = append(p.currentScope.Replaced, existing)
// If these are both functions, remove the overwritten declaration
if p.options.minifySyntax && kind.IsFunction() && symbol.Kind.IsFunction() {
symbol.Flags |= ast.RemoveOverwrittenFunctionDeclaration
}
case mergeBecomePrivateGetSetPair:
ref = existing.Ref
symbol.Kind = ast.SymbolPrivateGetSetPair
case mergeBecomePrivateStaticGetSetPair:
ref = existing.Ref
symbol.Kind = ast.SymbolPrivateStaticGetSetPair
case mergeOverwriteWithNew:
}
}
// Overwrite this name in the declaring scope
p.currentScope.Members[name] = js_ast.ScopeMember{Ref: ref, Loc: loc}
return ref
}
// This type is just so we can use Go's native sort function
type scopeMemberArray []js_ast.ScopeMember
func (a scopeMemberArray) Len() int { return len(a) }
func (a scopeMemberArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
func (a scopeMemberArray) Less(i int, j int) bool {
ai := a[i].Ref
bj := a[j].Ref
return ai.InnerIndex < bj.InnerIndex || (ai.InnerIndex == bj.InnerIndex && ai.SourceIndex < bj.SourceIndex)
}
func (p *parser) hoistSymbols(scope *js_ast.Scope) {
// Duplicate function declarations are forbidden in nested blocks in strict
// mode. Separately, they are also forbidden at the top-level of modules.
// This check needs to be delayed until now instead of being done when the
// functions are declared because we potentially need to scan the whole file
// to know if the file is considered to be in strict mode (or is considered
// to be a module). We might only encounter an "export {}" clause at the end
// of the file.
if (scope.StrictMode != js_ast.SloppyMode && scope.Kind == js_ast.ScopeBlock) || (scope.Parent == nil && p.isFileConsideredESM) {
for _, replaced := range scope.Replaced {
symbol := &p.symbols[replaced.Ref.InnerIndex]
if symbol.Kind.IsFunction() {
if member, ok := scope.Members[symbol.OriginalName]; ok && p.symbols[member.Ref.InnerIndex].Kind.IsFunction() {
var notes []logger.MsgData
if scope.Parent == nil && p.isFileConsideredESM {
_, notes = p.whyESModule()
notes[0].Text = fmt.Sprintf("Duplicate top-level function declarations are not allowed in an ECMAScript module. %s", notes[0].Text)
} else {
var where string
where, notes = p.whyStrictMode(scope)
notes[0].Text = fmt.Sprintf("Duplicate function declarations are not allowed in nested blocks %s. %s", where, notes[0].Text)
}
p.log.AddErrorWithNotes(&p.tracker,
js_lexer.RangeOfIdentifier(p.source, member.Loc),
fmt.Sprintf("The symbol %q has already been declared", symbol.OriginalName),
append([]logger.MsgData{p.tracker.MsgData(
js_lexer.RangeOfIdentifier(p.source, replaced.Loc),
fmt.Sprintf("The symbol %q was originally declared here:", symbol.OriginalName),
)}, notes...),
)
}
}
}
}
if !scope.Kind.StopsHoisting() {
// We create new symbols in the loop below, so the iteration order of the
// loop must be deterministic to avoid generating different minified names
sortedMembers := make(scopeMemberArray, 0, len(scope.Members))
for _, member := range scope.Members {
sortedMembers = append(sortedMembers, member)
}
sort.Sort(sortedMembers)
nextMember:
for _, member := range sortedMembers {
symbol := &p.symbols[member.Ref.InnerIndex]
// Handle non-hoisted collisions between catch bindings and the catch body.
// This implements "B.3.4 VariableStatements in Catch Blocks" from Annex B
// of the ECMAScript standard version 6+ (except for the hoisted case, which
// is handled later on below):
//
// * It is a Syntax Error if any element of the BoundNames of CatchParameter
// also occurs in the LexicallyDeclaredNames of Block.
//
// * It is a Syntax Error if any element of the BoundNames of CatchParameter
// also occurs in the VarDeclaredNames of Block unless CatchParameter is
// CatchParameter : BindingIdentifier .
//
if scope.Parent.Kind == js_ast.ScopeCatchBinding && symbol.Kind != ast.SymbolHoisted {
if existingMember, ok := scope.Parent.Members[symbol.OriginalName]; ok {
p.addSymbolAlreadyDeclaredError(symbol.OriginalName, member.Loc, existingMember.Loc)
continue
}
}
if !symbol.Kind.IsHoisted() {
continue
}
// Implement "Block-Level Function Declarations Web Legacy Compatibility
// Semantics" from Annex B of the ECMAScript standard version 6+
isSloppyModeBlockLevelFnStmt := false
originalMemberRef := member.Ref
if symbol.Kind == ast.SymbolHoistedFunction {
// Block-level function declarations behave like "let" in strict mode
if scope.StrictMode != js_ast.SloppyMode {
continue
}
// In sloppy mode, block level functions behave like "let" except with
// an assignment to "var", sort of. This code:
//
// if (x) {
// f();
// function f() {}
// }
// f();
//
// behaves like this code:
//
// if (x) {
// let f2 = function() {}
// var f = f2;
// f2();
// }
// f();
//
hoistedRef := p.newSymbol(ast.SymbolHoisted, symbol.OriginalName)
scope.Generated = append(scope.Generated, hoistedRef)
if p.hoistedRefForSloppyModeBlockFn == nil {
p.hoistedRefForSloppyModeBlockFn = make(map[ast.Ref]ast.Ref)
}
p.hoistedRefForSloppyModeBlockFn[member.Ref] = hoistedRef
symbol = &p.symbols[hoistedRef.InnerIndex]
member.Ref = hoistedRef
isSloppyModeBlockLevelFnStmt = true
}
// Check for collisions that would prevent to hoisting "var" symbols up to the enclosing function scope
s := scope.Parent
for {
// Variable declarations hoisted past a "with" statement may actually end
// up overwriting a property on the target of the "with" statement instead
// of initializing the variable. We must not rename them or we risk
// causing a behavior change.
//
// var obj = { foo: 1 }
// with (obj) { var foo = 2 }
// assert(foo === undefined)
// assert(obj.foo === 2)
//
if s.Kind == js_ast.ScopeWith {
symbol.Flags |= ast.MustNotBeRenamed
}
if existingMember, ok := s.Members[symbol.OriginalName]; ok {
existingSymbol := &p.symbols[existingMember.Ref.InnerIndex]
// We can hoist the symbol from the child scope into the symbol in
// this scope if:
//
// - The symbol is unbound (i.e. a global variable access)
// - The symbol is also another hoisted variable
// - The symbol is a function of any kind and we're in a function or module scope
//
// Is this unbound (i.e. a global access) or also hoisted?
if existingSymbol.Kind == ast.SymbolUnbound || existingSymbol.Kind == ast.SymbolHoisted ||
(existingSymbol.Kind.IsFunction() && (s.Kind == js_ast.ScopeEntry || s.Kind == js_ast.ScopeFunctionBody)) {
// Silently merge this symbol into the existing symbol
symbol.Link = existingMember.Ref
s.Members[symbol.OriginalName] = existingMember
continue nextMember
}
// Otherwise if this isn't a catch identifier or "arguments", it's a collision
if existingSymbol.Kind != ast.SymbolCatchIdentifier && existingSymbol.Kind != ast.SymbolArguments {
// An identifier binding from a catch statement and a function
// declaration can both silently shadow another hoisted symbol
if symbol.Kind != ast.SymbolCatchIdentifier && symbol.Kind != ast.SymbolHoistedFunction {
if !isSloppyModeBlockLevelFnStmt {
p.addSymbolAlreadyDeclaredError(symbol.OriginalName, member.Loc, existingMember.Loc)
} else if s == scope.Parent {
// Never mind about this, turns out it's not needed after all
delete(p.hoistedRefForSloppyModeBlockFn, originalMemberRef)
}
}
continue nextMember
}
// If this is a catch identifier, silently merge the existing symbol
// into this symbol but continue hoisting past this catch scope
existingSymbol.Link = member.Ref
s.Members[symbol.OriginalName] = member
}
if s.Kind.StopsHoisting() {
// Declare the member in the scope that stopped the hoisting
s.Members[symbol.OriginalName] = member
break
}
s = s.Parent
}
}
}
for _, child := range scope.Children {
p.hoistSymbols(child)
}
}
func (p *parser) declareBinding(kind ast.SymbolKind, binding js_ast.Binding, opts parseStmtOpts) {
js_ast.ForEachIdentifierBinding(binding, func(loc logger.Loc, b *js_ast.BIdentifier) {
if !opts.isTypeScriptDeclare || (opts.isNamespaceScope && opts.isExport) {
b.Ref = p.declareSymbol(kind, loc, p.loadNameFromRef(b.Ref))
}
})
}
func (p *parser) recordUsage(ref ast.Ref) {
// The use count stored in the symbol is used for generating symbol names
// during minification. These counts shouldn't include references inside dead
// code regions since those will be culled.
if !p.isControlFlowDead {
p.symbols[ref.InnerIndex].UseCountEstimate++
if part := p.currentPart; part != nil {
use := part.SymbolUses[ref]
use.CountEstimate++
part.SymbolUses[ref] = use
}
}
// The correctness of TypeScript-to-JavaScript conversion relies on accurate
// symbol use counts for the whole file, including dead code regions. This is
// tracked separately in a parser-only data structure.
if p.options.ts.Parse {
p.tsUseCounts[ref.InnerIndex]++
}
}
func (p *parser) ignoreUsage(ref ast.Ref) {
// Roll back the use count increment in recordUsage()
if !p.isControlFlowDead {
p.symbols[ref.InnerIndex].UseCountEstimate--
// Only remove this usage if we're currently processing a part
if part := p.currentPart; part != nil {
use := part.SymbolUses[ref]
use.CountEstimate--
if use.CountEstimate == 0 {
delete(part.SymbolUses, ref)
} else {
part.SymbolUses[ref] = use
}
}
}
// Don't roll back the "tsUseCounts" increment. This must be counted even if
// the value is ignored because that's what the TypeScript compiler does.
}
func (p *parser) ignoreUsageOfIdentifierInDotChain(expr js_ast.Expr) {
for {
switch e := expr.Data.(type) {
case *js_ast.EIdentifier:
p.ignoreUsage(e.Ref)
case *js_ast.EDot:
expr = e.Target
continue
case *js_ast.EIndex:
if _, ok := e.Index.Data.(*js_ast.EString); ok {
expr = e.Target
continue
}
}
return
}
}
func (p *parser) importFromRuntime(loc logger.Loc, name string) js_ast.Expr {
it, ok := p.runtimeImports[name]
if !ok {
it.Loc = loc
it.Ref = p.newSymbol(ast.SymbolOther, name)
p.moduleScope.Generated = append(p.moduleScope.Generated, it.Ref)
p.runtimeImports[name] = it
}
p.recordUsage(it.Ref)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: it.Ref}}
}
func (p *parser) callRuntime(loc logger.Loc, name string, args []js_ast.Expr) js_ast.Expr {
return js_ast.Expr{Loc: loc, Data: &js_ast.ECall{
Target: p.importFromRuntime(loc, name),
Args: args,
}}
}
type JSXImport uint8
const (
JSXImportJSX JSXImport = iota
JSXImportJSXS
JSXImportFragment
JSXImportCreateElement
)
func (p *parser) importJSXSymbol(loc logger.Loc, jsx JSXImport) js_ast.Expr {
var symbols map[string]ast.LocRef
var name string
switch jsx {
case JSXImportJSX:
symbols = p.jsxRuntimeImports
if p.options.jsx.Development {
name = "jsxDEV"
} else {
name = "jsx"
}
case JSXImportJSXS:
symbols = p.jsxRuntimeImports
if p.options.jsx.Development {
name = "jsxDEV"
} else {
name = "jsxs"
}
case JSXImportFragment:
symbols = p.jsxRuntimeImports
name = "Fragment"
case JSXImportCreateElement:
symbols = p.jsxLegacyImports
name = "createElement"
}
it, ok := symbols[name]
if !ok {
it.Loc = loc
it.Ref = p.newSymbol(ast.SymbolOther, name)
p.moduleScope.Generated = append(p.moduleScope.Generated, it.Ref)
p.isImportItem[it.Ref] = true
symbols[name] = it
}
p.recordUsage(it.Ref)
return p.handleIdentifier(loc, &js_ast.EIdentifier{Ref: it.Ref}, identifierOpts{
wasOriginallyIdentifier: true,
})
}
func (p *parser) valueToSubstituteForRequire(loc logger.Loc) js_ast.Expr {
if p.source.Index != runtime.SourceIndex &&
config.ShouldCallRuntimeRequire(p.options.mode, p.options.outputFormat) {
return p.importFromRuntime(loc, "__require")
}
p.recordUsage(p.requireRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: p.requireRef}}
}
func (p *parser) makePromiseRef() ast.Ref {
if p.promiseRef == ast.InvalidRef {
p.promiseRef = p.newSymbol(ast.SymbolUnbound, "Promise")
}
return p.promiseRef
}
func (p *parser) makeRegExpRef() ast.Ref {
if p.regExpRef == ast.InvalidRef {
p.regExpRef = p.newSymbol(ast.SymbolUnbound, "RegExp")
p.moduleScope.Generated = append(p.moduleScope.Generated, p.regExpRef)
}
return p.regExpRef
}
func (p *parser) makeBigIntRef() ast.Ref {
if p.bigIntRef == ast.InvalidRef {
p.bigIntRef = p.newSymbol(ast.SymbolUnbound, "BigInt")
p.moduleScope.Generated = append(p.moduleScope.Generated, p.bigIntRef)
}
return p.bigIntRef
}
// The name is temporarily stored in the ref until the scope traversal pass
// happens, at which point a symbol will be generated and the ref will point
// to the symbol instead.
//
// The scope traversal pass will reconstruct the name using one of two methods.
// In the common case, the name is a slice of the file itself. In that case we
// can just store the slice and not need to allocate any extra memory. In the
// rare case, the name is an externally-allocated string. In that case we store
// an index to the string and use that index during the scope traversal pass.
func (p *parser) storeNameInRef(name js_lexer.MaybeSubstring) ast.Ref {
// Is the data in "name" a subset of the data in "p.source.Contents"?
if name.Start.IsValid() {
// The name is a slice of the file contents, so we can just reference it by
// length and don't have to allocate anything. This is the common case.
//
// It's stored as a negative value so we'll crash if we try to use it. That
// way we'll catch cases where we've forgotten to call loadNameFromRef().
// The length is the negative part because we know it's non-zero.
return ast.Ref{SourceIndex: -uint32(len(name.String)), InnerIndex: uint32(name.Start.GetIndex())}
} else {
// The name is some memory allocated elsewhere. This is either an inline
// string constant in the parser or an identifier with escape sequences
// in the source code, which is very unusual. Stash it away for later.
// This uses allocations but it should hopefully be very uncommon.
ref := ast.Ref{SourceIndex: 0x80000000, InnerIndex: uint32(len(p.allocatedNames))}
p.allocatedNames = append(p.allocatedNames, name.String)
return ref
}
}
// This is the inverse of storeNameInRef() above
func (p *parser) loadNameFromRef(ref ast.Ref) string {
if ref.SourceIndex == 0x80000000 {
return p.allocatedNames[ref.InnerIndex]
} else {
if (ref.SourceIndex & 0x80000000) == 0 {
panic("Internal error: invalid symbol reference")
}
return p.source.Contents[ref.InnerIndex : int32(ref.InnerIndex)-int32(ref.SourceIndex)]
}
}
// Due to ES6 destructuring patterns, there are many cases where it's
// impossible to distinguish between an array or object literal and a
// destructuring assignment until we hit the "=" operator later on.
// This object defers errors about being in one state or the other
// until we discover which state we're in.
type deferredErrors struct {
// These are errors for expressions
invalidExprDefaultValue logger.Range
invalidExprAfterQuestion logger.Range
arraySpreadFeature logger.Range
// These errors are for arrow functions
invalidParens []logger.Range
}
func (from *deferredErrors) mergeInto(to *deferredErrors) {
if from.invalidExprDefaultValue.Len > 0 {
to.invalidExprDefaultValue = from.invalidExprDefaultValue
}
if from.invalidExprAfterQuestion.Len > 0 {
to.invalidExprAfterQuestion = from.invalidExprAfterQuestion
}
if from.arraySpreadFeature.Len > 0 {
to.arraySpreadFeature = from.arraySpreadFeature
}
if len(from.invalidParens) > 0 {
if len(to.invalidParens) > 0 {
to.invalidParens = append(to.invalidParens, from.invalidParens...)
} else {
to.invalidParens = from.invalidParens
}
}
}
func (p *parser) logExprErrors(errors *deferredErrors) {
if errors.invalidExprDefaultValue.Len > 0 {
p.log.AddError(&p.tracker, errors.invalidExprDefaultValue, "Unexpected \"=\"")
}
if errors.invalidExprAfterQuestion.Len > 0 {
r := errors.invalidExprAfterQuestion
p.log.AddError(&p.tracker, r, fmt.Sprintf("Unexpected %q", p.source.Contents[r.Loc.Start:r.Loc.Start+r.Len]))
}
if errors.arraySpreadFeature.Len > 0 {
p.markSyntaxFeature(compat.ArraySpread, errors.arraySpreadFeature)
}
}
func (p *parser) logDeferredArrowArgErrors(errors *deferredErrors) {
for _, paren := range errors.invalidParens {
p.log.AddError(&p.tracker, paren, "Invalid binding pattern")
}
}
func (p *parser) logNullishCoalescingErrorPrecedenceError(op string) {
prevOp := "??"
if p.lexer.Token == js_lexer.TQuestionQuestion {
op, prevOp = prevOp, op
}
// p.log.AddError(&p.tracker, p.lexer.Range(), fmt.Sprintf("The %q operator requires parentheses"))
p.log.AddErrorWithNotes(&p.tracker, p.lexer.Range(), fmt.Sprintf("Cannot use %q with %q without parentheses", op, prevOp),
[]logger.MsgData{{Text: fmt.Sprintf("Expressions of the form \"x %s y %s z\" are not allowed in JavaScript. "+
"You must disambiguate between \"(x %s y) %s z\" and \"x %s (y %s z)\" by adding parentheses.", prevOp, op, prevOp, op, prevOp, op)}})
}
func defineValueCanBeUsedInAssignTarget(data js_ast.E) bool {
switch data.(type) {
case *js_ast.EIdentifier, *js_ast.EDot:
return true
}
// Substituting a constant into an assignment target (e.g. "x = 1" becomes
// "0 = 1") will cause a syntax error, so we avoid doing this. The caller
// will log a warning instead.
return false
}
func (p *parser) logAssignToDefine(r logger.Range, name string, expr js_ast.Expr) {
// If this is a compound expression, pretty-print it for the error message.
// We don't use a literal slice of the source text in case it contains
// problematic things (e.g. spans multiple lines, has embedded comments).
if expr.Data != nil {
var parts []string
for {
if id, ok := expr.Data.(*js_ast.EIdentifier); ok {
parts = append(parts, p.loadNameFromRef(id.Ref))
break
} else if dot, ok := expr.Data.(*js_ast.EDot); ok {
parts = append(parts, dot.Name)
parts = append(parts, ".")
expr = dot.Target
} else if index, ok := expr.Data.(*js_ast.EIndex); ok {
if str, ok := index.Index.Data.(*js_ast.EString); ok {
parts = append(parts, "]")
parts = append(parts, string(helpers.QuoteSingle(helpers.UTF16ToString(str.Value), false)))
parts = append(parts, "[")
expr = index.Target
} else {
return
}
} else {
return
}
}
for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 {
parts[i], parts[j] = parts[j], parts[i]
}
name = strings.Join(parts, "")
}
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddIDWithNotes(logger.MsgID_JS_AssignToDefine, kind, &p.tracker, r,
fmt.Sprintf("Suspicious assignment to defined constant %q", name),
[]logger.MsgData{{Text: fmt.Sprintf(
"The expression %q has been configured to be replaced with a constant using the \"define\" feature. "+
"If this expression is supposed to be a compile-time constant, then it doesn't make sense to assign to it here. "+
"Or if this expression is supposed to change at run-time, this \"define\" substitution should be removed.", name)}})
}
// The "await" and "yield" expressions are never allowed in argument lists but
// may or may not be allowed otherwise depending on the details of the enclosing
// function or module. This needs to be handled when parsing an arrow function
// argument list because we don't know if these expressions are not allowed until
// we reach the "=>" token (or discover the absence of one).
//
// Specifically, for await:
//
// // This is ok
// async function foo() { (x = await y) }
//
// // This is an error
// async function foo() { (x = await y) => {} }
//
// And for yield:
//
// // This is ok
// function* foo() { (x = yield y) }
//
// // This is an error
// function* foo() { (x = yield y) => {} }
type deferredArrowArgErrors struct {
invalidExprAwait logger.Range
invalidExprYield logger.Range
}
func (p *parser) logArrowArgErrors(errors *deferredArrowArgErrors) {
if errors.invalidExprAwait.Len > 0 {
p.log.AddError(&p.tracker, errors.invalidExprAwait, "Cannot use an \"await\" expression here:")
}
if errors.invalidExprYield.Len > 0 {
p.log.AddError(&p.tracker, errors.invalidExprYield, "Cannot use a \"yield\" expression here:")
}
}
func (p *parser) keyNameForError(key js_ast.Expr) string {
switch k := key.Data.(type) {
case *js_ast.EString:
return fmt.Sprintf("%q", helpers.UTF16ToString(k.Value))
case *js_ast.EPrivateIdentifier:
return fmt.Sprintf("%q", p.loadNameFromRef(k.Ref))
}
return "property"
}
func (p *parser) checkForLegacyOctalLiteral(e js_ast.E) {
if p.lexer.IsLegacyOctalLiteral {
if p.legacyOctalLiterals == nil {
p.legacyOctalLiterals = make(map[js_ast.E]logger.Range)
}
p.legacyOctalLiterals[e] = p.lexer.Range()
}
}
func (p *parser) notesForAssertTypeJSON(record *ast.ImportRecord, alias string) []logger.MsgData {
return []logger.MsgData{p.tracker.MsgData(
js_lexer.RangeOfImportAssertOrWith(p.source, *ast.FindAssertOrWithEntry(record.AssertOrWith.Entries, "type"), js_lexer.KeyAndValueRange),
"The JSON import assertion is here:"),
{Text: fmt.Sprintf("You can either keep the import assertion and only use the \"default\" import, "+
"or you can remove the import assertion and use the %q import.", alias)}}
}
// This assumes the caller has already checked for TStringLiteral or TNoSubstitutionTemplateLiteral
func (p *parser) parseStringLiteral() js_ast.Expr {
var legacyOctalLoc logger.Loc
loc := p.lexer.Loc()
text := p.lexer.StringLiteral()
// Enable using a "/* @__KEY__ */" comment to turn a string into a key
hasPropertyKeyComment := (p.lexer.HasCommentBefore & js_lexer.KeyCommentBefore) != 0
if hasPropertyKeyComment {
if name := helpers.UTF16ToString(text); p.isMangledProp(name) {
value := js_ast.Expr{Loc: loc, Data: &js_ast.ENameOfSymbol{
Ref: p.storeNameInRef(js_lexer.MaybeSubstring{String: name}),
HasPropertyKeyComment: true,
}}
p.lexer.Next()
return value
}
}
if p.lexer.LegacyOctalLoc.Start > loc.Start {
legacyOctalLoc = p.lexer.LegacyOctalLoc
}
value := js_ast.Expr{Loc: loc, Data: &js_ast.EString{
Value: text,
LegacyOctalLoc: legacyOctalLoc,
PreferTemplate: p.lexer.Token == js_lexer.TNoSubstitutionTemplateLiteral,
HasPropertyKeyComment: hasPropertyKeyComment,
}}
p.lexer.Next()
return value
}
func (p *parser) parseBigIntOrStringIfUnsupported() js_ast.Expr {
if p.options.unsupportedJSFeatures.Has(compat.Bigint) {
var i big.Int
fmt.Sscan(p.lexer.Identifier.String, &i)
return js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EString{Value: helpers.StringToUTF16(i.String())}}
}
return js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EBigInt{Value: p.lexer.Identifier.String}}
}
type propertyOpts struct {
decorators []js_ast.Decorator
decoratorScope *js_ast.Scope
decoratorContext decoratorContextFlags
asyncRange logger.Range
generatorRange logger.Range
tsDeclareRange logger.Range
classKeyword logger.Range
isAsync bool
isGenerator bool
// Class-related options
isStatic bool
isTSAbstract bool
isClass bool
classHasExtends bool
}
func (p *parser) parseProperty(startLoc logger.Loc, kind js_ast.PropertyKind, opts propertyOpts, errors *deferredErrors) (js_ast.Property, bool) {
var flags js_ast.PropertyFlags
var key js_ast.Expr
var closeBracketLoc logger.Loc
keyRange := p.lexer.Range()
switch p.lexer.Token {
case js_lexer.TNumericLiteral:
key = js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.ENumber{Value: p.lexer.Number}}
p.checkForLegacyOctalLiteral(key.Data)
p.lexer.Next()
case js_lexer.TStringLiteral:
key = p.parseStringLiteral()
if !p.options.minifySyntax {
flags |= js_ast.PropertyPreferQuotedKey
}
case js_lexer.TBigIntegerLiteral:
key = p.parseBigIntOrStringIfUnsupported()
p.lexer.Next()
case js_lexer.TPrivateIdentifier:
if p.options.ts.Parse && p.options.ts.Config.ExperimentalDecorators == config.True && len(opts.decorators) > 0 {
p.log.AddError(&p.tracker, p.lexer.Range(), "TypeScript experimental decorators cannot be used on private identifiers")
} else if !opts.isClass {
p.lexer.Expected(js_lexer.TIdentifier)
} else if opts.tsDeclareRange.Len != 0 {
p.log.AddError(&p.tracker, opts.tsDeclareRange, "\"declare\" cannot be used with a private identifier")
}
name := p.lexer.Identifier
key = js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EPrivateIdentifier{Ref: p.storeNameInRef(name)}}
p.reportPrivateNameUsage(name.String)
p.lexer.Next()
case js_lexer.TOpenBracket:
flags |= js_ast.PropertyIsComputed
p.markSyntaxFeature(compat.ObjectExtensions, p.lexer.Range())
p.lexer.Next()
wasIdentifier := p.lexer.Token == js_lexer.TIdentifier
expr := p.parseExpr(js_ast.LComma)
// Handle index signatures
if p.options.ts.Parse && p.lexer.Token == js_lexer.TColon && wasIdentifier && opts.isClass {
if _, ok := expr.Data.(*js_ast.EIdentifier); ok {
if opts.tsDeclareRange.Len != 0 {
p.log.AddError(&p.tracker, opts.tsDeclareRange, "\"declare\" cannot be used with an index signature")
}
// "[key: string]: any;"
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
p.lexer.Expect(js_lexer.TCloseBracket)
p.lexer.Expect(js_lexer.TColon)
p.skipTypeScriptType(js_ast.LLowest)
p.lexer.ExpectOrInsertSemicolon()
// Skip this property entirely
return js_ast.Property{}, false
}
}
closeBracketLoc = p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBracket)
key = expr
case js_lexer.TAsterisk:
if kind != js_ast.PropertyField && (kind != js_ast.PropertyMethod || opts.isGenerator) {
p.lexer.Unexpected()
}
opts.isGenerator = true
opts.generatorRange = p.lexer.Range()
p.lexer.Next()
return p.parseProperty(startLoc, js_ast.PropertyMethod, opts, errors)
default:
name := p.lexer.Identifier
raw := p.lexer.Raw()
nameRange := p.lexer.Range()
if !p.lexer.IsIdentifierOrKeyword() {
p.lexer.Expect(js_lexer.TIdentifier)
}
p.lexer.Next()
// Support contextual keywords
if kind == js_ast.PropertyField {
// Does the following token look like a key?
couldBeModifierKeyword := p.lexer.IsIdentifierOrKeyword()
if !couldBeModifierKeyword {
switch p.lexer.Token {
case js_lexer.TOpenBracket, js_lexer.TNumericLiteral, js_lexer.TStringLiteral, js_lexer.TPrivateIdentifier:
couldBeModifierKeyword = true
case js_lexer.TAsterisk:
if opts.isAsync || (raw != "get" && raw != "set") {
couldBeModifierKeyword = true
}
}
}
// If so, check for a modifier keyword
if couldBeModifierKeyword {
switch raw {
case "get":
if !opts.isAsync {
p.markSyntaxFeature(compat.ObjectAccessors, nameRange)
return p.parseProperty(startLoc, js_ast.PropertyGetter, opts, nil)
}
case "set":
if !opts.isAsync {
p.markSyntaxFeature(compat.ObjectAccessors, nameRange)
return p.parseProperty(startLoc, js_ast.PropertySetter, opts, nil)
}
case "accessor":
if !p.lexer.HasNewlineBefore && !opts.isAsync && opts.isClass {
return p.parseProperty(startLoc, js_ast.PropertyAutoAccessor, opts, nil)
}
case "async":
if !p.lexer.HasNewlineBefore && !opts.isAsync {
opts.isAsync = true
opts.asyncRange = nameRange
return p.parseProperty(startLoc, js_ast.PropertyMethod, opts, nil)
}
case "static":
if !opts.isStatic && !opts.isAsync && opts.isClass {
opts.isStatic = true
return p.parseProperty(startLoc, kind, opts, nil)
}
case "declare":
if !p.lexer.HasNewlineBefore && opts.isClass && p.options.ts.Parse && opts.tsDeclareRange.Len == 0 {
opts.tsDeclareRange = nameRange
scopeIndex := len(p.scopesInOrder)
if prop, ok := p.parseProperty(startLoc, kind, opts, nil); ok &&
prop.Kind == js_ast.PropertyField && prop.ValueOrNil.Data == nil &&
(p.options.ts.Config.ExperimentalDecorators == config.True && len(opts.decorators) > 0) {
// If this is a well-formed class field with the "declare" keyword,
// only keep the declaration to preserve its side-effects when
// there are TypeScript experimental decorators present:
//
// class Foo {
// // Remove this
// declare [(console.log('side effect 1'), 'foo')]
//
// // Keep this
// @decorator(console.log('side effect 2')) declare bar
// }
//
// This behavior is surprisingly somehow valid with TypeScript
// experimental decorators, which was possibly by accident.
// TypeScript does not allow this with JavaScript decorators.
//
// References:
//
// https://github.com/evanw/esbuild/issues/1675
// https://github.com/microsoft/TypeScript/issues/46345
//
prop.Kind = js_ast.PropertyDeclareOrAbstract
return prop, true
}
p.discardScopesUpTo(scopeIndex)
return js_ast.Property{}, false
}
case "abstract":
if !p.lexer.HasNewlineBefore && opts.isClass && p.options.ts.Parse && !opts.isTSAbstract {
opts.isTSAbstract = true
scopeIndex := len(p.scopesInOrder)
if prop, ok := p.parseProperty(startLoc, kind, opts, nil); ok &&
prop.Kind == js_ast.PropertyField && prop.ValueOrNil.Data == nil &&
(p.options.ts.Config.ExperimentalDecorators == config.True && len(opts.decorators) > 0) {
// If this is a well-formed class field with the "abstract" keyword,
// only keep the declaration to preserve its side-effects when
// there are TypeScript experimental decorators present:
//
// abstract class Foo {
// // Remove this
// abstract [(console.log('side effect 1'), 'foo')]
//
// // Keep this
// @decorator(console.log('side effect 2')) abstract bar
// }
//
// This behavior is valid with TypeScript experimental decorators.
// TypeScript does not allow this with JavaScript decorators.
//
// References:
//
// https://github.com/evanw/esbuild/issues/3684
//
prop.Kind = js_ast.PropertyDeclareOrAbstract
return prop, true
}
p.discardScopesUpTo(scopeIndex)
return js_ast.Property{}, false
}
case "private", "protected", "public", "readonly", "override":
// Skip over TypeScript keywords
if opts.isClass && p.options.ts.Parse {
return p.parseProperty(startLoc, kind, opts, nil)
}
}
} else if p.lexer.Token == js_lexer.TOpenBrace && name.String == "static" && len(opts.decorators) == 0 {
loc := p.lexer.Loc()
p.lexer.Next()
oldFnOrArrowDataParse := p.fnOrArrowDataParse
p.fnOrArrowDataParse = fnOrArrowDataParse{
isReturnDisallowed: true,
allowSuperProperty: true,
await: forbidAll,
}
p.pushScopeForParsePass(js_ast.ScopeClassStaticInit, loc)
stmts := p.parseStmtsUpTo(js_lexer.TCloseBrace, parseStmtOpts{})
p.popScope()
p.fnOrArrowDataParse = oldFnOrArrowDataParse
closeBraceLoc := p.lexer.Loc()
p.lexer.Expect(js_lexer.TCloseBrace)
return js_ast.Property{
Kind: js_ast.PropertyClassStaticBlock,
Loc: startLoc,
ClassStaticBlock: &js_ast.ClassStaticBlock{
Loc: loc,
Block: js_ast.SBlock{Stmts: stmts, CloseBraceLoc: closeBraceLoc},
},
}, true
}
}
if p.isMangledProp(name.String) {
key = js_ast.Expr{Loc: nameRange.Loc, Data: &js_ast.ENameOfSymbol{
Ref: p.storeNameInRef(name),
HasPropertyKeyComment: true,
}}
} else {
key = js_ast.Expr{Loc: nameRange.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(name.String)}}
}
// Parse a shorthand property
if !opts.isClass && kind == js_ast.PropertyField && p.lexer.Token != js_lexer.TColon &&
p.lexer.Token != js_lexer.TOpenParen && p.lexer.Token != js_lexer.TLessThan &&
js_lexer.Keywords[name.String] == js_lexer.T(0) {
// Forbid invalid identifiers
if (p.fnOrArrowDataParse.await != allowIdent && name.String == "await") ||
(p.fnOrArrowDataParse.yield != allowIdent && name.String == "yield") {
p.log.AddError(&p.tracker, nameRange, fmt.Sprintf("Cannot use %q as an identifier here:", name.String))
}
ref := p.storeNameInRef(name)
value := js_ast.Expr{Loc: key.Loc, Data: &js_ast.EIdentifier{Ref: ref}}
// Destructuring patterns have an optional default value
var initializerOrNil js_ast.Expr
if errors != nil && p.lexer.Token == js_lexer.TEquals {
errors.invalidExprDefaultValue = p.lexer.Range()
p.lexer.Next()
initializerOrNil = p.parseExpr(js_ast.LComma)
}
return js_ast.Property{
Kind: kind,
Loc: startLoc,
Key: key,
ValueOrNil: value,
InitializerOrNil: initializerOrNil,
Flags: js_ast.PropertyWasShorthand,
}, true
}
}
hasTypeParameters := false
hasDefiniteAssignmentAssertionOperator := false
if p.options.ts.Parse {
if opts.isClass {
if p.lexer.Token == js_lexer.TQuestion {
// "class X { foo?: number }"
// "class X { foo?(): number }"
p.lexer.Next()
} else if p.lexer.Token == js_lexer.TExclamation && !p.lexer.HasNewlineBefore &&
(kind == js_ast.PropertyField || kind == js_ast.PropertyAutoAccessor) {
// "class X { foo!: number }"
p.lexer.Next()
hasDefiniteAssignmentAssertionOperator = true
}
}
// "class X { foo?<T>(): T }"
// "const x = { foo<T>(): T {} }"
if !hasDefiniteAssignmentAssertionOperator && kind != js_ast.PropertyAutoAccessor {
hasTypeParameters = p.skipTypeScriptTypeParameters(allowConstModifier) != didNotSkipAnything
}
}
// Parse a class field with an optional initial value
if kind == js_ast.PropertyAutoAccessor || (opts.isClass && kind == js_ast.PropertyField &&
!hasTypeParameters && (p.lexer.Token != js_lexer.TOpenParen || hasDefiniteAssignmentAssertionOperator)) {
var initializerOrNil js_ast.Expr
// Forbid the names "constructor" and "prototype" in some cases
if !flags.Has(js_ast.PropertyIsComputed) {
if str, ok := key.Data.(*js_ast.EString); ok && (helpers.UTF16EqualsString(str.Value, "constructor") ||
(opts.isStatic && helpers.UTF16EqualsString(str.Value, "prototype"))) {
p.log.AddError(&p.tracker, keyRange, fmt.Sprintf("Invalid field name %q", helpers.UTF16ToString(str.Value)))
}
}
// Skip over types
if p.options.ts.Parse && p.lexer.Token == js_lexer.TColon {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}
if p.lexer.Token == js_lexer.TEquals {
p.lexer.Next()
// "this" and "super" property access is allowed in field initializers
oldIsThisDisallowed := p.fnOrArrowDataParse.isThisDisallowed
oldAllowSuperProperty := p.fnOrArrowDataParse.allowSuperProperty
p.fnOrArrowDataParse.isThisDisallowed = false
p.fnOrArrowDataParse.allowSuperProperty = true
initializerOrNil = p.parseExpr(js_ast.LComma)
p.fnOrArrowDataParse.isThisDisallowed = oldIsThisDisallowed
p.fnOrArrowDataParse.allowSuperProperty = oldAllowSuperProperty
}
// Special-case private identifiers
if private, ok := key.Data.(*js_ast.EPrivateIdentifier); ok {
name := p.loadNameFromRef(private.Ref)
if name == "#constructor" {
p.log.AddError(&p.tracker, keyRange, fmt.Sprintf("Invalid field name %q", name))
}
var declare ast.SymbolKind
if kind == js_ast.PropertyAutoAccessor {
if opts.isStatic {
declare = ast.SymbolPrivateStaticGetSetPair
} else {
declare = ast.SymbolPrivateGetSetPair
}
private.Ref = p.declareSymbol(declare, key.Loc, name)
p.privateGetters[private.Ref] = p.newSymbol(ast.SymbolOther, name[1:]+"_get")
p.privateSetters[private.Ref] = p.newSymbol(ast.SymbolOther, name[1:]+"_set")
} else {
if opts.isStatic {
declare = ast.SymbolPrivateStaticField
} else {
declare = ast.SymbolPrivateField
}
private.Ref = p.declareSymbol(declare, key.Loc, name)
}
}
p.lexer.ExpectOrInsertSemicolon()
if opts.isStatic {
flags |= js_ast.PropertyIsStatic
}
return js_ast.Property{
Decorators: opts.decorators,
Loc: startLoc,
Kind: kind,
Flags: flags,
Key: key,
InitializerOrNil: initializerOrNil,
CloseBracketLoc: closeBracketLoc,
}, true
}
// Parse a method expression
if p.lexer.Token == js_lexer.TOpenParen || kind.IsMethodDefinition() || opts.isClass {
hasError := false
if !hasError && opts.tsDeclareRange.Len != 0 {
what := "method"
if kind == js_ast.PropertyGetter {
what = "getter"
} else if kind == js_ast.PropertySetter {
what = "setter"
}
p.log.AddError(&p.tracker, opts.tsDeclareRange, "\"declare\" cannot be used with a "+what)
hasError = true
}
if opts.isAsync && p.markAsyncFn(opts.asyncRange, opts.isGenerator) {
hasError = true
}
if !hasError && opts.isGenerator && p.markSyntaxFeature(compat.Generator, opts.generatorRange) {
hasError = true
}
if !hasError && p.lexer.Token == js_lexer.TOpenParen && kind != js_ast.PropertyGetter && kind != js_ast.PropertySetter && p.markSyntaxFeature(compat.ObjectExtensions, p.lexer.Range()) {
hasError = true
}
loc := p.lexer.Loc()
scopeIndex := p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, loc)
isConstructor := false
// Forbid the names "constructor" and "prototype" in some cases
if opts.isClass && !flags.Has(js_ast.PropertyIsComputed) {
if str, ok := key.Data.(*js_ast.EString); ok {
if !opts.isStatic && helpers.UTF16EqualsString(str.Value, "constructor") {
switch {
case kind == js_ast.PropertyGetter:
p.log.AddError(&p.tracker, keyRange, "Class constructor cannot be a getter")
case kind == js_ast.PropertySetter:
p.log.AddError(&p.tracker, keyRange, "Class constructor cannot be a setter")
case opts.isAsync:
p.log.AddError(&p.tracker, keyRange, "Class constructor cannot be an async function")
case opts.isGenerator:
p.log.AddError(&p.tracker, keyRange, "Class constructor cannot be a generator")
default:
isConstructor = true
}
} else if opts.isStatic && helpers.UTF16EqualsString(str.Value, "prototype") {
p.log.AddError(&p.tracker, keyRange, "Invalid static method name \"prototype\"")
}
}
}
await := allowIdent
yield := allowIdent
if opts.isAsync {
await = allowExpr
}
if opts.isGenerator {
yield = allowExpr
}
fn, hadBody := p.parseFn(nil, opts.classKeyword, opts.decoratorContext, fnOrArrowDataParse{
needsAsyncLoc: key.Loc,
asyncRange: opts.asyncRange,
await: await,
yield: yield,
allowSuperCall: opts.classHasExtends && isConstructor,
allowSuperProperty: true,
decoratorScope: opts.decoratorScope,
isConstructor: isConstructor,
// Only allow omitting the body if we're parsing TypeScript class
allowMissingBodyForTypeScript: p.options.ts.Parse && opts.isClass,
})
// "class Foo { foo(): void; foo(): void {} }"
if !hadBody {
// Skip this property entirely
p.popAndDiscardScope(scopeIndex)
return js_ast.Property{}, false
}
p.popScope()
fn.IsUniqueFormalParameters = true
value := js_ast.Expr{Loc: loc, Data: &js_ast.EFunction{Fn: fn}}
// Enforce argument rules for accessors
switch kind {
case js_ast.PropertyGetter:
if len(fn.Args) > 0 {
r := js_lexer.RangeOfIdentifier(p.source, fn.Args[0].Binding.Loc)
p.log.AddError(&p.tracker, r, fmt.Sprintf("Getter %s must have zero arguments", p.keyNameForError(key)))
}
case js_ast.PropertySetter:
if len(fn.Args) != 1 {
r := js_lexer.RangeOfIdentifier(p.source, key.Loc)
if len(fn.Args) > 1 {
r = js_lexer.RangeOfIdentifier(p.source, fn.Args[1].Binding.Loc)
}
p.log.AddError(&p.tracker, r, fmt.Sprintf("Setter %s must have exactly one argument", p.keyNameForError(key)))
}
default:
kind = js_ast.PropertyMethod
}
// Special-case private identifiers
if private, ok := key.Data.(*js_ast.EPrivateIdentifier); ok {
var declare ast.SymbolKind
var suffix string
switch kind {
case js_ast.PropertyGetter:
if opts.isStatic {
declare = ast.SymbolPrivateStaticGet
} else {
declare = ast.SymbolPrivateGet
}
suffix = "_get"
case js_ast.PropertySetter:
if opts.isStatic {
declare = ast.SymbolPrivateStaticSet
} else {
declare = ast.SymbolPrivateSet
}
suffix = "_set"
default:
if opts.isStatic {
declare = ast.SymbolPrivateStaticMethod
} else {
declare = ast.SymbolPrivateMethod
}
suffix = "_fn"
}
name := p.loadNameFromRef(private.Ref)
if name == "#constructor" {
p.log.AddError(&p.tracker, keyRange, fmt.Sprintf("Invalid method name %q", name))
}
private.Ref = p.declareSymbol(declare, key.Loc, name)
methodRef := p.newSymbol(ast.SymbolOther, name[1:]+suffix)
if kind == js_ast.PropertySetter {
p.privateSetters[private.Ref] = methodRef
} else {
p.privateGetters[private.Ref] = methodRef
}
}
if opts.isStatic {
flags |= js_ast.PropertyIsStatic
}
return js_ast.Property{
Decorators: opts.decorators,
Loc: startLoc,
Kind: kind,
Flags: flags,
Key: key,
ValueOrNil: value,
CloseBracketLoc: closeBracketLoc,
}, true
}
// Parse an object key/value pair
p.lexer.Expect(js_lexer.TColon)
value := p.parseExprOrBindings(js_ast.LComma, errors)
return js_ast.Property{
Loc: startLoc,
Kind: kind,
Flags: flags,
Key: key,
ValueOrNil: value,
CloseBracketLoc: closeBracketLoc,
}, true
}
func (p *parser) parsePropertyBinding() js_ast.PropertyBinding {
var key js_ast.Expr
var closeBracketLoc logger.Loc
isComputed := false
preferQuotedKey := false
loc := p.lexer.Loc()
switch p.lexer.Token {
case js_lexer.TDotDotDot:
p.lexer.Next()
value := js_ast.Binding{Loc: p.saveExprCommentsHere(), Data: &js_ast.BIdentifier{Ref: p.storeNameInRef(p.lexer.Identifier)}}
p.lexer.Expect(js_lexer.TIdentifier)
return js_ast.PropertyBinding{
Loc: loc,
IsSpread: true,
Value: value,
}
case js_lexer.TNumericLiteral:
key = js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.ENumber{Value: p.lexer.Number}}
p.checkForLegacyOctalLiteral(key.Data)
p.lexer.Next()
case js_lexer.TStringLiteral:
key = p.parseStringLiteral()
preferQuotedKey = !p.options.minifySyntax
case js_lexer.TBigIntegerLiteral:
key = p.parseBigIntOrStringIfUnsupported()
p.lexer.Next()
case js_lexer.TOpenBracket:
isComputed = true
p.lexer.Next()
key = p.parseExpr(js_ast.LComma)
closeBracketLoc = p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBracket)
default:
name := p.lexer.Identifier
nameRange := p.lexer.Range()
if !p.lexer.IsIdentifierOrKeyword() {
p.lexer.Expect(js_lexer.TIdentifier)
}
p.lexer.Next()
if p.isMangledProp(name.String) {
key = js_ast.Expr{Loc: nameRange.Loc, Data: &js_ast.ENameOfSymbol{Ref: p.storeNameInRef(name)}}
} else {
key = js_ast.Expr{Loc: nameRange.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(name.String)}}
}
if p.lexer.Token != js_lexer.TColon && p.lexer.Token != js_lexer.TOpenParen {
// Forbid invalid identifiers
if (p.fnOrArrowDataParse.await != allowIdent && name.String == "await") ||
(p.fnOrArrowDataParse.yield != allowIdent && name.String == "yield") {
p.log.AddError(&p.tracker, nameRange, fmt.Sprintf("Cannot use %q as an identifier here:", name.String))
}
ref := p.storeNameInRef(name)
value := js_ast.Binding{Loc: nameRange.Loc, Data: &js_ast.BIdentifier{Ref: ref}}
var defaultValueOrNil js_ast.Expr
if p.lexer.Token == js_lexer.TEquals {
p.lexer.Next()
defaultValueOrNil = p.parseExpr(js_ast.LComma)
}
return js_ast.PropertyBinding{
Loc: loc,
Key: key,
Value: value,
DefaultValueOrNil: defaultValueOrNil,
}
}
}
p.lexer.Expect(js_lexer.TColon)
value := p.parseBinding(parseBindingOpts{})
var defaultValueOrNil js_ast.Expr
if p.lexer.Token == js_lexer.TEquals {
p.lexer.Next()
defaultValueOrNil = p.parseExpr(js_ast.LComma)
}
return js_ast.PropertyBinding{
Loc: loc,
IsComputed: isComputed,
PreferQuotedKey: preferQuotedKey,
Key: key,
Value: value,
DefaultValueOrNil: defaultValueOrNil,
CloseBracketLoc: closeBracketLoc,
}
}
// These properties have special semantics in JavaScript. They must not be
// mangled or we could potentially fail to parse valid JavaScript syntax or
// generate invalid JavaScript syntax as output.
//
// This list is only intended to contain properties specific to the JavaScript
// language itself to avoid syntax errors in the generated output. It's not
// intended to contain properties for JavaScript APIs. Those must be provided
// by the user.
var permanentReservedProps = map[string]bool{
"__proto__": true,
"constructor": true,
"prototype": true,
}
func (p *parser) isMangledProp(name string) bool {
if p.options.mangleProps == nil {
return false
}
if p.options.mangleProps.MatchString(name) && !permanentReservedProps[name] && (p.options.reserveProps == nil || !p.options.reserveProps.MatchString(name)) {
return true
}
reservedProps := p.reservedProps
if reservedProps == nil {
reservedProps = make(map[string]bool)
p.reservedProps = reservedProps
}
reservedProps[name] = true
return false
}
func (p *parser) symbolForMangledProp(name string) ast.Ref {
mangledProps := p.mangledProps
if mangledProps == nil {
mangledProps = make(map[string]ast.Ref)
p.mangledProps = mangledProps
}
ref, ok := mangledProps[name]
if !ok {
ref = p.newSymbol(ast.SymbolMangledProp, name)
mangledProps[name] = ref
}
if !p.isControlFlowDead {
p.symbols[ref.InnerIndex].UseCountEstimate++
}
return ref
}
type wasOriginallyDotOrIndex uint8
const (
wasOriginallyDot wasOriginallyDotOrIndex = iota
wasOriginallyIndex
)
func (p *parser) dotOrMangledPropParse(
target js_ast.Expr,
name js_lexer.MaybeSubstring,
nameLoc logger.Loc,
optionalChain js_ast.OptionalChain,
original wasOriginallyDotOrIndex,
) js_ast.E {
if (original != wasOriginallyIndex || p.options.mangleQuoted) && p.isMangledProp(name.String) {
return &js_ast.EIndex{
Target: target,
Index: js_ast.Expr{Loc: nameLoc, Data: &js_ast.ENameOfSymbol{Ref: p.storeNameInRef(name)}},
OptionalChain: optionalChain,
}
}
return &js_ast.EDot{
Target: target,
Name: name.String,
NameLoc: nameLoc,
OptionalChain: optionalChain,
}
}
func (p *parser) dotOrMangledPropVisit(target js_ast.Expr, name string, nameLoc logger.Loc) js_ast.E {
if p.isMangledProp(name) {
return &js_ast.EIndex{
Target: target,
Index: js_ast.Expr{Loc: nameLoc, Data: &js_ast.ENameOfSymbol{Ref: p.symbolForMangledProp(name)}},
}
}
return &js_ast.EDot{
Target: target,
Name: name,
NameLoc: nameLoc,
}
}
func (p *parser) parseArrowBody(args []js_ast.Arg, data fnOrArrowDataParse) *js_ast.EArrow {
arrowLoc := p.lexer.Loc()
// Newlines are not allowed before "=>"
if p.lexer.HasNewlineBefore {
p.log.AddError(&p.tracker, p.lexer.Range(), "Unexpected newline before \"=>\"")
panic(js_lexer.LexerPanic{})
}
p.lexer.Expect(js_lexer.TEqualsGreaterThan)
for _, arg := range args {
p.declareBinding(ast.SymbolHoisted, arg.Binding, parseStmtOpts{})
}
// The ability to use "this" and "super" is inherited by arrow functions
data.isThisDisallowed = p.fnOrArrowDataParse.isThisDisallowed
data.allowSuperCall = p.fnOrArrowDataParse.allowSuperCall
data.allowSuperProperty = p.fnOrArrowDataParse.allowSuperProperty
if p.lexer.Token == js_lexer.TOpenBrace {
body := p.parseFnBody(data)
p.afterArrowBodyLoc = p.lexer.Loc()
return &js_ast.EArrow{Args: args, Body: body}
}
p.pushScopeForParsePass(js_ast.ScopeFunctionBody, arrowLoc)
defer p.popScope()
oldFnOrArrowData := p.fnOrArrowDataParse
p.fnOrArrowDataParse = data
expr := p.parseExpr(js_ast.LComma)
p.fnOrArrowDataParse = oldFnOrArrowData
return &js_ast.EArrow{
Args: args,
PreferExpr: true,
Body: js_ast.FnBody{Loc: arrowLoc, Block: js_ast.SBlock{Stmts: []js_ast.Stmt{{Loc: expr.Loc, Data: &js_ast.SReturn{ValueOrNil: expr}}}}},
}
}
func (p *parser) checkForArrowAfterTheCurrentToken() 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.Next()
isArrowAfterThisToken := p.lexer.Token == js_lexer.TEqualsGreaterThan
p.lexer = oldLexer
return isArrowAfterThisToken
}
// This parses an expression. This assumes we've already parsed the "async"
// keyword and are currently looking at the following token.
func (p *parser) parseAsyncPrefixExpr(asyncRange logger.Range, level js_ast.L, flags exprFlag) js_ast.Expr {
// "async function() {}"
if !p.lexer.HasNewlineBefore && p.lexer.Token == js_lexer.TFunction {
return p.parseFnExpr(asyncRange.Loc, true /* isAsync */, asyncRange)
}
// Check the precedence level to avoid parsing an arrow function in
// "new async () => {}". This also avoids parsing "new async()" as
// "new (async())()" instead.
if !p.lexer.HasNewlineBefore && level < js_ast.LMember {
switch p.lexer.Token {
// "async => {}"
case js_lexer.TEqualsGreaterThan:
if level <= js_ast.LAssign {
arg := js_ast.Arg{Binding: js_ast.Binding{Loc: asyncRange.Loc, Data: &js_ast.BIdentifier{
Ref: p.storeNameInRef(js_lexer.MaybeSubstring{String: "async"})}}}
p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, asyncRange.Loc)
defer p.popScope()
return js_ast.Expr{Loc: asyncRange.Loc, Data: p.parseArrowBody([]js_ast.Arg{arg}, fnOrArrowDataParse{
needsAsyncLoc: asyncRange.Loc,
})}
}
// "async x => {}"
case js_lexer.TIdentifier:
if level <= js_ast.LAssign {
isArrowFn := true
if (flags&exprFlagForLoopInit) != 0 && p.lexer.Identifier.String == "of" {
// See https://github.com/tc39/ecma262/issues/2034 for details
// "for (async of" is only an arrow function if the next token is "=>"
isArrowFn = p.checkForArrowAfterTheCurrentToken()
// Do not allow "for (async of []) ;" but do allow "for await (async of []) ;"
if !isArrowFn && (flags&exprFlagForAwaitLoopInit) == 0 && p.lexer.Raw() == "of" {
r := logger.Range{Loc: asyncRange.Loc, Len: p.lexer.Range().End() - asyncRange.Loc.Start}
p.log.AddError(&p.tracker, r, "For loop initializers cannot start with \"async of\"")
panic(js_lexer.LexerPanic{})
}
} else if p.options.ts.Parse && p.lexer.Token == js_lexer.TIdentifier {
// Make sure we can parse the following TypeScript code:
//
// export function open(async?: boolean): void {
// console.log(async as boolean)
// }
//
// TypeScript solves this by using a two-token lookahead to check for
// "=>" after an identifier after the "async". This is done in
// "isUnParenthesizedAsyncArrowFunctionWorker" which was introduced
// here: https://github.com/microsoft/TypeScript/pull/8444
isArrowFn = p.checkForArrowAfterTheCurrentToken()
}
if isArrowFn {
p.markAsyncFn(asyncRange, false)
ref := p.storeNameInRef(p.lexer.Identifier)
arg := js_ast.Arg{Binding: js_ast.Binding{Loc: p.lexer.Loc(), Data: &js_ast.BIdentifier{Ref: ref}}}
p.lexer.Next()
p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, asyncRange.Loc)
defer p.popScope()
arrow := p.parseArrowBody([]js_ast.Arg{arg}, fnOrArrowDataParse{
needsAsyncLoc: arg.Binding.Loc,
await: allowExpr,
})
arrow.IsAsync = true
return js_ast.Expr{Loc: asyncRange.Loc, Data: arrow}
}
}
// "async()"
// "async () => {}"
case js_lexer.TOpenParen:
p.lexer.Next()
return p.parseParenExpr(asyncRange.Loc, level, parenExprOpts{asyncRange: asyncRange})
// "async<T>()"
// "async <T>() => {}"
case js_lexer.TLessThan:
if p.options.ts.Parse && (!p.options.jsx.Parse || p.isTSArrowFnJSX()) {
if result := p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking(); result != didNotSkipAnything {
p.lexer.Next()
return p.parseParenExpr(asyncRange.Loc, level, parenExprOpts{
asyncRange: asyncRange,
forceArrowFn: result == definitelyTypeParameters,
})
}
}
}
}
// "async"
// "async + 1"
return js_ast.Expr{Loc: asyncRange.Loc, Data: &js_ast.EIdentifier{
Ref: p.storeNameInRef(js_lexer.MaybeSubstring{String: "async"})}}
}
func (p *parser) parseFnExpr(loc logger.Loc, isAsync bool, asyncRange logger.Range) js_ast.Expr {
p.lexer.Next()
isGenerator := p.lexer.Token == js_lexer.TAsterisk
hasError := false
if isAsync {
hasError = p.markAsyncFn(asyncRange, isGenerator)
}
if isGenerator {
if !hasError {
p.markSyntaxFeature(compat.Generator, p.lexer.Range())
}
p.lexer.Next()
}
var name *ast.LocRef
p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, loc)
defer p.popScope()
// The name is optional
if p.lexer.Token == js_lexer.TIdentifier {
// Don't declare the name "arguments" since it's shadowed and inaccessible
name = &ast.LocRef{Loc: p.lexer.Loc()}
if text := p.lexer.Identifier.String; text != "arguments" {
name.Ref = p.declareSymbol(ast.SymbolHoistedFunction, name.Loc, text)
} else {
name.Ref = p.newSymbol(ast.SymbolHoistedFunction, text)
}
p.lexer.Next()
}
// Even anonymous functions can have TypeScript type parameters
if p.options.ts.Parse {
p.skipTypeScriptTypeParameters(allowConstModifier)
}
await := allowIdent
yield := allowIdent
if isAsync {
await = allowExpr
}
if isGenerator {
yield = allowExpr
}
fn, _ := p.parseFn(name, logger.Range{}, 0, fnOrArrowDataParse{
needsAsyncLoc: loc,
asyncRange: asyncRange,
await: await,
yield: yield,
})
p.validateFunctionName(fn, fnExpr)
return js_ast.Expr{Loc: loc, Data: &js_ast.EFunction{Fn: fn}}
}
type parenExprOpts struct {
asyncRange logger.Range
forceArrowFn bool
isAfterQuestionAndBeforeColon bool
}
// This assumes that the open parenthesis has already been parsed by the caller
func (p *parser) parseParenExpr(loc logger.Loc, level js_ast.L, opts parenExprOpts) js_ast.Expr {
items := []js_ast.Expr{}
errors := deferredErrors{}
arrowArgErrors := deferredArrowArgErrors{}
spreadRange := logger.Range{}
typeColonRange := logger.Range{}
commaAfterSpread := logger.Loc{}
isAsync := opts.asyncRange.Len > 0
// Push a scope assuming this is an arrow function. It may not be, in which
// case we'll need to roll this change back. This has to be done ahead of
// parsing the arguments instead of later on when we hit the "=>" token and
// we know it's an arrow function because the arguments may have default
// values that introduce new scopes and declare new symbols. If this is an
// arrow function, then those new scopes will need to be parented under the
// scope of the arrow function itself.
scopeIndex := p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, loc)
// Allow "in" inside parentheses
oldAllowIn := p.allowIn
p.allowIn = true
// Forbid "await" and "yield", but only for arrow functions
oldFnOrArrowData := p.fnOrArrowDataParse
p.fnOrArrowDataParse.arrowArgErrors = &arrowArgErrors
// Scan over the comma-separated arguments or expressions
for p.lexer.Token != js_lexer.TCloseParen {
itemLoc := p.lexer.Loc()
isSpread := p.lexer.Token == js_lexer.TDotDotDot
if isSpread {
spreadRange = p.lexer.Range()
p.markSyntaxFeature(compat.RestArgument, spreadRange)
p.lexer.Next()
}
// We don't know yet whether these are arguments or expressions, so parse
// a superset of the expression syntax. Errors about things that are valid
// in one but not in the other are deferred.
p.latestArrowArgLoc = p.lexer.Loc()
item := p.parseExprOrBindings(js_ast.LComma, &errors)
if isSpread {
item = js_ast.Expr{Loc: itemLoc, Data: &js_ast.ESpread{Value: item}}
}
// Skip over types
if p.options.ts.Parse && p.lexer.Token == js_lexer.TColon {
typeColonRange = p.lexer.Range()
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}
// There may be a "=" after the type (but not after an "as" cast)
if p.options.ts.Parse && p.lexer.Token == js_lexer.TEquals && p.lexer.Loc() != p.forbidSuffixAfterAsLoc {
p.lexer.Next()
item = js_ast.Assign(item, p.parseExpr(js_ast.LComma))
}
items = append(items, item)
if p.lexer.Token != js_lexer.TComma {
break
}
// Spread arguments must come last. If there's a spread argument followed
// by a comma, throw an error if we use these expressions as bindings.
if isSpread {
commaAfterSpread = p.lexer.Loc()
}
// Eat the comma token
p.lexer.Next()
}
// The parenthetical construct must end with a close parenthesis
p.lexer.Expect(js_lexer.TCloseParen)
// Restore "in" operator status before we parse the arrow function body
p.allowIn = oldAllowIn
// Also restore "await" and "yield" expression errors
p.fnOrArrowDataParse = oldFnOrArrowData
// Are these arguments to an arrow function?
isArrowFn := p.lexer.Token == js_lexer.TEqualsGreaterThan
if isArrowFn || opts.forceArrowFn || (p.options.ts.Parse && p.lexer.Token == js_lexer.TColon) {
// Arrow functions are not allowed inside certain expressions
if level > js_ast.LAssign {
p.lexer.Unexpected()
}
var invalidLog invalidLog
args := []js_ast.Arg{}
if isAsync {
p.markAsyncFn(opts.asyncRange, false)
}
// First, try converting the expressions to bindings
for _, item := range items {
isSpread := false
if spread, ok := item.Data.(*js_ast.ESpread); ok {
item = spread.Value
isSpread = true
}
binding, initializerOrNil, log := p.convertExprToBindingAndInitializer(item, invalidLog, isSpread)
invalidLog = log
args = append(args, js_ast.Arg{Binding: binding, DefaultOrNil: initializerOrNil})
}
await := allowIdent
if isAsync {
await = allowExpr
}
// Avoid parsing TypeScript code like "a ? (1 + 2) : (3 + 4)" as an arrow
// function. The ":" after the ")" may be a return type annotation, so we
// attempt to convert the expressions to bindings first before deciding
// whether this is an arrow function, and only pick an arrow function if
// there were no conversion errors.
if p.options.ts.Parse && p.lexer.Token == js_lexer.TColon && len(invalidLog.invalidTokens) == 0 {
if opts.isAfterQuestionAndBeforeColon {
// Only do this very expensive check if we must
isArrowFn = p.isTypeScriptArrowReturnTypeAfterQuestionAndBeforeColon(await)
if isArrowFn {
// We know this will succeed because we've already done it once above
p.lexer.Next()
p.skipTypeScriptReturnType()
}
} else {
// Otherwise, do the less expensive check
isArrowFn = p.trySkipTypeScriptArrowReturnTypeWithBacktracking()
}
}
// Arrow function parsing may be forced if this parenthesized expression
// was prefixed by a TypeScript type parameter list such as "<T,>()"
if isArrowFn || opts.forceArrowFn {
if commaAfterSpread.Start != 0 {
p.log.AddError(&p.tracker, logger.Range{Loc: commaAfterSpread, Len: 1}, "Unexpected \",\" after rest pattern")
}
p.logArrowArgErrors(&arrowArgErrors)
p.logDeferredArrowArgErrors(&errors)
// Now that we've decided we're an arrow function, report binding pattern
// conversion errors
if len(invalidLog.invalidTokens) > 0 {
for _, token := range invalidLog.invalidTokens {
p.log.AddError(&p.tracker, token, "Invalid binding pattern")
}
panic(js_lexer.LexerPanic{})
}
// Also report syntax features used in bindings
for _, entry := range invalidLog.syntaxFeatures {
p.markSyntaxFeature(entry.feature, entry.token)
}
arrow := p.parseArrowBody(args, fnOrArrowDataParse{
needsAsyncLoc: loc,
await: await,
})
arrow.IsAsync = isAsync
arrow.HasRestArg = spreadRange.Len > 0
p.popScope()
return js_ast.Expr{Loc: loc, Data: arrow}
}
}
// If we get here, it's not an arrow function so undo the pushing of the
// scope we did earlier. This needs to flatten any child scopes into the
// parent scope as if the scope was never pushed in the first place.
p.popAndFlattenScope(scopeIndex)
// If this isn't an arrow function, then types aren't allowed
if typeColonRange.Len > 0 {
p.log.AddError(&p.tracker, typeColonRange, "Unexpected \":\"")
panic(js_lexer.LexerPanic{})
}
// Are these arguments for a call to a function named "async"?
if isAsync {
p.logExprErrors(&errors)
async := js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{
Ref: p.storeNameInRef(js_lexer.MaybeSubstring{String: "async"})}}
return js_ast.Expr{Loc: loc, Data: &js_ast.ECall{
Target: async,
Args: items,
}}
}
// Is this a chain of expressions and comma operators?
if len(items) > 0 {
p.logExprErrors(&errors)
if spreadRange.Len > 0 {
p.log.AddError(&p.tracker, spreadRange, "Unexpected \"...\"")
panic(js_lexer.LexerPanic{})
}
value := js_ast.JoinAllWithComma(items)
p.markExprAsParenthesized(value, loc, isAsync)
return value
}
// Indicate that we expected an arrow function
p.lexer.Expected(js_lexer.TEqualsGreaterThan)
return js_ast.Expr{}
}
type invalidLog struct {
invalidTokens []logger.Range
syntaxFeatures []syntaxFeature
}
type syntaxFeature struct {
feature compat.JSFeature
token logger.Range
}
func (p *parser) convertExprToBindingAndInitializer(
expr js_ast.Expr, invalidLog invalidLog, isSpread bool,
) (js_ast.Binding, js_ast.Expr, invalidLog) {
var initializerOrNil js_ast.Expr
if assign, ok := expr.Data.(*js_ast.EBinary); ok && assign.Op == js_ast.BinOpAssign {
initializerOrNil = assign.Right
expr = assign.Left
}
binding, invalidLog := p.convertExprToBinding(expr, invalidLog)
if initializerOrNil.Data != nil {
equalsRange := p.source.RangeOfOperatorBefore(initializerOrNil.Loc, "=")
if isSpread {
p.log.AddError(&p.tracker, equalsRange, "A rest argument cannot have a default initializer")
} else {
invalidLog.syntaxFeatures = append(invalidLog.syntaxFeatures, syntaxFeature{
feature: compat.DefaultArgument,
token: equalsRange,
})
}
}
return binding, initializerOrNil, invalidLog
}
// Note: do not write to "p.log" in this function. Any errors due to conversion
// from expression to binding should be written to "invalidLog" instead. That
// way we can potentially keep this as an expression if it turns out it's not
// needed as a binding after all.
func (p *parser) convertExprToBinding(expr js_ast.Expr, invalidLog invalidLog) (js_ast.Binding, invalidLog) {
switch e := expr.Data.(type) {
case *js_ast.EMissing:
return js_ast.Binding{Loc: expr.Loc, Data: js_ast.BMissingShared}, invalidLog
case *js_ast.EIdentifier:
return js_ast.Binding{Loc: expr.Loc, Data: &js_ast.BIdentifier{Ref: e.Ref}}, invalidLog
case *js_ast.EArray:
if e.CommaAfterSpread.Start != 0 {
invalidLog.invalidTokens = append(invalidLog.invalidTokens, logger.Range{Loc: e.CommaAfterSpread, Len: 1})
}
invalidLog.syntaxFeatures = append(invalidLog.syntaxFeatures,
syntaxFeature{feature: compat.Destructuring, token: p.source.RangeOfOperatorAfter(expr.Loc, "[")})
items := []js_ast.ArrayBinding{}
isSpread := false
for _, item := range e.Items {
if i, ok := item.Data.(*js_ast.ESpread); ok {
isSpread = true
item = i.Value
if _, ok := item.Data.(*js_ast.EIdentifier); !ok {
p.markSyntaxFeature(compat.NestedRestBinding, p.source.RangeOfOperatorAfter(item.Loc, "["))
}
}
binding, initializerOrNil, log := p.convertExprToBindingAndInitializer(item, invalidLog, isSpread)
invalidLog = log
items = append(items, js_ast.ArrayBinding{
Binding: binding,
DefaultValueOrNil: initializerOrNil,
Loc: item.Loc,
})
}
return js_ast.Binding{Loc: expr.Loc, Data: &js_ast.BArray{
Items: items,
HasSpread: isSpread,
IsSingleLine: e.IsSingleLine,
CloseBracketLoc: e.CloseBracketLoc,
}}, invalidLog
case *js_ast.EObject:
if e.CommaAfterSpread.Start != 0 {
invalidLog.invalidTokens = append(invalidLog.invalidTokens, logger.Range{Loc: e.CommaAfterSpread, Len: 1})
}
invalidLog.syntaxFeatures = append(invalidLog.syntaxFeatures,
syntaxFeature{feature: compat.Destructuring, token: p.source.RangeOfOperatorAfter(expr.Loc, "{")})
properties := []js_ast.PropertyBinding{}
for _, property := range e.Properties {
if property.Kind.IsMethodDefinition() {
invalidLog.invalidTokens = append(invalidLog.invalidTokens, js_lexer.RangeOfIdentifier(p.source, property.Key.Loc))
continue
}
binding, initializerOrNil, log := p.convertExprToBindingAndInitializer(property.ValueOrNil, invalidLog, false)
invalidLog = log
if initializerOrNil.Data == nil {
initializerOrNil = property.InitializerOrNil
}
properties = append(properties, js_ast.PropertyBinding{
Loc: property.Loc,
IsSpread: property.Kind == js_ast.PropertySpread,
IsComputed: property.Flags.Has(js_ast.PropertyIsComputed),
Key: property.Key,
Value: binding,
DefaultValueOrNil: initializerOrNil,
})
}
return js_ast.Binding{Loc: expr.Loc, Data: &js_ast.BObject{
Properties: properties,
IsSingleLine: e.IsSingleLine,
CloseBraceLoc: e.CloseBraceLoc,
}}, invalidLog
default:
invalidLog.invalidTokens = append(invalidLog.invalidTokens, logger.Range{Loc: expr.Loc})
return js_ast.Binding{}, invalidLog
}
}
func (p *parser) saveExprCommentsHere() logger.Loc {
loc := p.lexer.Loc()
if p.exprComments != nil && len(p.lexer.CommentsBeforeToken) > 0 {
comments := make([]string, len(p.lexer.CommentsBeforeToken))
for i, comment := range p.lexer.CommentsBeforeToken {
comments[i] = p.source.CommentTextWithoutIndent(comment)
}
p.exprComments[loc] = comments
p.lexer.CommentsBeforeToken = p.lexer.CommentsBeforeToken[0:]
}
return loc
}
type exprFlag uint8
const (
exprFlagDecorator exprFlag = 1 << iota
exprFlagForLoopInit
exprFlagForAwaitLoopInit
exprFlagAfterQuestionAndBeforeColon
exprFlagIsNewTarget
)
func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprFlag) js_ast.Expr {
loc := p.saveExprCommentsHere()
switch p.lexer.Token {
case js_lexer.TSuper:
superRange := p.lexer.Range()
p.lexer.Next()
switch p.lexer.Token {
case js_lexer.TOpenParen:
if level < js_ast.LCall && p.fnOrArrowDataParse.allowSuperCall {
return js_ast.Expr{Loc: loc, Data: js_ast.ESuperShared}
}
case js_lexer.TDot, js_lexer.TOpenBracket:
if p.fnOrArrowDataParse.allowSuperProperty {
return js_ast.Expr{Loc: loc, Data: js_ast.ESuperShared}
}
}
p.log.AddError(&p.tracker, superRange, "Unexpected \"super\"")
return js_ast.Expr{Loc: loc, Data: js_ast.ESuperShared}
case js_lexer.TOpenParen:
if errors != nil {
errors.invalidParens = append(errors.invalidParens, p.lexer.Range())
}
p.lexer.Next()
// Arrow functions aren't allowed in the middle of expressions
if level > js_ast.LAssign {
// Allow "in" inside parentheses
oldAllowIn := p.allowIn
p.allowIn = true
value := p.parseExpr(js_ast.LLowest)
// Don't consider the "@(...)" decorator syntax to be important parentheses to preserve
if (flags & exprFlagDecorator) == 0 {
p.markExprAsParenthesized(value, loc, false)
}
p.lexer.Expect(js_lexer.TCloseParen)
p.allowIn = oldAllowIn
return value
}
value := p.parseParenExpr(loc, level, parenExprOpts{
isAfterQuestionAndBeforeColon: (flags & exprFlagAfterQuestionAndBeforeColon) != 0,
})
return value
case js_lexer.TFalse:
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.EBoolean{Value: false}}
case js_lexer.TTrue:
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.EBoolean{Value: true}}
case js_lexer.TNull:
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: js_ast.ENullShared}
case js_lexer.TThis:
if p.fnOrArrowDataParse.isThisDisallowed {
p.log.AddError(&p.tracker, p.lexer.Range(), "Cannot use \"this\" here:")
}
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: js_ast.EThisShared}
case js_lexer.TPrivateIdentifier:
if !p.allowIn || level >= js_ast.LCompare {
p.lexer.Unexpected()
}
name := p.lexer.Identifier
p.lexer.Next()
// Check for "#foo in bar"
if p.lexer.Token != js_lexer.TIn {
p.lexer.Expected(js_lexer.TIn)
}
// Make sure to lower all matching private names
if p.options.unsupportedJSFeatures.Has(compat.ClassPrivateBrandCheck) {
if p.lowerAllOfThesePrivateNames == nil {
p.lowerAllOfThesePrivateNames = make(map[string]bool)
}
p.lowerAllOfThesePrivateNames[name.String] = true
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EPrivateIdentifier{Ref: p.storeNameInRef(name)}}
case js_lexer.TIdentifier:
name := p.lexer.Identifier
nameRange := p.lexer.Range()
raw := p.lexer.Raw()
p.lexer.Next()
// Handle async and await expressions
switch name.String {
case "async":
if raw == "async" {
return p.parseAsyncPrefixExpr(nameRange, level, flags)
}
case "await":
switch p.fnOrArrowDataParse.await {
case forbidAll:
p.log.AddError(&p.tracker, nameRange, "The keyword \"await\" cannot be used here:")
case allowExpr:
if raw != "await" {
p.log.AddError(&p.tracker, nameRange, "The keyword \"await\" cannot be escaped")
} else {
if p.fnOrArrowDataParse.isTopLevel {
p.topLevelAwaitKeyword = nameRange
}
if p.fnOrArrowDataParse.arrowArgErrors != nil {
p.fnOrArrowDataParse.arrowArgErrors.invalidExprAwait = nameRange
}
value := p.parseExpr(js_ast.LPrefix)
if p.lexer.Token == js_lexer.TAsteriskAsterisk {
p.lexer.Unexpected()
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EAwait{Value: value}}
}
case allowIdent:
p.lexer.PrevTokenWasAwaitKeyword = true
p.lexer.AwaitKeywordLoc = loc
p.lexer.FnOrArrowStartLoc = p.fnOrArrowDataParse.needsAsyncLoc
}
case "yield":
switch p.fnOrArrowDataParse.yield {
case forbidAll:
p.log.AddError(&p.tracker, nameRange, "The keyword \"yield\" cannot be used here:")
case allowExpr:
if raw != "yield" {
p.log.AddError(&p.tracker, nameRange, "The keyword \"yield\" cannot be escaped")
} else {
if level > js_ast.LAssign {
p.log.AddError(&p.tracker, nameRange, "Cannot use a \"yield\" expression here without parentheses:")
}
if p.fnOrArrowDataParse.arrowArgErrors != nil {
p.fnOrArrowDataParse.arrowArgErrors.invalidExprYield = nameRange
}
return p.parseYieldExpr(loc)
}
case allowIdent:
if !p.lexer.HasNewlineBefore {
// Try to gracefully recover if "yield" is used in the wrong place
switch p.lexer.Token {
case js_lexer.TNull, js_lexer.TIdentifier, js_lexer.TFalse, js_lexer.TTrue,
js_lexer.TNumericLiteral, js_lexer.TBigIntegerLiteral, js_lexer.TStringLiteral:
p.log.AddError(&p.tracker, nameRange, "Cannot use \"yield\" outside a generator function")
return p.parseYieldExpr(loc)
}
}
}
}
// Handle the start of an arrow expression
if p.lexer.Token == js_lexer.TEqualsGreaterThan && level <= js_ast.LAssign {
ref := p.storeNameInRef(name)
arg := js_ast.Arg{Binding: js_ast.Binding{Loc: loc, Data: &js_ast.BIdentifier{Ref: ref}}}
p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, loc)
defer p.popScope()
return js_ast.Expr{Loc: loc, Data: p.parseArrowBody([]js_ast.Arg{arg}, fnOrArrowDataParse{
needsAsyncLoc: loc,
})}
}
ref := p.storeNameInRef(name)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: ref}}
case js_lexer.TStringLiteral, js_lexer.TNoSubstitutionTemplateLiteral:
return p.parseStringLiteral()
case js_lexer.TTemplateHead:
var legacyOctalLoc logger.Loc
headLoc := p.lexer.Loc()
head := p.lexer.StringLiteral()
if p.lexer.LegacyOctalLoc.Start > loc.Start {
legacyOctalLoc = p.lexer.LegacyOctalLoc
}
parts, tailLegacyOctalLoc := p.parseTemplateParts(false /* includeRaw */)
if tailLegacyOctalLoc.Start > 0 {
legacyOctalLoc = tailLegacyOctalLoc
}
return js_ast.Expr{Loc: loc, Data: &js_ast.ETemplate{
HeadLoc: headLoc,
HeadCooked: head,
Parts: parts,
LegacyOctalLoc: legacyOctalLoc,
}}
case js_lexer.TNumericLiteral:
value := js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: p.lexer.Number}}
p.checkForLegacyOctalLiteral(value.Data)
p.lexer.Next()
return value
case js_lexer.TBigIntegerLiteral:
value := p.lexer.Identifier
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.EBigInt{Value: value.String}}
case js_lexer.TSlash, js_lexer.TSlashEquals:
p.lexer.ScanRegExp()
value := p.lexer.Raw()
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.ERegExp{Value: value}}
case js_lexer.TVoid:
p.lexer.Next()
value := p.parseExpr(js_ast.LPrefix)
if p.lexer.Token == js_lexer.TAsteriskAsterisk {
p.lexer.Unexpected()
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EUnary{Op: js_ast.UnOpVoid, Value: value}}
case js_lexer.TTypeof:
p.lexer.Next()
value := p.parseExpr(js_ast.LPrefix)
if p.lexer.Token == js_lexer.TAsteriskAsterisk {
p.lexer.Unexpected()
}
_, valueIsIdentifier := value.Data.(*js_ast.EIdentifier)
return js_ast.Expr{Loc: loc, Data: &js_ast.EUnary{
Op: js_ast.UnOpTypeof,
Value: value,
WasOriginallyTypeofIdentifier: valueIsIdentifier,
}}
case js_lexer.TDelete:
p.lexer.Next()
value := p.parseExpr(js_ast.LPrefix)
if p.lexer.Token == js_lexer.TAsteriskAsterisk {
p.lexer.Unexpected()
}
if index, ok := value.Data.(*js_ast.EIndex); ok {
if private, ok := index.Index.Data.(*js_ast.EPrivateIdentifier); ok {
name := p.loadNameFromRef(private.Ref)
r := logger.Range{Loc: index.Index.Loc, Len: int32(len(name))}
p.log.AddError(&p.tracker, r, fmt.Sprintf("Deleting the private name %q is forbidden", name))
}
}
_, valueIsIdentifier := value.Data.(*js_ast.EIdentifier)
return js_ast.Expr{Loc: loc, Data: &js_ast.EUnary{
Op: js_ast.UnOpDelete,
Value: value,
WasOriginallyDeleteOfIdentifierOrPropertyAccess: valueIsIdentifier || js_ast.IsPropertyAccess(value),
}}
case js_lexer.TPlus:
p.lexer.Next()
value := p.parseExpr(js_ast.LPrefix)
if p.lexer.Token == js_lexer.TAsteriskAsterisk {
p.lexer.Unexpected()
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EUnary{Op: js_ast.UnOpPos, Value: value}}
case js_lexer.TMinus:
p.lexer.Next()
value := p.parseExpr(js_ast.LPrefix)
if p.lexer.Token == js_lexer.TAsteriskAsterisk {
p.lexer.Unexpected()
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EUnary{Op: js_ast.UnOpNeg, Value: value}}
case js_lexer.TTilde:
p.lexer.Next()
value := p.parseExpr(js_ast.LPrefix)
if p.lexer.Token == js_lexer.TAsteriskAsterisk {
p.lexer.Unexpected()
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EUnary{Op: js_ast.UnOpCpl, Value: value}}
case js_lexer.TExclamation:
p.lexer.Next()
value := p.parseExpr(js_ast.LPrefix)
if p.lexer.Token == js_lexer.TAsteriskAsterisk {
p.lexer.Unexpected()
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EUnary{Op: js_ast.UnOpNot, Value: value}}
case js_lexer.TMinusMinus:
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.EUnary{Op: js_ast.UnOpPreDec, Value: p.parseExpr(js_ast.LPrefix)}}
case js_lexer.TPlusPlus:
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.EUnary{Op: js_ast.UnOpPreInc, Value: p.parseExpr(js_ast.LPrefix)}}
case js_lexer.TFunction:
return p.parseFnExpr(loc, false /* isAsync */, logger.Range{})
case js_lexer.TClass:
return p.parseClassExpr(nil)
case js_lexer.TAt:
// Parse decorators before class expressions
decorators := p.parseDecorators(p.currentScope, logger.Range{}, decoratorBeforeClassExpr)
return p.parseClassExpr(decorators)
case js_lexer.TNew:
p.lexer.Next()
// Special-case the weird "new.target" expression here
if p.lexer.Token == js_lexer.TDot {
p.lexer.Next()
if p.lexer.Token != js_lexer.TIdentifier || p.lexer.Raw() != "target" {
p.lexer.Unexpected()
}
r := logger.Range{Loc: loc, Len: p.lexer.Range().End() - loc.Start}
p.markSyntaxFeature(compat.NewTarget, r)
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.ENewTarget{Range: r}}
}
target := p.parseExprWithFlags(js_ast.LMember, flags|exprFlagIsNewTarget)
args := []js_ast.Expr{}
var closeParenLoc logger.Loc
var isMultiLine bool
if p.lexer.Token == js_lexer.TOpenParen {
args, closeParenLoc, isMultiLine = p.parseCallArgs()
}
return js_ast.Expr{Loc: loc, Data: &js_ast.ENew{
Target: target,
Args: args,
CloseParenLoc: closeParenLoc,
IsMultiLine: isMultiLine,
}}
case js_lexer.TOpenBracket:
p.lexer.Next()
isSingleLine := !p.lexer.HasNewlineBefore
items := []js_ast.Expr{}
selfErrors := deferredErrors{}
commaAfterSpread := logger.Loc{}
// Allow "in" inside arrays
oldAllowIn := p.allowIn
p.allowIn = true
for p.lexer.Token != js_lexer.TCloseBracket {
switch p.lexer.Token {
case js_lexer.TComma:
items = append(items, js_ast.Expr{Loc: p.lexer.Loc(), Data: js_ast.EMissingShared})
case js_lexer.TDotDotDot:
if errors != nil {
errors.arraySpreadFeature = p.lexer.Range()
} else {
p.markSyntaxFeature(compat.ArraySpread, p.lexer.Range())
}
dotsLoc := p.saveExprCommentsHere()
p.lexer.Next()
item := p.parseExprOrBindings(js_ast.LComma, &selfErrors)
items = append(items, js_ast.Expr{Loc: dotsLoc, Data: &js_ast.ESpread{Value: item}})
// Commas are not allowed here when destructuring
if p.lexer.Token == js_lexer.TComma {
commaAfterSpread = p.lexer.Loc()
}
default:
item := p.parseExprOrBindings(js_ast.LComma, &selfErrors)
items = append(items, item)
}
if p.lexer.Token != js_lexer.TComma {
break
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Next()
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
closeBracketLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBracket)
p.allowIn = oldAllowIn
if p.willNeedBindingPattern() {
// Is this a binding pattern?
} else if errors == nil {
// Is this an expression?
p.logExprErrors(&selfErrors)
} else {
// In this case, we can't distinguish between the two yet
selfErrors.mergeInto(errors)
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EArray{
Items: items,
CommaAfterSpread: commaAfterSpread,
IsSingleLine: isSingleLine,
CloseBracketLoc: closeBracketLoc,
}}
case js_lexer.TOpenBrace:
p.lexer.Next()
isSingleLine := !p.lexer.HasNewlineBefore
properties := []js_ast.Property{}
selfErrors := deferredErrors{}
commaAfterSpread := logger.Loc{}
// Allow "in" inside object literals
oldAllowIn := p.allowIn
p.allowIn = true
for p.lexer.Token != js_lexer.TCloseBrace {
if p.lexer.Token == js_lexer.TDotDotDot {
dotLoc := p.saveExprCommentsHere()
p.lexer.Next()
value := p.parseExprOrBindings(js_ast.LComma, &selfErrors)
properties = append(properties, js_ast.Property{
Kind: js_ast.PropertySpread,
Loc: dotLoc,
ValueOrNil: value,
})
// Commas are not allowed here when destructuring
if p.lexer.Token == js_lexer.TComma {
commaAfterSpread = p.lexer.Loc()
}
} else {
// This property may turn out to be a type in TypeScript, which should be ignored
if property, ok := p.parseProperty(p.saveExprCommentsHere(), js_ast.PropertyField, propertyOpts{}, &selfErrors); ok {
properties = append(properties, property)
}
}
if p.lexer.Token != js_lexer.TComma {
break
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Next()
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
closeBraceLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBrace)
p.allowIn = oldAllowIn
if p.willNeedBindingPattern() {
// Is this a binding pattern?
} else if errors == nil {
// Is this an expression?
p.logExprErrors(&selfErrors)
} else {
// In this case, we can't distinguish between the two yet
selfErrors.mergeInto(errors)
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EObject{
Properties: properties,
CommaAfterSpread: commaAfterSpread,
IsSingleLine: isSingleLine,
CloseBraceLoc: closeBraceLoc,
}}
case js_lexer.TLessThan:
// This is a very complicated and highly ambiguous area of TypeScript
// syntax. Many similar-looking things are overloaded.
//
// TS:
//
// A type cast:
// <A>(x)
// <[]>(x)
// <A[]>(x)
// <const>(x)
//
// An arrow function with type parameters:
// <A>(x) => {}
// <A, B>(x) => {}
// <A = B>(x) => {}
// <A extends B>(x) => {}
// <const A>(x) => {}
// <const A extends B>(x) => {}
//
// A syntax error:
// <>() => {}
//
// TSX:
//
// A JSX element:
// <>() => {}</>
// <A>(x) => {}</A>
// <A extends/>
// <A extends>(x) => {}</A>
// <A extends={false}>(x) => {}</A>
// <const A extends/>
// <const A extends>(x) => {}</const>
//
// An arrow function with type parameters:
// <A,>(x) => {}
// <A, B>(x) => {}
// <A = B>(x) => {}
// <A extends B>(x) => {}
// <const>(x)</const>
// <const A extends B>(x) => {}
//
// A syntax error:
// <[]>(x)
// <A[]>(x)
// <>() => {}
// <A>(x) => {}
if p.options.ts.Parse && p.options.jsx.Parse && p.isTSArrowFnJSX() {
p.skipTypeScriptTypeParameters(allowConstModifier)
p.lexer.Expect(js_lexer.TOpenParen)
return p.parseParenExpr(loc, level, parenExprOpts{forceArrowFn: true})
}
// Print a friendly error message when parsing JSX as JavaScript
if !p.options.jsx.Parse && !p.options.ts.Parse {
var how string
switch logger.API {
case logger.CLIAPI:
how = " You can use \"--loader:.js=jsx\" to do that."
case logger.JSAPI:
how = " You can use \"loader: { '.js': 'jsx' }\" to do that."
case logger.GoAPI:
how = " You can use 'Loader: map[string]api.Loader{\".js\": api.LoaderJSX}' to do that."
}
p.log.AddErrorWithNotes(&p.tracker, p.lexer.Range(), "The JSX syntax extension is not currently enabled", []logger.MsgData{{
Text: "The esbuild loader for this file is currently set to \"js\" but it must be set to \"jsx\" to be able to parse JSX syntax." + how}})
p.options.jsx.Parse = true
}
if p.options.jsx.Parse {
// Use NextInsideJSXElement() instead of Next() so we parse "<<" as "<"
p.lexer.NextInsideJSXElement()
element := p.parseJSXElement(loc)
// The call to parseJSXElement() above doesn't consume the last
// TGreaterThan because the caller knows what Next() function to call.
// Use Next() instead of NextInsideJSXElement() here since the next
// token is an expression.
p.lexer.Next()
return element
}
if p.options.ts.Parse {
// This is either an old-style type cast or a generic lambda function
// TypeScript 4.5 introduced the ".mts" and ".cts" extensions that forbid
// the use of an expression starting with "<" that would be ambiguous
// when the file is in JSX mode.
if p.options.ts.NoAmbiguousLessThan && !p.isTSArrowFnJSX() {
p.log.AddError(&p.tracker, p.lexer.Range(),
"This syntax is not allowed in files with the \".mts\" or \".cts\" extension")
}
// "<T>(x)"
// "<T>(x) => {}"
if result := p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking(); result != didNotSkipAnything {
p.lexer.Expect(js_lexer.TOpenParen)
return p.parseParenExpr(loc, level, parenExprOpts{
forceArrowFn: result == definitelyTypeParameters,
})
}
// "<T>x"
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
p.lexer.ExpectGreaterThan(false /* isInsideJSXElement */)
value := p.parsePrefix(level, errors, flags)
return value
}
p.lexer.Unexpected()
return js_ast.Expr{}
case js_lexer.TImport:
p.lexer.Next()
return p.parseImportExpr(loc, level)
default:
p.lexer.Unexpected()
return js_ast.Expr{}
}
}
func (p *parser) parseYieldExpr(loc logger.Loc) js_ast.Expr {
// Parse a yield-from expression, which yields from an iterator
isStar := p.lexer.Token == js_lexer.TAsterisk
if isStar && !p.lexer.HasNewlineBefore {
p.lexer.Next()
}
var valueOrNil js_ast.Expr
// The yield expression only has a value in certain cases
if isStar {
valueOrNil = p.parseExpr(js_ast.LYield)
} else {
switch p.lexer.Token {
case js_lexer.TCloseBrace, js_lexer.TCloseBracket, js_lexer.TCloseParen,
js_lexer.TColon, js_lexer.TComma, js_lexer.TSemicolon:
default:
if !p.lexer.HasNewlineBefore {
valueOrNil = p.parseExpr(js_ast.LYield)
}
}
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EYield{ValueOrNil: valueOrNil, IsStar: isStar}}
}
func (p *parser) willNeedBindingPattern() bool {
switch p.lexer.Token {
case js_lexer.TEquals:
// "[a] = b;"
return true
case js_lexer.TIn:
// "for ([a] in b) {}"
return !p.allowIn
case js_lexer.TIdentifier:
// "for ([a] of b) {}"
return !p.allowIn && p.lexer.IsContextualKeyword("of")
default:
return false
}
}
// Note: The caller has already parsed the "import" keyword
func (p *parser) parseImportExpr(loc logger.Loc, level js_ast.L) js_ast.Expr {
// Parse an "import.meta" expression
phase := ast.EvaluationPhase
if p.lexer.Token == js_lexer.TDot {
p.lexer.Next()
if p.lexer.IsContextualKeyword("meta") {
p.esmImportMeta = logger.Range{Loc: loc, Len: p.lexer.Range().End() - loc.Start}
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.EImportMeta{RangeLen: p.esmImportMeta.Len}}
} else if p.lexer.IsContextualKeyword("defer") {
p.markSyntaxFeature(compat.ImportDefer, p.lexer.Range())
phase = ast.DeferPhase
p.lexer.Next()
} else if p.lexer.IsContextualKeyword("source") {
p.markSyntaxFeature(compat.ImportSource, p.lexer.Range())
phase = ast.SourcePhase
p.lexer.Next()
} else {
p.lexer.Unexpected()
}
}
if level > js_ast.LCall {
r := js_lexer.RangeOfIdentifier(p.source, loc)
p.log.AddError(&p.tracker, r, "Cannot use an \"import\" expression here without parentheses:")
}
// Allow "in" inside call arguments
oldAllowIn := p.allowIn
p.allowIn = true
p.lexer.Expect(js_lexer.TOpenParen)
value := p.parseExpr(js_ast.LComma)
var optionsOrNil js_ast.Expr
if p.lexer.Token == js_lexer.TComma {
// "import('./foo.json', )"
p.lexer.Next()
if p.lexer.Token != js_lexer.TCloseParen {
// "import('./foo.json', { assert: { type: 'json' } })"
optionsOrNil = p.parseExpr(js_ast.LComma)
if p.lexer.Token == js_lexer.TComma {
// "import('./foo.json', { assert: { type: 'json' } }, )"
p.lexer.Next()
}
}
}
closeParenLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseParen)
p.allowIn = oldAllowIn
return js_ast.Expr{Loc: loc, Data: &js_ast.EImportCall{
Expr: value,
OptionsOrNil: optionsOrNil,
CloseParenLoc: closeParenLoc,
Phase: phase,
}}
}
func (p *parser) parseExprOrBindings(level js_ast.L, errors *deferredErrors) js_ast.Expr {
return p.parseExprCommon(level, errors, 0)
}
func (p *parser) parseExpr(level js_ast.L) js_ast.Expr {
return p.parseExprCommon(level, nil, 0)
}
func (p *parser) parseExprWithFlags(level js_ast.L, flags exprFlag) js_ast.Expr {
return p.parseExprCommon(level, nil, flags)
}
func (p *parser) parseExprCommon(level js_ast.L, errors *deferredErrors, flags exprFlag) js_ast.Expr {
lexerCommentFlags := p.lexer.HasCommentBefore
expr := p.parsePrefix(level, errors, flags)
if (lexerCommentFlags&(js_lexer.PureCommentBefore|js_lexer.NoSideEffectsCommentBefore)) != 0 && !p.options.ignoreDCEAnnotations {
if (lexerCommentFlags & js_lexer.NoSideEffectsCommentBefore) != 0 {
switch e := expr.Data.(type) {
case *js_ast.EArrow:
e.HasNoSideEffectsComment = true
case *js_ast.EFunction:
e.Fn.HasNoSideEffectsComment = true
}
}
// There is no formal spec for "__PURE__" comments but from reverse-
// engineering, it looks like they apply to the next CallExpression or
// NewExpression. So in "/* @__PURE__ */ a().b() + c()" the comment applies
// to the expression "a().b()".
if (lexerCommentFlags&js_lexer.PureCommentBefore) != 0 && level < js_ast.LCall {
expr = p.parseSuffix(expr, js_ast.LCall-1, errors, flags)
switch e := expr.Data.(type) {
case *js_ast.ECall:
e.CanBeUnwrappedIfUnused = true
case *js_ast.ENew:
e.CanBeUnwrappedIfUnused = true
}
}
}
return p.parseSuffix(expr, level, errors, flags)
}
func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredErrors, flags exprFlag) js_ast.Expr {
optionalChain := js_ast.OptionalChainNone
afterAsLoc := logger.Loc{Start: -1}
for {
if p.lexer.Loc() == p.afterArrowBodyLoc {
for {
switch p.lexer.Token {
case js_lexer.TComma:
if level >= js_ast.LComma {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpComma, Left: left, Right: p.parseExpr(js_ast.LComma)}}
default:
return left
}
}
}
// Stop now if this token is forbidden to follow a TypeScript "as" cast
operatorRange := p.lexer.Range()
if operatorRange.Loc == p.forbidSuffixAfterAsLoc {
return left
}
isAfterAs := operatorRange.Loc == afterAsLoc
// Reset the optional chain flag by default. That way we won't accidentally
// treat "c.d" as OptionalChainContinue in "a?.b + c.d".
oldOptionalChain := optionalChain
optionalChain = js_ast.OptionalChainNone
switch p.lexer.Token {
case js_lexer.TDot:
p.lexer.Next()
if p.lexer.Token == js_lexer.TPrivateIdentifier {
// "a.#b"
// "a?.b.#c"
if _, ok := left.Data.(*js_ast.ESuper); ok {
p.lexer.Expected(js_lexer.TIdentifier)
}
name := p.lexer.Identifier
nameLoc := p.lexer.Loc()
p.reportPrivateNameUsage(name.String)
p.lexer.Next()
ref := p.storeNameInRef(name)
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EIndex{
Target: left,
Index: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EPrivateIdentifier{Ref: ref}},
OptionalChain: oldOptionalChain,
}}
} else {
// "a.b"
// "a?.b.c"
if !p.lexer.IsIdentifierOrKeyword() {
p.lexer.Expect(js_lexer.TIdentifier)
}
name := p.lexer.Identifier
nameLoc := p.lexer.Loc()
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: p.dotOrMangledPropParse(left, name, nameLoc, oldOptionalChain, wasOriginallyDot)}
}
optionalChain = oldOptionalChain
case js_lexer.TQuestionDot:
if (flags & exprFlagIsNewTarget) != 0 {
p.log.AddError(&p.tracker, p.lexer.Range(), "Cannot use an unparenthesized optional chain inside the target of \"new\"")
flags &= ^exprFlagIsNewTarget // Don't report this error more than once in this spot
}
p.lexer.Next()
optionalStart := js_ast.OptionalChainStart
// Remove unnecessary optional chains
if p.options.minifySyntax {
if isNullOrUndefined, _, ok := js_ast.ToNullOrUndefinedWithSideEffects(left.Data); ok && !isNullOrUndefined {
optionalStart = js_ast.OptionalChainNone
}
}
switch p.lexer.Token {
case js_lexer.TOpenBracket:
// "a?.[b]"
p.lexer.Next()
// Allow "in" inside the brackets
oldAllowIn := p.allowIn
p.allowIn = true
index := p.parseExpr(js_ast.LLowest)
p.allowIn = oldAllowIn
closeBracketLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBracket)
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EIndex{
Target: left,
Index: index,
OptionalChain: optionalStart,
CloseBracketLoc: closeBracketLoc,
}}
case js_lexer.TOpenParen:
// "a?.()"
if level >= js_ast.LCall {
return left
}
kind := js_ast.NormalCall
if js_ast.IsPropertyAccess(left) {
kind = js_ast.TargetWasOriginallyPropertyAccess
}
args, closeParenLoc, isMultiLine := p.parseCallArgs()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.ECall{
Target: left,
Args: args,
CloseParenLoc: closeParenLoc,
OptionalChain: optionalStart,
IsMultiLine: isMultiLine,
Kind: kind,
}}
case js_lexer.TLessThan, js_lexer.TLessThanLessThan:
// "a?.<T>()"
// "a?.<<T>() => T>()"
if !p.options.ts.Parse {
p.lexer.Expected(js_lexer.TIdentifier)
}
p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{})
if p.lexer.Token != js_lexer.TOpenParen {
p.lexer.Expected(js_lexer.TOpenParen)
}
if level >= js_ast.LCall {
return left
}
kind := js_ast.NormalCall
if js_ast.IsPropertyAccess(left) {
kind = js_ast.TargetWasOriginallyPropertyAccess
}
args, closeParenLoc, isMultiLine := p.parseCallArgs()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.ECall{
Target: left,
Args: args,
CloseParenLoc: closeParenLoc,
OptionalChain: optionalStart,
IsMultiLine: isMultiLine,
Kind: kind,
}}
default:
if p.lexer.Token == js_lexer.TPrivateIdentifier {
// "a?.#b"
name := p.lexer.Identifier
nameLoc := p.lexer.Loc()
p.reportPrivateNameUsage(name.String)
p.lexer.Next()
ref := p.storeNameInRef(name)
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EIndex{
Target: left,
Index: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EPrivateIdentifier{Ref: ref}},
OptionalChain: optionalStart,
}}
} else {
// "a?.b"
if !p.lexer.IsIdentifierOrKeyword() {
p.lexer.Expect(js_lexer.TIdentifier)
}
name := p.lexer.Identifier
nameLoc := p.lexer.Loc()
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: p.dotOrMangledPropParse(left, name, nameLoc, optionalStart, wasOriginallyDot)}
}
}
// Only continue if we have started
if optionalStart == js_ast.OptionalChainStart {
optionalChain = js_ast.OptionalChainContinue
}
case js_lexer.TNoSubstitutionTemplateLiteral:
if oldOptionalChain != js_ast.OptionalChainNone {
p.log.AddError(&p.tracker, p.lexer.Range(), "Template literals cannot have an optional chain as a tag")
}
headLoc := p.lexer.Loc()
headCooked, headRaw := p.lexer.CookedAndRawTemplateContents()
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.ETemplate{
TagOrNil: left,
HeadLoc: headLoc,
HeadCooked: headCooked,
HeadRaw: headRaw,
TagWasOriginallyPropertyAccess: js_ast.IsPropertyAccess(left),
}}
case js_lexer.TTemplateHead:
if oldOptionalChain != js_ast.OptionalChainNone {
p.log.AddError(&p.tracker, p.lexer.Range(), "Template literals cannot have an optional chain as a tag")
}
headLoc := p.lexer.Loc()
headCooked, headRaw := p.lexer.CookedAndRawTemplateContents()
parts, _ := p.parseTemplateParts(true /* includeRaw */)
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.ETemplate{
TagOrNil: left,
HeadLoc: headLoc,
HeadCooked: headCooked,
HeadRaw: headRaw,
Parts: parts,
TagWasOriginallyPropertyAccess: js_ast.IsPropertyAccess(left),
}}
case js_lexer.TOpenBracket:
// When parsing a decorator, ignore EIndex expressions since they may be
// part of a computed property:
//
// class Foo {
// @foo ['computed']() {}
// }
//
// This matches the behavior of the TypeScript compiler.
if (flags & exprFlagDecorator) != 0 {
return left
}
p.lexer.Next()
// Allow "in" inside the brackets
oldAllowIn := p.allowIn
p.allowIn = true
index := p.parseExpr(js_ast.LLowest)
p.allowIn = oldAllowIn
closeBracketLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBracket)
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EIndex{
Target: left,
Index: index,
OptionalChain: oldOptionalChain,
CloseBracketLoc: closeBracketLoc,
}}
optionalChain = oldOptionalChain
case js_lexer.TOpenParen:
if level >= js_ast.LCall {
return left
}
kind := js_ast.NormalCall
if js_ast.IsPropertyAccess(left) {
kind = js_ast.TargetWasOriginallyPropertyAccess
}
args, closeParenLoc, isMultiLine := p.parseCallArgs()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.ECall{
Target: left,
Args: args,
CloseParenLoc: closeParenLoc,
OptionalChain: oldOptionalChain,
IsMultiLine: isMultiLine,
Kind: kind,
}}
optionalChain = oldOptionalChain
case js_lexer.TQuestion:
if level >= js_ast.LConditional {
return left
}
p.lexer.Next()
// Stop now if we're parsing one of these:
// "(a?) => {}"
// "(a?: b) => {}"
// "(a?, b?) => {}"
if p.options.ts.Parse && left.Loc == p.latestArrowArgLoc && (p.lexer.Token == js_lexer.TColon ||
p.lexer.Token == js_lexer.TCloseParen || p.lexer.Token == js_lexer.TComma) {
if errors == nil {
p.lexer.Unexpected()
}
errors.invalidExprAfterQuestion = p.lexer.Range()
return left
}
// Allow "in" in between "?" and ":"
oldAllowIn := p.allowIn
p.allowIn = true
yes := p.parseExprWithFlags(js_ast.LComma, exprFlagAfterQuestionAndBeforeColon)
p.allowIn = oldAllowIn
p.lexer.Expect(js_lexer.TColon)
no := p.parseExprWithFlags(js_ast.LComma, flags&exprFlagAfterQuestionAndBeforeColon)
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EIf{Test: left, Yes: yes, No: no}}
case js_lexer.TExclamation:
// Skip over TypeScript non-null assertions
if p.lexer.HasNewlineBefore {
return left
}
if !p.options.ts.Parse {
p.lexer.Unexpected()
}
p.lexer.Next()
optionalChain = oldOptionalChain
case js_lexer.TMinusMinus:
if p.lexer.HasNewlineBefore || level >= js_ast.LPostfix {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EUnary{Op: js_ast.UnOpPostDec, Value: left}}
case js_lexer.TPlusPlus:
if p.lexer.HasNewlineBefore || level >= js_ast.LPostfix {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EUnary{Op: js_ast.UnOpPostInc, Value: left}}
case js_lexer.TComma:
if level >= js_ast.LComma {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpComma, Left: left, Right: p.parseExpr(js_ast.LComma)}}
case js_lexer.TPlus:
if level >= js_ast.LAdd {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpAdd, Left: left, Right: p.parseExpr(js_ast.LAdd)}}
case js_lexer.TPlusEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpAddAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TMinus:
if level >= js_ast.LAdd {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpSub, Left: left, Right: p.parseExpr(js_ast.LAdd)}}
case js_lexer.TMinusEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpSubAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TAsterisk:
if level >= js_ast.LMultiply {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpMul, Left: left, Right: p.parseExpr(js_ast.LMultiply)}}
case js_lexer.TAsteriskAsterisk:
if level >= js_ast.LExponentiation {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpPow, Left: left, Right: p.parseExpr(js_ast.LExponentiation - 1)}}
case js_lexer.TAsteriskAsteriskEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpPowAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TAsteriskEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpMulAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TPercent:
if level >= js_ast.LMultiply {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpRem, Left: left, Right: p.parseExpr(js_ast.LMultiply)}}
case js_lexer.TPercentEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpRemAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TSlash:
if level >= js_ast.LMultiply {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpDiv, Left: left, Right: p.parseExpr(js_ast.LMultiply)}}
case js_lexer.TSlashEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpDivAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TEqualsEquals:
if level >= js_ast.LEquals {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpLooseEq, Left: left, Right: p.parseExpr(js_ast.LEquals)}}
case js_lexer.TExclamationEquals:
if level >= js_ast.LEquals {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpLooseNe, Left: left, Right: p.parseExpr(js_ast.LEquals)}}
case js_lexer.TEqualsEqualsEquals:
if level >= js_ast.LEquals {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpStrictEq, Left: left, Right: p.parseExpr(js_ast.LEquals)}}
case js_lexer.TExclamationEqualsEquals:
if level >= js_ast.LEquals {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpStrictNe, Left: left, Right: p.parseExpr(js_ast.LEquals)}}
case js_lexer.TLessThan:
// TypeScript allows type arguments to be specified with angle brackets
// inside an expression. Unlike in other languages, this unfortunately
// appears to require backtracking to parse.
if p.options.ts.Parse && p.trySkipTypeArgumentsInExpressionWithBacktracking() {
optionalChain = oldOptionalChain
continue
}
if level >= js_ast.LCompare {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpLt, Left: left, Right: p.parseExpr(js_ast.LCompare)}}
case js_lexer.TLessThanEquals:
if level >= js_ast.LCompare {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpLe, Left: left, Right: p.parseExpr(js_ast.LCompare)}}
case js_lexer.TGreaterThan:
if level >= js_ast.LCompare {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpGt, Left: left, Right: p.parseExpr(js_ast.LCompare)}}
case js_lexer.TGreaterThanEquals:
if level >= js_ast.LCompare {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpGe, Left: left, Right: p.parseExpr(js_ast.LCompare)}}
case js_lexer.TLessThanLessThan:
// TypeScript allows type arguments to be specified with angle brackets
// inside an expression. Unlike in other languages, this unfortunately
// appears to require backtracking to parse.
if p.options.ts.Parse && p.trySkipTypeArgumentsInExpressionWithBacktracking() {
optionalChain = oldOptionalChain
continue
}
if level >= js_ast.LShift {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpShl, Left: left, Right: p.parseExpr(js_ast.LShift)}}
case js_lexer.TLessThanLessThanEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpShlAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TGreaterThanGreaterThan:
if level >= js_ast.LShift {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpShr, Left: left, Right: p.parseExpr(js_ast.LShift)}}
case js_lexer.TGreaterThanGreaterThanEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpShrAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TGreaterThanGreaterThanGreaterThan:
if level >= js_ast.LShift {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpUShr, Left: left, Right: p.parseExpr(js_ast.LShift)}}
case js_lexer.TGreaterThanGreaterThanGreaterThanEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpUShrAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TQuestionQuestion:
if level >= js_ast.LNullishCoalescing {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpNullishCoalescing, Left: left, Right: p.parseExpr(js_ast.LNullishCoalescing)}}
case js_lexer.TQuestionQuestionEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpNullishCoalescingAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TBarBar:
if level >= js_ast.LLogicalOr {
return left
}
// Prevent "||" inside "??" from the right
if level == js_ast.LNullishCoalescing {
p.logNullishCoalescingErrorPrecedenceError("||")
}
p.lexer.Next()
right := p.parseExpr(js_ast.LLogicalOr)
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpLogicalOr, Left: left, Right: right}}
// Prevent "||" inside "??" from the left
if level < js_ast.LNullishCoalescing {
left = p.parseSuffix(left, js_ast.LNullishCoalescing+1, nil, flags)
if p.lexer.Token == js_lexer.TQuestionQuestion {
p.logNullishCoalescingErrorPrecedenceError("||")
}
}
case js_lexer.TBarBarEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpLogicalOrAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TAmpersandAmpersand:
if level >= js_ast.LLogicalAnd {
return left
}
// Prevent "&&" inside "??" from the right
if level == js_ast.LNullishCoalescing {
p.logNullishCoalescingErrorPrecedenceError("&&")
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpLogicalAnd, Left: left, Right: p.parseExpr(js_ast.LLogicalAnd)}}
// Prevent "&&" inside "??" from the left
if level < js_ast.LNullishCoalescing {
left = p.parseSuffix(left, js_ast.LNullishCoalescing+1, nil, flags)
if p.lexer.Token == js_lexer.TQuestionQuestion {
p.logNullishCoalescingErrorPrecedenceError("&&")
}
}
case js_lexer.TAmpersandAmpersandEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpLogicalAndAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TBar:
if level >= js_ast.LBitwiseOr {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpBitwiseOr, Left: left, Right: p.parseExpr(js_ast.LBitwiseOr)}}
case js_lexer.TBarEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpBitwiseOrAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TAmpersand:
if level >= js_ast.LBitwiseAnd {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpBitwiseAnd, Left: left, Right: p.parseExpr(js_ast.LBitwiseAnd)}}
case js_lexer.TAmpersandEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpBitwiseAndAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TCaret:
if level >= js_ast.LBitwiseXor {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpBitwiseXor, Left: left, Right: p.parseExpr(js_ast.LBitwiseXor)}}
case js_lexer.TCaretEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpBitwiseXorAssign, Left: left, Right: p.parseExpr(js_ast.LAssign - 1)}}
case js_lexer.TEquals:
if level >= js_ast.LAssign {
return left
}
p.lexer.Next()
left = js_ast.Assign(left, p.parseExpr(js_ast.LAssign-1))
case js_lexer.TIn:
if level >= js_ast.LCompare || !p.allowIn {
return left
}
// Warn about "!a in b" instead of "!(a in b)"
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
if e, ok := left.Data.(*js_ast.EUnary); ok && e.Op == js_ast.UnOpNot {
r := logger.Range{Loc: left.Loc, Len: p.source.LocBeforeWhitespace(p.lexer.Loc()).Start - left.Loc.Start}
data := p.tracker.MsgData(r, "Suspicious use of the \"!\" operator inside the \"in\" operator")
data.Location.Suggestion = textForParenthesesSuggestion(p.source.TextForRange(r))
p.log.AddMsgID(logger.MsgID_JS_SuspiciousBooleanNot, logger.Msg{
Kind: kind,
Data: data,
Notes: []logger.MsgData{{Text: "The code \"!x in y\" is parsed as \"(!x) in y\". " +
"You need to insert parentheses to get \"!(x in y)\" instead."}},
})
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpIn, Left: left, Right: p.parseExpr(js_ast.LCompare)}}
case js_lexer.TInstanceof:
if level >= js_ast.LCompare {
return left
}
// Warn about "!a instanceof b" instead of "!(a instanceof b)". Here's an
// example of code with this problem: https://github.com/mrdoob/three.js/pull/11182.
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
if e, ok := left.Data.(*js_ast.EUnary); ok && e.Op == js_ast.UnOpNot {
r := logger.Range{Loc: left.Loc, Len: p.source.LocBeforeWhitespace(p.lexer.Loc()).Start - left.Loc.Start}
data := p.tracker.MsgData(r, "Suspicious use of the \"!\" operator inside the \"instanceof\" operator")
data.Location.Suggestion = textForParenthesesSuggestion(p.source.TextForRange(r))
p.log.AddMsgID(logger.MsgID_JS_SuspiciousBooleanNot, logger.Msg{
Kind: kind,
Data: data,
Notes: []logger.MsgData{{Text: "The code \"!x instanceof y\" is parsed as \"(!x) instanceof y\". " +
"You need to insert parentheses to get \"!(x instanceof y)\" instead."}},
})
}
p.lexer.Next()
left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpInstanceof, Left: left, Right: p.parseExpr(js_ast.LCompare)}}
default:
// Handle the TypeScript "as"/"satisfies" operator
if p.options.ts.Parse && level < js_ast.LCompare && !p.lexer.HasNewlineBefore && (p.lexer.IsContextualKeyword("as") || p.lexer.IsContextualKeyword("satisfies")) {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
// These tokens are not allowed to follow a cast expression. This isn't
// an outright error because it may be on a new line, in which case it's
// the start of a new expression when it's after a cast:
//
// x = y as z
// (something);
//
switch p.lexer.Token {
case js_lexer.TPlusPlus, js_lexer.TMinusMinus, js_lexer.TNoSubstitutionTemplateLiteral,
js_lexer.TTemplateHead, js_lexer.TOpenParen, js_lexer.TOpenBracket, js_lexer.TQuestionDot:
p.forbidSuffixAfterAsLoc = p.lexer.Loc()
return left
}
if p.lexer.Token.IsAssign() {
p.forbidSuffixAfterAsLoc = p.lexer.Loc()
return left
}
afterAsLoc = p.lexer.Loc()
continue
}
return left
}
// See: https://github.com/microsoft/TypeScript/issues/63527
if isAfterAs {
if binary, ok := left.Data.(*js_ast.EBinary); ok {
if binaryLeft, ok := binary.Left.Data.(*js_ast.EBinary); ok && !binaryLeft.IsParenthesized {
rightLevel := js_ast.OpTable[binary.Op].Level
if binary.Op.IsRightAssociative() {
rightLevel++
}
if rightLevel > js_ast.OpTable[binaryLeft.Op].Level {
start := binary.Left.Loc
end := p.source.LocBeforeWhitespace(operatorRange.Loc)
wrapRange := logger.Range{Loc: start, Len: end.Start - start.Start}
note := logger.MsgData{Text: "This is a syntax error in newer versions of TypeScript because the type cast has unintuitive precedence in this case."}
if text := p.source.TextForRange(wrapRange); !strings.ContainsRune(text, '\n') {
note = p.tracker.MsgData(wrapRange, note.Text+" Surround the inner expression in parentheses to silence this warning:")
note.Location.Suggestion = textForParenthesesSuggestion(text)
}
p.log.AddIDWithNotes(logger.MsgID_JS_ConfusingTypeScriptCast, logger.Warning, &p.tracker, operatorRange,
fmt.Sprintf("Operator %q should not directly follow a TypeScript type cast after the %q operator",
js_ast.OpTable[binary.Op].Text, js_ast.OpTable[binaryLeft.Op].Text),
[]logger.MsgData{note},
)
}
}
}
}
}
}
func (p *parser) parseExprOrLetOrUsingStmt(opts parseStmtOpts) (js_ast.Expr, js_ast.Stmt, []js_ast.Decl) {
couldBeLet := false
couldBeUsing := false
couldBeAwaitUsing := false
tokenRange := p.lexer.Range()
if p.lexer.Token == js_lexer.TIdentifier {
raw := p.lexer.Raw()
couldBeLet = raw == "let"
couldBeUsing = raw == "using"
couldBeAwaitUsing = raw == "await" && p.fnOrArrowDataParse.await == allowExpr
}
if !couldBeLet && !couldBeUsing && !couldBeAwaitUsing {
var flags exprFlag
if opts.isForLoopInit {
flags |= exprFlagForLoopInit
}
if opts.isForAwaitLoopInit {
flags |= exprFlagForAwaitLoopInit
}
return p.parseExprCommon(js_ast.LLowest, nil, flags), js_ast.Stmt{}, nil
}
name := p.lexer.Identifier
p.lexer.Next()
if couldBeLet {
isLet := opts.isExport
switch p.lexer.Token {
case js_lexer.TIdentifier, js_lexer.TOpenBracket, js_lexer.TOpenBrace:
if opts.lexicalDecl == lexicalDeclAllowAll || !p.lexer.HasNewlineBefore || p.lexer.Token == js_lexer.TOpenBracket {
isLet = true
}
}
if isLet {
// Handle a "let" declaration
if opts.lexicalDecl != lexicalDeclAllowAll {
p.forbidLexicalDecl(tokenRange.Loc)
}
p.markSyntaxFeature(compat.ConstAndLet, tokenRange)
decls := p.parseAndDeclareDecls(ast.SymbolOther, opts)
return js_ast.Expr{}, js_ast.Stmt{Loc: tokenRange.Loc, Data: &js_ast.SLocal{
Kind: js_ast.LocalLet,
Decls: decls,
IsExport: opts.isExport,
}}, decls
}
} else if couldBeUsing && p.lexer.Token == js_lexer.TIdentifier && !p.lexer.HasNewlineBefore && (!opts.isForLoopInit || p.lexer.Raw() != "of") {
// Handle a "using" declaration
if opts.isCaseBody {
p.forbidUsingInSwitch(tokenRange.Loc)
} else if opts.lexicalDecl != lexicalDeclAllowAll {
p.forbidLexicalDecl(tokenRange.Loc)
}
opts.isUsingStmt = true
decls := p.parseAndDeclareDecls(ast.SymbolConst, opts)
if !opts.isForLoopInit {
p.requireInitializers(js_ast.LocalUsing, decls)
}
return js_ast.Expr{}, js_ast.Stmt{Loc: tokenRange.Loc, Data: &js_ast.SLocal{
Kind: js_ast.LocalUsing,
Decls: decls,
IsExport: opts.isExport,
}}, decls
} else if couldBeAwaitUsing {
// Handle an "await using" declaration
if p.fnOrArrowDataParse.isTopLevel {
p.topLevelAwaitKeyword = tokenRange
}
var value js_ast.Expr
if p.lexer.Token == js_lexer.TIdentifier && p.lexer.Raw() == "using" {
usingLoc := p.saveExprCommentsHere()
usingRange := p.lexer.Range()
p.lexer.Next()
if p.lexer.Token == js_lexer.TIdentifier && !p.lexer.HasNewlineBefore {
// It's an "await using" declaration if we get here
if opts.isCaseBody {
p.forbidUsingInSwitch(usingRange.Loc)
} else if opts.lexicalDecl != lexicalDeclAllowAll {
p.forbidLexicalDecl(usingRange.Loc)
}
opts.isUsingStmt = true
decls := p.parseAndDeclareDecls(ast.SymbolConst, opts)
if !opts.isForLoopInit {
p.requireInitializers(js_ast.LocalAwaitUsing, decls)
}
return js_ast.Expr{}, js_ast.Stmt{Loc: tokenRange.Loc, Data: &js_ast.SLocal{
Kind: js_ast.LocalAwaitUsing,
Decls: decls,
IsExport: opts.isExport,
}}, decls
}
value = js_ast.Expr{Loc: usingLoc, Data: &js_ast.EIdentifier{Ref: p.storeNameInRef(js_lexer.MaybeSubstring{String: "using"})}}
} else {
value = p.parseExpr(js_ast.LPrefix)
}
if p.lexer.Token == js_lexer.TAsteriskAsterisk {
p.lexer.Unexpected()
}
value = p.parseSuffix(value, js_ast.LPrefix, nil, 0)
expr := js_ast.Expr{Loc: tokenRange.Loc, Data: &js_ast.EAwait{Value: value}}
return p.parseSuffix(expr, js_ast.LLowest, nil, 0), js_ast.Stmt{}, nil
}
// Parse the remainder of this expression that starts with an identifier
expr := js_ast.Expr{Loc: tokenRange.Loc, Data: &js_ast.EIdentifier{Ref: p.storeNameInRef(name)}}
return p.parseSuffix(expr, js_ast.LLowest, nil, 0), js_ast.Stmt{}, nil
}
func (p *parser) parseCallArgs() (args []js_ast.Expr, closeParenLoc logger.Loc, isMultiLine bool) {
// Allow "in" inside call arguments
oldAllowIn := p.allowIn
p.allowIn = true
p.lexer.Expect(js_lexer.TOpenParen)
for p.lexer.Token != js_lexer.TCloseParen {
if p.lexer.HasNewlineBefore {
isMultiLine = true
}
loc := p.lexer.Loc()
isSpread := p.lexer.Token == js_lexer.TDotDotDot
if isSpread {
p.markSyntaxFeature(compat.RestArgument, p.lexer.Range())
p.lexer.Next()
}
arg := p.parseExpr(js_ast.LComma)
if isSpread {
arg = js_ast.Expr{Loc: loc, Data: &js_ast.ESpread{Value: arg}}
}
args = append(args, arg)
if p.lexer.Token != js_lexer.TComma {
break
}
if p.lexer.HasNewlineBefore {
isMultiLine = true
}
p.lexer.Next()
}
if p.lexer.HasNewlineBefore {
isMultiLine = true
}
closeParenLoc = p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseParen)
p.allowIn = oldAllowIn
return
}
func (p *parser) parseJSXNamespacedName() (logger.Range, js_lexer.MaybeSubstring) {
nameRange := p.lexer.Range()
name := p.lexer.Identifier
p.lexer.ExpectInsideJSXElement(js_lexer.TIdentifier)
// Parse JSX namespaces. These are not supported by React or TypeScript
// but someone using JSX syntax in more obscure ways may find a use for
// them. A namespaced name is just always turned into a string so you
// can't use this feature to reference JavaScript identifiers.
if p.lexer.Token == js_lexer.TColon {
// Parse the colon
nameRange.Len = p.lexer.Range().End() - nameRange.Loc.Start
ns := name.String + ":"
p.lexer.NextInsideJSXElement()
// Parse the second identifier
if p.lexer.Token == js_lexer.TIdentifier {
nameRange.Len = p.lexer.Range().End() - nameRange.Loc.Start
ns += p.lexer.Identifier.String
p.lexer.NextInsideJSXElement()
} else {
p.log.AddError(&p.tracker, logger.Range{Loc: logger.Loc{Start: nameRange.End()}},
fmt.Sprintf("Expected identifier after %q in namespaced JSX name", ns))
panic(js_lexer.LexerPanic{})
}
return nameRange, js_lexer.MaybeSubstring{String: ns}
}
return nameRange, name
}
func tagOrFragmentHelpText(tag string) string {
if tag == "" {
return "fragment tag"
}
return fmt.Sprintf("%q tag", tag)
}
func (p *parser) parseJSXTag() (logger.Range, string, js_ast.Expr) {
loc := p.lexer.Loc()
// A missing tag is a fragment
if p.lexer.Token == js_lexer.TGreaterThan {
return logger.Range{Loc: loc, Len: 0}, "", js_ast.Expr{}
}
// The tag is an identifier
tagRange, tagName := p.parseJSXNamespacedName()
// Certain identifiers are strings
if strings.ContainsAny(tagName.String, "-:") || (p.lexer.Token != js_lexer.TDot && tagName.String[0] >= 'a' && tagName.String[0] <= 'z') {
return tagRange, tagName.String, js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(tagName.String)}}
}
// Otherwise, this is an identifier
tag := js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: p.storeNameInRef(tagName)}}
// Parse a member expression chain
var chain strings.Builder
chain.WriteString(tagName.String)
for p.lexer.Token == js_lexer.TDot {
p.lexer.NextInsideJSXElement()
memberRange := p.lexer.Range()
member := p.lexer.Identifier
p.lexer.ExpectInsideJSXElement(js_lexer.TIdentifier)
// Dashes are not allowed in member expression chains
index := strings.IndexByte(member.String, '-')
if index >= 0 {
p.log.AddError(&p.tracker, logger.Range{Loc: logger.Loc{Start: memberRange.Loc.Start + int32(index)}},
"Unexpected \"-\"")
panic(js_lexer.LexerPanic{})
}
chain.WriteString("." + member.String)
tag = js_ast.Expr{Loc: loc, Data: p.dotOrMangledPropParse(tag, member, memberRange.Loc, js_ast.OptionalChainNone, wasOriginallyDot)}
tagRange.Len = memberRange.Loc.Start + memberRange.Len - tagRange.Loc.Start
}
return tagRange, chain.String(), tag
}
func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr {
// Keep track of the location of the first JSX element for error messages
if p.firstJSXElementLoc.Start == -1 {
p.firstJSXElementLoc = loc
}
// Parse the tag
startRange, startText, startTagOrNil := p.parseJSXTag()
// The tag may have TypeScript type arguments: "<Foo<T>/>"
if p.options.ts.Parse {
// Pass a flag to the type argument skipper because we need to call
// js_lexer.NextInsideJSXElement() after we hit the closing ">". The next
// token after the ">" might be an attribute name with a dash in it
// like this: "<Foo<T> data-disabled/>"
p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{isInsideJSXElement: true})
}
// Parse attributes
var previousStringWithBackslashLoc logger.Loc
properties := []js_ast.Property{}
isSingleLine := true
if startTagOrNil.Data != nil {
parseAttributes:
for {
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
switch p.lexer.Token {
case js_lexer.TIdentifier:
// Parse the key
keyRange, keyName := p.parseJSXNamespacedName()
var key js_ast.Expr
if p.isMangledProp(keyName.String) && !strings.ContainsRune(keyName.String, ':') {
key = js_ast.Expr{Loc: keyRange.Loc, Data: &js_ast.ENameOfSymbol{Ref: p.storeNameInRef(keyName)}}
} else {
key = js_ast.Expr{Loc: keyRange.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(keyName.String)}}
}
// Parse the value
var value js_ast.Expr
var flags js_ast.PropertyFlags
if p.lexer.Token != js_lexer.TEquals {
// Implicitly true value
flags |= js_ast.PropertyWasShorthand
value = js_ast.Expr{Loc: logger.Loc{Start: keyRange.Loc.Start + keyRange.Len}, Data: &js_ast.EBoolean{Value: true}}
} else {
// Use NextInsideJSXElement() not Next() so we can parse a JSX-style string literal
p.lexer.NextInsideJSXElement()
if p.lexer.Token == js_lexer.TStringLiteral {
stringLoc := p.lexer.Loc()
if p.lexer.PreviousBackslashQuoteInJSX.Loc.Start > stringLoc.Start {
previousStringWithBackslashLoc = stringLoc
}
if p.options.jsx.Preserve {
value = js_ast.Expr{Loc: stringLoc, Data: &js_ast.EJSXText{Raw: p.lexer.Raw()}}
} else {
value = js_ast.Expr{Loc: stringLoc, Data: &js_ast.EString{Value: p.lexer.StringLiteral()}}
}
p.lexer.NextInsideJSXElement()
} else if p.lexer.Token == js_lexer.TLessThan {
// This may be removed in the future: https://github.com/facebook/jsx/issues/53
loc := p.lexer.Loc()
p.lexer.NextInsideJSXElement()
flags |= js_ast.PropertyWasShorthand
value = p.parseJSXElement(loc)
// The call to parseJSXElement() above doesn't consume the last
// TGreaterThan because the caller knows what Next() function to call.
// Use NextJSXElementChild() here since the next token is inside a JSX
// element.
p.lexer.NextInsideJSXElement()
} else {
// Use Expect() not ExpectInsideJSXElement() so we can parse expression tokens
p.lexer.Expect(js_lexer.TOpenBrace)
value = p.parseExpr(js_ast.LLowest)
p.lexer.ExpectInsideJSXElement(js_lexer.TCloseBrace)
}
}
// Add a property
properties = append(properties, js_ast.Property{
Loc: keyRange.Loc,
Key: key,
ValueOrNil: value,
Flags: flags,
})
case js_lexer.TOpenBrace:
// Use Next() not ExpectInsideJSXElement() so we can parse "..."
p.lexer.Next()
dotLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TDotDotDot)
value := p.parseExpr(js_ast.LComma)
properties = append(properties, js_ast.Property{
Kind: js_ast.PropertySpread,
Loc: dotLoc,
ValueOrNil: value,
})
// Use NextInsideJSXElement() not Next() so we can parse ">>" as ">"
p.lexer.NextInsideJSXElement()
default:
break parseAttributes
}
}
// Check for and warn about duplicate attributes
if len(properties) > 1 && !p.suppressWarningsAboutWeirdCode {
keys := make(map[string]logger.Loc)
for _, property := range properties {
if property.Kind != js_ast.PropertySpread {
if str, ok := property.Key.Data.(*js_ast.EString); ok {
key := helpers.UTF16ToString(str.Value)
if prevLoc, ok := keys[key]; ok {
r := js_lexer.RangeOfIdentifier(p.source, property.Key.Loc)
p.log.AddIDWithNotes(logger.MsgID_JS_DuplicateObjectKey, logger.Warning, &p.tracker, r,
fmt.Sprintf("Duplicate %q attribute in JSX element", key),
[]logger.MsgData{p.tracker.MsgData(js_lexer.RangeOfIdentifier(p.source, prevLoc),
fmt.Sprintf("The original %q attribute is here:", key))})
}
keys[key] = property.Key.Loc
}
}
}
}
}
// People sometimes try to use the output of "JSON.stringify()" as a JSX
// attribute when automatically-generating JSX code. Doing so is incorrect
// because JSX strings work like XML instead of like JS (since JSX is XML-in-
// JS). Specifically, using a backslash before a quote does not cause it to
// be escaped:
//
// JSX ends the "content" attribute here and sets "content" to 'some so-called \\'
// v
// <Button content="some so-called \"button text\"" />
// ^
// There is no "=" after the JSX attribute "text", so we expect a ">"
//
// This code special-cases this error to provide a less obscure error message.
if p.lexer.Token == js_lexer.TSyntaxError && p.lexer.Raw() == "\\" && previousStringWithBackslashLoc.Start > 0 {
msg := logger.Msg{Kind: logger.Error, Data: p.tracker.MsgData(p.lexer.Range(),
"Unexpected backslash in JSX element")}
// Option 1: Suggest using an XML escape
jsEscape := p.source.TextForRange(p.lexer.PreviousBackslashQuoteInJSX)
xmlEscape := ""
if jsEscape == "\\\"" {
xmlEscape = "&quot;"
} else if jsEscape == "\\'" {
xmlEscape = "&apos;"
}
if xmlEscape != "" {
data := p.tracker.MsgData(p.lexer.PreviousBackslashQuoteInJSX,
"Quoted JSX attributes use XML-style escapes instead of JavaScript-style escapes:")
data.Location.Suggestion = xmlEscape
msg.Notes = append(msg.Notes, data)
}
// Option 2: Suggest using a JavaScript string
if stringRange := p.source.RangeOfString(previousStringWithBackslashLoc); stringRange.Len > 0 {
data := p.tracker.MsgData(stringRange,
"Consider using a JavaScript string inside {...} instead of a quoted JSX attribute:")
data.Location.Suggestion = fmt.Sprintf("{%s}", p.source.TextForRange(stringRange))
msg.Notes = append(msg.Notes, data)
}
p.log.AddMsg(msg)
panic(js_lexer.LexerPanic{})
}
// A slash here is a self-closing element
if p.lexer.Token == js_lexer.TSlash {
// Use NextInsideJSXElement() not Next() so we can parse ">>" as ">"
closeLoc := p.lexer.Loc()
p.lexer.NextInsideJSXElement()
if p.lexer.Token != js_lexer.TGreaterThan {
p.lexer.Expected(js_lexer.TGreaterThan)
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EJSXElement{
TagOrNil: startTagOrNil,
Properties: properties,
CloseLoc: closeLoc,
IsTagSingleLine: isSingleLine,
}}
}
// Attempt to provide a better error message for people incorrectly trying to
// use arrow functions in TSX (which doesn't work because they are JSX elements)
if p.options.ts.Parse && len(properties) == 0 && startText != "" && p.lexer.Token == js_lexer.TGreaterThan &&
strings.HasPrefix(p.source.Contents[p.lexer.Loc().Start:], ">(") {
badArrowInTSXRange := p.lexer.BadArrowInTSXRange
badArrowInTSXSuggestion := p.lexer.BadArrowInTSXSuggestion
p.lexer.CouldBeBadArrowInTSX++
p.lexer.BadArrowInTSXRange = logger.Range{Loc: loc, Len: p.lexer.Range().End() - loc.Start}
p.lexer.BadArrowInTSXSuggestion = fmt.Sprintf("<%s,>", startText)
defer func() {
p.lexer.CouldBeBadArrowInTSX--
p.lexer.BadArrowInTSXRange = badArrowInTSXRange
p.lexer.BadArrowInTSXSuggestion = badArrowInTSXSuggestion
}()
}
// Use ExpectJSXElementChild() so we parse child strings
p.lexer.ExpectJSXElementChild(js_lexer.TGreaterThan)
// Parse the children of this element
nullableChildren := []js_ast.Expr{}
for {
switch p.lexer.Token {
case js_lexer.TStringLiteral:
if p.options.jsx.Preserve {
nullableChildren = append(nullableChildren, js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EJSXText{Raw: p.lexer.Raw()}})
} else if str := p.lexer.StringLiteral(); len(str) > 0 {
nullableChildren = append(nullableChildren, js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EString{Value: str}})
} else {
// Skip this token if it turned out to be empty after trimming
}
p.lexer.NextJSXElementChild()
case js_lexer.TOpenBrace:
// Use Next() instead of NextJSXElementChild() here since the next token is an expression
p.lexer.Next()
// The expression is optional, and may be absent
if p.lexer.Token == js_lexer.TCloseBrace {
// Save comments even for absent expressions
nullableChildren = append(nullableChildren, js_ast.Expr{Loc: p.saveExprCommentsHere(), Data: nil})
} else {
if p.lexer.Token == js_lexer.TDotDotDot {
// TypeScript preserves "..." before JSX child expressions here.
// Babel gives the error "Spread children are not supported in React"
// instead, so it should be safe to support this TypeScript-specific
// behavior. Note that TypeScript's behavior changed in TypeScript 4.5.
// Before that, the "..." was omitted instead of being preserved.
itemLoc := p.lexer.Loc()
p.markSyntaxFeature(compat.RestArgument, p.lexer.Range())
p.lexer.Next()
nullableChildren = append(nullableChildren, js_ast.Expr{Loc: itemLoc, Data: &js_ast.ESpread{Value: p.parseExpr(js_ast.LLowest)}})
} else {
nullableChildren = append(nullableChildren, p.parseExpr(js_ast.LLowest))
}
}
// Use ExpectJSXElementChild() so we parse child strings
p.lexer.ExpectJSXElementChild(js_lexer.TCloseBrace)
case js_lexer.TLessThan:
lessThanLoc := p.lexer.Loc()
p.lexer.NextInsideJSXElement()
if p.lexer.Token != js_lexer.TSlash {
// This is a child element
nullableChildren = append(nullableChildren, p.parseJSXElement(lessThanLoc))
// The call to parseJSXElement() above doesn't consume the last
// TGreaterThan because the caller knows what Next() function to call.
// Use NextJSXElementChild() here since the next token is an element
// child.
p.lexer.NextJSXElementChild()
continue
}
// This is the closing element
p.lexer.NextInsideJSXElement()
endRange, endText, _ := p.parseJSXTag()
if startText != endText {
startTag := tagOrFragmentHelpText(startText)
endTag := tagOrFragmentHelpText(endText)
msg := logger.Msg{
Kind: logger.Error,
Data: p.tracker.MsgData(endRange, fmt.Sprintf("Unexpected closing %s does not match opening %s", endTag, startTag)),
Notes: []logger.MsgData{p.tracker.MsgData(startRange, fmt.Sprintf("The opening %s is here:", startTag))},
}
msg.Data.Location.Suggestion = startText
p.log.AddMsg(msg)
}
if p.lexer.Token != js_lexer.TGreaterThan {
p.lexer.Expected(js_lexer.TGreaterThan)
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EJSXElement{
TagOrNil: startTagOrNil,
Properties: properties,
NullableChildren: nullableChildren,
CloseLoc: lessThanLoc,
IsTagSingleLine: isSingleLine,
}}
case js_lexer.TEndOfFile:
startTag := tagOrFragmentHelpText(startText)
msg := logger.Msg{
Kind: logger.Error,
Data: p.tracker.MsgData(p.lexer.Range(), fmt.Sprintf("Unexpected end of file before a closing %s", startTag)),
Notes: []logger.MsgData{p.tracker.MsgData(startRange, fmt.Sprintf("The opening %s is here:", startTag))},
}
msg.Data.Location.Suggestion = fmt.Sprintf("</%s>", startText)
p.log.AddMsg(msg)
panic(js_lexer.LexerPanic{})
default:
p.lexer.Unexpected()
}
}
}
func (p *parser) parseTemplateParts(includeRaw bool) (parts []js_ast.TemplatePart, legacyOctalLoc logger.Loc) {
// Allow "in" inside template literals
oldAllowIn := p.allowIn
p.allowIn = true
for {
p.lexer.Next()
value := p.parseExpr(js_ast.LLowest)
tailLoc := p.lexer.Loc()
p.lexer.RescanCloseBraceAsTemplateToken()
if includeRaw {
tailCooked, tailRaw := p.lexer.CookedAndRawTemplateContents()
parts = append(parts, js_ast.TemplatePart{
Value: value,
TailLoc: tailLoc,
TailCooked: tailCooked,
TailRaw: tailRaw,
})
} else {
parts = append(parts, js_ast.TemplatePart{
Value: value,
TailLoc: tailLoc,
TailCooked: p.lexer.StringLiteral(),
})
if p.lexer.LegacyOctalLoc.Start > tailLoc.Start {
legacyOctalLoc = p.lexer.LegacyOctalLoc
}
}
if p.lexer.Token == js_lexer.TTemplateTail {
p.lexer.Next()
break
}
}
p.allowIn = oldAllowIn
return parts, legacyOctalLoc
}
func (p *parser) parseAndDeclareDecls(kind ast.SymbolKind, opts parseStmtOpts) []js_ast.Decl {
decls := []js_ast.Decl{}
for {
// Forbid "let let" and "const let" but not "var let"
if (kind == ast.SymbolOther || kind == ast.SymbolConst) && p.lexer.IsContextualKeyword("let") {
p.log.AddError(&p.tracker, p.lexer.Range(), "Cannot use \"let\" as an identifier here:")
}
var valueOrNil js_ast.Expr
local := p.parseBinding(parseBindingOpts{isUsingStmt: opts.isUsingStmt})
p.declareBinding(kind, local, opts)
// Skip over types
if p.options.ts.Parse {
// "let foo!"
isDefiniteAssignmentAssertion := p.lexer.Token == js_lexer.TExclamation && !p.lexer.HasNewlineBefore
if isDefiniteAssignmentAssertion {
p.lexer.Next()
}
// "let foo: number"
if isDefiniteAssignmentAssertion || p.lexer.Token == js_lexer.TColon {
p.lexer.Expect(js_lexer.TColon)
p.skipTypeScriptType(js_ast.LLowest)
}
}
if p.lexer.Token == js_lexer.TEquals {
p.lexer.Next()
valueOrNil = p.parseExpr(js_ast.LComma)
// Rollup (the tool that invented the "@__NO_SIDE_EFFECTS__" comment) only
// applies this to the first declaration, and only when it's a "const".
// For more info see: https://github.com/rollup/rollup/pull/5024/files
if !p.options.ignoreDCEAnnotations && kind == ast.SymbolConst {
switch e := valueOrNil.Data.(type) {
case *js_ast.EArrow:
if opts.hasNoSideEffectsComment {
e.HasNoSideEffectsComment = true
}
if e.HasNoSideEffectsComment && !opts.isTypeScriptDeclare {
if b, ok := local.Data.(*js_ast.BIdentifier); ok {
p.symbols[b.Ref.InnerIndex].Flags |= ast.CallCanBeUnwrappedIfUnused
}
}
case *js_ast.EFunction:
if opts.hasNoSideEffectsComment {
e.Fn.HasNoSideEffectsComment = true
}
if e.Fn.HasNoSideEffectsComment && !opts.isTypeScriptDeclare {
if b, ok := local.Data.(*js_ast.BIdentifier); ok {
p.symbols[b.Ref.InnerIndex].Flags |= ast.CallCanBeUnwrappedIfUnused
}
}
}
// Only apply this to the first declaration
opts.hasNoSideEffectsComment = false
}
}
decls = append(decls, js_ast.Decl{Binding: local, ValueOrNil: valueOrNil})
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
return decls
}
func (p *parser) requireInitializers(kind js_ast.LocalKind, decls []js_ast.Decl) {
for _, d := range decls {
if d.ValueOrNil.Data == nil {
what := "constant"
if kind == js_ast.LocalUsing {
what = "declaration"
}
if id, ok := d.Binding.Data.(*js_ast.BIdentifier); ok {
r := js_lexer.RangeOfIdentifier(p.source, d.Binding.Loc)
p.log.AddError(&p.tracker, r,
fmt.Sprintf("The %s %q must be initialized", what, p.symbols[id.Ref.InnerIndex].OriginalName))
} else {
p.log.AddError(&p.tracker, logger.Range{Loc: d.Binding.Loc},
fmt.Sprintf("This %s must be initialized", what))
}
}
}
}
func (p *parser) forbidInitializers(decls []js_ast.Decl, loopType string, isVar bool) {
if len(decls) > 1 {
p.log.AddError(&p.tracker, logger.Range{Loc: decls[0].Binding.Loc},
fmt.Sprintf("for-%s loops must have a single declaration", loopType))
} else if len(decls) == 1 && decls[0].ValueOrNil.Data != nil {
if isVar {
if _, ok := decls[0].Binding.Data.(*js_ast.BIdentifier); ok {
// This is a weird special case. Initializers are allowed in "var"
// statements with identifier bindings.
return
}
}
p.log.AddError(&p.tracker, logger.Range{Loc: decls[0].ValueOrNil.Loc},
fmt.Sprintf("for-%s loop variables cannot have an initializer", loopType))
}
}
func (p *parser) parseClauseAlias(kind string) js_lexer.MaybeSubstring {
loc := p.lexer.Loc()
// The alias may now be a string (see https://github.com/tc39/ecma262/pull/2154)
if p.lexer.Token == js_lexer.TStringLiteral {
r := p.source.RangeOfString(loc)
alias, problem, ok := helpers.UTF16ToStringWithValidation(p.lexer.StringLiteral())
if !ok {
p.log.AddError(&p.tracker, r,
fmt.Sprintf("This %s alias is invalid because it contains the unpaired Unicode surrogate U+%X", kind, problem))
}
return js_lexer.MaybeSubstring{String: alias}
}
// The alias may be a keyword
if !p.lexer.IsIdentifierOrKeyword() {
p.lexer.Expect(js_lexer.TIdentifier)
}
alias := p.lexer.Identifier
p.checkForUnrepresentableIdentifier(loc, alias.String)
return alias
}
func (p *parser) parseImportClause() ([]js_ast.ClauseItem, bool) {
items := []js_ast.ClauseItem{}
p.lexer.Expect(js_lexer.TOpenBrace)
isSingleLine := !p.lexer.HasNewlineBefore
for p.lexer.Token != js_lexer.TCloseBrace {
isIdentifier := p.lexer.Token == js_lexer.TIdentifier
aliasLoc := p.lexer.Loc()
alias := p.parseClauseAlias("import")
name := ast.LocRef{Loc: aliasLoc, Ref: p.storeNameInRef(alias)}
originalName := alias
p.lexer.Next()
// "import { type xx } from 'mod'"
// "import { type xx as yy } from 'mod'"
// "import { type 'xx' as yy } from 'mod'"
// "import { type as } from 'mod'"
// "import { type as as } from 'mod'"
// "import { type as as as } from 'mod'"
if p.options.ts.Parse && alias.String == "type" && p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TCloseBrace {
if p.lexer.IsContextualKeyword("as") {
p.lexer.Next()
if p.lexer.IsContextualKeyword("as") {
originalName = p.lexer.Identifier
name = ast.LocRef{Loc: p.lexer.Loc(), Ref: p.storeNameInRef(originalName)}
p.lexer.Next()
if p.lexer.Token == js_lexer.TIdentifier {
// "import { type as as as } from 'mod'"
// "import { type as as foo } from 'mod'"
p.lexer.Next()
} else {
// "import { type as as } from 'mod'"
items = append(items, js_ast.ClauseItem{
Alias: alias.String,
AliasLoc: aliasLoc,
Name: name,
OriginalName: originalName.String,
})
}
} else if p.lexer.Token == js_lexer.TIdentifier {
// "import { type as xxx } from 'mod'"
originalName = p.lexer.Identifier
name = ast.LocRef{Loc: p.lexer.Loc(), Ref: p.storeNameInRef(originalName)}
p.lexer.Expect(js_lexer.TIdentifier)
// Reject forbidden names
if isEvalOrArguments(originalName.String) {
r := js_lexer.RangeOfIdentifier(p.source, name.Loc)
p.log.AddError(&p.tracker, r, fmt.Sprintf("Cannot use %q as an identifier here:", originalName.String))
}
items = append(items, js_ast.ClauseItem{
Alias: alias.String,
AliasLoc: aliasLoc,
Name: name,
OriginalName: originalName.String,
})
}
} else {
isIdentifier := p.lexer.Token == js_lexer.TIdentifier
// "import { type xx } from 'mod'"
// "import { type xx as yy } from 'mod'"
// "import { type if as yy } from 'mod'"
// "import { type 'xx' as yy } from 'mod'"
p.parseClauseAlias("import")
p.lexer.Next()
if p.lexer.IsContextualKeyword("as") {
p.lexer.Next()
p.lexer.Expect(js_lexer.TIdentifier)
} else if !isIdentifier {
// An import where the name is a keyword must have an alias
p.lexer.ExpectedString("\"as\"")
}
}
} else {
if p.lexer.IsContextualKeyword("as") {
p.lexer.Next()
originalName = p.lexer.Identifier
name = ast.LocRef{Loc: p.lexer.Loc(), Ref: p.storeNameInRef(originalName)}
p.lexer.Expect(js_lexer.TIdentifier)
} else if !isIdentifier {
// An import where the name is a keyword must have an alias
p.lexer.ExpectedString("\"as\"")
}
// Reject forbidden names
if isEvalOrArguments(originalName.String) {
r := js_lexer.RangeOfIdentifier(p.source, name.Loc)
p.log.AddError(&p.tracker, r, fmt.Sprintf("Cannot use %q as an identifier here:", originalName.String))
}
items = append(items, js_ast.ClauseItem{
Alias: alias.String,
AliasLoc: aliasLoc,
Name: name,
OriginalName: originalName.String,
})
}
if p.lexer.Token != js_lexer.TComma {
break
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Next()
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Expect(js_lexer.TCloseBrace)
return items, isSingleLine
}
func (p *parser) parseExportClause() ([]js_ast.ClauseItem, bool) {
items := []js_ast.ClauseItem{}
firstNonIdentifierLoc := logger.Loc{}
p.lexer.Expect(js_lexer.TOpenBrace)
isSingleLine := !p.lexer.HasNewlineBefore
for p.lexer.Token != js_lexer.TCloseBrace {
alias := p.parseClauseAlias("export")
aliasLoc := p.lexer.Loc()
name := ast.LocRef{Loc: aliasLoc, Ref: p.storeNameInRef(alias)}
originalName := alias
// The name can actually be a keyword if we're really an "export from"
// statement. However, we won't know until later. Allow keywords as
// identifiers for now and throw an error later if there's no "from".
//
// // This is fine
// export { default } from 'path'
//
// // This is a syntax error
// export { default }
//
if p.lexer.Token != js_lexer.TIdentifier && firstNonIdentifierLoc.Start == 0 {
firstNonIdentifierLoc = p.lexer.Loc()
}
p.lexer.Next()
if p.options.ts.Parse && alias.String == "type" && p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TCloseBrace {
if p.lexer.IsContextualKeyword("as") {
p.lexer.Next()
if p.lexer.IsContextualKeyword("as") {
alias = p.parseClauseAlias("export")
aliasLoc = p.lexer.Loc()
p.lexer.Next()
if p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TCloseBrace {
// "export { type as as as }"
// "export { type as as foo }"
// "export { type as as 'foo' }"
p.parseClauseAlias("export")
p.lexer.Next()
} else {
// "export { type as as }"
items = append(items, js_ast.ClauseItem{
Alias: alias.String,
AliasLoc: aliasLoc,
Name: name,
OriginalName: originalName.String,
})
}
} else if p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TCloseBrace {
// "export { type as xxx }"
// "export { type as 'xxx' }"
alias = p.parseClauseAlias("export")
aliasLoc = p.lexer.Loc()
p.lexer.Next()
items = append(items, js_ast.ClauseItem{
Alias: alias.String,
AliasLoc: aliasLoc,
Name: name,
OriginalName: originalName.String,
})
}
} else {
// The name can actually be a keyword if we're really an "export from"
// statement. However, we won't know until later. Allow keywords as
// identifiers for now and throw an error later if there's no "from".
//
// // This is fine
// export { type default } from 'path'
//
// // This is a syntax error
// export { type default }
//
if p.lexer.Token != js_lexer.TIdentifier && firstNonIdentifierLoc.Start == 0 {
firstNonIdentifierLoc = p.lexer.Loc()
}
// "export { type xx }"
// "export { type xx as yy }"
// "export { type xx as if }"
// "export { type default } from 'path'"
// "export { type default as if } from 'path'"
// "export { type xx as 'yy' }"
// "export { type 'xx' } from 'mod'"
p.parseClauseAlias("export")
p.lexer.Next()
if p.lexer.IsContextualKeyword("as") {
p.lexer.Next()
p.parseClauseAlias("export")
p.lexer.Next()
}
}
} else {
if p.lexer.IsContextualKeyword("as") {
p.lexer.Next()
alias = p.parseClauseAlias("export")
aliasLoc = p.lexer.Loc()
p.lexer.Next()
}
items = append(items, js_ast.ClauseItem{
Alias: alias.String,
AliasLoc: aliasLoc,
Name: name,
OriginalName: originalName.String,
})
}
if p.lexer.Token != js_lexer.TComma {
break
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Next()
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Expect(js_lexer.TCloseBrace)
// Throw an error here if we found a keyword earlier and this isn't an
// "export from" statement after all
if firstNonIdentifierLoc.Start != 0 && !p.lexer.IsContextualKeyword("from") {
r := js_lexer.RangeOfIdentifier(p.source, firstNonIdentifierLoc)
p.log.AddError(&p.tracker, r, fmt.Sprintf("Expected identifier but found %q", p.source.TextForRange(r)))
panic(js_lexer.LexerPanic{})
}
return items, isSingleLine
}
type parseBindingOpts struct {
isUsingStmt bool
}
func (p *parser) parseBinding(opts parseBindingOpts) js_ast.Binding {
loc := p.lexer.Loc()
switch p.lexer.Token {
case js_lexer.TIdentifier:
name := p.lexer.Identifier
// Forbid invalid identifiers
if (p.fnOrArrowDataParse.await != allowIdent && name.String == "await") ||
(p.fnOrArrowDataParse.yield != allowIdent && name.String == "yield") {
p.log.AddError(&p.tracker, p.lexer.Range(), fmt.Sprintf("Cannot use %q as an identifier here:", name.String))
}
ref := p.storeNameInRef(name)
p.lexer.Next()
return js_ast.Binding{Loc: loc, Data: &js_ast.BIdentifier{Ref: ref}}
case js_lexer.TOpenBracket:
if opts.isUsingStmt {
break
}
p.markSyntaxFeature(compat.Destructuring, p.lexer.Range())
p.lexer.Next()
isSingleLine := !p.lexer.HasNewlineBefore
items := []js_ast.ArrayBinding{}
hasSpread := false
// "in" expressions are allowed
oldAllowIn := p.allowIn
p.allowIn = true
for p.lexer.Token != js_lexer.TCloseBracket {
itemLoc := p.saveExprCommentsHere()
if p.lexer.Token == js_lexer.TComma {
binding := js_ast.Binding{Loc: itemLoc, Data: js_ast.BMissingShared}
items = append(items, js_ast.ArrayBinding{
Binding: binding,
Loc: itemLoc,
})
} else {
if p.lexer.Token == js_lexer.TDotDotDot {
p.lexer.Next()
hasSpread = true
// This was a bug in the ES2015 spec that was fixed in ES2016
if p.lexer.Token != js_lexer.TIdentifier {
p.markSyntaxFeature(compat.NestedRestBinding, p.lexer.Range())
}
}
p.saveExprCommentsHere()
binding := p.parseBinding(parseBindingOpts{})
var defaultValueOrNil js_ast.Expr
if !hasSpread && p.lexer.Token == js_lexer.TEquals {
p.lexer.Next()
defaultValueOrNil = p.parseExpr(js_ast.LComma)
}
items = append(items, js_ast.ArrayBinding{
Binding: binding,
DefaultValueOrNil: defaultValueOrNil,
Loc: itemLoc,
})
// Commas after spread elements are not allowed
if hasSpread && p.lexer.Token == js_lexer.TComma {
p.log.AddError(&p.tracker, p.lexer.Range(), "Unexpected \",\" after rest pattern")
panic(js_lexer.LexerPanic{})
}
}
if p.lexer.Token != js_lexer.TComma {
break
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Next()
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
}
p.allowIn = oldAllowIn
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
closeBracketLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBracket)
return js_ast.Binding{Loc: loc, Data: &js_ast.BArray{
Items: items,
HasSpread: hasSpread,
IsSingleLine: isSingleLine,
CloseBracketLoc: closeBracketLoc,
}}
case js_lexer.TOpenBrace:
if opts.isUsingStmt {
break
}
p.markSyntaxFeature(compat.Destructuring, p.lexer.Range())
p.lexer.Next()
isSingleLine := !p.lexer.HasNewlineBefore
properties := []js_ast.PropertyBinding{}
// "in" expressions are allowed
oldAllowIn := p.allowIn
p.allowIn = true
for p.lexer.Token != js_lexer.TCloseBrace {
p.saveExprCommentsHere()
property := p.parsePropertyBinding()
properties = append(properties, property)
// Commas after spread elements are not allowed
if property.IsSpread && p.lexer.Token == js_lexer.TComma {
p.log.AddError(&p.tracker, p.lexer.Range(), "Unexpected \",\" after rest pattern")
panic(js_lexer.LexerPanic{})
}
if p.lexer.Token != js_lexer.TComma {
break
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Next()
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
}
p.allowIn = oldAllowIn
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
closeBraceLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBrace)
return js_ast.Binding{Loc: loc, Data: &js_ast.BObject{
Properties: properties,
IsSingleLine: isSingleLine,
CloseBraceLoc: closeBraceLoc,
}}
}
p.lexer.Expect(js_lexer.TIdentifier)
return js_ast.Binding{}
}
func (p *parser) parseFn(
name *ast.LocRef,
classKeyword logger.Range,
decoratorContext decoratorContextFlags,
data fnOrArrowDataParse,
) (fn js_ast.Fn, hadBody bool) {
fn.Name = name
fn.HasRestArg = false
fn.IsAsync = data.await == allowExpr
fn.IsGenerator = data.yield == allowExpr
fn.ArgumentsRef = ast.InvalidRef
fn.OpenParenLoc = p.lexer.Loc()
p.lexer.Expect(js_lexer.TOpenParen)
// Await and yield are not allowed in function arguments
oldFnOrArrowData := p.fnOrArrowDataParse
if data.await == allowExpr {
p.fnOrArrowDataParse.await = forbidAll
} else {
p.fnOrArrowDataParse.await = allowIdent
}
if data.yield == allowExpr {
p.fnOrArrowDataParse.yield = forbidAll
} else {
p.fnOrArrowDataParse.yield = allowIdent
}
// Don't suggest inserting "async" before anything if "await" is found
p.fnOrArrowDataParse.needsAsyncLoc.Start = -1
// If "super" is allowed in the body, it's allowed in the arguments
p.fnOrArrowDataParse.allowSuperCall = data.allowSuperCall
p.fnOrArrowDataParse.allowSuperProperty = data.allowSuperProperty
for p.lexer.Token != js_lexer.TCloseParen {
// Skip over "this" type annotations
if p.options.ts.Parse && p.lexer.Token == js_lexer.TThis {
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()
continue
}
var decorators []js_ast.Decorator
if data.decoratorScope != nil {
oldAwait := p.fnOrArrowDataParse.await
oldNeedsAsyncLoc := p.fnOrArrowDataParse.needsAsyncLoc
// While TypeScript parameter decorators are expressions, they are not
// evaluated where they exist in the code. They are moved to after the
// class declaration and evaluated there instead. Specifically this
// TypeScript code:
//
// class Foo {
// foo(@bar() baz) {}
// }
//
// becomes this JavaScript code:
//
// class Foo {
// foo(baz) {}
// }
// __decorate([
// __param(0, bar())
// ], Foo.prototype, "foo", null);
//
// One consequence of this is that whether "await" is allowed or not
// depends on whether the class declaration itself is inside an "async"
// function or not. The TypeScript compiler allows code that does this:
//
// async function fn(foo) {
// class Foo {
// foo(@bar(await foo) baz) {}
// }
// return Foo
// }
//
// because that becomes the following valid JavaScript:
//
// async function fn(foo) {
// class Foo {
// foo(baz) {}
// }
// __decorate([
// __param(0, bar(await foo))
// ], Foo.prototype, "foo", null);
// return Foo;
// }
//
if oldFnOrArrowData.await == allowExpr {
p.fnOrArrowDataParse.await = allowExpr
} else {
p.fnOrArrowDataParse.needsAsyncLoc = oldFnOrArrowData.needsAsyncLoc
}
decorators = p.parseDecorators(data.decoratorScope, classKeyword, decoratorContext|decoratorInFnArgs)
p.fnOrArrowDataParse.await = oldAwait
p.fnOrArrowDataParse.needsAsyncLoc = oldNeedsAsyncLoc
}
if !fn.HasRestArg && p.lexer.Token == js_lexer.TDotDotDot {
p.markSyntaxFeature(compat.RestArgument, p.lexer.Range())
p.lexer.Next()
fn.HasRestArg = true
}
isTypeScriptCtorField := false
isIdentifier := p.lexer.Token == js_lexer.TIdentifier
text := p.lexer.Identifier.String
arg := p.parseBinding(parseBindingOpts{})
if p.options.ts.Parse {
// Skip over TypeScript accessibility modifiers, which turn this argument
// into a class field when used inside a class constructor. This is known
// as a "parameter property" in TypeScript.
if isIdentifier && data.isConstructor {
for p.lexer.Token == js_lexer.TIdentifier || p.lexer.Token == js_lexer.TOpenBrace || p.lexer.Token == js_lexer.TOpenBracket {
if text != "public" && text != "private" && text != "protected" && text != "readonly" && text != "override" {
break
}
isTypeScriptCtorField = true
// TypeScript requires an identifier binding
if p.lexer.Token != js_lexer.TIdentifier {
p.lexer.Expect(js_lexer.TIdentifier)
}
text = p.lexer.Identifier.String
// Re-parse the binding (the current binding is the TypeScript keyword)
arg = p.parseBinding(parseBindingOpts{})
}
}
// "function foo(a?) {}"
if p.lexer.Token == js_lexer.TQuestion {
p.lexer.Next()
}
// "function foo(a: any) {}"
if p.lexer.Token == js_lexer.TColon {
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}
}
p.declareBinding(ast.SymbolHoisted, arg, parseStmtOpts{})
var defaultValueOrNil js_ast.Expr
if !fn.HasRestArg && p.lexer.Token == js_lexer.TEquals {
p.markSyntaxFeature(compat.DefaultArgument, p.lexer.Range())
p.lexer.Next()
defaultValueOrNil = p.parseExpr(js_ast.LComma)
}
fn.Args = append(fn.Args, js_ast.Arg{
Decorators: decorators,
Binding: arg,
DefaultOrNil: defaultValueOrNil,
// We need to track this because it affects code generation
IsTypeScriptCtorField: isTypeScriptCtorField,
})
if p.lexer.Token != js_lexer.TComma {
break
}
if fn.HasRestArg {
// JavaScript does not allow a comma after a rest argument
if data.isTypeScriptDeclare {
// TypeScript does allow a comma after a rest argument in a "declare" context
p.lexer.Next()
} else {
p.lexer.Expect(js_lexer.TCloseParen)
}
break
}
p.lexer.Next()
}
// Reserve the special name "arguments" in this scope. This ensures that it
// shadows any variable called "arguments" in any parent scopes. But only do
// this if it wasn't already declared above because arguments are allowed to
// be called "arguments", in which case the real "arguments" is inaccessible.
if _, ok := p.currentScope.Members["arguments"]; !ok {
fn.ArgumentsRef = p.declareSymbol(ast.SymbolArguments, fn.OpenParenLoc, "arguments")
p.symbols[fn.ArgumentsRef.InnerIndex].Flags |= ast.MustNotBeRenamed
}
p.lexer.Expect(js_lexer.TCloseParen)
p.fnOrArrowDataParse = oldFnOrArrowData
// "function foo(): any {}"
if p.options.ts.Parse && p.lexer.Token == js_lexer.TColon {
p.lexer.Next()
p.skipTypeScriptReturnType()
}
// "function foo(): any;"
if data.allowMissingBodyForTypeScript && p.lexer.Token != js_lexer.TOpenBrace {
p.lexer.ExpectOrInsertSemicolon()
return
}
fn.Body = p.parseFnBody(data)
hadBody = true
return
}
type fnKind uint8
const (
fnStmt fnKind = iota
fnExpr
)
func (p *parser) validateFunctionName(fn js_ast.Fn, kind fnKind) {
// Prevent the function name from being the same as a function-specific keyword
if fn.Name != nil {
if fn.IsAsync && p.symbols[fn.Name.Ref.InnerIndex].OriginalName == "await" {
p.log.AddError(&p.tracker, js_lexer.RangeOfIdentifier(p.source, fn.Name.Loc),
"An async function cannot be named \"await\"")
} else if fn.IsGenerator && p.symbols[fn.Name.Ref.InnerIndex].OriginalName == "yield" && kind == fnExpr {
p.log.AddError(&p.tracker, js_lexer.RangeOfIdentifier(p.source, fn.Name.Loc),
"A generator function expression cannot be named \"yield\"")
}
}
}
func (p *parser) validateDeclaredSymbolName(loc logger.Loc, name string) {
if js_lexer.StrictModeReservedWords[name] {
p.markStrictModeFeature(reservedWord, js_lexer.RangeOfIdentifier(p.source, loc), name)
} else if isEvalOrArguments(name) {
p.markStrictModeFeature(evalOrArguments, js_lexer.RangeOfIdentifier(p.source, loc), name)
}
}
func (p *parser) parseClassStmt(loc logger.Loc, opts parseStmtOpts) js_ast.Stmt {
var name *ast.LocRef
classKeyword := p.lexer.Range()
if p.lexer.Token == js_lexer.TClass {
p.markSyntaxFeature(compat.Class, classKeyword)
p.lexer.Next()
} else {
p.lexer.Expected(js_lexer.TClass)
}
if !opts.isNameOptional || (p.lexer.Token == js_lexer.TIdentifier && (!p.options.ts.Parse || p.lexer.Identifier.String != "implements")) {
nameLoc := p.lexer.Loc()
nameText := p.lexer.Identifier.String
p.lexer.Expect(js_lexer.TIdentifier)
if p.fnOrArrowDataParse.await != allowIdent && nameText == "await" {
p.log.AddError(&p.tracker, js_lexer.RangeOfIdentifier(p.source, nameLoc), "Cannot use \"await\" as an identifier here:")
}
name = &ast.LocRef{Loc: nameLoc, Ref: ast.InvalidRef}
if !opts.isTypeScriptDeclare {
name.Ref = p.declareSymbol(ast.SymbolClass, nameLoc, nameText)
}
}
// Even anonymous classes can have TypeScript type parameters
if p.options.ts.Parse {
p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowConstModifier)
}
classOpts := parseClassOpts{
isTypeScriptDeclare: opts.isTypeScriptDeclare,
}
if opts.deferredDecorators != nil {
classOpts.decorators = opts.deferredDecorators.decorators
}
scopeIndex := p.pushScopeForParsePass(js_ast.ScopeClassName, loc)
class := p.parseClass(classKeyword, name, classOpts)
if opts.isTypeScriptDeclare {
p.popAndDiscardScope(scopeIndex)
if opts.isNamespaceScope && opts.isExport {
p.hasNonLocalExportDeclareInsideNamespace = true
}
// Remember that this was a "declare class" so we can allow decorators on it
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptSharedWasDeclareClass}
}
p.popScope()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SClass{Class: class, IsExport: opts.isExport}}
}
func (p *parser) parseClassExpr(decorators []js_ast.Decorator) js_ast.Expr {
classKeyword := p.lexer.Range()
p.markSyntaxFeature(compat.Class, classKeyword)
p.lexer.Expect(js_lexer.TClass)
var name *ast.LocRef
opts := parseClassOpts{
decorators: decorators,
decoratorContext: decoratorInClassExpr,
}
p.pushScopeForParsePass(js_ast.ScopeClassName, classKeyword.Loc)
// Parse an optional class name
if p.lexer.Token == js_lexer.TIdentifier {
if nameText := p.lexer.Identifier.String; !p.options.ts.Parse || nameText != "implements" {
if p.fnOrArrowDataParse.await != allowIdent && nameText == "await" {
p.log.AddError(&p.tracker, p.lexer.Range(), "Cannot use \"await\" as an identifier here:")
}
name = &ast.LocRef{Loc: p.lexer.Loc(), Ref: p.newSymbol(ast.SymbolOther, nameText)}
p.lexer.Next()
}
}
// Even anonymous classes can have TypeScript type parameters
if p.options.ts.Parse {
p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowConstModifier)
}
class := p.parseClass(classKeyword, name, opts)
p.popScope()
return js_ast.Expr{Loc: classKeyword.Loc, Data: &js_ast.EClass{Class: class}}
}
type parseClassOpts struct {
decorators []js_ast.Decorator
decoratorContext decoratorContextFlags
isTypeScriptDeclare bool
}
// By the time we call this, the identifier and type parameters have already
// been parsed. We need to start parsing from the "extends" clause.
func (p *parser) parseClass(classKeyword logger.Range, name *ast.LocRef, classOpts parseClassOpts) js_ast.Class {
var extendsOrNil js_ast.Expr
if p.lexer.Token == js_lexer.TExtends {
p.lexer.Next()
extendsOrNil = p.parseExpr(js_ast.LNew)
// TypeScript's type argument parser inside expressions backtracks if the
// first token after the end of the type parameter list is "{", so the
// parsed expression above will have backtracked if there are any type
// arguments. This means we have to re-parse for any type arguments here.
// This seems kind of wasteful to me but it's what the official compiler
// does and it probably doesn't have that high of a performance overhead
// because "extends" clauses aren't that frequent, so it should be ok.
if p.options.ts.Parse {
p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{})
}
}
if p.options.ts.Parse && p.lexer.IsContextualKeyword("implements") {
p.lexer.Next()
for {
p.skipTypeScriptType(js_ast.LLowest)
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
}
bodyLoc := p.lexer.Loc()
p.lexer.Expect(js_lexer.TOpenBrace)
properties := []js_ast.Property{}
hasPropertyDecorator := false
// Allow "in" and private fields inside class bodies
oldAllowIn := p.allowIn
p.allowIn = true
// A scope is needed for private identifiers
scopeIndex := p.pushScopeForParsePass(js_ast.ScopeClassBody, bodyLoc)
opts := propertyOpts{
isClass: true,
decoratorScope: p.currentScope,
decoratorContext: classOpts.decoratorContext,
classHasExtends: extendsOrNil.Data != nil,
classKeyword: classKeyword,
}
hasConstructor := false
for p.lexer.Token != js_lexer.TCloseBrace {
if p.lexer.Token == js_lexer.TSemicolon {
p.lexer.Next()
continue
}
// Parse decorators for this property
firstDecoratorLoc := p.lexer.Loc()
scopeIndex := len(p.scopesInOrder)
opts.decorators = p.parseDecorators(p.currentScope, classKeyword, opts.decoratorContext)
if len(opts.decorators) > 0 {
hasPropertyDecorator = true
}
// This property may turn out to be a type in TypeScript, which should be ignored
if property, ok := p.parseProperty(p.saveExprCommentsHere(), js_ast.PropertyField, opts, nil); ok {
properties = append(properties, property)
// Forbid decorators on class constructors
if key, ok := property.Key.Data.(*js_ast.EString); ok && helpers.UTF16EqualsString(key.Value, "constructor") {
if len(opts.decorators) > 0 {
p.log.AddError(&p.tracker, logger.Range{Loc: firstDecoratorLoc},
"Decorators are not allowed on class constructors")
}
if property.Kind.IsMethodDefinition() && !property.Flags.Has(js_ast.PropertyIsStatic) && !property.Flags.Has(js_ast.PropertyIsComputed) {
if hasConstructor {
p.log.AddError(&p.tracker, js_lexer.RangeOfIdentifier(p.source, property.Key.Loc),
"Classes cannot contain more than one constructor")
}
hasConstructor = true
}
}
} else if !classOpts.isTypeScriptDeclare && len(opts.decorators) > 0 {
p.log.AddError(&p.tracker, logger.Range{Loc: firstDecoratorLoc, Len: 1}, "Decorators are not valid here")
p.discardScopesUpTo(scopeIndex)
}
}
// Discard the private identifier scope inside a TypeScript "declare class"
if classOpts.isTypeScriptDeclare {
p.popAndDiscardScope(scopeIndex)
} else {
p.popScope()
}
p.allowIn = oldAllowIn
closeBraceLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBrace)
// TypeScript has legacy behavior that uses assignment semantics instead of
// define semantics for class fields when "useDefineForClassFields" is enabled
// (in which case TypeScript behaves differently than JavaScript, which is
// arguably "wrong").
//
// This legacy behavior exists because TypeScript added class fields to
// TypeScript before they were added to JavaScript. They decided to go with
// assignment semantics for whatever reason. Later on TC39 decided to go with
// define semantics for class fields instead. This behaves differently if the
// base class has a setter with the same name.
//
// The value of "useDefineForClassFields" defaults to false when it's not
// specified and the target is earlier than "ES2022" since the class field
// language feature was added in ES2022. However, TypeScript's "target"
// setting currently defaults to "ES3" which unfortunately means that the
// "useDefineForClassFields" setting defaults to false (i.e. to "wrong").
//
// We default "useDefineForClassFields" to true (i.e. to "correct") instead.
// This is partially because our target defaults to "esnext", and partially
// because this is a legacy behavior that no one should be using anymore.
// Users that want the wrong behavior can either set "useDefineForClassFields"
// to false in "tsconfig.json" explicitly, or set TypeScript's "target" to
// "ES2021" or earlier in their in "tsconfig.json" file.
useDefineForClassFields := !p.options.ts.Parse || p.options.ts.Config.UseDefineForClassFields == config.True ||
(p.options.ts.Config.UseDefineForClassFields == config.Unspecified && p.options.ts.Config.Target != config.TSTargetBelowES2022)
return js_ast.Class{
ClassKeyword: classKeyword,
Decorators: classOpts.decorators,
Name: name,
ExtendsOrNil: extendsOrNil,
BodyLoc: bodyLoc,
Properties: properties,
CloseBraceLoc: closeBraceLoc,
// Always lower standard decorators if they are present and TypeScript's
// "useDefineForClassFields" setting is false even if the configured target
// environment supports decorators. This setting changes the behavior of
// class fields, and so we must lower decorators so they behave correctly.
ShouldLowerStandardDecorators: (len(classOpts.decorators) > 0 || hasPropertyDecorator) &&
((!p.options.ts.Parse && p.options.unsupportedJSFeatures.Has(compat.Decorators)) ||
(p.options.ts.Parse && p.options.ts.Config.ExperimentalDecorators != config.True &&
(p.options.unsupportedJSFeatures.Has(compat.Decorators) || !useDefineForClassFields))),
UseDefineForClassFields: useDefineForClassFields,
}
}
func (p *parser) parseLabelName() *ast.LocRef {
if p.lexer.Token != js_lexer.TIdentifier || p.lexer.HasNewlineBefore {
return nil
}
name := ast.LocRef{Loc: p.lexer.Loc(), Ref: p.storeNameInRef(p.lexer.Identifier)}
p.lexer.Next()
return &name
}
func (p *parser) parsePath() (logger.Range, string, *ast.ImportAssertOrWith, ast.ImportRecordFlags) {
var flags ast.ImportRecordFlags
pathRange := p.lexer.Range()
pathText := helpers.UTF16ToString(p.lexer.StringLiteral())
if p.lexer.Token == js_lexer.TNoSubstitutionTemplateLiteral {
p.lexer.Next()
} else {
p.lexer.Expect(js_lexer.TStringLiteral)
}
// See https://github.com/tc39/proposal-import-attributes for more info
var assertOrWith *ast.ImportAssertOrWith
if p.lexer.Token == js_lexer.TWith || (!p.lexer.HasNewlineBefore && p.lexer.IsContextualKeyword("assert")) {
// "import './foo.json' assert { type: 'json' }"
// "import './foo.json' with { type: 'json' }"
var entries []ast.AssertOrWithEntry
duplicates := make(map[string]logger.Range)
keyword := ast.WithKeyword
if p.lexer.Token != js_lexer.TWith {
keyword = ast.AssertKeyword
}
keywordLoc := p.saveExprCommentsHere()
p.lexer.Next()
openBraceLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TOpenBrace)
for p.lexer.Token != js_lexer.TCloseBrace {
// Parse the key
keyLoc := p.saveExprCommentsHere()
preferQuotedKey := false
var key []uint16
var keyText string
if p.lexer.IsIdentifierOrKeyword() {
keyText = p.lexer.Identifier.String
key = helpers.StringToUTF16(keyText)
} else if p.lexer.Token == js_lexer.TStringLiteral {
key = p.lexer.StringLiteral()
keyText = helpers.UTF16ToString(key)
preferQuotedKey = !p.options.minifySyntax
} else {
p.lexer.Expect(js_lexer.TIdentifier)
}
if prevRange, ok := duplicates[keyText]; ok {
what := "attribute"
if keyword == ast.AssertKeyword {
what = "assertion"
}
p.log.AddErrorWithNotes(&p.tracker, p.lexer.Range(), fmt.Sprintf("Duplicate import %s %q", what, keyText),
[]logger.MsgData{p.tracker.MsgData(prevRange, fmt.Sprintf("The first %q was here:", keyText))})
}
duplicates[keyText] = p.lexer.Range()
p.lexer.Next()
p.lexer.Expect(js_lexer.TColon)
// Parse the value
valueLoc := p.saveExprCommentsHere()
value := p.lexer.StringLiteral()
p.lexer.Expect(js_lexer.TStringLiteral)
entries = append(entries, ast.AssertOrWithEntry{
Key: key,
KeyLoc: keyLoc,
Value: value,
ValueLoc: valueLoc,
PreferQuotedKey: preferQuotedKey,
})
// Using "assert: { type: 'json' }" triggers special behavior
if keyword == ast.AssertKeyword && helpers.UTF16EqualsString(key, "type") && helpers.UTF16EqualsString(value, "json") {
flags |= ast.AssertTypeJSON
}
if p.lexer.Token != js_lexer.TComma {
break
}
p.lexer.Next()
}
closeBraceLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseBrace)
if keyword == ast.AssertKeyword {
p.maybeWarnAboutAssertKeyword(keywordLoc)
}
assertOrWith = &ast.ImportAssertOrWith{
Entries: entries,
Keyword: keyword,
KeywordLoc: keywordLoc,
InnerOpenBraceLoc: openBraceLoc,
InnerCloseBraceLoc: closeBraceLoc,
}
}
return pathRange, pathText, assertOrWith, flags
}
// Let people know if they probably should be using "with" instead of "assert"
func (p *parser) maybeWarnAboutAssertKeyword(loc logger.Loc) {
if p.options.unsupportedJSFeatures.Has(compat.ImportAssertions) && !p.options.unsupportedJSFeatures.Has(compat.ImportAttributes) {
where := config.PrettyPrintTargetEnvironment(p.options.originalTargetEnv, p.options.unsupportedJSFeatureOverridesMask)
msg := logger.Msg{
Kind: logger.Warning,
Data: p.tracker.MsgData(js_lexer.RangeOfIdentifier(p.source, loc), "The \"assert\" keyword is not supported in "+where),
Notes: []logger.MsgData{{Text: "Did you mean to use \"with\" instead of \"assert\"?"}},
}
msg.Data.Location.Suggestion = "with"
p.log.AddMsgID(logger.MsgID_JS_AssertToWith, msg)
}
}
// This assumes the "function" token has already been parsed
func (p *parser) parseFnStmt(loc logger.Loc, opts parseStmtOpts, isAsync bool, asyncRange logger.Range) js_ast.Stmt {
isGenerator := p.lexer.Token == js_lexer.TAsterisk
hasError := false
if isAsync {
hasError = p.markAsyncFn(asyncRange, isGenerator)
}
if isGenerator {
if !hasError {
p.markSyntaxFeature(compat.Generator, p.lexer.Range())
}
p.lexer.Next()
}
switch opts.lexicalDecl {
case lexicalDeclForbid:
p.forbidLexicalDecl(loc)
// Allow certain function statements in certain single-statement contexts
case lexicalDeclAllowFnInsideIf, lexicalDeclAllowFnInsideLabel:
if opts.isTypeScriptDeclare || isGenerator || isAsync {
p.forbidLexicalDecl(loc)
}
}
var name *ast.LocRef
var nameText string
// The name is optional for "export default function() {}" pseudo-statements
if !opts.isNameOptional || p.lexer.Token == js_lexer.TIdentifier {
nameLoc := p.lexer.Loc()
nameText = p.lexer.Identifier.String
if !isAsync && p.fnOrArrowDataParse.await != allowIdent && nameText == "await" {
p.log.AddError(&p.tracker, js_lexer.RangeOfIdentifier(p.source, nameLoc), "Cannot use \"await\" as an identifier here:")
}
p.lexer.Expect(js_lexer.TIdentifier)
name = &ast.LocRef{Loc: nameLoc, Ref: ast.InvalidRef}
}
// Even anonymous functions can have TypeScript type parameters
if p.options.ts.Parse {
p.skipTypeScriptTypeParameters(allowConstModifier)
}
// Introduce a fake block scope for function declarations inside if statements
var ifStmtScopeIndex int
hasIfScope := opts.lexicalDecl == lexicalDeclAllowFnInsideIf
if hasIfScope {
ifStmtScopeIndex = p.pushScopeForParsePass(js_ast.ScopeBlock, loc)
}
scopeIndex := p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, p.lexer.Loc())
await := allowIdent
yield := allowIdent
if isAsync {
await = allowExpr
}
if isGenerator {
yield = allowExpr
}
fn, hadBody := p.parseFn(name, logger.Range{}, 0, fnOrArrowDataParse{
needsAsyncLoc: loc,
asyncRange: asyncRange,
await: await,
yield: yield,
isTypeScriptDeclare: opts.isTypeScriptDeclare,
// Only allow omitting the body if we're parsing TypeScript
allowMissingBodyForTypeScript: p.options.ts.Parse,
})
// Don't output anything if it's just a forward declaration of a function
if opts.isTypeScriptDeclare || !hadBody {
p.popAndDiscardScope(scopeIndex)
// Balance the fake block scope introduced above
if hasIfScope {
p.popAndDiscardScope(ifStmtScopeIndex)
}
if opts.isTypeScriptDeclare && opts.isNamespaceScope && opts.isExport {
p.hasNonLocalExportDeclareInsideNamespace = true
}
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
p.popScope()
// Only declare the function after we know if it had a body or not. Otherwise
// TypeScript code such as this will double-declare the symbol:
//
// function foo(): void;
// function foo(): void {}
//
if name != nil {
kind := ast.SymbolHoistedFunction
if isGenerator || isAsync {
kind = ast.SymbolGeneratorOrAsyncFunction
}
name.Ref = p.declareSymbol(kind, name.Loc, nameText)
}
// Balance the fake block scope introduced above
if hasIfScope {
p.popScope()
}
fn.HasIfScope = hasIfScope
p.validateFunctionName(fn, fnStmt)
if opts.hasNoSideEffectsComment && !p.options.ignoreDCEAnnotations {
fn.HasNoSideEffectsComment = true
if name != nil && !opts.isTypeScriptDeclare {
p.symbols[name.Ref.InnerIndex].Flags |= ast.CallCanBeUnwrappedIfUnused
}
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SFunction{Fn: fn, IsExport: opts.isExport}}
}
type deferredDecorators struct {
decorators []js_ast.Decorator
}
type decoratorContextFlags uint8
const (
decoratorBeforeClassExpr = 1 << iota
decoratorInClassExpr
decoratorInFnArgs
)
func (p *parser) parseDecorators(decoratorScope *js_ast.Scope, classKeyword logger.Range, context decoratorContextFlags) (decorators []js_ast.Decorator) {
if p.lexer.Token == js_lexer.TAt {
if p.options.ts.Parse {
if p.options.ts.Config.ExperimentalDecorators == config.True {
if (context & decoratorInClassExpr) != 0 {
p.lexer.AddRangeErrorWithNotes(p.lexer.Range(), "TypeScript experimental decorators can only be used with class declarations",
[]logger.MsgData{p.tracker.MsgData(classKeyword, "This is a class expression, not a class declaration:")})
} else if (context & decoratorBeforeClassExpr) != 0 {
p.log.AddError(&p.tracker, p.lexer.Range(), "TypeScript experimental decorators cannot be used in expression position")
}
} else {
if (context&decoratorInFnArgs) != 0 && p.options.ts.Config.ExperimentalDecorators != config.True {
p.log.AddErrorWithNotes(&p.tracker, p.lexer.Range(), "Parameter decorators only work when experimental decorators are enabled", []logger.MsgData{{
Text: "You can enable experimental decorators by adding \"experimentalDecorators\": true to your \"tsconfig.json\" file.",
}})
}
}
} else {
if (context & decoratorInFnArgs) != 0 {
p.log.AddError(&p.tracker, p.lexer.Range(), "Parameter decorators are not allowed in JavaScript")
}
}
}
// TypeScript decorators cause us to temporarily revert to the scope that
// encloses the class declaration, since that's where the generated code
// for TypeScript decorators will be inserted.
oldScope := p.currentScope
p.currentScope = decoratorScope
for p.lexer.Token == js_lexer.TAt {
atLoc := p.lexer.Loc()
p.lexer.Next()
var value js_ast.Expr
if p.options.ts.Parse && p.options.ts.Config.ExperimentalDecorators == config.True {
// TypeScript's experimental decorator syntax is more permissive than
// JavaScript. Parse a new/call expression with "exprFlagDecorator" so
// we ignore EIndex expressions, since they may be part of a computed
// property:
//
// class Foo {
// @foo ['computed']() {}
// }
//
// This matches the behavior of the TypeScript compiler.
p.parseExperimentalDecoratorNesting++
value = p.parseExprWithFlags(js_ast.LNew, exprFlagDecorator)
p.parseExperimentalDecoratorNesting--
} else {
// JavaScript's decorator syntax is more restrictive. Parse it using a
// special parser that doesn't allow normal expressions (e.g. "?.").
value = p.parseDecorator()
}
decorators = append(decorators, js_ast.Decorator{
Value: value,
AtLoc: atLoc,
OmitNewlineAfter: !p.lexer.HasNewlineBefore,
})
}
// Avoid "popScope" because this decorator scope is not hierarchical
p.currentScope = oldScope
return decorators
}
func (p *parser) parseDecorator() js_ast.Expr {
if p.lexer.Token == js_lexer.TOpenParen {
p.lexer.Next()
value := p.parseExpr(js_ast.LLowest)
p.lexer.Expect(js_lexer.TCloseParen)
return value
}
name := p.lexer.Identifier
nameRange := p.lexer.Range()
p.lexer.Expect(js_lexer.TIdentifier)
// Forbid invalid identifiers
if (p.fnOrArrowDataParse.await != allowIdent && name.String == "await") ||
(p.fnOrArrowDataParse.yield != allowIdent && name.String == "yield") {
p.log.AddError(&p.tracker, nameRange, fmt.Sprintf("Cannot use %q as an identifier here:", name.String))
}
memberExpr := js_ast.Expr{Loc: nameRange.Loc, Data: &js_ast.EIdentifier{Ref: p.storeNameInRef(name)}}
// Custom error reporting for error recovery
var syntaxError logger.MsgData
wrapRange := nameRange
loop:
for {
switch p.lexer.Token {
case js_lexer.TExclamation:
// Skip over TypeScript non-null assertions
if p.lexer.HasNewlineBefore {
break loop
}
if !p.options.ts.Parse {
p.lexer.Unexpected()
}
wrapRange.Len = p.lexer.Range().End() - wrapRange.Loc.Start
p.lexer.Next()
case js_lexer.TDot, js_lexer.TQuestionDot:
// The grammar for "DecoratorMemberExpression" currently forbids "?."
if p.lexer.Token == js_lexer.TQuestionDot && syntaxError.Location == nil {
syntaxError = p.tracker.MsgData(p.lexer.Range(), "JavaScript decorator syntax does not allow \"?.\" here")
}
p.lexer.Next()
wrapRange.Len = p.lexer.Range().End() - wrapRange.Loc.Start
if p.lexer.Token == js_lexer.TPrivateIdentifier {
name := p.lexer.Identifier
memberExpr.Data = &js_ast.EIndex{
Target: memberExpr,
Index: js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EPrivateIdentifier{Ref: p.storeNameInRef(name)}},
}
p.reportPrivateNameUsage(name.String)
p.lexer.Next()
} else {
memberExpr.Data = &js_ast.EDot{
Target: memberExpr,
Name: p.lexer.Identifier.String,
NameLoc: p.lexer.Loc(),
}
p.lexer.Expect(js_lexer.TIdentifier)
}
case js_lexer.TOpenParen:
args, closeParenLoc, isMultiLine := p.parseCallArgs()
memberExpr.Data = &js_ast.ECall{
Target: memberExpr,
Args: args,
CloseParenLoc: closeParenLoc,
IsMultiLine: isMultiLine,
Kind: js_ast.TargetWasOriginallyPropertyAccess,
}
wrapRange.Len = closeParenLoc.Start + 1 - wrapRange.Loc.Start
// The grammar for "DecoratorCallExpression" currently forbids anything after it
if p.lexer.Token == js_lexer.TDot {
if syntaxError.Location == nil {
syntaxError = p.tracker.MsgData(p.lexer.Range(), "JavaScript decorator syntax does not allow \".\" after a call expression")
}
continue
}
break loop
default:
// "@x<y>"
// "@x.y<z>"
if !p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{}) {
break loop
}
}
}
// Suggest that non-decorator expressions be wrapped in parentheses
if syntaxError.Location != nil {
var notes []logger.MsgData
if text := p.source.TextForRange(wrapRange); !strings.ContainsRune(text, '\n') {
note := p.tracker.MsgData(wrapRange, "Wrap this decorator in parentheses to allow arbitrary expressions:")
note.Location.Suggestion = textForParenthesesSuggestion(text)
notes = []logger.MsgData{note}
}
p.log.AddMsg(logger.Msg{
Kind: logger.Error,
Data: syntaxError,
Notes: notes,
})
}
return memberExpr
}
func textForParenthesesSuggestion(text string) string {
count := utf8.RuneCountInString(text) - 2
if count < 1 {
count = 1
}
return fmt.Sprintf("(%s)", strings.Repeat(" ", count))
}
type lexicalDecl uint8
const (
lexicalDeclForbid lexicalDecl = iota
lexicalDeclAllowAll
lexicalDeclAllowFnInsideIf
lexicalDeclAllowFnInsideLabel
)
type parseStmtOpts struct {
deferredDecorators *deferredDecorators
lexicalDecl lexicalDecl
isModuleScope bool
isNamespaceScope bool
isExport bool
isExportDefault bool
isNameOptional bool // For "export default" pseudo-statements
isTypeScriptDeclare bool
isForLoopInit bool
isForAwaitLoopInit bool
allowDirectivePrologue bool
hasNoSideEffectsComment bool
isUsingStmt bool
isCaseBody bool
}
func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
loc := p.lexer.Loc()
if (p.lexer.HasCommentBefore & js_lexer.NoSideEffectsCommentBefore) != 0 {
opts.hasNoSideEffectsComment = true
}
// Do not attach any leading comments to the next expression
p.lexer.CommentsBeforeToken = p.lexer.CommentsBeforeToken[:0]
switch p.lexer.Token {
case js_lexer.TSemicolon:
p.lexer.Next()
return js_ast.Stmt{Loc: loc, Data: js_ast.SEmptyShared}
case js_lexer.TExport:
previousExportKeyword := p.esmExportKeyword
if opts.isModuleScope {
p.esmExportKeyword = p.lexer.Range()
} else if !opts.isNamespaceScope {
p.lexer.Unexpected()
}
p.lexer.Next()
switch p.lexer.Token {
case js_lexer.TClass, js_lexer.TConst, js_lexer.TFunction, js_lexer.TVar, js_lexer.TAt:
opts.isExport = true
return p.parseStmt(opts)
case js_lexer.TImport:
// "export import foo = bar"
if p.options.ts.Parse && (opts.isModuleScope || opts.isNamespaceScope) {
opts.isExport = true
return p.parseStmt(opts)
}
p.lexer.Unexpected()
return js_ast.Stmt{}
case js_lexer.TEnum:
if !p.options.ts.Parse {
p.lexer.Unexpected()
}
opts.isExport = true
return p.parseStmt(opts)
case js_lexer.TIdentifier:
if p.lexer.IsContextualKeyword("let") {
opts.isExport = true
return p.parseStmt(opts)
}
if p.lexer.IsContextualKeyword("as") {
// "export as namespace ns;"
p.lexer.Next()
p.lexer.ExpectContextualKeyword("namespace")
p.lexer.Expect(js_lexer.TIdentifier)
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
if p.lexer.IsContextualKeyword("async") {
// "export async function foo() {}"
asyncRange := p.lexer.Range()
p.lexer.Next()
if p.lexer.HasNewlineBefore {
p.log.AddError(&p.tracker, logger.Range{Loc: logger.Loc{Start: asyncRange.End()}},
"Unexpected newline after \"async\"")
panic(js_lexer.LexerPanic{})
}
p.lexer.Expect(js_lexer.TFunction)
opts.isExport = true
return p.parseFnStmt(loc, opts, true /* isAsync */, asyncRange)
}
if p.options.ts.Parse {
switch p.lexer.Identifier.String {
case "type":
// "export type foo = ..."
typeRange := p.lexer.Range()
p.lexer.Next()
if p.lexer.HasNewlineBefore && p.lexer.Token != js_lexer.TOpenBrace && p.lexer.Token != js_lexer.TAsterisk {
p.log.AddError(&p.tracker, logger.Range{Loc: logger.Loc{Start: typeRange.End()}},
"Unexpected newline after \"type\"")
panic(js_lexer.LexerPanic{})
}
p.skipTypeScriptTypeStmt(parseStmtOpts{isModuleScope: opts.isModuleScope, isExport: true})
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
case "namespace", "abstract", "module", "interface":
// "export namespace Foo {}"
// "export abstract class Foo {}"
// "export module Foo {}"
// "export interface Foo {}"
opts.isExport = true
return p.parseStmt(opts)
case "declare":
// "export declare class Foo {}"
opts.isExport = true
opts.lexicalDecl = lexicalDeclAllowAll
opts.isTypeScriptDeclare = true
return p.parseStmt(opts)
}
}
p.lexer.Unexpected()
return js_ast.Stmt{}
case js_lexer.TDefault:
if !opts.isModuleScope && (!opts.isNamespaceScope || !opts.isTypeScriptDeclare) {
p.lexer.Unexpected()
}
defaultLoc := p.lexer.Loc()
p.lexer.Next()
// Also pick up comments after the "default" keyword
if (p.lexer.HasCommentBefore & js_lexer.NoSideEffectsCommentBefore) != 0 {
opts.hasNoSideEffectsComment = true
}
// The default name is lazily generated only if no other name is present
createDefaultName := func() ast.LocRef {
// This must be named "default" for when "--keep-names" is active
defaultName := ast.LocRef{Loc: defaultLoc, Ref: p.newSymbol(ast.SymbolOther, "default")}
p.currentScope.Generated = append(p.currentScope.Generated, defaultName.Ref)
return defaultName
}
// "export default async function() {}"
// "export default async function foo() {}"
if p.lexer.IsContextualKeyword("async") {
asyncRange := p.lexer.Range()
p.lexer.Next()
if p.lexer.Token == js_lexer.TFunction && !p.lexer.HasNewlineBefore {
p.lexer.Next()
stmt := p.parseFnStmt(loc, parseStmtOpts{
isNameOptional: true,
lexicalDecl: lexicalDeclAllowAll,
hasNoSideEffectsComment: opts.hasNoSideEffectsComment,
}, true /* isAsync */, asyncRange)
if _, ok := stmt.Data.(*js_ast.STypeScript); ok {
return stmt // This was just a type annotation
}
// Use the statement name if present, since it's a better name
var defaultName ast.LocRef
if s, ok := stmt.Data.(*js_ast.SFunction); ok && s.Fn.Name != nil {
defaultName = ast.LocRef{Loc: defaultLoc, Ref: s.Fn.Name.Ref}
} else {
defaultName = createDefaultName()
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportDefault{DefaultName: defaultName, Value: stmt}}
}
defaultName := createDefaultName()
expr := p.parseSuffix(p.parseAsyncPrefixExpr(asyncRange, js_ast.LComma, 0), js_ast.LComma, nil, 0)
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportDefault{
DefaultName: defaultName, Value: js_ast.Stmt{Loc: loc, Data: &js_ast.SExpr{Value: expr}}}}
}
// "export default class {}"
// "export default class Foo {}"
// "export default @x class {}"
// "export default @x class Foo {}"
// "export default function() {}"
// "export default function foo() {}"
// "export default interface Foo {}"
// "export default interface + 1"
if p.lexer.Token == js_lexer.TFunction || p.lexer.Token == js_lexer.TClass || p.lexer.Token == js_lexer.TAt ||
(p.options.ts.Parse && p.lexer.IsContextualKeyword("interface")) {
stmt := p.parseStmt(parseStmtOpts{
deferredDecorators: opts.deferredDecorators,
isNameOptional: true,
isExportDefault: true,
lexicalDecl: lexicalDeclAllowAll,
hasNoSideEffectsComment: opts.hasNoSideEffectsComment,
})
// Use the statement name if present, since it's a better name
var defaultName ast.LocRef
switch s := stmt.Data.(type) {
case *js_ast.STypeScript, *js_ast.SExpr:
return stmt // Handle the "interface" case above
case *js_ast.SFunction:
if s.Fn.Name != nil {
defaultName = ast.LocRef{Loc: defaultLoc, Ref: s.Fn.Name.Ref}
} else {
defaultName = createDefaultName()
}
case *js_ast.SClass:
if s.Class.Name != nil {
defaultName = ast.LocRef{Loc: defaultLoc, Ref: s.Class.Name.Ref}
} else {
defaultName = createDefaultName()
}
default:
panic("Internal error")
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportDefault{DefaultName: defaultName, Value: stmt}}
}
isIdentifier := p.lexer.Token == js_lexer.TIdentifier
name := p.lexer.Identifier.String
expr := p.parseExpr(js_ast.LComma)
// "export default abstract class {}"
// "export default abstract class Foo {}"
if p.options.ts.Parse && isIdentifier && name == "abstract" && !p.lexer.HasNewlineBefore {
if _, ok := expr.Data.(*js_ast.EIdentifier); ok && p.lexer.Token == js_lexer.TClass {
stmt := p.parseClassStmt(loc, parseStmtOpts{
deferredDecorators: opts.deferredDecorators,
isNameOptional: true,
})
// Use the statement name if present, since it's a better name
var defaultName ast.LocRef
if s, ok := stmt.Data.(*js_ast.SClass); ok && s.Class.Name != nil {
defaultName = ast.LocRef{Loc: defaultLoc, Ref: s.Class.Name.Ref}
} else {
defaultName = createDefaultName()
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportDefault{DefaultName: defaultName, Value: stmt}}
}
}
p.lexer.ExpectOrInsertSemicolon()
defaultName := createDefaultName()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportDefault{
DefaultName: defaultName, Value: js_ast.Stmt{Loc: loc, Data: &js_ast.SExpr{Value: expr}}}}
case js_lexer.TAsterisk:
if !opts.isModuleScope && (!opts.isNamespaceScope || !opts.isTypeScriptDeclare) {
p.lexer.Unexpected()
}
p.lexer.Next()
var namespaceRef ast.Ref
var alias *js_ast.ExportStarAlias
var pathRange logger.Range
var pathText string
var assertOrWith *ast.ImportAssertOrWith
var flags ast.ImportRecordFlags
if p.lexer.IsContextualKeyword("as") {
// "export * as ns from 'path'"
p.lexer.Next()
name := p.parseClauseAlias("export")
namespaceRef = p.storeNameInRef(name)
alias = &js_ast.ExportStarAlias{Loc: p.lexer.Loc(), OriginalName: name.String}
p.lexer.Next()
p.lexer.ExpectContextualKeyword("from")
pathRange, pathText, assertOrWith, flags = p.parsePath()
} else {
// "export * from 'path'"
p.lexer.ExpectContextualKeyword("from")
pathRange, pathText, assertOrWith, flags = p.parsePath()
name := js_ast.GenerateNonUniqueNameFromPath(pathText) + "_star"
namespaceRef = p.storeNameInRef(js_lexer.MaybeSubstring{String: name})
}
importRecordIndex := p.addImportRecord(ast.ImportStmt, ast.EvaluationPhase, pathRange, pathText, assertOrWith, flags)
// Export-star statements anywhere in the file disable top-level const
// local prefix because import cycles can be used to trigger TDZ
p.currentScope.IsAfterConstLocalPrefix = true
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportStar{
NamespaceRef: namespaceRef,
Alias: alias,
ImportRecordIndex: importRecordIndex,
}}
case js_lexer.TOpenBrace:
if !opts.isModuleScope && (!opts.isNamespaceScope || !opts.isTypeScriptDeclare) {
p.lexer.Unexpected()
}
items, isSingleLine := p.parseExportClause()
if p.lexer.IsContextualKeyword("from") {
// "export {} from 'path'"
p.lexer.Next()
pathLoc, pathText, assertOrWith, flags := p.parsePath()
importRecordIndex := p.addImportRecord(ast.ImportStmt, ast.EvaluationPhase, pathLoc, pathText, assertOrWith, flags)
name := "import_" + js_ast.GenerateNonUniqueNameFromPath(pathText)
namespaceRef := p.storeNameInRef(js_lexer.MaybeSubstring{String: name})
// Export clause statements anywhere in the file disable top-level const
// local prefix because import cycles can be used to trigger TDZ
p.currentScope.IsAfterConstLocalPrefix = true
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportFrom{
Items: items,
NamespaceRef: namespaceRef,
ImportRecordIndex: importRecordIndex,
IsSingleLine: isSingleLine,
}}
}
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportClause{Items: items, IsSingleLine: isSingleLine}}
case js_lexer.TEquals:
// "export = value;"
p.esmExportKeyword = previousExportKeyword // This wasn't an ESM export statement after all
if p.options.ts.Parse {
p.lexer.Next()
value := p.parseExpr(js_ast.LLowest)
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportEquals{Value: value}}
}
p.lexer.Unexpected()
return js_ast.Stmt{}
default:
p.lexer.Unexpected()
return js_ast.Stmt{}
}
case js_lexer.TFunction:
p.lexer.Next()
return p.parseFnStmt(loc, opts, false /* isAsync */, logger.Range{})
case js_lexer.TEnum:
if !p.options.ts.Parse {
p.lexer.Unexpected()
}
return p.parseTypeScriptEnumStmt(loc, opts)
case js_lexer.TAt:
// Parse decorators before class statements, which are potentially exported
scopeIndex := len(p.scopesInOrder)
decorators := p.parseDecorators(p.currentScope, logger.Range{}, 0)
// "@x export @y class Foo {}"
if opts.deferredDecorators != nil {
p.log.AddError(&p.tracker, logger.Range{Loc: loc, Len: 1}, "Decorators are not valid here")
p.discardScopesUpTo(scopeIndex)
return p.parseStmt(opts)
}
// If this turns out to be a "declare class" statement, we need to undo the
// scopes that were potentially pushed while parsing the decorator arguments.
// That can look like any one of the following:
//
// "@decorator declare class Foo {}"
// "@decorator declare abstract class Foo {}"
// "@decorator export declare class Foo {}"
// "@decorator export declare abstract class Foo {}"
//
opts.deferredDecorators = &deferredDecorators{
decorators: decorators,
}
stmt := p.parseStmt(opts)
// Check for valid decorator targets
switch s := stmt.Data.(type) {
case *js_ast.SClass:
return stmt
case *js_ast.SExportDefault:
switch s.Value.Data.(type) {
case *js_ast.SClass:
return stmt
}
case *js_ast.STypeScript:
if s.WasDeclareClass {
// If this is a type declaration, discard any scopes that were pushed
// while parsing decorators. Unlike with the class statements above,
// these scopes won't end up being visited during the upcoming visit
// pass because type declarations aren't visited at all.
p.discardScopesUpTo(scopeIndex)
return stmt
}
}
// Forbid decorators on anything other than a class statement
p.log.AddError(&p.tracker, logger.Range{Loc: loc, Len: 1}, "Decorators are not valid here")
stmt.Data = js_ast.STypeScriptShared
p.discardScopesUpTo(scopeIndex)
return stmt
case js_lexer.TClass:
if opts.lexicalDecl != lexicalDeclAllowAll {
p.forbidLexicalDecl(loc)
}
return p.parseClassStmt(loc, opts)
case js_lexer.TVar:
p.lexer.Next()
decls := p.parseAndDeclareDecls(ast.SymbolHoisted, opts)
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SLocal{
Kind: js_ast.LocalVar,
Decls: decls,
IsExport: opts.isExport,
}}
case js_lexer.TConst:
if opts.lexicalDecl != lexicalDeclAllowAll {
p.forbidLexicalDecl(loc)
}
p.markSyntaxFeature(compat.ConstAndLet, p.lexer.Range())
p.lexer.Next()
if p.options.ts.Parse && p.lexer.Token == js_lexer.TEnum {
return p.parseTypeScriptEnumStmt(loc, opts)
}
decls := p.parseAndDeclareDecls(ast.SymbolConst, opts)
p.lexer.ExpectOrInsertSemicolon()
if !opts.isTypeScriptDeclare {
p.requireInitializers(js_ast.LocalConst, decls)
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SLocal{
Kind: js_ast.LocalConst,
Decls: decls,
IsExport: opts.isExport,
}}
case js_lexer.TIf:
p.lexer.Next()
p.lexer.Expect(js_lexer.TOpenParen)
test := p.parseExpr(js_ast.LLowest)
p.lexer.Expect(js_lexer.TCloseParen)
isSingleLineYes := !p.lexer.HasNewlineBefore && p.lexer.Token != js_lexer.TOpenBrace
yes := p.parseStmt(parseStmtOpts{lexicalDecl: lexicalDeclAllowFnInsideIf})
var noOrNil js_ast.Stmt
var isSingleLineNo bool
if p.lexer.Token == js_lexer.TElse {
p.lexer.Next()
isSingleLineNo = !p.lexer.HasNewlineBefore && p.lexer.Token != js_lexer.TOpenBrace
noOrNil = p.parseStmt(parseStmtOpts{lexicalDecl: lexicalDeclAllowFnInsideIf})
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SIf{Test: test, Yes: yes, NoOrNil: noOrNil, IsSingleLineYes: isSingleLineYes, IsSingleLineNo: isSingleLineNo}}
case js_lexer.TDo:
p.lexer.Next()
body := p.parseStmt(parseStmtOpts{})
p.lexer.Expect(js_lexer.TWhile)
p.lexer.Expect(js_lexer.TOpenParen)
test := p.parseExpr(js_ast.LLowest)
p.lexer.Expect(js_lexer.TCloseParen)
// This is a weird corner case where automatic semicolon insertion applies
// even without a newline present
if p.lexer.Token == js_lexer.TSemicolon {
p.lexer.Next()
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SDoWhile{Body: body, Test: test}}
case js_lexer.TWhile:
p.lexer.Next()
p.lexer.Expect(js_lexer.TOpenParen)
test := p.parseExpr(js_ast.LLowest)
p.lexer.Expect(js_lexer.TCloseParen)
isSingleLineBody := !p.lexer.HasNewlineBefore && p.lexer.Token != js_lexer.TOpenBrace
body := p.parseStmt(parseStmtOpts{})
return js_ast.Stmt{Loc: loc, Data: &js_ast.SWhile{Test: test, Body: body, IsSingleLineBody: isSingleLineBody}}
case js_lexer.TWith:
p.lexer.Next()
p.lexer.Expect(js_lexer.TOpenParen)
test := p.parseExpr(js_ast.LLowest)
bodyLoc := p.lexer.Loc()
p.lexer.Expect(js_lexer.TCloseParen)
// Push a scope so we make sure to prevent any bare identifiers referenced
// within the body from being renamed. Renaming them might change the
// semantics of the code.
p.pushScopeForParsePass(js_ast.ScopeWith, bodyLoc)
isSingleLineBody := !p.lexer.HasNewlineBefore && p.lexer.Token != js_lexer.TOpenBrace
body := p.parseStmt(parseStmtOpts{})
p.popScope()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SWith{Value: test, BodyLoc: bodyLoc, Body: body, IsSingleLineBody: isSingleLineBody}}
case js_lexer.TSwitch:
p.lexer.Next()
p.lexer.Expect(js_lexer.TOpenParen)
test := p.parseExpr(js_ast.LLowest)
p.lexer.Expect(js_lexer.TCloseParen)
bodyLoc := p.lexer.Loc()
p.pushScopeForParsePass(js_ast.ScopeBlock, bodyLoc)
defer p.popScope()
p.lexer.Expect(js_lexer.TOpenBrace)
cases := []js_ast.Case{}
foundDefault := false
switchScopeStart := len(p.scopesInOrder)
var caseScopeMap map[*js_ast.Scope]struct{}
for p.lexer.Token != js_lexer.TCloseBrace {
var value js_ast.Expr
body := []js_ast.Stmt{}
caseLoc := p.saveExprCommentsHere()
caseScopeStart := len(p.scopesInOrder)
if p.lexer.Token == js_lexer.TDefault {
if foundDefault {
p.log.AddError(&p.tracker, p.lexer.Range(), "Multiple default clauses are not allowed")
panic(js_lexer.LexerPanic{})
}
foundDefault = true
p.lexer.Next()
p.lexer.Expect(js_lexer.TColon)
} else {
p.lexer.Expect(js_lexer.TCase)
value = p.parseExpr(js_ast.LLowest)
p.lexer.Expect(js_lexer.TColon)
}
// Keep track of any scopes created by case values. This can happen if
// code uses anonymous functions inside a case value. For example:
//
// switch (x) {
// case y.map(z => -z).join(':'):
// return y
// }
//
if caseScopeStart < len(p.scopesInOrder) {
if caseScopeMap == nil {
caseScopeMap = make(map[*js_ast.Scope]struct{})
}
for {
caseScopeMap[p.scopesInOrder[caseScopeStart].scope] = struct{}{}
caseScopeStart++
if caseScopeStart == len(p.scopesInOrder) {
break
}
}
}
caseBody:
for {
switch p.lexer.Token {
case js_lexer.TCloseBrace, js_lexer.TCase, js_lexer.TDefault:
break caseBody
default:
body = append(body, p.parseStmt(parseStmtOpts{
lexicalDecl: lexicalDeclAllowAll,
isCaseBody: true,
}))
}
}
cases = append(cases, js_ast.Case{ValueOrNil: value, Body: body, Loc: caseLoc})
}
// If any case contains values that create a scope, reorder those scopes to
// come first before any scopes created by case bodies. This reflects the
// order in which we will visit the AST in our second parsing pass. The
// second parsing pass visits things in a different order because it uses
// case values to determine liveness, and then uses the liveness information
// when visiting the case bodies (e.g. avoid "require()" calls in dead code).
// For example:
//
// switch (1) {
// case y(() => 1):
// z = () => 2;
// break;
//
// case y(() => 3):
// z = () => 4;
// break;
// }
//
// This is parsed in the order 1,2,3,4 but visited in the order 1,3,2,4.
if len(caseScopeMap) > 0 {
caseScopes := make([]scopeOrder, 0, len(caseScopeMap))
bodyScopes := make([]scopeOrder, 0, len(p.scopesInOrder)-switchScopeStart-len(caseScopeMap))
for i := switchScopeStart; i < len(p.scopesInOrder); i++ {
it := p.scopesInOrder[i]
if _, ok := caseScopeMap[it.scope]; ok {
caseScopes = append(caseScopes, it)
} else {
bodyScopes = append(bodyScopes, it)
}
}
copy(p.scopesInOrder[switchScopeStart:switchScopeStart+len(caseScopeMap)], caseScopes)
copy(p.scopesInOrder[switchScopeStart+len(caseScopeMap):], bodyScopes)
}
closeBraceLoc := p.lexer.Loc()
p.lexer.Expect(js_lexer.TCloseBrace)
return js_ast.Stmt{Loc: loc, Data: &js_ast.SSwitch{
Test: test,
Cases: cases,
BodyLoc: bodyLoc,
CloseBraceLoc: closeBraceLoc,
}}
case js_lexer.TTry:
p.lexer.Next()
blockLoc := p.lexer.Loc()
p.lexer.Expect(js_lexer.TOpenBrace)
p.pushScopeForParsePass(js_ast.ScopeBlock, loc)
body := p.parseStmtsUpTo(js_lexer.TCloseBrace, parseStmtOpts{})
p.popScope()
closeBraceLoc := p.lexer.Loc()
p.lexer.Next()
var catch *js_ast.Catch = nil
var finally *js_ast.Finally = nil
if p.lexer.Token == js_lexer.TCatch {
catchLoc := p.lexer.Loc()
p.pushScopeForParsePass(js_ast.ScopeCatchBinding, catchLoc)
p.lexer.Next()
var bindingOrNil js_ast.Binding
// The catch binding is optional, and can be omitted
if p.lexer.Token == js_lexer.TOpenBrace {
if p.options.unsupportedJSFeatures.Has(compat.OptionalCatchBinding) {
// Generate a new symbol for the catch binding for older browsers
ref := p.newSymbol(ast.SymbolOther, "e")
p.currentScope.Generated = append(p.currentScope.Generated, ref)
bindingOrNil = js_ast.Binding{Loc: p.lexer.Loc(), Data: &js_ast.BIdentifier{Ref: ref}}
}
} else {
p.lexer.Expect(js_lexer.TOpenParen)
bindingOrNil = p.parseBinding(parseBindingOpts{})
// Skip over types
if p.options.ts.Parse && p.lexer.Token == js_lexer.TColon {
p.lexer.Expect(js_lexer.TColon)
p.skipTypeScriptType(js_ast.LLowest)
}
p.lexer.Expect(js_lexer.TCloseParen)
// Bare identifiers are a special case
kind := ast.SymbolOther
if _, ok := bindingOrNil.Data.(*js_ast.BIdentifier); ok {
kind = ast.SymbolCatchIdentifier
}
p.declareBinding(kind, bindingOrNil, parseStmtOpts{})
}
blockLoc := p.lexer.Loc()
p.lexer.Expect(js_lexer.TOpenBrace)
p.pushScopeForParsePass(js_ast.ScopeBlock, blockLoc)
stmts := p.parseStmtsUpTo(js_lexer.TCloseBrace, parseStmtOpts{})
p.popScope()
closeBraceLoc := p.lexer.Loc()
p.lexer.Next()
catch = &js_ast.Catch{Loc: catchLoc, BindingOrNil: bindingOrNil, BlockLoc: blockLoc, Block: js_ast.SBlock{Stmts: stmts, CloseBraceLoc: closeBraceLoc}}
p.popScope()
}
if p.lexer.Token == js_lexer.TFinally || catch == nil {
finallyLoc := p.lexer.Loc()
p.pushScopeForParsePass(js_ast.ScopeBlock, finallyLoc)
p.lexer.Expect(js_lexer.TFinally)
p.lexer.Expect(js_lexer.TOpenBrace)
stmts := p.parseStmtsUpTo(js_lexer.TCloseBrace, parseStmtOpts{})
closeBraceLoc := p.lexer.Loc()
p.lexer.Next()
finally = &js_ast.Finally{Loc: finallyLoc, Block: js_ast.SBlock{Stmts: stmts, CloseBraceLoc: closeBraceLoc}}
p.popScope()
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.STry{
BlockLoc: blockLoc,
Block: js_ast.SBlock{Stmts: body, CloseBraceLoc: closeBraceLoc},
Catch: catch,
Finally: finally,
}}
case js_lexer.TFor:
p.pushScopeForParsePass(js_ast.ScopeBlock, loc)
defer p.popScope()
p.lexer.Next()
// "for await (let x of y) {}"
var awaitRange logger.Range
if p.lexer.IsContextualKeyword("await") {
awaitRange = p.lexer.Range()
if p.fnOrArrowDataParse.await != allowExpr {
p.log.AddError(&p.tracker, awaitRange, "Cannot use \"await\" outside an async function")
awaitRange = logger.Range{}
} else {
didGenerateError := false
if p.fnOrArrowDataParse.isTopLevel {
p.topLevelAwaitKeyword = awaitRange
}
if !didGenerateError && p.options.unsupportedJSFeatures.Has(compat.AsyncAwait) && p.options.unsupportedJSFeatures.Has(compat.Generator) {
// If for-await loops aren't supported, then we only support lowering
// if either async/await or generators is supported. Otherwise we
// cannot lower for-await loops.
p.markSyntaxFeature(compat.ForAwait, awaitRange)
}
}
p.lexer.Next()
}
p.lexer.Expect(js_lexer.TOpenParen)
var initOrNil js_ast.Stmt
var testOrNil js_ast.Expr
var updateOrNil js_ast.Expr
// "in" expressions aren't allowed here
p.allowIn = false
var badLetRange logger.Range
if p.lexer.IsContextualKeyword("let") {
badLetRange = p.lexer.Range()
}
decls := []js_ast.Decl{}
initLoc := p.lexer.Loc()
isVar := false
switch p.lexer.Token {
case js_lexer.TVar:
isVar = true
p.lexer.Next()
decls = p.parseAndDeclareDecls(ast.SymbolHoisted, parseStmtOpts{})
initOrNil = js_ast.Stmt{Loc: initLoc, Data: &js_ast.SLocal{Kind: js_ast.LocalVar, Decls: decls}}
case js_lexer.TConst:
p.markSyntaxFeature(compat.ConstAndLet, p.lexer.Range())
p.lexer.Next()
decls = p.parseAndDeclareDecls(ast.SymbolConst, parseStmtOpts{})
initOrNil = js_ast.Stmt{Loc: initLoc, Data: &js_ast.SLocal{Kind: js_ast.LocalConst, Decls: decls}}
case js_lexer.TSemicolon:
default:
var expr js_ast.Expr
var stmt js_ast.Stmt
expr, stmt, decls = p.parseExprOrLetOrUsingStmt(parseStmtOpts{
lexicalDecl: lexicalDeclAllowAll,
isForLoopInit: true,
isForAwaitLoopInit: awaitRange.Len > 0,
})
if stmt.Data != nil {
badLetRange = logger.Range{}
initOrNil = stmt
} else {
initOrNil = js_ast.Stmt{Loc: expr.Loc, Data: &js_ast.SExpr{Value: expr}}
}
}
// "in" expressions are allowed again
p.allowIn = true
// Detect for-of loops
if p.lexer.IsContextualKeyword("of") || awaitRange.Len > 0 {
if badLetRange.Len > 0 {
p.log.AddError(&p.tracker, badLetRange, "\"let\" must be wrapped in parentheses to be used as an expression here:")
}
if awaitRange.Len > 0 && !p.lexer.IsContextualKeyword("of") {
if initOrNil.Data != nil {
p.lexer.ExpectedString("\"of\"")
} else {
p.lexer.Unexpected()
}
}
p.forbidInitializers(decls, "of", false)
p.markSyntaxFeature(compat.ForOf, p.lexer.Range())
p.lexer.Next()
value := p.parseExpr(js_ast.LComma)
p.lexer.Expect(js_lexer.TCloseParen)
isSingleLineBody := !p.lexer.HasNewlineBefore && p.lexer.Token != js_lexer.TOpenBrace
body := p.parseStmt(parseStmtOpts{})
return js_ast.Stmt{Loc: loc, Data: &js_ast.SForOf{Await: awaitRange, Init: initOrNil, Value: value, Body: body, IsSingleLineBody: isSingleLineBody}}
}
// Detect for-in loops
if p.lexer.Token == js_lexer.TIn {
p.forbidInitializers(decls, "in", isVar)
if len(decls) == 1 {
if local, ok := initOrNil.Data.(*js_ast.SLocal); ok {
if local.Kind == js_ast.LocalUsing {
p.log.AddError(&p.tracker, js_lexer.RangeOfIdentifier(p.source, initOrNil.Loc), "\"using\" declarations are not allowed here")
} else if local.Kind == js_ast.LocalAwaitUsing {
p.log.AddError(&p.tracker, js_lexer.RangeOfIdentifier(p.source, initOrNil.Loc), "\"await using\" declarations are not allowed here")
}
}
}
p.lexer.Next()
value := p.parseExpr(js_ast.LLowest)
p.lexer.Expect(js_lexer.TCloseParen)
isSingleLineBody := !p.lexer.HasNewlineBefore && p.lexer.Token != js_lexer.TOpenBrace
body := p.parseStmt(parseStmtOpts{})
return js_ast.Stmt{Loc: loc, Data: &js_ast.SForIn{Init: initOrNil, Value: value, Body: body, IsSingleLineBody: isSingleLineBody}}
}
p.lexer.Expect(js_lexer.TSemicolon)
// "await using" declarations are only allowed in for-of loops
if local, ok := initOrNil.Data.(*js_ast.SLocal); ok && local.Kind == js_ast.LocalAwaitUsing {
p.log.AddError(&p.tracker, js_lexer.RangeOfIdentifier(p.source, initOrNil.Loc), "\"await using\" declarations are not allowed here")
}
// Only require "const" statement initializers when we know we're a normal for loop
if local, ok := initOrNil.Data.(*js_ast.SLocal); ok && (local.Kind == js_ast.LocalConst || local.Kind == js_ast.LocalUsing) {
p.requireInitializers(local.Kind, decls)
}
if p.lexer.Token != js_lexer.TSemicolon {
testOrNil = p.parseExpr(js_ast.LLowest)
}
p.lexer.Expect(js_lexer.TSemicolon)
if p.lexer.Token != js_lexer.TCloseParen {
updateOrNil = p.parseExpr(js_ast.LLowest)
}
p.lexer.Expect(js_lexer.TCloseParen)
isSingleLineBody := !p.lexer.HasNewlineBefore && p.lexer.Token != js_lexer.TOpenBrace
body := p.parseStmt(parseStmtOpts{})
return js_ast.Stmt{Loc: loc, Data: &js_ast.SFor{
InitOrNil: initOrNil,
TestOrNil: testOrNil,
UpdateOrNil: updateOrNil,
Body: body,
IsSingleLineBody: isSingleLineBody,
}}
case js_lexer.TImport:
previousImportStatementKeyword := p.esmImportStatementKeyword
p.esmImportStatementKeyword = p.lexer.Range()
p.lexer.Next()
stmt := js_ast.SImport{}
phase := ast.EvaluationPhase
wasOriginallyBareImport := false
// "export import foo = bar"
// "import foo = bar" in a namespace
if (opts.isExport || (opts.isNamespaceScope && !opts.isTypeScriptDeclare)) && p.lexer.Token != js_lexer.TIdentifier {
p.lexer.Expected(js_lexer.TIdentifier)
}
syntaxBeforePath:
switch p.lexer.Token {
case js_lexer.TOpenParen, js_lexer.TDot:
// "import('path')"
// "import.meta"
p.esmImportStatementKeyword = previousImportStatementKeyword // This wasn't an ESM import statement after all
expr := p.parseSuffix(p.parseImportExpr(loc, js_ast.LLowest), js_ast.LLowest, nil, 0)
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExpr{Value: expr}}
case js_lexer.TStringLiteral, js_lexer.TNoSubstitutionTemplateLiteral:
// "import 'path'"
if !opts.isModuleScope && (!opts.isNamespaceScope || !opts.isTypeScriptDeclare) {
p.lexer.Unexpected()
return js_ast.Stmt{}
}
wasOriginallyBareImport = true
case js_lexer.TAsterisk:
// "import * as ns from 'path'"
if !opts.isModuleScope && (!opts.isNamespaceScope || !opts.isTypeScriptDeclare) {
p.lexer.Unexpected()
return js_ast.Stmt{}
}
p.lexer.Next()
p.lexer.ExpectContextualKeyword("as")
stmt.NamespaceRef = p.storeNameInRef(p.lexer.Identifier)
starLoc := p.lexer.Loc()
stmt.StarNameLoc = &starLoc
p.lexer.Expect(js_lexer.TIdentifier)
p.lexer.ExpectContextualKeyword("from")
case js_lexer.TOpenBrace:
// "import {item1, item2} from 'path'"
if !opts.isModuleScope && (!opts.isNamespaceScope || !opts.isTypeScriptDeclare) {
p.lexer.Unexpected()
return js_ast.Stmt{}
}
items, isSingleLine := p.parseImportClause()
stmt.Items = &items
stmt.IsSingleLine = isSingleLine
p.lexer.ExpectContextualKeyword("from")
case js_lexer.TIdentifier:
// "import defaultItem from 'path'"
// "import foo = bar"
if !opts.isModuleScope && !opts.isNamespaceScope {
p.lexer.Unexpected()
return js_ast.Stmt{}
}
defaultName := p.lexer.Identifier
defaultLoc := p.lexer.Loc()
isDeferName := p.lexer.Raw() == "defer"
isSourceName := p.lexer.Raw() == "source"
p.lexer.Next()
if isDeferName && p.lexer.Token == js_lexer.TAsterisk {
// "import defer * as foo from 'bar';"
p.markSyntaxFeature(compat.ImportDefer, js_lexer.RangeOfIdentifier(p.source, defaultLoc))
phase = ast.DeferPhase
p.lexer.Next()
p.lexer.ExpectContextualKeyword("as")
stmt.NamespaceRef = p.storeNameInRef(p.lexer.Identifier)
starLoc := p.lexer.Loc()
stmt.StarNameLoc = &starLoc
p.lexer.Expect(js_lexer.TIdentifier)
p.lexer.ExpectContextualKeyword("from")
break
}
if isSourceName && p.lexer.Token == js_lexer.TIdentifier {
if p.lexer.Raw() == "from" {
nameSubstring := p.lexer.Identifier
nameLoc := p.lexer.Loc()
p.lexer.Next()
if p.lexer.IsContextualKeyword("from") {
// "import source from from 'foo';"
p.markSyntaxFeature(compat.ImportSource, js_lexer.RangeOfIdentifier(p.source, defaultLoc))
phase = ast.SourcePhase
stmt.DefaultName = &ast.LocRef{Loc: nameLoc, Ref: p.storeNameInRef(nameSubstring)}
p.lexer.Next()
} else {
// "import source from 'foo';"
stmt.DefaultName = &ast.LocRef{Loc: defaultLoc, Ref: p.storeNameInRef(defaultName)}
}
break
}
// "import source foo from 'bar';"
p.markSyntaxFeature(compat.ImportSource, js_lexer.RangeOfIdentifier(p.source, defaultLoc))
phase = ast.SourcePhase
stmt.DefaultName = &ast.LocRef{Loc: p.lexer.Loc(), Ref: p.storeNameInRef(p.lexer.Identifier)}
p.lexer.Next()
p.lexer.ExpectContextualKeyword("from")
break
}
stmt.DefaultName = &ast.LocRef{Loc: defaultLoc, Ref: p.storeNameInRef(defaultName)}
if p.options.ts.Parse {
// Skip over type-only imports
if defaultName.String == "type" {
switch p.lexer.Token {
case js_lexer.TIdentifier:
nameSubstring := p.lexer.Identifier
nameLoc := p.lexer.Loc()
p.lexer.Next()
if p.lexer.Token == js_lexer.TEquals {
// "import type foo = require('bar');"
// "import type foo = bar.baz;"
opts.isTypeScriptDeclare = true
return p.parseTypeScriptImportEqualsStmt(loc, opts, nameLoc, nameSubstring.String)
} else if p.lexer.Token == js_lexer.TStringLiteral && nameSubstring.String == "from" {
// "import type from 'bar';"
break syntaxBeforePath
} else {
// "import type foo from 'bar';"
p.lexer.ExpectContextualKeyword("from")
p.parsePath()
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
case js_lexer.TAsterisk:
// "import type * as foo from 'bar';"
p.lexer.Next()
p.lexer.ExpectContextualKeyword("as")
p.lexer.Expect(js_lexer.TIdentifier)
p.lexer.ExpectContextualKeyword("from")
p.parsePath()
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
case js_lexer.TOpenBrace:
// "import type {foo} from 'bar';"
p.parseImportClause()
p.lexer.ExpectContextualKeyword("from")
p.parsePath()
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
}
// Parse TypeScript import assignment statements
if p.lexer.Token == js_lexer.TEquals || opts.isExport || (opts.isNamespaceScope && !opts.isTypeScriptDeclare) {
p.esmImportStatementKeyword = previousImportStatementKeyword // This wasn't an ESM import statement after all
return p.parseTypeScriptImportEqualsStmt(loc, opts, stmt.DefaultName.Loc, defaultName.String)
}
}
if p.lexer.Token == js_lexer.TComma {
p.lexer.Next()
switch p.lexer.Token {
case js_lexer.TAsterisk:
// "import defaultItem, * as ns from 'path'"
p.lexer.Next()
p.lexer.ExpectContextualKeyword("as")
stmt.NamespaceRef = p.storeNameInRef(p.lexer.Identifier)
starLoc := p.lexer.Loc()
stmt.StarNameLoc = &starLoc
p.lexer.Expect(js_lexer.TIdentifier)
case js_lexer.TOpenBrace:
// "import defaultItem, {item1, item2} from 'path'"
items, isSingleLine := p.parseImportClause()
stmt.Items = &items
stmt.IsSingleLine = isSingleLine
default:
p.lexer.Unexpected()
}
}
p.lexer.ExpectContextualKeyword("from")
default:
p.lexer.Unexpected()
return js_ast.Stmt{}
}
pathLoc, pathText, assertOrWith, flags := p.parsePath()
p.lexer.ExpectOrInsertSemicolon()
// If TypeScript's "preserveValueImports": true setting is active, TypeScript's
// "importsNotUsedAsValues": "preserve" setting is NOT active, and the import
// clause is present and empty (or is non-empty but filled with type-only
// items), then the import statement should still be removed entirely to match
// the behavior of the TypeScript compiler:
//
// // Keep these
// import 'x'
// import { y } from 'x'
// import { y, type z } from 'x'
//
// // Remove these
// import {} from 'x'
// import { type y } from 'x'
//
// // Remove the items from these
// import d, {} from 'x'
// import d, { type y } from 'x'
//
if p.options.ts.Parse && p.options.ts.Config.UnusedImportFlags() == config.TSUnusedImport_KeepValues && stmt.Items != nil && len(*stmt.Items) == 0 {
if stmt.DefaultName == nil {
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
stmt.Items = nil
}
if wasOriginallyBareImport {
flags |= ast.WasOriginallyBareImport
}
stmt.ImportRecordIndex = p.addImportRecord(ast.ImportStmt, phase, pathLoc, pathText, assertOrWith, flags)
if stmt.StarNameLoc != nil {
name := p.loadNameFromRef(stmt.NamespaceRef)
stmt.NamespaceRef = p.declareSymbol(ast.SymbolImport, *stmt.StarNameLoc, name)
} else {
// Generate a symbol for the namespace
name := "import_" + js_ast.GenerateNonUniqueNameFromPath(pathText)
stmt.NamespaceRef = p.newSymbol(ast.SymbolOther, name)
p.currentScope.Generated = append(p.currentScope.Generated, stmt.NamespaceRef)
}
itemRefs := make(map[string]ast.LocRef)
// Link the default item to the namespace
if stmt.DefaultName != nil {
name := p.loadNameFromRef(stmt.DefaultName.Ref)
ref := p.declareSymbol(ast.SymbolImport, stmt.DefaultName.Loc, name)
p.isImportItem[ref] = true
stmt.DefaultName.Ref = ref
}
// Link each import item to the namespace
if stmt.Items != nil {
for i, item := range *stmt.Items {
name := p.loadNameFromRef(item.Name.Ref)
ref := p.declareSymbol(ast.SymbolImport, item.Name.Loc, name)
p.checkForUnrepresentableIdentifier(item.AliasLoc, item.Alias)
p.isImportItem[ref] = true
(*stmt.Items)[i].Name.Ref = ref
itemRefs[item.Alias] = ast.LocRef{Loc: item.Name.Loc, Ref: ref}
}
}
// Track the items for this namespace
p.importItemsForNamespace[stmt.NamespaceRef] = namespaceImportItems{
entries: itemRefs,
importRecordIndex: stmt.ImportRecordIndex,
}
// Import statements anywhere in the file disable top-level const
// local prefix because import cycles can be used to trigger TDZ
p.currentScope.IsAfterConstLocalPrefix = true
return js_ast.Stmt{Loc: loc, Data: &stmt}
case js_lexer.TBreak:
p.lexer.Next()
name := p.parseLabelName()
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SBreak{Label: name}}
case js_lexer.TContinue:
p.lexer.Next()
name := p.parseLabelName()
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SContinue{Label: name}}
case js_lexer.TReturn:
if p.fnOrArrowDataParse.isReturnDisallowed {
p.log.AddError(&p.tracker, p.lexer.Range(), "A return statement cannot be used here:")
}
p.lexer.Next()
var value js_ast.Expr
if p.lexer.Token != js_lexer.TSemicolon &&
!p.lexer.HasNewlineBefore &&
p.lexer.Token != js_lexer.TCloseBrace &&
p.lexer.Token != js_lexer.TEndOfFile {
value = p.parseExpr(js_ast.LLowest)
}
p.latestReturnHadSemicolon = p.lexer.Token == js_lexer.TSemicolon
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SReturn{ValueOrNil: value}}
case js_lexer.TThrow:
p.lexer.Next()
if p.lexer.HasNewlineBefore {
endLoc := logger.Loc{Start: loc.Start + 5}
p.log.AddError(&p.tracker, logger.Range{Loc: endLoc},
"Unexpected newline after \"throw\"")
return js_ast.Stmt{Loc: loc, Data: &js_ast.SThrow{Value: js_ast.Expr{Loc: endLoc, Data: js_ast.ENullShared}}}
}
expr := p.parseExpr(js_ast.LLowest)
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SThrow{Value: expr}}
case js_lexer.TDebugger:
p.lexer.Next()
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: js_ast.SDebuggerShared}
case js_lexer.TOpenBrace:
p.pushScopeForParsePass(js_ast.ScopeBlock, loc)
defer p.popScope()
p.lexer.Next()
stmts := p.parseStmtsUpTo(js_lexer.TCloseBrace, parseStmtOpts{})
closeBraceLoc := p.lexer.Loc()
p.lexer.Next()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SBlock{Stmts: stmts, CloseBraceLoc: closeBraceLoc}}
default:
isIdentifier := p.lexer.Token == js_lexer.TIdentifier
nameRange := p.lexer.Range()
name := p.lexer.Identifier.String
// Parse either an async function, an async expression, or a normal expression
var expr js_ast.Expr
if isIdentifier && p.lexer.Raw() == "async" {
p.lexer.Next()
if p.lexer.Token == js_lexer.TFunction && !p.lexer.HasNewlineBefore {
p.lexer.Next()
return p.parseFnStmt(nameRange.Loc, opts, true /* isAsync */, nameRange)
}
expr = p.parseSuffix(p.parseAsyncPrefixExpr(nameRange, js_ast.LLowest, 0), js_ast.LLowest, nil, 0)
} else {
var stmt js_ast.Stmt
expr, stmt, _ = p.parseExprOrLetOrUsingStmt(opts)
if stmt.Data != nil {
p.lexer.ExpectOrInsertSemicolon()
return stmt
}
}
if isIdentifier {
if ident, ok := expr.Data.(*js_ast.EIdentifier); ok {
if p.lexer.Token == js_lexer.TColon && opts.deferredDecorators == nil {
p.pushScopeForParsePass(js_ast.ScopeLabel, loc)
defer p.popScope()
// Parse a labeled statement
p.lexer.Next()
name := ast.LocRef{Loc: expr.Loc, Ref: ident.Ref}
nestedOpts := parseStmtOpts{}
if opts.lexicalDecl == lexicalDeclAllowAll || opts.lexicalDecl == lexicalDeclAllowFnInsideLabel {
nestedOpts.lexicalDecl = lexicalDeclAllowFnInsideLabel
}
isSingleLineStmt := !p.lexer.HasNewlineBefore && p.lexer.Token != js_lexer.TOpenBrace
stmt := p.parseStmt(nestedOpts)
return js_ast.Stmt{Loc: loc, Data: &js_ast.SLabel{Name: name, Stmt: stmt, IsSingleLineStmt: isSingleLineStmt}}
}
if p.options.ts.Parse {
switch name {
case "type":
if !p.lexer.HasNewlineBefore && p.lexer.Token == js_lexer.TIdentifier {
// "type Foo = any"
p.skipTypeScriptTypeStmt(parseStmtOpts{isModuleScope: opts.isModuleScope})
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
case "namespace", "module":
// "namespace Foo {}"
// "module Foo {}"
// "declare module 'fs' {}"
// "declare module 'fs';"
if !p.lexer.HasNewlineBefore && (opts.isModuleScope || opts.isNamespaceScope) && (p.lexer.Token == js_lexer.TIdentifier ||
(p.lexer.Token == js_lexer.TStringLiteral && opts.isTypeScriptDeclare)) {
return p.parseTypeScriptNamespaceStmt(loc, opts)
}
case "interface":
// "interface Foo {}"
// "export default interface Foo {}"
// "export default interface \n Foo {}"
if !p.lexer.HasNewlineBefore || opts.isExportDefault {
p.skipTypeScriptInterfaceStmt(parseStmtOpts{isModuleScope: opts.isModuleScope})
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
// "interface \n Foo {}"
// "export interface \n Foo {}"
if opts.isExport {
p.log.AddError(&p.tracker, nameRange, "Unexpected \"interface\"")
panic(js_lexer.LexerPanic{})
}
case "abstract":
if !p.lexer.HasNewlineBefore && p.lexer.Token == js_lexer.TClass {
return p.parseClassStmt(loc, opts)
}
case "global":
// "declare module 'fs' { global { namespace NodeJS {} } }"
if opts.isNamespaceScope && opts.isTypeScriptDeclare && p.lexer.Token == js_lexer.TOpenBrace {
p.lexer.Next()
p.parseStmtsUpTo(js_lexer.TCloseBrace, opts)
p.lexer.Next()
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
case "declare":
if !p.lexer.HasNewlineBefore {
opts.lexicalDecl = lexicalDeclAllowAll
opts.isTypeScriptDeclare = true
// "declare global { ... }"
if p.lexer.IsContextualKeyword("global") {
p.lexer.Next()
p.lexer.Expect(js_lexer.TOpenBrace)
p.parseStmtsUpTo(js_lexer.TCloseBrace, opts)
p.lexer.Next()
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}
// "declare const x: any"
scopeIndex := len(p.scopesInOrder)
oldLexer := p.lexer
stmt := p.parseStmt(opts)
typeDeclarationData := js_ast.STypeScriptShared
switch s := stmt.Data.(type) {
case *js_ast.SEmpty:
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExpr{Value: expr}}
case *js_ast.STypeScript:
// Type declarations are expected. Propagate the "declare class"
// status in case our caller is a decorator that needs to know
// this was a "declare class" statement.
typeDeclarationData = s
case *js_ast.SLocal:
// This is also a type declaration (but doesn't use "STypeScript"
// because we need to be able to handle namespace exports below)
default:
// Anything that we don't expect is a syntax error. For example,
// we consider this a syntax error:
//
// declare let declare: any, foo: any
// declare foo
//
// Strangely TypeScript allows this code starting with version
// 4.4, but I assume this is a bug. This bug was reported here:
// https://github.com/microsoft/TypeScript/issues/54602
p.lexer = oldLexer
p.lexer.Unexpected()
}
p.discardScopesUpTo(scopeIndex)
// Unlike almost all uses of "declare", statements that use
// "export declare" with "var/let/const" inside a namespace affect
// code generation. They cause any declared bindings to be
// considered exports of the namespace. Identifier references to
// those names must be converted into property accesses off the
// namespace object:
//
// namespace ns {
// export declare const x
// export function y() { return x }
// }
//
// (ns as any).x = 1
// console.log(ns.y())
//
// In this example, "return x" must be replaced with "return ns.x".
// This is handled by replacing each "export declare" statement
// inside a namespace with an "export var" statement containing all
// of the declared bindings. That "export var" statement will later
// cause identifiers to be transformed into property accesses.
if opts.isNamespaceScope && opts.isExport {
var decls []js_ast.Decl
if s, ok := stmt.Data.(*js_ast.SLocal); ok {
js_ast.ForEachIdentifierBindingInDecls(s.Decls, func(loc logger.Loc, b *js_ast.BIdentifier) {
decls = append(decls, js_ast.Decl{Binding: js_ast.Binding{Loc: loc, Data: b}})
})
}
if len(decls) > 0 {
return js_ast.Stmt{Loc: loc, Data: &js_ast.SLocal{
Kind: js_ast.LocalVar,
IsExport: true,
Decls: decls,
}}
}
}
return js_ast.Stmt{Loc: loc, Data: typeDeclarationData}
}
}
}
}
}
p.lexer.ExpectOrInsertSemicolon()
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExpr{Value: expr}}
}
}
func (p *parser) addImportRecord(
kind ast.ImportKind,
phase ast.ImportPhase,
pathRange logger.Range,
text string,
assertOrWith *ast.ImportAssertOrWith,
flags ast.ImportRecordFlags,
) uint32 {
index := uint32(len(p.importRecords))
p.importRecords = append(p.importRecords, ast.ImportRecord{
Kind: kind,
Range: pathRange,
Path: logger.Path{Text: text},
AssertOrWith: assertOrWith,
Phase: phase,
Flags: flags,
})
return index
}
func (p *parser) parseFnBody(data fnOrArrowDataParse) js_ast.FnBody {
oldFnOrArrowData := p.fnOrArrowDataParse
oldAllowIn := p.allowIn
p.fnOrArrowDataParse = data
p.allowIn = true
loc := p.lexer.Loc()
p.pushScopeForParsePass(js_ast.ScopeFunctionBody, loc)
defer p.popScope()
p.lexer.Expect(js_lexer.TOpenBrace)
stmts := p.parseStmtsUpTo(js_lexer.TCloseBrace, parseStmtOpts{
allowDirectivePrologue: true,
})
closeBraceLoc := p.lexer.Loc()
p.lexer.Next()
p.allowIn = oldAllowIn
p.fnOrArrowDataParse = oldFnOrArrowData
return js_ast.FnBody{Loc: loc, Block: js_ast.SBlock{Stmts: stmts, CloseBraceLoc: closeBraceLoc}}
}
func (p *parser) forbidLexicalDecl(loc logger.Loc) {
r := js_lexer.RangeOfIdentifier(p.source, loc)
p.log.AddErrorWithNotes(&p.tracker, r, "Cannot use a declaration in a single-statement context",
[]logger.MsgData{{Text: "Wrap this declaration in a block statement to use it here."}})
}
func (p *parser) forbidUsingInSwitch(loc logger.Loc) {
r := js_lexer.RangeOfIdentifier(p.source, loc)
p.log.AddErrorWithNotes(&p.tracker, r, "Cannot use a \"using\" declaration directly inside a switch case",
[]logger.MsgData{{Text: "Wrap this declaration in a block statement to use it here."}})
}
func (p *parser) parseStmtsUpTo(end js_lexer.T, opts parseStmtOpts) []js_ast.Stmt {
stmts := []js_ast.Stmt{}
returnWithoutSemicolonStart := int32(-1)
opts.lexicalDecl = lexicalDeclAllowAll
isDirectivePrologue := opts.allowDirectivePrologue
for {
// Preserve some statement-level comments
comments := p.lexer.LegalCommentsBeforeToken
if len(comments) > 0 {
for _, comment := range comments {
stmts = append(stmts, js_ast.Stmt{
Loc: comment.Loc,
Data: &js_ast.SComment{
Text: p.source.CommentTextWithoutIndent(comment),
IsLegalComment: true,
},
})
}
}
if p.lexer.Token == end {
break
}
stmt := p.parseStmt(opts)
// Skip TypeScript types entirely
if p.options.ts.Parse {
if _, ok := stmt.Data.(*js_ast.STypeScript); ok {
continue
}
}
// Parse one or more directives at the beginning
if isDirectivePrologue {
isDirectivePrologue = false
if expr, ok := stmt.Data.(*js_ast.SExpr); ok {
if str, ok := expr.Value.Data.(*js_ast.EString); ok && !str.PreferTemplate {
stmt.Data = &js_ast.SDirective{Value: str.Value, LegacyOctalLoc: str.LegacyOctalLoc}
isDirectivePrologue = true
if helpers.UTF16EqualsString(str.Value, "use strict") {
// Track "use strict" directives
p.currentScope.StrictMode = js_ast.ExplicitStrictMode
p.currentScope.UseStrictLoc = expr.Value.Loc
// Inside a function, strict mode actually propagates from the child
// scope to the parent scope:
//
// // This is a syntax error
// function fn(arguments) {
// "use strict";
// }
//
if p.currentScope.Kind == js_ast.ScopeFunctionBody &&
p.currentScope.Parent.Kind == js_ast.ScopeFunctionArgs &&
p.currentScope.Parent.StrictMode == js_ast.SloppyMode {
p.currentScope.Parent.StrictMode = js_ast.ExplicitStrictMode
p.currentScope.Parent.UseStrictLoc = expr.Value.Loc
}
} else if helpers.UTF16EqualsString(str.Value, "use asm") {
// Deliberately remove "use asm" directives. The asm.js subset of
// JavaScript has complicated validation rules that are triggered
// by this directive. This parser is not designed with asm.js in
// mind and round-tripping asm.js code through esbuild will very
// likely cause it to no longer validate as asm.js. When this
// happens, V8 prints a warning and people don't like seeing the
// warning.
//
// We deliberately do not attempt to preserve the validity of
// asm.js code because it's a complicated legacy format and it's
// obsolete now that WebAssembly exists. By removing this directive
// it will just become normal JavaScript, which will work fine and
// won't generate a warning (but will run slower). We don't generate
// a warning ourselves in this case because there isn't necessarily
// anything easy and actionable that the user can do to fix this.
stmt.Data = &js_ast.SEmpty{}
}
}
}
}
stmts = append(stmts, stmt)
// Warn about ASI and return statements. Here's an example of code with
// this problem: https://github.com/rollup/rollup/issues/3729
if !p.suppressWarningsAboutWeirdCode {
if s, ok := stmt.Data.(*js_ast.SReturn); ok && s.ValueOrNil.Data == nil && !p.latestReturnHadSemicolon {
returnWithoutSemicolonStart = stmt.Loc.Start
} else {
if returnWithoutSemicolonStart != -1 {
if _, ok := stmt.Data.(*js_ast.SExpr); ok {
p.log.AddID(logger.MsgID_JS_SemicolonAfterReturn, logger.Warning, &p.tracker, logger.Range{Loc: logger.Loc{Start: returnWithoutSemicolonStart + 6}},
"The following expression is not returned because of an automatically-inserted semicolon")
}
}
returnWithoutSemicolonStart = -1
}
}
}
return stmts
}
type generateTempRefArg uint8
const (
tempRefNeedsDeclare generateTempRefArg = iota
tempRefNoDeclare
// This is used when the generated temporary may a) be used inside of a loop
// body and b) may be used inside of a closure. In that case we can't use
// "var" for the temporary and we can't declare the temporary at the top of
// the enclosing function. Instead, we need to use "let" and we need to
// declare the temporary in the enclosing block (so it's inside of the loop
// body).
tempRefNeedsDeclareMayBeCapturedInsideLoop
)
func (p *parser) generateTempRef(declare generateTempRefArg, optionalName string) ast.Ref {
scope := p.currentScope
if declare != tempRefNeedsDeclareMayBeCapturedInsideLoop {
for !scope.Kind.StopsHoisting() {
scope = scope.Parent
}
}
if optionalName == "" {
optionalName = "_" + ast.DefaultNameMinifierJS.NumberToMinifiedName(p.tempRefCount)
p.tempRefCount++
}
ref := p.newSymbol(ast.SymbolOther, optionalName)
if declare == tempRefNeedsDeclareMayBeCapturedInsideLoop && !scope.Kind.StopsHoisting() {
p.tempLetsToDeclare = append(p.tempLetsToDeclare, ref)
} else if declare != tempRefNoDeclare {
p.tempRefsToDeclare = append(p.tempRefsToDeclare, tempRef{ref: ref})
}
scope.Generated = append(scope.Generated, ref)
return ref
}
func (p *parser) generateTopLevelTempRef() ast.Ref {
ref := p.newSymbol(ast.SymbolOther, "_"+ast.DefaultNameMinifierJS.NumberToMinifiedName(p.topLevelTempRefCount))
p.topLevelTempRefsToDeclare = append(p.topLevelTempRefsToDeclare, tempRef{ref: ref})
p.moduleScope.Generated = append(p.moduleScope.Generated, ref)
p.topLevelTempRefCount++
return ref
}
func (p *parser) pushScopeForVisitPass(kind js_ast.ScopeKind, loc logger.Loc) {
order := p.scopesInOrder[0]
// Sanity-check that the scopes generated by the first and second passes match
if order.loc != loc || order.scope.Kind != kind {
panic(fmt.Sprintf("Expected scope (%d, %d) in %q, found scope (%d, %d)",
kind, loc.Start,
p.source.PrettyPaths.Select(p.options.logPathStyle),
order.scope.Kind, order.loc.Start))
}
p.scopesInOrder = p.scopesInOrder[1:]
p.currentScope = order.scope
if part := p.currentPart; part != nil {
part.Scopes = append(part.Scopes, order.scope)
}
}
type findSymbolResult struct {
ref ast.Ref
declareLoc logger.Loc
isInsideWithScope bool
}
func (p *parser) findSymbol(loc logger.Loc, name string) findSymbolResult {
var ref ast.Ref
var declareLoc logger.Loc
isInsideWithScope := false
didForbidArguments := false
s := p.currentScope
for {
// Track if we're inside a "with" statement body
if s.Kind == js_ast.ScopeWith {
isInsideWithScope = true
}
// Forbid referencing "arguments" inside class bodies
if s.ForbidArguments && name == "arguments" && !didForbidArguments {
r := js_lexer.RangeOfIdentifier(p.source, loc)
p.log.AddError(&p.tracker, r, fmt.Sprintf("Cannot access %q here:", name))
didForbidArguments = true
}
// Is the symbol a member of this scope?
if member, ok := s.Members[name]; ok {
ref = member.Ref
declareLoc = member.Loc
break
}
// Is the symbol a member of this scope's TypeScript namespace?
if tsNamespace := s.TSNamespace; tsNamespace != nil {
if member, ok := tsNamespace.ExportedMembers[name]; ok && tsNamespace.IsEnumScope == member.IsEnumValue {
// If this is an identifier from a sibling TypeScript namespace, then we're
// going to have to generate a property access instead of a simple reference.
// Lazily-generate an identifier that represents this property access.
cache := tsNamespace.LazilyGeneratedProperyAccesses
if cache == nil {
cache = make(map[string]ast.Ref)
tsNamespace.LazilyGeneratedProperyAccesses = cache
}
ref, ok = cache[name]
if !ok {
ref = p.newSymbol(ast.SymbolOther, name)
p.symbols[ref.InnerIndex].NamespaceAlias = &ast.NamespaceAlias{
NamespaceRef: tsNamespace.ArgRef,
Alias: name,
}
cache[name] = ref
}
declareLoc = member.Loc
break
}
}
s = s.Parent
if s == nil {
// Allocate an "unbound" symbol
p.checkForUnrepresentableIdentifier(loc, name)
ref = p.newSymbol(ast.SymbolUnbound, name)
declareLoc = loc
p.moduleScope.Members[name] = js_ast.ScopeMember{Ref: ref, Loc: logger.Loc{Start: -1}}
break
}
}
// If we had to pass through a "with" statement body to get to the symbol
// declaration, then this reference could potentially also refer to a
// property on the target object of the "with" statement. We must not rename
// it or we risk changing the behavior of the code.
if isInsideWithScope {
p.symbols[ref.InnerIndex].Flags |= ast.MustNotBeRenamed
}
// Track how many times we've referenced this symbol
p.recordUsage(ref)
return findSymbolResult{ref, declareLoc, isInsideWithScope}
}
func (p *parser) findLabelSymbol(loc logger.Loc, name string) (ref ast.Ref, isLoop bool, ok bool) {
for s := p.currentScope; s != nil && !s.Kind.StopsHoisting(); s = s.Parent {
if s.Kind == js_ast.ScopeLabel && name == p.symbols[s.Label.Ref.InnerIndex].OriginalName {
// Track how many times we've referenced this symbol
p.recordUsage(s.Label.Ref)
ref = s.Label.Ref
isLoop = s.LabelStmtIsLoop
ok = true
return
}
}
r := js_lexer.RangeOfIdentifier(p.source, loc)
p.log.AddError(&p.tracker, r, fmt.Sprintf("There is no containing label named %q", name))
// Allocate an "unbound" symbol
ref = p.newSymbol(ast.SymbolUnbound, name)
// Track how many times we've referenced this symbol
p.recordUsage(ref)
return
}
func findIdentifiers(binding js_ast.Binding, identifiers []js_ast.Decl) []js_ast.Decl {
switch b := binding.Data.(type) {
case *js_ast.BIdentifier:
identifiers = append(identifiers, js_ast.Decl{Binding: binding})
case *js_ast.BArray:
for _, item := range b.Items {
identifiers = findIdentifiers(item.Binding, identifiers)
}
case *js_ast.BObject:
for _, property := range b.Properties {
identifiers = findIdentifiers(property.Value, identifiers)
}
}
return identifiers
}
func shouldKeepStmtsInDeadControlFlow(stmts []js_ast.Stmt) bool {
for _, child := range stmts {
if shouldKeepStmtInDeadControlFlow(child) {
return true
}
}
return false
}
// If this is in a dead branch, then we want to trim as much dead code as we
// can. Everything can be trimmed except for hoisted declarations ("var" and
// "function"), which affect the parent scope. For example:
//
// function foo() {
// if (false) { var x; }
// x = 1;
// }
//
// We can't trim the entire branch as dead or calling foo() will incorrectly
// assign to a global variable instead.
func shouldKeepStmtInDeadControlFlow(stmt js_ast.Stmt) bool {
switch s := stmt.Data.(type) {
case *js_ast.SEmpty, *js_ast.SExpr, *js_ast.SThrow, *js_ast.SReturn,
*js_ast.SBreak, *js_ast.SContinue, *js_ast.SClass, *js_ast.SDebugger:
// Omit these statements entirely
return false
case *js_ast.SLocal:
if s.Kind != js_ast.LocalVar {
// Omit these statements entirely
return false
}
// Omit everything except the identifiers
identifiers := []js_ast.Decl{}
for _, decl := range s.Decls {
identifiers = findIdentifiers(decl.Binding, identifiers)
}
if len(identifiers) == 0 {
return false
}
s.Decls = identifiers
return true
case *js_ast.SBlock:
return shouldKeepStmtsInDeadControlFlow(s.Stmts)
case *js_ast.STry:
return shouldKeepStmtsInDeadControlFlow(s.Block.Stmts) ||
(s.Catch != nil && shouldKeepStmtsInDeadControlFlow(s.Catch.Block.Stmts)) ||
(s.Finally != nil && shouldKeepStmtsInDeadControlFlow(s.Finally.Block.Stmts))
case *js_ast.SIf:
return shouldKeepStmtInDeadControlFlow(s.Yes) || (s.NoOrNil.Data != nil && shouldKeepStmtInDeadControlFlow(s.NoOrNil))
case *js_ast.SWhile:
return shouldKeepStmtInDeadControlFlow(s.Body)
case *js_ast.SDoWhile:
return shouldKeepStmtInDeadControlFlow(s.Body)
case *js_ast.SFor:
return (s.InitOrNil.Data != nil && shouldKeepStmtInDeadControlFlow(s.InitOrNil)) || shouldKeepStmtInDeadControlFlow(s.Body)
case *js_ast.SForIn:
return shouldKeepStmtInDeadControlFlow(s.Init) || shouldKeepStmtInDeadControlFlow(s.Body)
case *js_ast.SForOf:
return shouldKeepStmtInDeadControlFlow(s.Init) || shouldKeepStmtInDeadControlFlow(s.Body)
case *js_ast.SLabel:
return shouldKeepStmtInDeadControlFlow(s.Stmt)
default:
// Everything else must be kept
return true
}
}
type prependTempRefsOpts struct {
fnBodyLoc *logger.Loc
kind stmtsKind
}
func (p *parser) visitStmtsAndPrependTempRefs(stmts []js_ast.Stmt, opts prependTempRefsOpts) []js_ast.Stmt {
oldTempRefs := p.tempRefsToDeclare
oldTempRefCount := p.tempRefCount
p.tempRefsToDeclare = nil
p.tempRefCount = 0
stmts = p.visitStmts(stmts, opts.kind)
// Prepend values for "this" and "arguments"
if opts.fnBodyLoc != nil {
// Capture "this"
if ref := p.fnOnlyDataVisit.thisCaptureRef; ref != nil {
p.tempRefsToDeclare = append(p.tempRefsToDeclare, tempRef{
ref: *ref,
valueOrNil: js_ast.Expr{Loc: *opts.fnBodyLoc, Data: js_ast.EThisShared},
})
p.currentScope.Generated = append(p.currentScope.Generated, *ref)
}
// Capture "arguments"
if ref := p.fnOnlyDataVisit.argumentsCaptureRef; ref != nil {
p.tempRefsToDeclare = append(p.tempRefsToDeclare, tempRef{
ref: *ref,
valueOrNil: js_ast.Expr{Loc: *opts.fnBodyLoc, Data: &js_ast.EIdentifier{Ref: *p.fnOnlyDataVisit.argumentsRef}},
})
p.currentScope.Generated = append(p.currentScope.Generated, *ref)
}
}
// There may also be special top-level-only temporaries to declare
if p.currentScope == p.moduleScope && p.topLevelTempRefsToDeclare != nil {
p.tempRefsToDeclare = append(p.tempRefsToDeclare, p.topLevelTempRefsToDeclare...)
p.topLevelTempRefsToDeclare = nil
}
// Prepend the generated temporary variables to the beginning of the statement list
decls := []js_ast.Decl{}
for _, temp := range p.tempRefsToDeclare {
if p.symbols[temp.ref.InnerIndex].UseCountEstimate > 0 {
decls = append(decls, js_ast.Decl{Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: temp.ref}}, ValueOrNil: temp.valueOrNil})
p.recordDeclaredSymbol(temp.ref)
}
}
if len(decls) > 0 {
// Skip past leading directives and comments
split := 0
for split < len(stmts) {
switch stmts[split].Data.(type) {
case *js_ast.SComment, *js_ast.SDirective:
split++
continue
}
break
}
stmts = append(
append(
append(
[]js_ast.Stmt{},
stmts[:split]...),
js_ast.Stmt{Data: &js_ast.SLocal{Kind: js_ast.LocalVar, Decls: decls}}),
stmts[split:]...)
}
p.tempRefsToDeclare = oldTempRefs
p.tempRefCount = oldTempRefCount
return stmts
}
type stmtsKind uint8
const (
stmtsNormal stmtsKind = iota
stmtsLoopBody
stmtsFnBody
)
func (p *parser) visitStmts(stmts []js_ast.Stmt, kind stmtsKind) []js_ast.Stmt {
// Save the current control-flow liveness. This represents if we are
// currently inside an "if (false) { ... }" block.
oldIsControlFlowDead := p.isControlFlowDead
oldTempLetsToDeclare := p.tempLetsToDeclare
p.tempLetsToDeclare = nil
// Visit all statements first
visited := make([]js_ast.Stmt, 0, len(stmts))
var before []js_ast.Stmt
var after []js_ast.Stmt
var preprocessedEnums map[int][]js_ast.Stmt
if p.scopesInOrderForEnum != nil {
// Preprocess TypeScript enums to improve code generation. Otherwise
// uses of an enum before that enum has been declared won't be inlined:
//
// console.log(Foo.FOO) // We want "FOO" to be inlined here
// const enum Foo { FOO = 0 }
//
// The TypeScript compiler itself contains code with this pattern, so
// it's important to implement this optimization.
for i, stmt := range stmts {
if _, ok := stmt.Data.(*js_ast.SEnum); ok {
if preprocessedEnums == nil {
preprocessedEnums = make(map[int][]js_ast.Stmt)
}
oldScopesInOrder := p.scopesInOrder
p.scopesInOrder = p.scopesInOrderForEnum[stmt.Loc]
preprocessedEnums[i] = p.visitAndAppendStmt(nil, stmt)
p.scopesInOrder = oldScopesInOrder
}
}
}
for i, stmt := range stmts {
switch s := stmt.Data.(type) {
case *js_ast.SExportEquals:
// TypeScript "export = value;" becomes "module.exports = value;". This
// must happen at the end after everything is parsed because TypeScript
// moves this statement to the end when it generates code.
after = p.visitAndAppendStmt(after, stmt)
continue
case *js_ast.SFunction:
// Manually hoist block-level function declarations to preserve semantics.
// This is only done for function declarations that are not generators
// or async functions, since this is a backwards-compatibility hack from
// Annex B of the JavaScript standard.
if !p.currentScope.Kind.StopsHoisting() && p.symbols[int(s.Fn.Name.Ref.InnerIndex)].Kind == ast.SymbolHoistedFunction {
before = p.visitAndAppendStmt(before, stmt)
continue
}
case *js_ast.SEnum:
visited = append(visited, preprocessedEnums[i]...)
p.scopesInOrder = p.scopesInOrder[len(p.scopesInOrderForEnum[stmt.Loc]):]
continue
}
visited = p.visitAndAppendStmt(visited, stmt)
}
// This is used for temporary variables that could be captured in a closure,
// and therefore need to be generated inside the nearest enclosing block in
// case they are generated inside a loop.
if len(p.tempLetsToDeclare) > 0 {
decls := make([]js_ast.Decl, 0, len(p.tempLetsToDeclare))
for _, ref := range p.tempLetsToDeclare {
decls = append(decls, js_ast.Decl{Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: ref}}})
}
before = append(before, js_ast.Stmt{Data: &js_ast.SLocal{Kind: js_ast.LocalLet, Decls: decls}})
}
p.tempLetsToDeclare = oldTempLetsToDeclare
// Transform block-level function declarations into variable declarations
if len(before) > 0 {
var letDecls []js_ast.Decl
var varDecls []js_ast.Decl
var nonFnStmts []js_ast.Stmt
fnStmts := make(map[ast.Ref]int)
for _, stmt := range before {
s, ok := stmt.Data.(*js_ast.SFunction)
if !ok {
// We may get non-function statements here in certain scenarios such as when "KeepNames" is enabled
nonFnStmts = append(nonFnStmts, stmt)
continue
}
// This transformation of function declarations in nested scopes is
// intended to preserve the hoisting semantics of the original code. In
// JavaScript, function hoisting works differently in strict mode vs.
// sloppy mode code. We want the code we generate to use the semantics of
// the original environment, not the generated environment. However, if
// direct "eval" is present then it's not possible to preserve the
// semantics because we need two identifiers to do that and direct "eval"
// means neither identifier can be renamed to something else. So in that
// case we give up and do not preserve the semantics of the original code.
if p.currentScope.ContainsDirectEval {
if hoistedRef, ok := p.hoistedRefForSloppyModeBlockFn[s.Fn.Name.Ref]; ok {
// Merge the two identifiers back into a single one
p.symbols[hoistedRef.InnerIndex].Link = s.Fn.Name.Ref
}
nonFnStmts = append(nonFnStmts, stmt)
continue
}
index, ok := fnStmts[s.Fn.Name.Ref]
if !ok {
index = len(letDecls)
fnStmts[s.Fn.Name.Ref] = index
letDecls = append(letDecls, js_ast.Decl{Binding: js_ast.Binding{
Loc: s.Fn.Name.Loc, Data: &js_ast.BIdentifier{Ref: s.Fn.Name.Ref}}})
// Also write the function to the hoisted sibling symbol if applicable
if hoistedRef, ok := p.hoistedRefForSloppyModeBlockFn[s.Fn.Name.Ref]; ok {
p.recordDeclaredSymbol(hoistedRef)
p.recordUsage(s.Fn.Name.Ref)
varDecls = append(varDecls, js_ast.Decl{
Binding: js_ast.Binding{Loc: s.Fn.Name.Loc, Data: &js_ast.BIdentifier{Ref: hoistedRef}},
ValueOrNil: js_ast.Expr{Loc: s.Fn.Name.Loc, Data: &js_ast.EIdentifier{Ref: s.Fn.Name.Ref}},
})
}
}
// The last function statement for a given symbol wins
s.Fn.Name = nil
letDecls[index].ValueOrNil = js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EFunction{Fn: s.Fn}}
}
// Reuse memory from "before"
before = before[:0]
kind := js_ast.LocalLet
if p.options.unsupportedJSFeatures.Has(compat.ConstAndLet) {
kind = js_ast.LocalVar
}
if len(letDecls) > 0 {
before = append(before, js_ast.Stmt{Loc: letDecls[0].ValueOrNil.Loc, Data: &js_ast.SLocal{Kind: kind, Decls: letDecls}})
}
if len(varDecls) > 0 {
// Potentially relocate "var" declarations to the top level
if assign, ok := p.maybeRelocateVarsToTopLevel(varDecls, relocateVarsNormal); ok {
if assign.Data != nil {
before = append(before, assign)
}
} else {
before = append(before, js_ast.Stmt{Loc: varDecls[0].ValueOrNil.Loc, Data: &js_ast.SLocal{Kind: js_ast.LocalVar, Decls: varDecls}})
}
}
before = append(before, nonFnStmts...)
visited = append(before, visited...)
}
// Move TypeScript "export =" statements to the end
visited = append(visited, after...)
// Restore the current control-flow liveness if it was changed inside the
// loop above. This is important because the caller will not restore it.
p.isControlFlowDead = oldIsControlFlowDead
// Lower using declarations
if p.shouldLowerUsingDeclarations(visited) {
ctx := p.lowerUsingDeclarationContext()
ctx.scanStmts(p, visited)
visited = ctx.finalize(p, visited, p.currentScope.Parent == nil)
}
// Stop now if we're not mangling
if !p.options.minifySyntax {
return visited
}
// If this is in a dead branch, trim as much dead code as we can
if p.isControlFlowDead {
end := 0
for _, stmt := range visited {
if !shouldKeepStmtInDeadControlFlow(stmt) {
continue
}
// Merge adjacent var statements
if s, ok := stmt.Data.(*js_ast.SLocal); ok && s.Kind == js_ast.LocalVar && end > 0 {
prevStmt := visited[end-1]
if prevS, ok := prevStmt.Data.(*js_ast.SLocal); ok && prevS.Kind == js_ast.LocalVar && s.IsExport == prevS.IsExport {
prevS.Decls = append(prevS.Decls, s.Decls...)
continue
}
}
visited[end] = stmt
end++
}
return visited[:end]
}
return p.mangleStmts(visited, kind)
}
func (p *parser) mangleStmts(stmts []js_ast.Stmt, kind stmtsKind) []js_ast.Stmt {
// Remove inlined constants now that we know whether any of these statements
// contained a direct eval() or not. This can't be done earlier when we
// encounter the constant because we haven't encountered the eval() yet.
// Inlined constants are not removed if they are in a top-level scope or
// if they are exported (which could be in a nested TypeScript namespace).
if p.currentScope.Parent != nil && !p.currentScope.ContainsDirectEval {
for i, stmt := range stmts {
switch s := stmt.Data.(type) {
case *js_ast.SEmpty, *js_ast.SComment, *js_ast.SDirective, *js_ast.SDebugger, *js_ast.STypeScript:
continue
case *js_ast.SLocal:
if !s.IsExport {
end := 0
for _, d := range s.Decls {
if id, ok := d.Binding.Data.(*js_ast.BIdentifier); ok {
if _, ok := p.constValues[id.Ref]; ok && p.symbols[id.Ref.InnerIndex].UseCountEstimate == 0 {
continue
}
}
s.Decls[end] = d
end++
}
if end == 0 {
stmts[i].Data = js_ast.SEmptyShared
} else {
s.Decls = s.Decls[:end]
}
}
continue
}
break
}
}
// Merge adjacent statements during mangling
result := make([]js_ast.Stmt, 0, len(stmts))
isControlFlowDead := false
for i, stmt := range stmts {
if isControlFlowDead && !shouldKeepStmtInDeadControlFlow(stmt) {
// Strip unnecessary statements if the control flow is dead here
continue
}
// Inline single-use variable declarations where possible:
//
// // Before
// let x = fn();
// return x.y();
//
// // After
// return fn().y();
//
// The declaration must not be exported. We can't just check for the
// "export" keyword because something might do "export {id};" later on.
// Instead we just ignore all top-level declarations for now. That means
// this optimization currently only applies in nested scopes.
//
// Ignore declarations if the scope is shadowed by a direct "eval" call.
// The eval'd code may indirectly reference this symbol and the actual
// use count may be greater than 1.
if p.currentScope != p.moduleScope && !p.currentScope.ContainsDirectEval {
// Keep inlining variables until a failure or until there are none left.
// That handles cases like this:
//
// // Before
// let x = fn();
// let y = x.prop;
// return y;
//
// // After
// return fn().prop;
//
for len(result) > 0 {
// Ignore "var" declarations since those have function-level scope and
// we may not have visited all of their uses yet by this point. We
// should have visited all the uses of "let" and "const" declarations
// by now since they are scoped to this block which we just finished
// visiting.
if prevS, ok := result[len(result)-1].Data.(*js_ast.SLocal); ok && (prevS.Kind == js_ast.LocalLet || prevS.Kind == js_ast.LocalConst) {
last := prevS.Decls[len(prevS.Decls)-1]
// The binding must be an identifier that is only used once.
// Ignore destructuring bindings since that's not the simple case.
// Destructuring bindings could potentially execute side-effecting
// code which would invalidate reordering.
if id, ok := last.Binding.Data.(*js_ast.BIdentifier); ok {
// Don't do this if "__name" was called on this symbol. In that
// case there is actually more than one use even though it says
// there is only one. The "__name" use isn't counted so that
// tree shaking still works when names are kept.
if symbol := p.symbols[id.Ref.InnerIndex]; symbol.UseCountEstimate == 1 && !symbol.Flags.Has(ast.DidKeepName) {
replacement := last.ValueOrNil
// The variable must be initialized, since we will be substituting
// the value into the usage.
if replacement.Data == nil {
replacement = js_ast.Expr{Loc: last.Binding.Loc, Data: js_ast.EUndefinedShared}
}
// Try to substitute the identifier with the initializer. This will
// fail if something with side effects is in between the declaration
// and the usage.
if p.substituteSingleUseSymbolInStmt(stmt, id.Ref, replacement) {
// Remove the previous declaration, since the substitution was
// successful.
if len(prevS.Decls) == 1 {
result = result[:len(result)-1]
} else {
prevS.Decls = prevS.Decls[:len(prevS.Decls)-1]
}
// Loop back to try again
continue
}
}
}
}
// Substitution failed so stop trying
break
}
}
switch s := stmt.Data.(type) {
case *js_ast.SEmpty:
// Strip empty statements
continue
case *js_ast.SLocal:
// Merge adjacent local statements
if len(result) > 0 {
prevStmt := result[len(result)-1]
if prevS, ok := prevStmt.Data.(*js_ast.SLocal); ok && s.Kind == prevS.Kind && s.IsExport == prevS.IsExport {
prevS.Decls = append(prevS.Decls, s.Decls...)
continue
}
}
case *js_ast.SExpr:
// Trim expressions without side effects
s.Value = p.astHelpers.SimplifyUnusedExpr(s.Value, p.options.unsupportedJSFeatures)
if s.Value.Data == nil {
continue
}
// Merge adjacent expression statements
if len(result) > 0 {
prevStmt := result[len(result)-1]
if prevS, ok := prevStmt.Data.(*js_ast.SExpr); ok {
if !s.IsFromClassOrFnThatCanBeRemovedIfUnused {
prevS.IsFromClassOrFnThatCanBeRemovedIfUnused = false
}
prevS.Value = js_ast.JoinWithComma(prevS.Value, s.Value)
continue
}
}
case *js_ast.SSwitch:
// Absorb a previous expression statement
if len(result) > 0 {
prevStmt := result[len(result)-1]
if prevS, ok := prevStmt.Data.(*js_ast.SExpr); ok {
s.Test = js_ast.JoinWithComma(prevS.Value, s.Test)
result = result[:len(result)-1]
}
}
case *js_ast.SIf:
// Absorb a previous expression statement
if len(result) > 0 {
prevStmt := result[len(result)-1]
if prevS, ok := prevStmt.Data.(*js_ast.SExpr); ok {
s.Test = js_ast.JoinWithComma(prevS.Value, s.Test)
result = result[:len(result)-1]
}
}
if isJumpStatement(s.Yes.Data) {
optimizeImplicitJump := false
// Absorb a previous if statement
if len(result) > 0 {
prevStmt := result[len(result)-1]
if prevS, ok := prevStmt.Data.(*js_ast.SIf); ok && prevS.NoOrNil.Data == nil && jumpStmtsLookTheSame(prevS.Yes.Data, s.Yes.Data) {
// "if (a) break c; if (b) break c;" => "if (a || b) break c;"
// "if (a) continue c; if (b) continue c;" => "if (a || b) continue c;"
// "if (a) return c; if (b) return c;" => "if (a || b) return c;"
// "if (a) throw c; if (b) throw c;" => "if (a || b) throw c;"
s.Test = js_ast.JoinWithLeftAssociativeOp(js_ast.BinOpLogicalOr, prevS.Test, s.Test)
result = result[:len(result)-1]
}
}
// "while (x) { if (y) continue; z(); }" => "while (x) { if (!y) z(); }"
// "while (x) { if (y) continue; else z(); w(); }" => "while (x) { if (!y) { z(); w(); } }" => "for (; x;) !y && (z(), w());"
if kind == stmtsLoopBody {
if continueS, ok := s.Yes.Data.(*js_ast.SContinue); ok && continueS.Label == nil {
optimizeImplicitJump = true
}
}
// "let x = () => { if (y) return; z(); };" => "let x = () => { if (!y) z(); };"
// "let x = () => { if (y) return; else z(); w(); };" => "let x = () => { if (!y) { z(); w(); } };" => "let x = () => { !y && (z(), w()); };"
if kind == stmtsFnBody {
if returnS, ok := s.Yes.Data.(*js_ast.SReturn); ok && returnS.ValueOrNil.Data == nil {
optimizeImplicitJump = true
}
}
if optimizeImplicitJump {
var body []js_ast.Stmt
if s.NoOrNil.Data != nil {
body = append(body, s.NoOrNil)
}
body = append(body, stmts[i+1:]...)
// Don't do this transformation if the branch condition could
// potentially access symbols declared later on this scope below.
// If so, inverting the branch condition and nesting statements after
// this in a block would break that access which is a behavior change.
//
// // This transformation is incorrect
// if (a()) return; function a() {}
// if (!a()) { function a() {} }
//
// // This transformation is incorrect
// if (a(() => b)) return; let b;
// if (a(() => b)) { let b; }
//
if !stmtsCareAboutScope(body) {
body = p.mangleStmts(body, kind)
bodyLoc := s.Yes.Loc
if len(body) > 0 {
bodyLoc = body[0].Loc
}
return p.mangleIf(result, stmt.Loc, &js_ast.SIf{
Test: p.astHelpers.SimplifyBooleanExpr(js_ast.Not(s.Test)),
Yes: stmtsToSingleStmt(bodyLoc, body, logger.Loc{}),
})
}
}
if s.NoOrNil.Data != nil {
// "if (a) return b; else if (c) return d; else return e;" => "if (a) return b; if (c) return d; return e;"
for {
result = append(result, stmt)
stmt = s.NoOrNil
s.NoOrNil = js_ast.Stmt{}
var ok bool
s, ok = stmt.Data.(*js_ast.SIf)
if !ok || !isJumpStatement(s.Yes.Data) || s.NoOrNil.Data == nil {
break
}
}
result = appendIfOrLabelBodyPreservingScope(result, stmt)
if isJumpStatement(stmt.Data) {
isControlFlowDead = true
}
continue
}
}
case *js_ast.SReturn:
// Merge return statements with the previous expression statement
if len(result) > 0 && s.ValueOrNil.Data != nil {
prevStmt := result[len(result)-1]
if prevS, ok := prevStmt.Data.(*js_ast.SExpr); ok {
result[len(result)-1] = js_ast.Stmt{Loc: prevStmt.Loc,
Data: &js_ast.SReturn{ValueOrNil: js_ast.JoinWithComma(prevS.Value, s.ValueOrNil)}}
continue
}
}
isControlFlowDead = true
case *js_ast.SThrow:
// Merge throw statements with the previous expression statement
if len(result) > 0 {
prevStmt := result[len(result)-1]
if prevS, ok := prevStmt.Data.(*js_ast.SExpr); ok {
result[len(result)-1] = js_ast.Stmt{Loc: prevStmt.Loc, Data: &js_ast.SThrow{Value: js_ast.JoinWithComma(prevS.Value, s.Value)}}
continue
}
}
isControlFlowDead = true
case *js_ast.SBreak, *js_ast.SContinue:
isControlFlowDead = true
case *js_ast.SFor:
if len(result) > 0 {
prevStmt := result[len(result)-1]
if prevS, ok := prevStmt.Data.(*js_ast.SExpr); ok {
// Insert the previous expression into the for loop initializer
if s.InitOrNil.Data == nil {
result[len(result)-1] = stmt
s.InitOrNil = js_ast.Stmt{Loc: prevStmt.Loc, Data: &js_ast.SExpr{Value: prevS.Value}}
continue
} else if s2, ok := s.InitOrNil.Data.(*js_ast.SExpr); ok {
result[len(result)-1] = stmt
s.InitOrNil = js_ast.Stmt{Loc: prevStmt.Loc, Data: &js_ast.SExpr{Value: js_ast.JoinWithComma(prevS.Value, s2.Value)}}
continue
}
} else {
// Insert the previous variable declaration into the for loop
// initializer if it's a "var" declaration, since the scope
// doesn't matter due to scope hoisting
if s.InitOrNil.Data == nil {
if s2, ok := prevStmt.Data.(*js_ast.SLocal); ok && s2.Kind == js_ast.LocalVar && !s2.IsExport {
result[len(result)-1] = stmt
s.InitOrNil = prevStmt
continue
}
} else {
if s2, ok := prevStmt.Data.(*js_ast.SLocal); ok && s2.Kind == js_ast.LocalVar && !s2.IsExport {
if s3, ok := s.InitOrNil.Data.(*js_ast.SLocal); ok && s3.Kind == js_ast.LocalVar {
result[len(result)-1] = stmt
s.InitOrNil.Data = &js_ast.SLocal{Kind: js_ast.LocalVar, Decls: append(s2.Decls, s3.Decls...)}
continue
}
}
}
}
}
case *js_ast.STry:
// Drop an unused identifier binding if the optional catch binding feature is supported
if !p.options.unsupportedJSFeatures.Has(compat.OptionalCatchBinding) && s.Catch != nil {
if id, ok := s.Catch.BindingOrNil.Data.(*js_ast.BIdentifier); ok {
if symbol := p.symbols[id.Ref.InnerIndex]; symbol.UseCountEstimate == 0 {
if symbol.Link != ast.InvalidRef {
// We cannot transform "try { x() } catch (y) { var y = 1 }" into
// "try { x() } catch { var y = 1 }" even though "y" is never used
// because the hoisted variable "y" would have different values
// after the statement ends due to a strange JavaScript quirk:
//
// try { x() } catch (y) { var y = 1 }
// console.log(y) // undefined
//
// try { x() } catch { var y = 1 }
// console.log(y) // 1
//
} else if p.currentScope.ContainsDirectEval {
// We cannot transform "try { x() } catch (y) { eval('z = y') }"
// into "try { x() } catch { eval('z = y') }" because the variable
// "y" is actually still used.
} else {
// "try { x() } catch (y) {}" => "try { x() } catch {}"
s.Catch.BindingOrNil.Data = nil
}
}
}
}
}
result = append(result, stmt)
}
// Drop a trailing unconditional jump statement if applicable
if len(result) > 0 {
switch kind {
case stmtsLoopBody:
// "while (x) { y(); continue; }" => "while (x) { y(); }"
if continueS, ok := result[len(result)-1].Data.(*js_ast.SContinue); ok && continueS.Label == nil {
result = result[:len(result)-1]
}
case stmtsFnBody:
if returnS, ok := result[len(result)-1].Data.(*js_ast.SReturn); ok {
if returnS.ValueOrNil.Data == nil {
// "function f() { x(); return; }" => "function f() { x(); }"
result = result[:len(result)-1]
} else if unary, ok := returnS.ValueOrNil.Data.(*js_ast.EUnary); ok && unary.Op == js_ast.UnOpVoid {
// "function f() { return void x(); }" => "function f() { x(); }"
result[len(result)-1].Data = &js_ast.SExpr{Value: unary.Value}
}
}
}
}
// Merge certain statements in reverse order
if len(result) >= 2 {
lastStmt := result[len(result)-1]
if lastReturn, ok := lastStmt.Data.(*js_ast.SReturn); ok {
// "if (a) return b; if (c) return d; return e;" => "return a ? b : c ? d : e;"
returnLoop:
for len(result) >= 2 {
prevIndex := len(result) - 2
prevStmt := result[prevIndex]
switch prevS := prevStmt.Data.(type) {
case *js_ast.SExpr:
// This return statement must have a value
if lastReturn.ValueOrNil.Data == nil {
break returnLoop
}
// "a(); return b;" => "return a(), b;"
lastReturn = &js_ast.SReturn{ValueOrNil: js_ast.JoinWithComma(prevS.Value, lastReturn.ValueOrNil)}
// Merge the last two statements
lastStmt = js_ast.Stmt{Loc: prevStmt.Loc, Data: lastReturn}
result[prevIndex] = lastStmt
result = result[:len(result)-1]
case *js_ast.SIf:
// The previous statement must be an if statement with no else clause
if prevS.NoOrNil.Data != nil {
break returnLoop
}
// The then clause must be a return
prevReturn, ok := prevS.Yes.Data.(*js_ast.SReturn)
if !ok {
break returnLoop
}
// Handle some or all of the values being undefined
left := prevReturn.ValueOrNil
right := lastReturn.ValueOrNil
if left.Data == nil {
// "if (a) return; return b;" => "return a ? void 0 : b;"
left = js_ast.Expr{Loc: prevS.Yes.Loc, Data: js_ast.EUndefinedShared}
}
if right.Data == nil {
// "if (a) return a; return;" => "return a ? b : void 0;"
right = js_ast.Expr{Loc: lastStmt.Loc, Data: js_ast.EUndefinedShared}
}
// "if (!a) return b; return c;" => "return a ? c : b;"
if not, ok := prevS.Test.Data.(*js_ast.EUnary); ok && not.Op == js_ast.UnOpNot {
prevS.Test = not.Value
left, right = right, left
}
if comma, ok := prevS.Test.Data.(*js_ast.EBinary); ok && comma.Op == js_ast.BinOpComma {
// "if (a, b) return c; return d;" => "return a, b ? c : d;"
lastReturn = &js_ast.SReturn{ValueOrNil: js_ast.JoinWithComma(comma.Left,
p.astHelpers.MangleIfExpr(comma.Right.Loc, &js_ast.EIf{Test: comma.Right, Yes: left, No: right}, p.options.unsupportedJSFeatures))}
} else {
// "if (a) return b; return c;" => "return a ? b : c;"
lastReturn = &js_ast.SReturn{ValueOrNil: p.astHelpers.MangleIfExpr(
prevS.Test.Loc, &js_ast.EIf{Test: prevS.Test, Yes: left, No: right}, p.options.unsupportedJSFeatures)}
}
// Merge the last two statements
lastStmt = js_ast.Stmt{Loc: prevStmt.Loc, Data: lastReturn}
result[prevIndex] = lastStmt
result = result[:len(result)-1]
default:
break returnLoop
}
}
} else if lastThrow, ok := lastStmt.Data.(*js_ast.SThrow); ok {
// "if (a) throw b; if (c) throw d; throw e;" => "throw a ? b : c ? d : e;"
throwLoop:
for len(result) >= 2 {
prevIndex := len(result) - 2
prevStmt := result[prevIndex]
switch prevS := prevStmt.Data.(type) {
case *js_ast.SExpr:
// "a(); throw b;" => "throw a(), b;"
lastThrow = &js_ast.SThrow{Value: js_ast.JoinWithComma(prevS.Value, lastThrow.Value)}
// Merge the last two statements
lastStmt = js_ast.Stmt{Loc: prevStmt.Loc, Data: lastThrow}
result[prevIndex] = lastStmt
result = result[:len(result)-1]
case *js_ast.SIf:
// The previous statement must be an if statement with no else clause
if prevS.NoOrNil.Data != nil {
break throwLoop
}
// The then clause must be a throw
prevThrow, ok := prevS.Yes.Data.(*js_ast.SThrow)
if !ok {
break throwLoop
}
left := prevThrow.Value
right := lastThrow.Value
// "if (!a) throw b; throw c;" => "throw a ? c : b;"
if not, ok := prevS.Test.Data.(*js_ast.EUnary); ok && not.Op == js_ast.UnOpNot {
prevS.Test = not.Value
left, right = right, left
}
// Merge the last two statements
if comma, ok := prevS.Test.Data.(*js_ast.EBinary); ok && comma.Op == js_ast.BinOpComma {
// "if (a, b) return c; return d;" => "return a, b ? c : d;"
lastThrow = &js_ast.SThrow{Value: js_ast.JoinWithComma(comma.Left,
p.astHelpers.MangleIfExpr(comma.Right.Loc, &js_ast.EIf{Test: comma.Right, Yes: left, No: right}, p.options.unsupportedJSFeatures))}
} else {
// "if (a) return b; return c;" => "return a ? b : c;"
lastThrow = &js_ast.SThrow{
Value: p.astHelpers.MangleIfExpr(prevS.Test.Loc, &js_ast.EIf{Test: prevS.Test, Yes: left, No: right}, p.options.unsupportedJSFeatures)}
}
lastStmt = js_ast.Stmt{Loc: prevStmt.Loc, Data: lastThrow}
result[prevIndex] = lastStmt
result = result[:len(result)-1]
default:
break throwLoop
}
}
}
}
return result
}
func (p *parser) substituteSingleUseSymbolInStmt(stmt js_ast.Stmt, ref ast.Ref, replacement js_ast.Expr) bool {
var expr *js_ast.Expr
switch s := stmt.Data.(type) {
case *js_ast.SExpr:
expr = &s.Value
case *js_ast.SThrow:
expr = &s.Value
case *js_ast.SReturn:
expr = &s.ValueOrNil
case *js_ast.SIf:
expr = &s.Test
case *js_ast.SSwitch:
expr = &s.Test
case *js_ast.SLocal:
// Only try substituting into the initializer for the first declaration
if first := &s.Decls[0]; first.ValueOrNil.Data != nil {
// Make sure there isn't destructuring, which could evaluate code
if _, ok := first.Binding.Data.(*js_ast.BIdentifier); ok {
expr = &first.ValueOrNil
}
}
}
if expr != nil {
// Only continue trying to insert this replacement into sub-expressions
// after the first one if the replacement has no side effects:
//
// // Substitution is ok
// let replacement = 123;
// return x + replacement;
//
// // Substitution is not ok because "fn()" may change "x"
// let replacement = fn();
// return x + replacement;
//
// // Substitution is not ok because "x == x" may change "x" due to "valueOf()" evaluation
// let replacement = [x];
// return (x == x) + replacement;
//
replacementCanBeRemoved := p.astHelpers.ExprCanBeRemovedIfUnused(replacement)
if new, status := p.substituteSingleUseSymbolInExpr(*expr, ref, replacement, replacementCanBeRemoved); status == substituteSuccess {
*expr = new
return true
}
}
return false
}
type substituteStatus uint8
const (
substituteContinue substituteStatus = iota
substituteSuccess
substituteFailure
)
func (p *parser) substituteSingleUseSymbolInExpr(
expr js_ast.Expr,
ref ast.Ref,
replacement js_ast.Expr,
replacementCanBeRemoved bool,
) (js_ast.Expr, substituteStatus) {
switch e := expr.Data.(type) {
case *js_ast.EIdentifier:
if e.Ref == ref {
p.ignoreUsage(ref)
return replacement, substituteSuccess
}
case *js_ast.ESpread:
if value, status := p.substituteSingleUseSymbolInExpr(e.Value, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Value = value
return expr, status
}
case *js_ast.EAwait:
if value, status := p.substituteSingleUseSymbolInExpr(e.Value, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Value = value
return expr, status
}
case *js_ast.EYield:
if e.ValueOrNil.Data != nil {
if value, status := p.substituteSingleUseSymbolInExpr(e.ValueOrNil, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.ValueOrNil = value
return expr, status
}
}
case *js_ast.EImportCall:
if value, status := p.substituteSingleUseSymbolInExpr(e.Expr, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Expr = value
return expr, status
}
// The "import()" expression has side effects but the side effects are
// always asynchronous so there is no way for the side effects to modify
// the replacement value. So it's ok to reorder the replacement value
// past the "import()" expression assuming everything else checks out.
if replacementCanBeRemoved && p.astHelpers.ExprCanBeRemovedIfUnused(e.Expr) {
return expr, substituteContinue
}
case *js_ast.EUnary:
switch e.Op {
case js_ast.UnOpPreInc, js_ast.UnOpPostInc, js_ast.UnOpPreDec, js_ast.UnOpPostDec, js_ast.UnOpDelete:
// Do not substitute into an assignment position
default:
if value, status := p.substituteSingleUseSymbolInExpr(e.Value, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Value = value
return expr, status
}
}
case *js_ast.EDot:
if value, status := p.substituteSingleUseSymbolInExpr(e.Target, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Target = value
return expr, status
}
case *js_ast.EBinary:
// Do not substitute into an assignment position
if e.Op.BinaryAssignTarget() == js_ast.AssignTargetNone {
if value, status := p.substituteSingleUseSymbolInExpr(e.Left, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Left = value
return expr, status
}
} else if !p.astHelpers.ExprCanBeRemovedIfUnused(e.Left) {
// Do not reorder past a side effect in an assignment target, as that may
// change the replacement value. For example, "fn()" may change "a" here:
//
// let a = 1;
// foo[fn()] = a;
//
return expr, substituteFailure
} else if e.Op.BinaryAssignTarget() == js_ast.AssignTargetUpdate && !replacementCanBeRemoved {
// If this is a read-modify-write assignment and the replacement has side
// effects, don't reorder it past the assignment target. The assignment
// target is being read so it may be changed by the side effect. For
// example, "fn()" may change "foo" here:
//
// let a = fn();
// foo += a;
//
return expr, substituteFailure
}
// If we get here then it should be safe to attempt to substitute the
// replacement past the left operand into the right operand.
if value, status := p.substituteSingleUseSymbolInExpr(e.Right, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Right = value
return expr, status
}
case *js_ast.EIf:
if value, status := p.substituteSingleUseSymbolInExpr(e.Test, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Test = value
return expr, status
}
// Do not substitute our unconditionally-executed value into a branch
// unless the value itself has no side effects
if replacementCanBeRemoved {
// Unlike other branches in this function such as "a && b" or "a?.[b]",
// the "a ? b : c" form has potential code evaluation along both control
// flow paths. Handle this by allowing substitution into either branch.
// Side effects in one branch should not prevent the substitution into
// the other branch.
yesValue, yesStatus := p.substituteSingleUseSymbolInExpr(e.Yes, ref, replacement, replacementCanBeRemoved)
if yesStatus == substituteSuccess {
e.Yes = yesValue
return expr, yesStatus
}
noValue, noStatus := p.substituteSingleUseSymbolInExpr(e.No, ref, replacement, replacementCanBeRemoved)
if noStatus == substituteSuccess {
e.No = noValue
return expr, noStatus
}
// Side effects in either branch should stop us from continuing to try to
// substitute the replacement after the control flow branches merge again.
if yesStatus != substituteContinue || noStatus != substituteContinue {
return expr, substituteFailure
}
}
case *js_ast.EIndex:
if value, status := p.substituteSingleUseSymbolInExpr(e.Target, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Target = value
return expr, status
}
// Do not substitute our unconditionally-executed value into a branch
// unless the value itself has no side effects
if replacementCanBeRemoved || e.OptionalChain == js_ast.OptionalChainNone {
if value, status := p.substituteSingleUseSymbolInExpr(e.Index, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Index = value
return expr, status
}
}
case *js_ast.ECall:
// Don't substitute something into a call target that could change "this"
_, isDot := replacement.Data.(*js_ast.EDot)
_, isIndex := replacement.Data.(*js_ast.EIndex)
if isDot || isIndex {
if id, ok := e.Target.Data.(*js_ast.EIdentifier); ok && id.Ref == ref {
break
}
}
if value, status := p.substituteSingleUseSymbolInExpr(e.Target, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Target = value
if status == substituteSuccess {
// "const y = () => x; y()" => "(() => x)()" => "x"
if value, ok := p.maybeInlineIIFE(expr.Loc, e); ok {
return value, substituteSuccess
}
}
return expr, status
}
// Do not substitute our unconditionally-executed value into a branch
// unless the value itself has no side effects
if replacementCanBeRemoved || e.OptionalChain == js_ast.OptionalChainNone {
for i, arg := range e.Args {
if value, status := p.substituteSingleUseSymbolInExpr(arg, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Args[i] = value
return expr, status
}
}
}
case *js_ast.EArray:
for i, item := range e.Items {
if value, status := p.substituteSingleUseSymbolInExpr(item, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Items[i] = value
return expr, status
}
}
case *js_ast.EObject:
for i, property := range e.Properties {
// Check the key
if property.Flags.Has(js_ast.PropertyIsComputed) {
if value, status := p.substituteSingleUseSymbolInExpr(property.Key, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Properties[i].Key = value
return expr, status
}
// Stop now because both computed keys and property spread have side effects
return expr, substituteFailure
}
// Check the value
if property.ValueOrNil.Data != nil {
if value, status := p.substituteSingleUseSymbolInExpr(property.ValueOrNil, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Properties[i].ValueOrNil = value
return expr, status
}
}
}
case *js_ast.ETemplate:
if e.TagOrNil.Data != nil {
if value, status := p.substituteSingleUseSymbolInExpr(e.TagOrNil, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.TagOrNil = value
return expr, status
}
}
for i, part := range e.Parts {
if value, status := p.substituteSingleUseSymbolInExpr(part.Value, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Parts[i].Value = value
// If we substituted a primitive, merge it into the template
if js_ast.IsPrimitiveLiteral(value.Data) {
expr = js_ast.InlinePrimitivesIntoTemplate(expr.Loc, e)
}
return expr, status
}
}
}
// If both the replacement and this expression have no observable side
// effects, then we can reorder the replacement past this expression
if replacementCanBeRemoved && p.astHelpers.ExprCanBeRemovedIfUnused(expr) {
return expr, substituteContinue
}
// We can always reorder past primitive values
if js_ast.IsPrimitiveLiteral(expr.Data) || js_ast.IsPrimitiveLiteral(replacement.Data) {
return expr, substituteContinue
}
// Otherwise we should stop trying to substitute past this point
return expr, substituteFailure
}
func (p *parser) visitLoopBody(stmt js_ast.Stmt) js_ast.Stmt {
oldIsInsideLoop := p.fnOrArrowDataVisit.isInsideLoop
p.fnOrArrowDataVisit.isInsideLoop = true
p.loopBody = stmt.Data
stmt = p.visitSingleStmt(stmt, stmtsLoopBody)
p.fnOrArrowDataVisit.isInsideLoop = oldIsInsideLoop
return stmt
}
func (p *parser) visitSingleStmt(stmt js_ast.Stmt, kind stmtsKind) js_ast.Stmt {
// To reduce stack depth, special-case blocks and process their children directly
if block, ok := stmt.Data.(*js_ast.SBlock); ok {
p.pushScopeForVisitPass(js_ast.ScopeBlock, stmt.Loc)
block.Stmts = p.visitStmts(block.Stmts, kind)
p.popScope()
if p.options.minifySyntax {
stmt = stmtsToSingleStmt(stmt.Loc, block.Stmts, block.CloseBraceLoc)
}
return stmt
}
// Introduce a fake block scope for function declarations inside if statements
fn, ok := stmt.Data.(*js_ast.SFunction)
hasIfScope := ok && fn.Fn.HasIfScope
if hasIfScope {
p.pushScopeForVisitPass(js_ast.ScopeBlock, stmt.Loc)
if p.isStrictMode() {
p.markStrictModeFeature(ifElseFunctionStmt, js_lexer.RangeOfIdentifier(p.source, stmt.Loc), "")
}
}
p.singleStmtDepth++
stmts := p.visitStmts([]js_ast.Stmt{stmt}, kind)
p.singleStmtDepth--
// Balance the fake block scope introduced above
if hasIfScope {
p.popScope()
}
return stmtsToSingleStmt(stmt.Loc, stmts, logger.Loc{})
}
// One statement could potentially expand to several statements
func stmtsToSingleStmt(loc logger.Loc, stmts []js_ast.Stmt, closeBraceLoc logger.Loc) js_ast.Stmt {
if len(stmts) == 0 {
return js_ast.Stmt{Loc: loc, Data: js_ast.SEmptyShared}
}
if len(stmts) == 1 && !stmtCaresAboutScope(stmts[0]) {
return stmts[0]
}
return js_ast.Stmt{Loc: loc, Data: &js_ast.SBlock{Stmts: stmts, CloseBraceLoc: closeBraceLoc}}
}
func (p *parser) visitForLoopInit(stmt js_ast.Stmt, isInOrOf bool) js_ast.Stmt {
switch s := stmt.Data.(type) {
case *js_ast.SExpr:
assignTarget := js_ast.AssignTargetNone
if isInOrOf {
assignTarget = js_ast.AssignTargetReplace
}
p.stmtExprValue = s.Value.Data
s.Value, _ = p.visitExprInOut(s.Value, exprIn{assignTarget: assignTarget})
case *js_ast.SLocal:
for i := range s.Decls {
d := &s.Decls[i]
p.visitBinding(d.Binding, bindingOpts{})
if d.ValueOrNil.Data != nil {
d.ValueOrNil = p.visitExpr(d.ValueOrNil)
}
}
s.Decls = p.lowerObjectRestInDecls(s.Decls)
s.Kind = p.selectLocalKind(s.Kind)
default:
panic("Internal error")
}
return stmt
}
func (p *parser) recordDeclaredSymbol(ref ast.Ref) {
isTopLevel := p.currentScope == p.moduleScope
// Check whether this symbol was hoisted out of a nested scope into the module scope
if !isTopLevel {
if symbol := p.symbols[ref.InnerIndex]; symbol.Kind.IsHoisted() && p.moduleScope.Members[symbol.OriginalName].Ref == ref {
isTopLevel = true
}
}
p.currentPart.DeclaredSymbols = append(p.currentPart.DeclaredSymbols, js_ast.DeclaredSymbol{
Ref: ref,
IsTopLevel: isTopLevel,
})
}
type bindingOpts struct {
duplicateArgCheck map[string]logger.Range
}
func (p *parser) visitBinding(binding js_ast.Binding, opts bindingOpts) {
switch b := binding.Data.(type) {
case *js_ast.BMissing:
case *js_ast.BIdentifier:
p.recordDeclaredSymbol(b.Ref)
name := p.symbols[b.Ref.InnerIndex].OriginalName
p.validateDeclaredSymbolName(binding.Loc, name)
if opts.duplicateArgCheck != nil {
r := js_lexer.RangeOfIdentifier(p.source, binding.Loc)
if firstRange := opts.duplicateArgCheck[name]; firstRange.Len > 0 {
p.log.AddErrorWithNotes(&p.tracker, r,
fmt.Sprintf("%q cannot be bound multiple times in the same parameter list", name),
[]logger.MsgData{p.tracker.MsgData(firstRange, fmt.Sprintf("The name %q was originally bound here:", name))})
} else {
opts.duplicateArgCheck[name] = r
}
}
case *js_ast.BArray:
for i := range b.Items {
item := &b.Items[i]
p.visitBinding(item.Binding, opts)
if item.DefaultValueOrNil.Data != nil {
// Propagate the name to keep from the binding into the initializer
if id, ok := item.Binding.Data.(*js_ast.BIdentifier); ok {
p.nameToKeep = p.symbols[id.Ref.InnerIndex].OriginalName
p.nameToKeepIsFor = item.DefaultValueOrNil.Data
}
item.DefaultValueOrNil = p.visitExpr(item.DefaultValueOrNil)
}
}
case *js_ast.BObject:
for i, property := range b.Properties {
if !property.IsSpread {
property.Key, _ = p.visitExprInOut(property.Key, exprIn{
shouldMangleStringsAsProps: true,
})
}
p.visitBinding(property.Value, opts)
if property.DefaultValueOrNil.Data != nil {
// Propagate the name to keep from the binding into the initializer
if id, ok := property.Value.Data.(*js_ast.BIdentifier); ok {
p.nameToKeep = p.symbols[id.Ref.InnerIndex].OriginalName
p.nameToKeepIsFor = property.DefaultValueOrNil.Data
}
property.DefaultValueOrNil = p.visitExpr(property.DefaultValueOrNil)
}
b.Properties[i] = property
}
default:
panic("Internal error")
}
}
func stmtCaresAboutScope(stmt js_ast.Stmt) bool {
switch s := stmt.Data.(type) {
case *js_ast.SBlock, *js_ast.SEmpty, *js_ast.SDebugger, *js_ast.SExpr, *js_ast.SIf,
*js_ast.SFor, *js_ast.SForIn, *js_ast.SForOf, *js_ast.SDoWhile, *js_ast.SWhile,
*js_ast.SWith, *js_ast.STry, *js_ast.SSwitch, *js_ast.SReturn, *js_ast.SThrow,
*js_ast.SBreak, *js_ast.SContinue, *js_ast.SDirective, *js_ast.SLabel:
return false
case *js_ast.SLocal:
return s.Kind != js_ast.LocalVar
default:
return true
}
}
func stmtsCareAboutScope(stmts []js_ast.Stmt) bool {
for _, stmt := range stmts {
if stmtCaresAboutScope(stmt) {
return true
}
}
return false
}
func dropFirstStatement(body js_ast.Stmt, replaceOrNil js_ast.Stmt) js_ast.Stmt {
if block, ok := body.Data.(*js_ast.SBlock); ok && len(block.Stmts) > 0 {
if replaceOrNil.Data != nil {
block.Stmts[0] = replaceOrNil
} else if len(block.Stmts) == 2 && !stmtCaresAboutScope(block.Stmts[1]) {
return block.Stmts[1]
} else {
block.Stmts = block.Stmts[1:]
}
return body
}
if replaceOrNil.Data != nil {
return replaceOrNil
}
return js_ast.Stmt{Loc: body.Loc, Data: js_ast.SEmptyShared}
}
func mangleFor(s *js_ast.SFor) {
// Get the first statement in the loop
first := s.Body
if block, ok := first.Data.(*js_ast.SBlock); ok && len(block.Stmts) > 0 {
first = block.Stmts[0]
}
if ifS, ok := first.Data.(*js_ast.SIf); ok {
// "for (;;) if (x) break;" => "for (; !x;) ;"
// "for (; a;) if (x) break;" => "for (; a && !x;) ;"
// "for (;;) if (x) break; else y();" => "for (; !x;) y();"
// "for (; a;) if (x) break; else y();" => "for (; a && !x;) y();"
if breakS, ok := ifS.Yes.Data.(*js_ast.SBreak); ok && breakS.Label == nil {
var not js_ast.Expr
if unary, ok := ifS.Test.Data.(*js_ast.EUnary); ok && unary.Op == js_ast.UnOpNot {
not = unary.Value
} else {
not = js_ast.Not(ifS.Test)
}
if s.TestOrNil.Data != nil {
s.TestOrNil = js_ast.Expr{Loc: s.TestOrNil.Loc, Data: &js_ast.EBinary{
Op: js_ast.BinOpLogicalAnd,
Left: s.TestOrNil,
Right: not,
}}
} else {
s.TestOrNil = not
}
s.Body = dropFirstStatement(s.Body, ifS.NoOrNil)
return
}
// "for (;;) if (x) y(); else break;" => "for (; x;) y();"
// "for (; a;) if (x) y(); else break;" => "for (; a && x;) y();"
if ifS.NoOrNil.Data != nil {
if breakS, ok := ifS.NoOrNil.Data.(*js_ast.SBreak); ok && breakS.Label == nil {
if s.TestOrNil.Data != nil {
s.TestOrNil = js_ast.Expr{Loc: s.TestOrNil.Loc, Data: &js_ast.EBinary{
Op: js_ast.BinOpLogicalAnd,
Left: s.TestOrNil,
Right: ifS.Test,
}}
} else {
s.TestOrNil = ifS.Test
}
s.Body = dropFirstStatement(s.Body, ifS.Yes)
return
}
}
}
}
func appendIfOrLabelBodyPreservingScope(stmts []js_ast.Stmt, body js_ast.Stmt) []js_ast.Stmt {
if block, ok := body.Data.(*js_ast.SBlock); ok && !stmtsCareAboutScope(block.Stmts) {
return append(stmts, block.Stmts...)
}
if stmtCaresAboutScope(body) {
return append(stmts, js_ast.Stmt{Loc: body.Loc, Data: &js_ast.SBlock{Stmts: []js_ast.Stmt{body}}})
}
return append(stmts, body)
}
func (p *parser) mangleIf(stmts []js_ast.Stmt, loc logger.Loc, s *js_ast.SIf) []js_ast.Stmt {
// Constant folding using the test expression
if boolean, sideEffects, ok := js_ast.ToBooleanWithSideEffects(s.Test.Data); ok {
if boolean {
// The test is truthy
if s.NoOrNil.Data == nil || !shouldKeepStmtInDeadControlFlow(s.NoOrNil) {
// We can drop the "no" branch
if sideEffects == js_ast.CouldHaveSideEffects {
// Keep the condition if it could have side effects (but is still known to be truthy)
if test := p.astHelpers.SimplifyUnusedExpr(s.Test, p.options.unsupportedJSFeatures); test.Data != nil {
stmts = append(stmts, js_ast.Stmt{Loc: s.Test.Loc, Data: &js_ast.SExpr{Value: test}})
}
}
return appendIfOrLabelBodyPreservingScope(stmts, s.Yes)
} else {
// We have to keep the "no" branch
}
} else {
// The test is falsy
if !shouldKeepStmtInDeadControlFlow(s.Yes) {
// We can drop the "yes" branch
if sideEffects == js_ast.CouldHaveSideEffects {
// Keep the condition if it could have side effects (but is still known to be falsy)
if test := p.astHelpers.SimplifyUnusedExpr(s.Test, p.options.unsupportedJSFeatures); test.Data != nil {
stmts = append(stmts, js_ast.Stmt{Loc: s.Test.Loc, Data: &js_ast.SExpr{Value: test}})
}
}
if s.NoOrNil.Data == nil {
return stmts
}
return appendIfOrLabelBodyPreservingScope(stmts, s.NoOrNil)
} else {
// We have to keep the "yes" branch
}
}
// Use "1" and "0" instead of "true" and "false" to be shorter
if sideEffects == js_ast.NoSideEffects {
if boolean {
s.Test.Data = &js_ast.ENumber{Value: 1}
} else {
s.Test.Data = &js_ast.ENumber{Value: 0}
}
}
}
var expr js_ast.Expr
if yes, ok := s.Yes.Data.(*js_ast.SExpr); ok {
// "yes" is an expression
if s.NoOrNil.Data == nil {
if not, ok := s.Test.Data.(*js_ast.EUnary); ok && not.Op == js_ast.UnOpNot {
// "if (!a) b();" => "a || b();"
expr = js_ast.JoinWithLeftAssociativeOp(js_ast.BinOpLogicalOr, not.Value, yes.Value)
} else {
// "if (a) b();" => "a && b();"
expr = js_ast.JoinWithLeftAssociativeOp(js_ast.BinOpLogicalAnd, s.Test, yes.Value)
}
} else if no, ok := s.NoOrNil.Data.(*js_ast.SExpr); ok {
// "if (a) b(); else c();" => "a ? b() : c();"
expr = p.astHelpers.MangleIfExpr(loc, &js_ast.EIf{
Test: s.Test,
Yes: yes.Value,
No: no.Value,
}, p.options.unsupportedJSFeatures)
}
} else if _, ok := s.Yes.Data.(*js_ast.SEmpty); ok {
// "yes" is missing
if s.NoOrNil.Data == nil {
// "yes" and "no" are both missing
if p.astHelpers.ExprCanBeRemovedIfUnused(s.Test) {
// "if (1) {}" => ""
return stmts
} else {
// "if (a) {}" => "a;"
expr = s.Test
}
} else if no, ok := s.NoOrNil.Data.(*js_ast.SExpr); ok {
if not, ok := s.Test.Data.(*js_ast.EUnary); ok && not.Op == js_ast.UnOpNot {
// "if (!a) {} else b();" => "a && b();"
expr = js_ast.JoinWithLeftAssociativeOp(js_ast.BinOpLogicalAnd, not.Value, no.Value)
} else {
// "if (a) {} else b();" => "a || b();"
expr = js_ast.JoinWithLeftAssociativeOp(js_ast.BinOpLogicalOr, s.Test, no.Value)
}
} else {
// "yes" is missing and "no" is not missing (and is not an expression)
if not, ok := s.Test.Data.(*js_ast.EUnary); ok && not.Op == js_ast.UnOpNot {
// "if (!a) {} else throw b;" => "if (a) throw b;"
s.Test = not.Value
s.Yes = s.NoOrNil
s.NoOrNil = js_ast.Stmt{}
} else {
// "if (a) {} else throw b;" => "if (!a) throw b;"
s.Test = js_ast.Not(s.Test)
s.Yes = s.NoOrNil
s.NoOrNil = js_ast.Stmt{}
}
}
} else {
// "yes" is not missing (and is not an expression)
if s.NoOrNil.Data != nil {
// "yes" is not missing (and is not an expression) and "no" is not missing
if not, ok := s.Test.Data.(*js_ast.EUnary); ok && not.Op == js_ast.UnOpNot {
// "if (!a) return b; else return c;" => "if (a) return c; else return b;"
s.Test = not.Value
s.Yes, s.NoOrNil = s.NoOrNil, s.Yes
}
} else {
// "no" is missing
if s2, ok := s.Yes.Data.(*js_ast.SIf); ok && s2.NoOrNil.Data == nil {
// "if (a) if (b) return c;" => "if (a && b) return c;"
s.Test = js_ast.JoinWithLeftAssociativeOp(js_ast.BinOpLogicalAnd, s.Test, s2.Test)
s.Yes = s2.Yes
}
}
}
// Return an expression if we replaced the if statement with an expression above
if expr.Data != nil {
expr = p.astHelpers.SimplifyUnusedExpr(expr, p.options.unsupportedJSFeatures)
return append(stmts, js_ast.Stmt{Loc: loc, Data: &js_ast.SExpr{Value: expr}})
}
return append(stmts, js_ast.Stmt{Loc: loc, Data: s})
}
func (p *parser) keepExprSymbolName(value js_ast.Expr, name string) js_ast.Expr {
value = p.callRuntime(value.Loc, "__name", []js_ast.Expr{value,
{Loc: value.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(name)}},
})
// Make sure tree shaking removes this if the function is never used
value.Data.(*js_ast.ECall).CanBeUnwrappedIfUnused = true
return value
}
func (p *parser) keepClassOrFnSymbolName(loc logger.Loc, expr js_ast.Expr, name string) js_ast.Stmt {
return js_ast.Stmt{Loc: loc, Data: &js_ast.SExpr{
Value: p.callRuntime(loc, "__name", []js_ast.Expr{
expr,
{Loc: loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(name)}},
}),
IsFromClassOrFnThatCanBeRemovedIfUnused: true,
}}
}
func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ast.Stmt {
// By default any statement ends the const local prefix
wasAfterAfterConstLocalPrefix := p.currentScope.IsAfterConstLocalPrefix
p.currentScope.IsAfterConstLocalPrefix = true
switch s := stmt.Data.(type) {
case *js_ast.SEmpty, *js_ast.SComment:
// Comments do not end the const local prefix
p.currentScope.IsAfterConstLocalPrefix = wasAfterAfterConstLocalPrefix
case *js_ast.SDebugger:
// Debugger statements do not end the const local prefix
p.currentScope.IsAfterConstLocalPrefix = wasAfterAfterConstLocalPrefix
if p.options.dropDebugger {
return stmts
}
case *js_ast.STypeScript:
// Type annotations do not end the const local prefix
p.currentScope.IsAfterConstLocalPrefix = wasAfterAfterConstLocalPrefix
// Erase TypeScript constructs from the output completely
return stmts
case *js_ast.SDirective:
// Directives do not end the const local prefix
p.currentScope.IsAfterConstLocalPrefix = wasAfterAfterConstLocalPrefix
if p.isStrictMode() && s.LegacyOctalLoc.Start > 0 {
p.markStrictModeFeature(legacyOctalEscape, p.source.RangeOfLegacyOctalEscape(s.LegacyOctalLoc), "")
}
case *js_ast.SImport:
p.recordDeclaredSymbol(s.NamespaceRef)
if s.DefaultName != nil {
p.recordDeclaredSymbol(s.DefaultName.Ref)
}
if s.Items != nil {
for _, item := range *s.Items {
p.recordDeclaredSymbol(item.Name.Ref)
}
}
case *js_ast.SExportClause:
// "export {foo}"
end := 0
for _, item := range s.Items {
name := p.loadNameFromRef(item.Name.Ref)
ref := p.findSymbol(item.AliasLoc, name).ref
if p.symbols[ref.InnerIndex].Kind == ast.SymbolUnbound {
// Silently strip exports of non-local symbols in TypeScript, since
// those likely correspond to type-only exports. But report exports of
// non-local symbols as errors in JavaScript.
if !p.options.ts.Parse {
r := js_lexer.RangeOfIdentifier(p.source, item.Name.Loc)
p.log.AddError(&p.tracker, r, fmt.Sprintf("%q is not declared in this file", name))
}
continue
}
item.Name.Ref = ref
s.Items[end] = item
end++
}
// Note: do not remove empty export statements since TypeScript uses them as module markers
s.Items = s.Items[:end]
case *js_ast.SExportFrom:
// "export {foo} from 'path'"
name := p.loadNameFromRef(s.NamespaceRef)
s.NamespaceRef = p.newSymbol(ast.SymbolOther, name)
p.currentScope.Generated = append(p.currentScope.Generated, s.NamespaceRef)
p.recordDeclaredSymbol(s.NamespaceRef)
// This is a re-export and the symbols created here are used to reference
// names in another file. This means the symbols are really aliases.
for i, item := range s.Items {
name := p.loadNameFromRef(item.Name.Ref)
ref := p.newSymbol(ast.SymbolOther, name)
p.currentScope.Generated = append(p.currentScope.Generated, ref)
p.recordDeclaredSymbol(ref)
s.Items[i].Name.Ref = ref
}
case *js_ast.SExportStar:
// "export * from 'path'"
// "export * as ns from 'path'"
name := p.loadNameFromRef(s.NamespaceRef)
s.NamespaceRef = p.newSymbol(ast.SymbolOther, name)
p.currentScope.Generated = append(p.currentScope.Generated, s.NamespaceRef)
p.recordDeclaredSymbol(s.NamespaceRef)
// "export * as ns from 'path'"
if s.Alias != nil {
// "import * as ns from 'path'"
// "export {ns}"
if p.options.unsupportedJSFeatures.Has(compat.ExportStarAs) {
p.recordUsage(s.NamespaceRef)
return append(stmts,
js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SImport{
NamespaceRef: s.NamespaceRef,
StarNameLoc: &s.Alias.Loc,
ImportRecordIndex: s.ImportRecordIndex,
}},
js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SExportClause{
Items: []js_ast.ClauseItem{{
Alias: s.Alias.OriginalName,
OriginalName: s.Alias.OriginalName,
AliasLoc: s.Alias.Loc,
Name: ast.LocRef{Loc: s.Alias.Loc, Ref: s.NamespaceRef},
}},
IsSingleLine: true,
}},
)
}
}
case *js_ast.SExportDefault:
p.recordDeclaredSymbol(s.DefaultName.Ref)
switch s2 := s.Value.Data.(type) {
case *js_ast.SExpr:
// Propagate the name to keep from the export into the value
p.nameToKeep = "default"
p.nameToKeepIsFor = s2.Value.Data
s2.Value = p.visitExpr(s2.Value)
// Discard type-only export default statements
if p.options.ts.Parse {
if id, ok := s2.Value.Data.(*js_ast.EIdentifier); ok {
symbol := p.symbols[id.Ref.InnerIndex]
if symbol.Kind == ast.SymbolUnbound && p.localTypeNames[symbol.OriginalName] {
return stmts
}
}
}
// If there are lowered "using" declarations, change this into a "var"
if p.currentScope.Parent == nil && p.willWrapModuleInTryCatchForUsing {
stmts = append(stmts,
js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SLocal{
Decls: []js_ast.Decl{{
Binding: js_ast.Binding{Loc: s.DefaultName.Loc, Data: &js_ast.BIdentifier{Ref: s.DefaultName.Ref}},
ValueOrNil: s2.Value,
}},
}},
js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SExportClause{Items: []js_ast.ClauseItem{{
Alias: "default",
AliasLoc: s.DefaultName.Loc,
Name: s.DefaultName,
}}}},
)
break
}
stmts = append(stmts, stmt)
case *js_ast.SFunction:
// If we need to preserve the name but there is no name, generate a name
var name string
if p.options.keepNames {
if s2.Fn.Name == nil {
clone := s.DefaultName
s2.Fn.Name = &clone
name = "default"
} else {
name = p.symbols[s2.Fn.Name.Ref.InnerIndex].OriginalName
}
}
p.visitFn(&s2.Fn, s2.Fn.OpenParenLoc, visitFnOpts{})
stmts = append(stmts, stmt)
// Optionally preserve the name
if p.options.keepNames {
p.symbols[s2.Fn.Name.Ref.InnerIndex].Flags |= ast.DidKeepName
fn := js_ast.Expr{Loc: s2.Fn.Name.Loc, Data: &js_ast.EIdentifier{Ref: s2.Fn.Name.Ref}}
stmts = append(stmts, p.keepClassOrFnSymbolName(s2.Fn.Name.Loc, fn, name))
}
case *js_ast.SClass:
result := p.visitClass(s.Value.Loc, &s2.Class, s.DefaultName.Ref, "default")
// Lower class field syntax for browsers that don't support it
classStmts, _ := p.lowerClass(stmt, js_ast.Expr{}, result, "")
// Remember if the class was side-effect free before lowering
if result.canBeRemovedIfUnused {
for _, classStmt := range classStmts {
if s2, ok := classStmt.Data.(*js_ast.SExpr); ok {
s2.IsFromClassOrFnThatCanBeRemovedIfUnused = true
}
}
}
stmts = append(stmts, classStmts...)
default:
panic("Internal error")
}
// Use a more friendly name than "default" now that "--keep-names" has
// been applied and has made sure to enforce the name "default"
if p.symbols[s.DefaultName.Ref.InnerIndex].OriginalName == "default" {
p.symbols[s.DefaultName.Ref.InnerIndex].OriginalName = p.source.IdentifierName + "_default"
}
return stmts
case *js_ast.SExportEquals:
// "module.exports = value"
stmts = append(stmts, js_ast.AssignStmt(
js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EDot{
Target: js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: p.moduleRef}},
Name: "exports",
NameLoc: stmt.Loc,
}},
p.visitExpr(s.Value),
))
p.recordUsage(p.moduleRef)
return stmts
case *js_ast.SBreak:
if s.Label != nil {
name := p.loadNameFromRef(s.Label.Ref)
s.Label.Ref, _, _ = p.findLabelSymbol(s.Label.Loc, name)
} else if !p.fnOrArrowDataVisit.isInsideLoop && !p.fnOrArrowDataVisit.isInsideSwitch {
r := js_lexer.RangeOfIdentifier(p.source, stmt.Loc)
p.log.AddError(&p.tracker, r, "Cannot use \"break\" here:")
}
case *js_ast.SContinue:
if s.Label != nil {
name := p.loadNameFromRef(s.Label.Ref)
var isLoop, ok bool
s.Label.Ref, isLoop, ok = p.findLabelSymbol(s.Label.Loc, name)
if ok && !isLoop {
r := js_lexer.RangeOfIdentifier(p.source, s.Label.Loc)
p.log.AddError(&p.tracker, r, fmt.Sprintf("Cannot continue to label \"%s\"", name))
}
} else if !p.fnOrArrowDataVisit.isInsideLoop {
r := js_lexer.RangeOfIdentifier(p.source, stmt.Loc)
p.log.AddError(&p.tracker, r, "Cannot use \"continue\" here:")
}
case *js_ast.SLabel:
// Forbid functions inside labels in strict mode
if p.isStrictMode() {
if _, ok := s.Stmt.Data.(*js_ast.SFunction); ok {
p.markStrictModeFeature(labelFunctionStmt, js_lexer.RangeOfIdentifier(p.source, s.Stmt.Loc), "")
}
}
p.pushScopeForVisitPass(js_ast.ScopeLabel, stmt.Loc)
name := p.loadNameFromRef(s.Name.Ref)
if js_lexer.StrictModeReservedWords[name] {
p.markStrictModeFeature(reservedWord, js_lexer.RangeOfIdentifier(p.source, s.Name.Loc), name)
}
ref := p.newSymbol(ast.SymbolLabel, name)
s.Name.Ref = ref
// Duplicate labels are an error
for scope := p.currentScope.Parent; scope != nil; scope = scope.Parent {
if scope.Label.Ref != ast.InvalidRef && name == p.symbols[scope.Label.Ref.InnerIndex].OriginalName {
p.log.AddErrorWithNotes(&p.tracker, js_lexer.RangeOfIdentifier(p.source, s.Name.Loc),
fmt.Sprintf("Duplicate label %q", name),
[]logger.MsgData{p.tracker.MsgData(js_lexer.RangeOfIdentifier(p.source, scope.Label.Loc),
fmt.Sprintf("The original label %q is here:", name))})
break
}
if scope.Kind == js_ast.ScopeFunctionBody {
// Labels are only visible within the function they are defined in.
break
}
}
p.currentScope.Label = ast.LocRef{Loc: s.Name.Loc, Ref: ref}
switch s.Stmt.Data.(type) {
case *js_ast.SFor, *js_ast.SForIn, *js_ast.SForOf, *js_ast.SWhile, *js_ast.SDoWhile:
p.currentScope.LabelStmtIsLoop = true
}
// If we're dropping this statement, consider control flow to be dead
_, shouldDropLabel := p.dropLabelsMap[name]
old := p.isControlFlowDead
if shouldDropLabel {
p.isControlFlowDead = true
}
s.Stmt = p.visitSingleStmt(s.Stmt, stmtsNormal)
p.popScope()
// Drop this entire statement if requested
if shouldDropLabel {
p.isControlFlowDead = old
return stmts
}
if p.options.minifySyntax {
// Optimize "x: break x" which some people apparently write by hand
if child, ok := s.Stmt.Data.(*js_ast.SBreak); ok && child.Label != nil && child.Label.Ref == s.Name.Ref {
return stmts
}
// Remove the label if it's not necessary
if p.symbols[ref.InnerIndex].UseCountEstimate == 0 {
return appendIfOrLabelBodyPreservingScope(stmts, s.Stmt)
}
}
// Handle "for await" that has been lowered by moving this label inside the "try"
if try, ok := s.Stmt.Data.(*js_ast.STry); ok && len(try.Block.Stmts) == 1 {
if loop, ok := try.Block.Stmts[0].Data.(*js_ast.SFor); ok && loop.IsLoweredForAwait {
try.Block.Stmts[0] = js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SLabel{
Stmt: try.Block.Stmts[0],
Name: s.Name,
IsSingleLineStmt: s.IsSingleLineStmt,
}}
return append(stmts, s.Stmt)
}
}
case *js_ast.SLocal:
// Silently remove unsupported top-level "await" in dead code branches
if s.Kind == js_ast.LocalAwaitUsing && p.fnOrArrowDataVisit.isOutsideFnOrArrow {
if p.isControlFlowDead && (p.options.unsupportedJSFeatures.Has(compat.TopLevelAwait) || !p.options.outputFormat.KeepESMImportExportSyntax()) {
s.Kind = js_ast.LocalUsing
} else {
p.liveTopLevelAwaitKeyword = logger.Range{Loc: stmt.Loc, Len: 5}
p.markSyntaxFeature(compat.TopLevelAwait, logger.Range{Loc: stmt.Loc, Len: 5})
}
}
// Local statements do not end the const local prefix
p.currentScope.IsAfterConstLocalPrefix = wasAfterAfterConstLocalPrefix
for i := range s.Decls {
d := &s.Decls[i]
p.visitBinding(d.Binding, bindingOpts{})
// Visit the initializer
if d.ValueOrNil.Data != nil {
// Fold numeric constants in the initializer
oldShouldFoldTypeScriptConstantExpressions := p.shouldFoldTypeScriptConstantExpressions
p.shouldFoldTypeScriptConstantExpressions = p.options.minifySyntax && !p.currentScope.IsAfterConstLocalPrefix
// Propagate the name to keep from the binding into the initializer
if id, ok := d.Binding.Data.(*js_ast.BIdentifier); ok {
p.nameToKeep = p.symbols[id.Ref.InnerIndex].OriginalName
p.nameToKeepIsFor = d.ValueOrNil.Data
}
d.ValueOrNil = p.visitExpr(d.ValueOrNil)
p.shouldFoldTypeScriptConstantExpressions = oldShouldFoldTypeScriptConstantExpressions
// Initializing to undefined is implicit, but be careful to not
// accidentally cause a syntax error or behavior change by removing
// the value
//
// Good:
// "let a = undefined;" => "let a;"
//
// Bad (a syntax error):
// "let {} = undefined;" => "let {};"
//
// Bad (a behavior change):
// "a = 123; var a = undefined;" => "a = 123; var a;"
//
if p.options.minifySyntax && s.Kind == js_ast.LocalLet {
if _, ok := d.Binding.Data.(*js_ast.BIdentifier); ok {
if _, ok := d.ValueOrNil.Data.(*js_ast.EUndefined); ok {
d.ValueOrNil = js_ast.Expr{}
}
}
}
// Yarn's PnP data may be stored in a variable: https://github.com/yarnpkg/berry/pull/4320
if p.options.decodeHydrateRuntimeStateYarnPnP {
if str, ok := d.ValueOrNil.Data.(*js_ast.EString); ok {
if id, ok := d.Binding.Data.(*js_ast.BIdentifier); ok {
if p.stringLocalsForYarnPnP == nil {
p.stringLocalsForYarnPnP = make(map[ast.Ref]stringLocalForYarnPnP)
}
p.stringLocalsForYarnPnP[id.Ref] = stringLocalForYarnPnP{value: str.Value, loc: d.ValueOrNil.Loc}
}
}
}
}
// Attempt to continue the const local prefix
if p.options.minifySyntax && !p.currentScope.IsAfterConstLocalPrefix {
if id, ok := d.Binding.Data.(*js_ast.BIdentifier); ok {
if s.Kind == js_ast.LocalConst && d.ValueOrNil.Data != nil {
if value := js_ast.ExprToConstValue(d.ValueOrNil); value.Kind != js_ast.ConstValueNone {
if p.constValues == nil {
p.constValues = make(map[ast.Ref]js_ast.ConstValue)
}
p.constValues[id.Ref] = value
continue
}
}
if d.ValueOrNil.Data != nil && !isSafeForConstLocalPrefix(d.ValueOrNil) {
p.currentScope.IsAfterConstLocalPrefix = true
}
} else {
// A non-identifier binding ends the const local prefix
p.currentScope.IsAfterConstLocalPrefix = true
}
}
}
// Handle being exported inside a namespace
if s.IsExport && p.enclosingNamespaceArgRef != nil {
wrapIdentifier := func(loc logger.Loc, ref ast.Ref) js_ast.Expr {
p.recordUsage(*p.enclosingNamespaceArgRef)
return js_ast.Expr{Loc: loc, Data: p.dotOrMangledPropVisit(
js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: *p.enclosingNamespaceArgRef}},
p.symbols[ref.InnerIndex].OriginalName,
loc,
)}
}
for _, decl := range s.Decls {
if decl.ValueOrNil.Data != nil {
target := js_ast.ConvertBindingToExpr(decl.Binding, wrapIdentifier)
if result, ok := p.lowerAssign(target, decl.ValueOrNil, objRestReturnValueIsUnused); ok {
target = result
} else {
target = js_ast.Assign(target, decl.ValueOrNil)
}
stmts = append(stmts, js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SExpr{Value: target}})
}
}
return stmts
}
s.Decls = p.lowerObjectRestInDecls(s.Decls)
// Optimization: Avoid unnecessary "using" machinery by changing ones
// initialized to "null" or "undefined" into a normal variable. Note that
// "await using" still needs the "await", so we can't do it for those.
if p.options.minifySyntax && s.Kind == js_ast.LocalUsing {
s.Kind = js_ast.LocalConst
for _, decl := range s.Decls {
if t := js_ast.KnownPrimitiveType(decl.ValueOrNil.Data); t != js_ast.PrimitiveNull && t != js_ast.PrimitiveUndefined {
s.Kind = js_ast.LocalUsing
break
}
}
}
s.Kind = p.selectLocalKind(s.Kind)
// Potentially relocate "var" declarations to the top level
if s.Kind == js_ast.LocalVar {
if assign, ok := p.maybeRelocateVarsToTopLevel(s.Decls, relocateVarsNormal); ok {
if assign.Data != nil {
stmts = append(stmts, assign)
}
return stmts
}
}
case *js_ast.SExpr:
shouldTrimUnsightlyPrimitives := !p.options.minifySyntax && !isUnsightlyPrimitive(s.Value.Data)
p.stmtExprValue = s.Value.Data
s.Value = p.visitExpr(s.Value)
// Expressions that have been simplified down to a single primitive don't
// have any effect, and are automatically removed during minification.
// However, some people are really bothered by seeing them. Remove them
// so we don't bother these people.
if shouldTrimUnsightlyPrimitives && isUnsightlyPrimitive(s.Value.Data) {
return stmts
}
case *js_ast.SThrow:
s.Value = p.visitExpr(s.Value)
case *js_ast.SReturn:
// Forbid top-level return inside modules with ECMAScript syntax
if p.fnOrArrowDataVisit.isOutsideFnOrArrow {
if p.isFileConsideredESM {
_, notes := p.whyESModule()
p.log.AddErrorWithNotes(&p.tracker, js_lexer.RangeOfIdentifier(p.source, stmt.Loc),
"Top-level return cannot be used inside an ECMAScript module", notes)
} else {
p.hasTopLevelReturn = true
}
}
if s.ValueOrNil.Data != nil {
s.ValueOrNil = p.visitExpr(s.ValueOrNil)
// Returning undefined is implicit except when inside an async generator
// function, where "return undefined" behaves like "return await undefined"
// but just "return" has no "await".
if p.options.minifySyntax && (!p.fnOrArrowDataVisit.isAsync || !p.fnOrArrowDataVisit.isGenerator) {
if _, ok := s.ValueOrNil.Data.(*js_ast.EUndefined); ok {
s.ValueOrNil = js_ast.Expr{}
}
}
}
case *js_ast.SBlock:
p.pushScopeForVisitPass(js_ast.ScopeBlock, stmt.Loc)
// Pass the "is loop body" status on to the direct children of a block used
// as a loop body. This is used to enable optimizations specific to the
// topmost scope in a loop body block.
if p.loopBody == s {
s.Stmts = p.visitStmts(s.Stmts, stmtsLoopBody)
} else {
s.Stmts = p.visitStmts(s.Stmts, stmtsNormal)
}
p.popScope()
if p.options.minifySyntax {
if len(s.Stmts) == 1 && !stmtCaresAboutScope(s.Stmts[0]) {
// Unwrap blocks containing a single statement
stmt = s.Stmts[0]
} else if len(s.Stmts) == 0 {
// Trim empty blocks
stmt = js_ast.Stmt{Loc: stmt.Loc, Data: js_ast.SEmptyShared}
}
}
case *js_ast.SWith:
p.markStrictModeFeature(withStatement, js_lexer.RangeOfIdentifier(p.source, stmt.Loc), "")
s.Value = p.visitExpr(s.Value)
p.pushScopeForVisitPass(js_ast.ScopeWith, s.BodyLoc)
s.Body = p.visitSingleStmt(s.Body, stmtsNormal)
p.popScope()
case *js_ast.SWhile:
s.Test = p.visitExpr(s.Test)
s.Body = p.visitLoopBody(s.Body)
if p.options.minifySyntax {
s.Test = p.astHelpers.SimplifyBooleanExpr(s.Test)
// A true value is implied
testOrNil := s.Test
if boolean, sideEffects, ok := js_ast.ToBooleanWithSideEffects(s.Test.Data); ok && boolean && sideEffects == js_ast.NoSideEffects {
testOrNil = js_ast.Expr{}
}
// "while (a) {}" => "for (;a;) {}"
forS := &js_ast.SFor{TestOrNil: testOrNil, Body: s.Body, IsSingleLineBody: s.IsSingleLineBody}
mangleFor(forS)
stmt = js_ast.Stmt{Loc: stmt.Loc, Data: forS}
}
case *js_ast.SDoWhile:
s.Body = p.visitLoopBody(s.Body)
s.Test = p.visitExpr(s.Test)
if p.options.minifySyntax {
s.Test = p.astHelpers.SimplifyBooleanExpr(s.Test)
}
case *js_ast.SIf:
s.Test = p.visitExpr(s.Test)
if p.options.minifySyntax {
s.Test = p.astHelpers.SimplifyBooleanExpr(s.Test)
}
// Fold constants
boolean, _, ok := js_ast.ToBooleanWithSideEffects(s.Test.Data)
// Mark the control flow as dead if the branch is never taken
if ok && !boolean {
old := p.isControlFlowDead
p.isControlFlowDead = true
s.Yes = p.visitSingleStmt(s.Yes, stmtsNormal)
p.isControlFlowDead = old
} else {
s.Yes = p.visitSingleStmt(s.Yes, stmtsNormal)
}
// The "else" clause is optional
if s.NoOrNil.Data != nil {
// Mark the control flow as dead if the branch is never taken
if ok && boolean {
old := p.isControlFlowDead
p.isControlFlowDead = true
s.NoOrNil = p.visitSingleStmt(s.NoOrNil, stmtsNormal)
p.isControlFlowDead = old
} else {
s.NoOrNil = p.visitSingleStmt(s.NoOrNil, stmtsNormal)
}
// Trim unnecessary "else" clauses
if p.options.minifySyntax {
if _, ok := s.NoOrNil.Data.(*js_ast.SEmpty); ok {
s.NoOrNil = js_ast.Stmt{}
}
}
}
if p.options.minifySyntax {
return p.mangleIf(stmts, stmt.Loc, s)
}
case *js_ast.SFor:
p.pushScopeForVisitPass(js_ast.ScopeBlock, stmt.Loc)
if s.InitOrNil.Data != nil {
p.visitForLoopInit(s.InitOrNil, false)
}
if s.TestOrNil.Data != nil {
s.TestOrNil = p.visitExpr(s.TestOrNil)
if p.options.minifySyntax {
s.TestOrNil = p.astHelpers.SimplifyBooleanExpr(s.TestOrNil)
// A true value is implied
if boolean, sideEffects, ok := js_ast.ToBooleanWithSideEffects(s.TestOrNil.Data); ok && boolean && sideEffects == js_ast.NoSideEffects {
s.TestOrNil = js_ast.Expr{}
}
}
}
if s.UpdateOrNil.Data != nil {
s.UpdateOrNil = p.visitExpr(s.UpdateOrNil)
}
s.Body = p.visitLoopBody(s.Body)
// Potentially relocate "var" declarations to the top level. Note that this
// must be done inside the scope of the for loop or they won't be relocated.
if s.InitOrNil.Data != nil {
if init, ok := s.InitOrNil.Data.(*js_ast.SLocal); ok && init.Kind == js_ast.LocalVar {
if assign, ok := p.maybeRelocateVarsToTopLevel(init.Decls, relocateVarsNormal); ok {
if assign.Data != nil {
s.InitOrNil = assign
} else {
s.InitOrNil = js_ast.Stmt{}
}
}
}
}
p.popScope()
if p.options.minifySyntax {
mangleFor(s)
}
case *js_ast.SForIn:
p.pushScopeForVisitPass(js_ast.ScopeBlock, stmt.Loc)
p.visitForLoopInit(s.Init, true)
s.Value = p.visitExpr(s.Value)
s.Body = p.visitLoopBody(s.Body)
// Check for a variable initializer
if local, ok := s.Init.Data.(*js_ast.SLocal); ok && local.Kind == js_ast.LocalVar && len(local.Decls) == 1 {
decl := &local.Decls[0]
if id, ok := decl.Binding.Data.(*js_ast.BIdentifier); ok && decl.ValueOrNil.Data != nil {
p.markStrictModeFeature(forInVarInit, p.source.RangeOfOperatorBefore(decl.ValueOrNil.Loc, "="), "")
// Lower for-in variable initializers in case the output is used in strict mode
stmts = append(stmts, js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SExpr{Value: js_ast.Assign(
js_ast.Expr{Loc: decl.Binding.Loc, Data: &js_ast.EIdentifier{Ref: id.Ref}},
decl.ValueOrNil,
)}})
decl.ValueOrNil = js_ast.Expr{}
}
}
// Potentially relocate "var" declarations to the top level. Note that this
// must be done inside the scope of the for loop or they won't be relocated.
if init, ok := s.Init.Data.(*js_ast.SLocal); ok && init.Kind == js_ast.LocalVar {
if replacement, ok := p.maybeRelocateVarsToTopLevel(init.Decls, relocateVarsForInOrForOf); ok {
s.Init = replacement
}
}
p.popScope()
p.lowerObjectRestInForLoopInit(s.Init, &s.Body)
case *js_ast.SForOf:
// Silently remove unsupported top-level "await" in dead code branches
if s.Await.Len > 0 && p.fnOrArrowDataVisit.isOutsideFnOrArrow {
if p.isControlFlowDead && (p.options.unsupportedJSFeatures.Has(compat.TopLevelAwait) || !p.options.outputFormat.KeepESMImportExportSyntax()) {
s.Await = logger.Range{}
} else {
p.liveTopLevelAwaitKeyword = s.Await
p.markSyntaxFeature(compat.TopLevelAwait, s.Await)
}
}
p.pushScopeForVisitPass(js_ast.ScopeBlock, stmt.Loc)
p.visitForLoopInit(s.Init, true)
s.Value = p.visitExpr(s.Value)
s.Body = p.visitLoopBody(s.Body)
// Potentially relocate "var" declarations to the top level. Note that this
// must be done inside the scope of the for loop or they won't be relocated.
if init, ok := s.Init.Data.(*js_ast.SLocal); ok && init.Kind == js_ast.LocalVar {
if replacement, ok := p.maybeRelocateVarsToTopLevel(init.Decls, relocateVarsForInOrForOf); ok {
s.Init = replacement
}
}
// Handle "for (using x of y)" and "for (await using x of y)"
if local, ok := s.Init.Data.(*js_ast.SLocal); ok {
if local.Kind == js_ast.LocalUsing && p.options.unsupportedJSFeatures.Has(compat.Using) {
p.lowerUsingDeclarationInForOf(s.Init.Loc, local, &s.Body)
} else if local.Kind == js_ast.LocalAwaitUsing {
if p.fnOrArrowDataVisit.isOutsideFnOrArrow {
if p.isControlFlowDead && (p.options.unsupportedJSFeatures.Has(compat.TopLevelAwait) || !p.options.outputFormat.KeepESMImportExportSyntax()) {
// Silently remove unsupported top-level "await" in dead code branches
local.Kind = js_ast.LocalUsing
} else {
p.liveTopLevelAwaitKeyword = logger.Range{Loc: s.Init.Loc, Len: 5}
p.markSyntaxFeature(compat.TopLevelAwait, p.liveTopLevelAwaitKeyword)
}
if p.options.unsupportedJSFeatures.Has(compat.Using) {
p.lowerUsingDeclarationInForOf(s.Init.Loc, local, &s.Body)
}
} else if p.options.unsupportedJSFeatures.Has(compat.Using) || p.options.unsupportedJSFeatures.Has(compat.AsyncAwait) ||
(p.options.unsupportedJSFeatures.Has(compat.AsyncGenerator) && p.fnOrArrowDataVisit.isGenerator) {
p.lowerUsingDeclarationInForOf(s.Init.Loc, local, &s.Body)
}
}
}
p.popScope()
p.lowerObjectRestInForLoopInit(s.Init, &s.Body)
// Lower "for await" if it's unsupported if it's in a lowered async generator
if s.Await.Len > 0 && (p.options.unsupportedJSFeatures.Has(compat.ForAwait) ||
(p.options.unsupportedJSFeatures.Has(compat.AsyncGenerator) && p.fnOrArrowDataVisit.isGenerator)) {
return p.lowerForAwaitLoop(stmt.Loc, s, stmts)
}
case *js_ast.STry:
p.pushScopeForVisitPass(js_ast.ScopeBlock, stmt.Loc)
if p.fnOrArrowDataVisit.tryBodyCount == 0 {
if s.Catch != nil {
p.fnOrArrowDataVisit.tryCatchLoc = s.Catch.Loc
} else {
p.fnOrArrowDataVisit.tryCatchLoc = stmt.Loc
}
}
p.fnOrArrowDataVisit.tryBodyCount++
s.Block.Stmts = p.visitStmts(s.Block.Stmts, stmtsNormal)
p.fnOrArrowDataVisit.tryBodyCount--
p.popScope()
if s.Catch != nil {
old := p.isControlFlowDead
// If the try body is empty, then the catch body is dead
if len(s.Block.Stmts) == 0 {
p.isControlFlowDead = true
}
p.pushScopeForVisitPass(js_ast.ScopeCatchBinding, s.Catch.Loc)
if s.Catch.BindingOrNil.Data != nil {
p.visitBinding(s.Catch.BindingOrNil, bindingOpts{})
}
p.pushScopeForVisitPass(js_ast.ScopeBlock, s.Catch.BlockLoc)
s.Catch.Block.Stmts = p.visitStmts(s.Catch.Block.Stmts, stmtsNormal)
p.popScope()
p.lowerObjectRestInCatchBinding(s.Catch)
p.popScope()
p.isControlFlowDead = old
}
if s.Finally != nil {
p.pushScopeForVisitPass(js_ast.ScopeBlock, s.Finally.Loc)
s.Finally.Block.Stmts = p.visitStmts(s.Finally.Block.Stmts, stmtsNormal)
p.popScope()
}
if p.options.minifySyntax {
if len(s.Block.Stmts) == 0 {
// Try to drop the whole thing if the try body is empty
keepCatch := false
// Certain "catch" blocks need to be preserved:
//
// try {} catch { let foo } // Can be removed
// try {} catch { var foo } // Must be kept
//
if s.Catch != nil {
for _, stmt2 := range s.Catch.Block.Stmts {
if shouldKeepStmtInDeadControlFlow(stmt2) {
keepCatch = true
break
}
}
}
// Make sure to preserve the "finally" block if present
if !keepCatch {
if s.Finally == nil {
return stmts
}
if !stmtsCareAboutScope(s.Finally.Block.Stmts) {
return append(stmts, s.Finally.Block.Stmts...)
}
block := s.Finally.Block
stmt = js_ast.Stmt{Loc: s.Finally.Loc, Data: &block}
}
} else if s.Finally != nil && len(s.Finally.Block.Stmts) == 0 {
if s.Catch != nil {
// Just remove the "finally" block if there's a "catch"
s.Finally = nil
} else {
// Otherwise, try to unwrap the whole "try" statement
if !stmtsCareAboutScope(s.Block.Stmts) {
return append(stmts, s.Block.Stmts...)
}
block := s.Block
stmt = js_ast.Stmt{Loc: s.Finally.Loc, Data: &block}
}
}
}
case *js_ast.SSwitch:
s.Test = p.visitExpr(s.Test)
p.pushScopeForVisitPass(js_ast.ScopeBlock, s.BodyLoc)
oldIsInsideSwitch := p.fnOrArrowDataVisit.isInsideSwitch
p.fnOrArrowDataVisit.isInsideSwitch = true
// Disable const inlining in switch cases. They all share the same scope
// and can be evaluated in an unusual order. For example:
//
// // This should not become "return 0"
// switch (1) {
// case 0:
// const x = 0
// case 1:
// return x
// }
//
// // This should not become "return 0"
// switch (0) {
// case 0:
// return x
// case 1:
// const x = 0
// }
//
p.currentScope.IsAfterConstLocalPrefix = true
// Visit case values first
for i := range s.Cases {
c := &s.Cases[i]
if c.ValueOrNil.Data != nil {
c.ValueOrNil = p.visitExpr(c.ValueOrNil)
p.warnAboutEqualityCheck("case", c.ValueOrNil, c.ValueOrNil.Loc)
p.warnAboutTypeofAndString(s.Test, c.ValueOrNil, onlyCheckOriginalOrder)
}
}
// Check for duplicate case values
p.duplicateCaseChecker.reset()
for _, c := range s.Cases {
if c.ValueOrNil.Data != nil {
p.duplicateCaseChecker.check(p, c.ValueOrNil)
}
}
// Then analyze the cases to determine which ones are live and/or dead
cases := analyzeSwitchCasesForLiveness(s)
// Then visit case bodies, and potentially filter out dead cases
end := 0
for i, c := range s.Cases {
isAlwaysDead := cases[i].status == alwaysDead
// Potentially treat the case body as dead code
old := p.isControlFlowDead
if isAlwaysDead {
p.isControlFlowDead = true
}
c.Body = p.visitStmts(c.Body, stmtsNormal)
p.isControlFlowDead = old
// Filter out this case when minifying if it's known to be dead. Visiting
// the body above should already have removed any statements that can be
// removed safely, so if the body isn't empty then that means it contains
// some statements that can't be removed safely (e.g. a hoisted "var").
// So don't remove this case if the body isn't empty.
if p.options.minifySyntax && isAlwaysDead && len(c.Body) == 0 {
continue
}
// Make sure the assignment to the body above is preserved
s.Cases[end] = c
end++
}
s.Cases = s.Cases[:end]
p.fnOrArrowDataVisit.isInsideSwitch = oldIsInsideSwitch
p.popScope()
// Unwrap switch statements in dead code
if p.options.minifySyntax && p.isControlFlowDead {
for _, c := range s.Cases {
stmts = append(stmts, c.Body...)
}
return stmts
}
if p.options.minifySyntax {
return p.minifySwitchStmt(stmt.Loc, s, stmts)
}
case *js_ast.SFunction:
p.visitFn(&s.Fn, s.Fn.OpenParenLoc, visitFnOpts{})
// Strip this function declaration if it was overwritten
if p.symbols[s.Fn.Name.Ref.InnerIndex].Flags.Has(ast.RemoveOverwrittenFunctionDeclaration) && !s.IsExport {
return stmts
}
if p.options.minifySyntax && !s.Fn.IsGenerator && !s.Fn.IsAsync && !s.Fn.HasRestArg && s.Fn.Name != nil {
if len(s.Fn.Body.Block.Stmts) == 0 {
// Mark if this function is an empty function
hasSideEffectFreeArguments := true
for _, arg := range s.Fn.Args {
if _, ok := arg.Binding.Data.(*js_ast.BIdentifier); !ok {
hasSideEffectFreeArguments = false
break
}
}
if hasSideEffectFreeArguments {
p.symbols[s.Fn.Name.Ref.InnerIndex].Flags |= ast.IsEmptyFunction
}
} else if len(s.Fn.Args) == 1 && len(s.Fn.Body.Block.Stmts) == 1 {
// Mark if this function is an identity function
if arg := s.Fn.Args[0]; arg.DefaultOrNil.Data == nil {
if id, ok := arg.Binding.Data.(*js_ast.BIdentifier); ok {
if ret, ok := s.Fn.Body.Block.Stmts[0].Data.(*js_ast.SReturn); ok {
if retID, ok := ret.ValueOrNil.Data.(*js_ast.EIdentifier); ok && id.Ref == retID.Ref {
p.symbols[s.Fn.Name.Ref.InnerIndex].Flags |= ast.IsIdentityFunction
}
}
}
}
}
}
// Handle exporting this function from a namespace
if s.IsExport && p.enclosingNamespaceArgRef != nil {
s.IsExport = false
stmts = append(stmts, stmt, js_ast.AssignStmt(
js_ast.Expr{Loc: stmt.Loc, Data: p.dotOrMangledPropVisit(
js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: *p.enclosingNamespaceArgRef}},
p.symbols[s.Fn.Name.Ref.InnerIndex].OriginalName,
s.Fn.Name.Loc,
)},
js_ast.Expr{Loc: s.Fn.Name.Loc, Data: &js_ast.EIdentifier{Ref: s.Fn.Name.Ref}},
))
} else {
stmts = append(stmts, stmt)
}
// Optionally preserve the name
if p.options.keepNames {
symbol := &p.symbols[s.Fn.Name.Ref.InnerIndex]
symbol.Flags |= ast.DidKeepName
fn := js_ast.Expr{Loc: s.Fn.Name.Loc, Data: &js_ast.EIdentifier{Ref: s.Fn.Name.Ref}}
stmts = append(stmts, p.keepClassOrFnSymbolName(s.Fn.Name.Loc, fn, symbol.OriginalName))
}
return stmts
case *js_ast.SClass:
result := p.visitClass(stmt.Loc, &s.Class, ast.InvalidRef, "")
// Remove the export flag inside a namespace
var nameToExport string
wasExportInsideNamespace := s.IsExport && p.enclosingNamespaceArgRef != nil
if wasExportInsideNamespace {
nameToExport = p.symbols[s.Class.Name.Ref.InnerIndex].OriginalName
s.IsExport = false
}
// Lower class field syntax for browsers that don't support it
classStmts, _ := p.lowerClass(stmt, js_ast.Expr{}, result, "")
// Remember if the class was side-effect free before lowering
if result.canBeRemovedIfUnused {
for _, classStmt := range classStmts {
if s2, ok := classStmt.Data.(*js_ast.SExpr); ok {
s2.IsFromClassOrFnThatCanBeRemovedIfUnused = true
}
}
}
stmts = append(stmts, classStmts...)
// Handle exporting this class from a namespace
if wasExportInsideNamespace {
stmts = append(stmts, js_ast.AssignStmt(
js_ast.Expr{Loc: stmt.Loc, Data: p.dotOrMangledPropVisit(
js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: *p.enclosingNamespaceArgRef}},
nameToExport,
s.Class.Name.Loc,
)},
js_ast.Expr{Loc: s.Class.Name.Loc, Data: &js_ast.EIdentifier{Ref: s.Class.Name.Ref}},
))
}
return stmts
case *js_ast.SEnum:
// Do not end the const local prefix after TypeScript enums. We process
// them first within their scope so that they are inlined into all code in
// that scope. We don't want that to cause the const local prefix to end.
p.currentScope.IsAfterConstLocalPrefix = wasAfterAfterConstLocalPrefix
// Track cross-module enum constants during bundling
var tsTopLevelEnumValues map[string]js_ast.TSEnumValue
if p.currentScope == p.moduleScope && p.options.mode == config.ModeBundle {
tsTopLevelEnumValues = make(map[string]js_ast.TSEnumValue)
}
p.recordDeclaredSymbol(s.Name.Ref)
p.pushScopeForVisitPass(js_ast.ScopeEntry, stmt.Loc)
p.recordDeclaredSymbol(s.Arg)
// Scan ahead for any variables inside this namespace. This must be done
// ahead of time before visiting any statements inside the namespace
// because we may end up visiting the uses before the declarations.
// We need to convert the uses into property accesses on the namespace.
for _, value := range s.Values {
if value.Ref != ast.InvalidRef {
p.isExportedInsideNamespace[value.Ref] = s.Arg
}
}
// Values without initializers are initialized to one more than the
// previous value if the previous value is numeric. Otherwise values
// without initializers are initialized to undefined.
nextNumericValue := float64(0)
hasNumericValue := true
valueExprs := []js_ast.Expr{}
allValuesArePure := true
// Update the exported members of this enum as we constant fold each one
exportedMembers := p.currentScope.TSNamespace.ExportedMembers
// We normally don't fold numeric constants because they might increase code
// size, but it's important to fold numeric constants inside enums since
// that's what the TypeScript compiler does.
oldShouldFoldTypeScriptConstantExpressions := p.shouldFoldTypeScriptConstantExpressions
p.shouldFoldTypeScriptConstantExpressions = true
// Create an assignment for each enum value
for _, value := range s.Values {
name := helpers.UTF16ToString(value.Name)
var assignTarget js_ast.Expr
hasStringValue := false
if value.ValueOrNil.Data != nil {
value.ValueOrNil = p.visitExpr(value.ValueOrNil)
hasNumericValue = false
// "See through" any wrapped comments
underlyingValue := value.ValueOrNil
if inlined, ok := value.ValueOrNil.Data.(*js_ast.EInlinedEnum); ok {
underlyingValue = inlined.Value
}
switch e := underlyingValue.Data.(type) {
case *js_ast.ENumber:
if tsTopLevelEnumValues != nil {
tsTopLevelEnumValues[name] = js_ast.TSEnumValue{Number: e.Value}
}
member := exportedMembers[name]
member.Data = &js_ast.TSNamespaceMemberEnumNumber{Value: e.Value}
exportedMembers[name] = member
p.refToTSNamespaceMemberData[value.Ref] = member.Data
hasNumericValue = true
nextNumericValue = e.Value + 1
case *js_ast.EString:
if tsTopLevelEnumValues != nil {
tsTopLevelEnumValues[name] = js_ast.TSEnumValue{String: e.Value}
}
member := exportedMembers[name]
member.Data = &js_ast.TSNamespaceMemberEnumString{Value: e.Value}
exportedMembers[name] = member
p.refToTSNamespaceMemberData[value.Ref] = member.Data
hasStringValue = true
default:
if js_ast.KnownPrimitiveType(underlyingValue.Data) == js_ast.PrimitiveString {
hasStringValue = true
}
if !p.astHelpers.ExprCanBeRemovedIfUnused(underlyingValue) {
allValuesArePure = false
}
}
} else if hasNumericValue {
if tsTopLevelEnumValues != nil {
tsTopLevelEnumValues[name] = js_ast.TSEnumValue{Number: nextNumericValue}
}
member := exportedMembers[name]
member.Data = &js_ast.TSNamespaceMemberEnumNumber{Value: nextNumericValue}
exportedMembers[name] = member
p.refToTSNamespaceMemberData[value.Ref] = member.Data
value.ValueOrNil = js_ast.Expr{Loc: value.Loc, Data: &js_ast.ENumber{Value: nextNumericValue}}
nextNumericValue++
} else {
value.ValueOrNil = js_ast.Expr{Loc: value.Loc, Data: js_ast.EUndefinedShared}
}
if p.options.minifySyntax && js_ast.IsIdentifier(name) {
// "Enum.Name = value"
assignTarget = js_ast.Assign(
js_ast.Expr{Loc: value.Loc, Data: &js_ast.EDot{
Target: js_ast.Expr{Loc: value.Loc, Data: &js_ast.EIdentifier{Ref: s.Arg}},
Name: name,
NameLoc: value.Loc,
}},
value.ValueOrNil,
)
} else {
// "Enum['Name'] = value"
assignTarget = js_ast.Assign(
js_ast.Expr{Loc: value.Loc, Data: &js_ast.EIndex{
Target: js_ast.Expr{Loc: value.Loc, Data: &js_ast.EIdentifier{Ref: s.Arg}},
Index: js_ast.Expr{Loc: value.Loc, Data: &js_ast.EString{Value: value.Name}},
}},
value.ValueOrNil,
)
}
p.recordUsage(s.Arg)
// String-valued enums do not form a two-way map
if hasStringValue {
valueExprs = append(valueExprs, assignTarget)
} else {
// "Enum[assignTarget] = 'Name'"
valueExprs = append(valueExprs, js_ast.Assign(
js_ast.Expr{Loc: value.Loc, Data: &js_ast.EIndex{
Target: js_ast.Expr{Loc: value.Loc, Data: &js_ast.EIdentifier{Ref: s.Arg}},
Index: assignTarget,
}},
js_ast.Expr{Loc: value.Loc, Data: &js_ast.EString{Value: value.Name}},
))
p.recordUsage(s.Arg)
}
}
p.popScope()
p.shouldFoldTypeScriptConstantExpressions = oldShouldFoldTypeScriptConstantExpressions
// Track all exported top-level enums for cross-module inlining
if tsTopLevelEnumValues != nil {
if p.tsEnums == nil {
p.tsEnums = make(map[ast.Ref]map[string]js_ast.TSEnumValue)
}
p.tsEnums[s.Name.Ref] = tsTopLevelEnumValues
}
// Wrap this enum definition in a closure
stmts = p.generateClosureForTypeScriptEnum(
stmts, stmt.Loc, s.IsExport, s.Name.Loc, s.Name.Ref, s.Arg, valueExprs, allValuesArePure)
return stmts
case *js_ast.SNamespace:
p.recordDeclaredSymbol(s.Name.Ref)
// Scan ahead for any variables inside this namespace. This must be done
// ahead of time before visiting any statements inside the namespace
// because we may end up visiting the uses before the declarations.
// We need to convert the uses into property accesses on the namespace.
for _, childStmt := range s.Stmts {
if local, ok := childStmt.Data.(*js_ast.SLocal); ok {
if local.IsExport {
js_ast.ForEachIdentifierBindingInDecls(local.Decls, func(loc logger.Loc, b *js_ast.BIdentifier) {
p.isExportedInsideNamespace[b.Ref] = s.Arg
})
}
}
}
oldEnclosingNamespaceArgRef := p.enclosingNamespaceArgRef
p.enclosingNamespaceArgRef = &s.Arg
p.pushScopeForVisitPass(js_ast.ScopeEntry, stmt.Loc)
p.recordDeclaredSymbol(s.Arg)
stmtsInsideNamespace := p.visitStmtsAndPrependTempRefs(s.Stmts, prependTempRefsOpts{kind: stmtsFnBody})
p.popScope()
p.enclosingNamespaceArgRef = oldEnclosingNamespaceArgRef
// Generate a closure for this namespace
stmts = p.generateClosureForTypeScriptNamespaceOrEnum(
stmts, stmt.Loc, s.IsExport, s.Name.Loc, s.Name.Ref, s.Arg, stmtsInsideNamespace)
return stmts
default:
panic("Internal error")
}
stmts = append(stmts, stmt)
return stmts
}
func (p *parser) minifySwitchStmt(loc logger.Loc, s *js_ast.SSwitch, stmts []js_ast.Stmt) []js_ast.Stmt {
// Trim empty cases before a trailing default clause
if len(s.Cases) > 0 {
if i := len(s.Cases) - 1; s.Cases[i].ValueOrNil.Data == nil {
// "switch (x) { case 0: default: y() }" => "switch (x) { default: y() }"
for i > 0 && len(s.Cases[i-1].Body) == 0 && js_ast.IsPrimitiveLiteral(s.Cases[i-1].ValueOrNil.Data) {
i--
}
s.Cases = append(s.Cases[:i], s.Cases[len(s.Cases)-1])
}
}
// Attempt to partially-evaluate statically-determined switch statements
if js_ast.IsPrimitiveLiteral(s.Test.Data) {
allCasesArePrimitives := true
defaultIndex := -1
// Pass 1: Check for primitives and find the "default" case
for i, c := range s.Cases {
if c.ValueOrNil.Data == nil {
defaultIndex = i
} else if !js_ast.IsPrimitiveLiteral(c.ValueOrNil.Data) {
allCasesArePrimitives = false
}
}
// To simplify analysis, only continue when all cases are primitives
if allCasesArePrimitives {
takenIndex := -1
// Find the case that compares equal and will be taken
for i, c := range s.Cases {
if isEqualToTest, ok := js_ast.CheckEqualityIfNoSideEffects(s.Test.Data, c.ValueOrNil.Data, js_ast.StrictEquality); ok && isEqualToTest {
takenIndex = i
break
}
}
if takenIndex == -1 {
takenIndex = defaultIndex
}
// Partially evaluate the cases
if takenIndex != -1 {
isFallThrough := false
liveIndex := -1
end := 0
for i, c := range s.Cases {
isTaken := i == takenIndex
if isTaken {
liveIndex = end
}
if isFallThrough {
live := &s.Cases[liveIndex]
live.Body = append(live.Body, c.Body...)
} else if isTaken || len(c.Body) > 0 {
s.Cases[end] = c
end++
}
if isTaken || isFallThrough {
isFallThrough = caseBodyCouldHaveFallThrough(c.Body)
}
}
s.Cases = s.Cases[:end]
}
}
}
// Handle empty switch statements
if len(s.Cases) == 0 {
if p.astHelpers.ExprCanBeRemovedIfUnused(s.Test) {
// Remove everything
return stmts
} else {
// Just keep the test expression
return append(stmts, js_ast.Stmt{Loc: s.Test.Loc, Data: &js_ast.SExpr{Value: s.Test}})
}
}
// Handle a switch statement containing only a "default" clause
if len(s.Cases) == 1 {
if c := s.Cases[0]; c.ValueOrNil.Data == nil && p.astHelpers.ExprCanBeRemovedIfUnused(s.Test) {
if body, ok := tryToInlineCaseBody(s.BodyLoc, c.Body, s.CloseBraceLoc); ok {
return append(stmts, body...)
}
}
}
// Try to turn this into an if-else statement
var yesCase js_ast.Case
var noCase js_ast.Case
if len(s.Cases) == 1 {
if yes := s.Cases[0]; yes.ValueOrNil.Data != nil {
yesCase = yes
}
} else if len(s.Cases) == 2 {
// "switch (x) { case y: a(); break; default: b() }"
if yes := s.Cases[0]; yes.ValueOrNil.Data != nil && !caseBodyCouldHaveFallThrough(yes.Body) {
if no := s.Cases[1]; no.ValueOrNil.Data == nil {
yesCase = yes
noCase = no
}
}
// "switch (x) { default: a(); break; case y: b() }"
if no := s.Cases[0]; no.ValueOrNil.Data == nil && !caseBodyCouldHaveFallThrough(no.Body) {
if yes := s.Cases[1]; yes.ValueOrNil.Data != nil {
yesCase = yes
noCase = no
}
}
}
if yesCase.ValueOrNil.Data != nil {
if yesBody, ok := tryToInlineCaseBody(s.BodyLoc, yesCase.Body, s.CloseBraceLoc); ok {
if noBody, ok := tryToInlineCaseBody(s.BodyLoc, noCase.Body, s.CloseBraceLoc); ok {
ifElse := js_ast.SIf{
Test: js_ast.Expr{Loc: s.Test.Loc},
Yes: stmtsToSingleStmt(yesCase.Loc, yesBody, logger.Loc{}),
}
if isEqualToTest, ok := js_ast.CheckEqualityIfNoSideEffects(s.Test.Data, yesCase.ValueOrNil.Data, js_ast.StrictEquality); ok {
ifElse.Test.Data = &js_ast.EBoolean{Value: isEqualToTest}
} else {
ifElse.Test.Data = &js_ast.EBinary{Op: js_ast.BinOpStrictEq, Left: s.Test, Right: yesCase.ValueOrNil}
}
if len(noBody) > 0 {
ifElse.NoOrNil = stmtsToSingleStmt(noCase.Loc, noBody, logger.Loc{})
}
return p.mangleIf(stmts, loc, &ifElse)
}
}
}
return append(stmts, js_ast.Stmt{Loc: loc, Data: s})
}
func tryToInlineCaseBody(openBraceLoc logger.Loc, stmts []js_ast.Stmt, closeBraceLoc logger.Loc) ([]js_ast.Stmt, bool) {
if len(stmts) == 1 {
if block, ok := stmts[0].Data.(*js_ast.SBlock); ok {
return tryToInlineCaseBody(stmts[0].Loc, block.Stmts, block.CloseBraceLoc)
}
}
caresAboutScope := false
loop:
for i, stmt := range stmts {
switch s := stmt.Data.(type) {
case *js_ast.SEmpty, *js_ast.SDirective, *js_ast.SComment, *js_ast.SExpr,
*js_ast.SDebugger, *js_ast.SContinue, *js_ast.SReturn, *js_ast.SThrow:
// These can all be inlined outside of the switch without problems
continue
case *js_ast.SLocal:
if s.Kind != js_ast.LocalVar {
caresAboutScope = true
}
case *js_ast.SBreak:
if s.Label != nil {
// The break label could target this switch, but we don't know whether that's the case or not here
return nil, false
}
// An unlabeled "break" inside a switch breaks out of the case
stmts = stmts[:i]
break loop
default:
// Assume anything else can't be inlined
return nil, false
}
}
// If we still need a scope, wrap the result in a block
if caresAboutScope {
return []js_ast.Stmt{{Loc: openBraceLoc, Data: &js_ast.SBlock{Stmts: stmts, CloseBraceLoc: closeBraceLoc}}}, true
}
return stmts, true
}
func isUnsightlyPrimitive(data js_ast.E) bool {
switch data.(type) {
case *js_ast.EBoolean, *js_ast.ENull, *js_ast.EUndefined, *js_ast.ENumber, *js_ast.EBigInt, *js_ast.EString:
return true
}
return false
}
// If we encounter a variable initializer that could possibly trigger access to
// a constant declared later on, then we need to end the const local prefix.
// We want to avoid situations like this:
//
// const x = y; // This is supposed to throw due to TDZ
// const y = 1;
//
// or this:
//
// const x = 1;
// const y = foo(); // This is supposed to throw due to TDZ
// const z = 2;
// const foo = () => z;
//
// But a situation like this is ok:
//
// const x = 1;
// const y = [() => x + z];
// const z = 2;
func isSafeForConstLocalPrefix(expr js_ast.Expr) bool {
switch e := expr.Data.(type) {
case *js_ast.EMissing, *js_ast.EString, *js_ast.ERegExp, *js_ast.EBigInt, *js_ast.EFunction, *js_ast.EArrow:
return true
case *js_ast.EArray:
for _, item := range e.Items {
if !isSafeForConstLocalPrefix(item) {
return false
}
}
return true
case *js_ast.EObject:
// For now just allow "{}" and forbid everything else
return len(e.Properties) == 0
}
return false
}
type relocateVarsMode uint8
const (
relocateVarsNormal relocateVarsMode = iota
relocateVarsForInOrForOf
)
// If we are currently in a hoisted child of the module scope, relocate these
// declarations to the top level and return an equivalent assignment statement.
// Make sure to check that the declaration kind is "var" before calling this.
// And make sure to check that the returned statement is not the zero value.
//
// This is done to make it easier to traverse top-level declarations in the linker
// during bundling. Now it is sufficient to just scan the top-level statements
// instead of having to traverse recursively into the statement tree.
func (p *parser) maybeRelocateVarsToTopLevel(decls []js_ast.Decl, mode relocateVarsMode) (js_ast.Stmt, bool) {
// Only do this when bundling, and not when the scope is already top-level
if p.options.mode != config.ModeBundle || (p.currentScope == p.moduleScope && p.singleStmtDepth == 0) {
return js_ast.Stmt{}, false
}
// Only do this if we're not inside a function
scope := p.currentScope
for !scope.Kind.StopsHoisting() {
scope = scope.Parent
}
if scope != p.moduleScope {
return js_ast.Stmt{}, false
}
// Convert the declarations to assignments
wrapIdentifier := func(loc logger.Loc, ref ast.Ref) js_ast.Expr {
p.relocatedTopLevelVars = append(p.relocatedTopLevelVars, ast.LocRef{Loc: loc, Ref: ref})
p.recordUsage(ref)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: ref}}
}
var value js_ast.Expr
for _, decl := range decls {
binding := js_ast.ConvertBindingToExpr(decl.Binding, wrapIdentifier)
if decl.ValueOrNil.Data != nil {
value = js_ast.JoinWithComma(value, js_ast.Assign(binding, decl.ValueOrNil))
} else if mode == relocateVarsForInOrForOf {
value = js_ast.JoinWithComma(value, binding)
}
}
if value.Data == nil {
// If none of the variables had any initializers, just remove the declarations
return js_ast.Stmt{}, true
}
return js_ast.Stmt{Loc: value.Loc, Data: &js_ast.SExpr{Value: value}}, true
}
func (p *parser) markExprAsParenthesized(value js_ast.Expr, openParenLoc logger.Loc, isAsync bool) {
// Don't lose comments due to parentheses. For example, we don't want to lose
// the comment here:
//
// ( /* comment */ (foo) );
//
if !isAsync {
if comments, ok := p.exprComments[openParenLoc]; ok {
delete(p.exprComments, openParenLoc)
p.exprComments[value.Loc] = append(comments, p.exprComments[value.Loc]...)
}
}
switch e := value.Data.(type) {
case *js_ast.EArray:
e.IsParenthesized = true
case *js_ast.EObject:
e.IsParenthesized = true
case *js_ast.EFunction:
e.IsParenthesized = true
case *js_ast.EArrow:
e.IsParenthesized = true
case *js_ast.EBinary:
e.IsParenthesized = true
}
}
func (p *parser) maybeTransposeIfExprChain(expr js_ast.Expr, visit func(js_ast.Expr) js_ast.Expr) js_ast.Expr {
if e, ok := expr.Data.(*js_ast.EIf); ok {
e.Yes = p.maybeTransposeIfExprChain(e.Yes, visit)
e.No = p.maybeTransposeIfExprChain(e.No, visit)
return expr
}
return visit(expr)
}
func (p *parser) maybeInlineIIFE(loc logger.Loc, e *js_ast.ECall) (js_ast.Expr, bool) {
if len(e.Args) != 0 {
return js_ast.Expr{}, false
}
// Note: Do not inline async arrow functions as they are not IIFEs. In
// particular, they are not necessarily invoked immediately, and any
// exceptions involved in their evaluation will be swallowed without
// bubbling up to the surrounding context.
if arrow, ok := e.Target.Data.(*js_ast.EArrow); ok && len(arrow.Args) == 0 && !arrow.IsAsync {
stmts := arrow.Body.Block.Stmts
// "(() => {})()" => "void 0"
if len(stmts) == 0 {
return js_ast.Expr{Loc: loc, Data: js_ast.EUndefinedShared}, true
}
if len(stmts) == 1 {
var value js_ast.Expr
switch s := stmts[0].Data.(type) {
// "(() => { return })()" => "void 0"
// "(() => { return 123 })()" => "123"
case *js_ast.SReturn:
value = s.ValueOrNil
if value.Data == nil {
value.Data = js_ast.EUndefinedShared
}
// "(() => { x })()" => "void x"
case *js_ast.SExpr:
value = s.Value
value.Data = &js_ast.EUnary{Op: js_ast.UnOpVoid, Value: value}
}
if value.Data != nil {
// Be careful about "/* @__PURE__ */" comments:
//
// OK: "(() => x)()" => "x"
// BAD: "/* @__PURE__ */ (() => x)()" => "x"
//
// The comment indicates that the function body is eligible for
// dead code elimination. Since we don't have a direct AST node
// for that, we can't currently unwrap that and preserve the
// intent. So if it does have a pure comment, only remove it if
// the value itself is already pure.
if !e.CanBeUnwrappedIfUnused || p.astHelpers.ExprCanBeRemovedIfUnused(value) {
return value, true
}
}
}
}
return js_ast.Expr{}, false
}
func (p *parser) iifeCanBeRemovedIfUnused(args []js_ast.Arg, body js_ast.FnBody) bool {
for _, arg := range args {
if arg.DefaultOrNil.Data != nil && !p.astHelpers.ExprCanBeRemovedIfUnused(arg.DefaultOrNil) {
// The default value has a side effect
return false
}
if _, ok := arg.Binding.Data.(*js_ast.BIdentifier); !ok {
// Destructuring is a side effect (due to property access)
return false
}
}
// Check whether any statements have side effects or not. Consider return
// statements as not having side effects because if the IIFE can be removed
// then we know the return value is unused, so we know that returning the
// value has no side effects.
return p.astHelpers.StmtsCanBeRemovedIfUnused(body.Block.Stmts, js_ast.ReturnCanBeRemovedIfUnused)
}
type captureValueMode uint8
const (
valueDefinitelyNotMutated captureValueMode = iota
valueCouldBeMutated
)
// This is a helper function to use when you need to capture a value that may
// have side effects so you can use it multiple times. It guarantees that the
// side effects take place exactly once.
//
// Example usage:
//
// // "value" => "value + value"
// // "value()" => "(_a = value(), _a + _a)"
// valueFunc, wrapFunc := p.captureValueWithPossibleSideEffects(loc, 2, value)
// return wrapFunc(js_ast.Expr{Loc: loc, Data: &js_ast.EBinary{
// Op: js_ast.BinOpAdd,
// Left: valueFunc(),
// Right: valueFunc(),
// }})
//
// This returns a function for generating references instead of a raw reference
// because AST nodes are supposed to be unique in memory, not aliases of other
// AST nodes. That way you can mutate one during lowering without having to
// worry about messing up other nodes.
func (p *parser) captureValueWithPossibleSideEffects(
loc logger.Loc, // The location to use for the generated references
count int, // The expected number of references to generate
value js_ast.Expr, // The value that might have side effects
mode captureValueMode, // Say if "value" might be mutated and must be captured
) (
func() js_ast.Expr, // Generates reference expressions "_a"
func(js_ast.Expr) js_ast.Expr, // Call this on the final expression
) {
wrapFunc := func(expr js_ast.Expr) js_ast.Expr {
// Make sure side effects still happen if no expression was generated
if expr.Data == nil {
return value
}
return expr
}
// Referencing certain expressions more than once has no side effects, so we
// can just create them inline without capturing them in a temporary variable
var valueFunc func() js_ast.Expr
switch e := value.Data.(type) {
case *js_ast.ENull:
valueFunc = func() js_ast.Expr { return js_ast.Expr{Loc: loc, Data: js_ast.ENullShared} }
case *js_ast.EUndefined:
valueFunc = func() js_ast.Expr { return js_ast.Expr{Loc: loc, Data: js_ast.EUndefinedShared} }
case *js_ast.EThis:
valueFunc = func() js_ast.Expr { return js_ast.Expr{Loc: loc, Data: js_ast.EThisShared} }
case *js_ast.EBoolean:
valueFunc = func() js_ast.Expr { return js_ast.Expr{Loc: loc, Data: &js_ast.EBoolean{Value: e.Value}} }
case *js_ast.ENumber:
valueFunc = func() js_ast.Expr { return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: e.Value}} }
case *js_ast.EBigInt:
valueFunc = func() js_ast.Expr { return js_ast.Expr{Loc: loc, Data: &js_ast.EBigInt{Value: e.Value}} }
case *js_ast.EString:
valueFunc = func() js_ast.Expr { return js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: e.Value}} }
case *js_ast.EPrivateIdentifier:
valueFunc = func() js_ast.Expr { return js_ast.Expr{Loc: loc, Data: &js_ast.EPrivateIdentifier{Ref: e.Ref}} }
case *js_ast.EIdentifier:
if mode == valueDefinitelyNotMutated {
valueFunc = func() js_ast.Expr {
// Make sure we record this usage in the usage count so that duplicating
// a single-use reference means it's no longer considered a single-use
// reference. Otherwise the single-use reference inlining code may
// incorrectly inline the initializer into the first reference, leaving
// the second reference without a definition.
p.recordUsage(e.Ref)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: e.Ref}}
}
}
}
if valueFunc != nil {
return valueFunc, wrapFunc
}
// We don't need to worry about side effects if the value won't be used
// multiple times. This special case lets us avoid generating a temporary
// reference.
if count < 2 {
return func() js_ast.Expr {
return value
}, wrapFunc
}
// Otherwise, fall back to generating a temporary reference
tempRef := ast.InvalidRef
// If we're in a function argument scope, then we won't be able to generate
// symbols in this scope to store stuff, since there's nowhere to put the
// variable declaration. We don't want to put the variable declaration
// outside the function since some code in the argument list may cause the
// function to be reentrant, and we can't put the variable declaration in
// the function body since that's not accessible by the argument list.
//
// Instead, we use an immediately-invoked arrow function to create a new
// symbol inline by introducing a new scope. Make sure to only use it for
// symbol declaration and still initialize the variable inline to preserve
// side effect order.
if p.currentScope.Kind == js_ast.ScopeFunctionArgs {
return func() js_ast.Expr {
if tempRef == ast.InvalidRef {
tempRef = p.generateTempRef(tempRefNoDeclare, "")
// Assign inline so the order of side effects remains the same
p.recordUsage(tempRef)
return js_ast.Assign(js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: tempRef}}, value)
}
p.recordUsage(tempRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: tempRef}}
}, func(expr js_ast.Expr) js_ast.Expr {
// Make sure side effects still happen if no expression was generated
if expr.Data == nil {
return value
}
// Generate a new variable using an arrow function to avoid messing with "this"
return js_ast.Expr{Loc: loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: loc, Data: &js_ast.EArrow{
Args: []js_ast.Arg{{Binding: js_ast.Binding{Loc: loc, Data: &js_ast.BIdentifier{Ref: tempRef}}}},
PreferExpr: true,
Body: js_ast.FnBody{Loc: loc, Block: js_ast.SBlock{Stmts: []js_ast.Stmt{{Loc: loc, Data: &js_ast.SReturn{ValueOrNil: expr}}}}},
}},
}}
}
}
return func() js_ast.Expr {
if tempRef == ast.InvalidRef {
tempRef = p.generateTempRef(tempRefNeedsDeclare, "")
p.recordUsage(tempRef)
return js_ast.Assign(js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: tempRef}}, value)
}
p.recordUsage(tempRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: tempRef}}
}, wrapFunc
}
func (p *parser) visitDecorators(decorators []js_ast.Decorator, decoratorScope *js_ast.Scope) []js_ast.Decorator {
if decorators != nil {
// Decorators cause us to temporarily revert to the scope that encloses the
// class declaration, since that's where the generated code for decorators
// will be inserted. I believe this currently only matters for parameter
// decorators, where the scope should not be within the argument list.
oldScope := p.currentScope
p.currentScope = decoratorScope
for i, decorator := range decorators {
decorators[i].Value = p.visitExpr(decorator.Value)
}
// Avoid "popScope" because this decorator scope is not hierarchical
p.currentScope = oldScope
}
return decorators
}
type visitClassResult struct {
bodyScope *js_ast.Scope
innerClassNameRef ast.Ref
superCtorRef ast.Ref
// If true, the class was determined to be safe to remove if the class is
// never used (i.e. the class definition is side-effect free). This is
// determined after visiting but before lowering since lowering may generate
// class mutations that cannot be automatically analyzed as side-effect free.
canBeRemovedIfUnused bool
}
func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class, defaultNameRef ast.Ref, nameToKeep string) (result visitClassResult) {
class.Decorators = p.visitDecorators(class.Decorators, p.currentScope)
if class.Name != nil {
p.recordDeclaredSymbol(class.Name.Ref)
if p.options.keepNames {
nameToKeep = p.symbols[class.Name.Ref.InnerIndex].OriginalName
}
}
// Replace "this" with a reference to the class inside static field
// initializers if static fields are being lowered, since that relocates the
// field initializers outside of the class body and "this" will no longer
// reference the same thing.
classLoweringInfo := p.computeClassLoweringInfo(class)
recomputeClassLoweringInfo := false
// Sometimes we need to lower private members even though they are supported.
// This flags them for lowering so that we lower references to them as we
// traverse the class body.
//
// We don't need to worry about possible references to the class shadowing
// symbol inside the class body changing our decision to lower private members
// later on because that shouldn't be possible.
if classLoweringInfo.lowerAllStaticFields {
for _, prop := range class.Properties {
// We need to lower all private members if fields of that type are lowered,
// not just private fields (methods and accessors too):
//
// class Foo {
// get #foo() {}
// static bar = new Foo().#foo
// }
//
// We can't transform that to this:
//
// class Foo {
// get #foo() {}
// }
// Foo.bar = new Foo().#foo;
//
// The private getter must be lowered too.
if private, ok := prop.Key.Data.(*js_ast.EPrivateIdentifier); ok {
p.symbols[private.Ref.InnerIndex].Flags |= ast.PrivateSymbolMustBeLowered
recomputeClassLoweringInfo = true
}
}
}
// Conservatively lower all private names that have been used in a private
// brand check anywhere in the file. See the comment on this map for details.
if p.lowerAllOfThesePrivateNames != nil {
for _, prop := range class.Properties {
if private, ok := prop.Key.Data.(*js_ast.EPrivateIdentifier); ok {
if symbol := &p.symbols[private.Ref.InnerIndex]; p.lowerAllOfThesePrivateNames[symbol.OriginalName] {
symbol.Flags |= ast.PrivateSymbolMustBeLowered
recomputeClassLoweringInfo = true
}
}
}
}
// If we changed private symbol lowering decisions, then recompute class
// lowering info because that may have changed other decisions too
if recomputeClassLoweringInfo {
classLoweringInfo = p.computeClassLoweringInfo(class)
}
p.pushScopeForVisitPass(js_ast.ScopeClassName, nameScopeLoc)
oldEnclosingClassKeyword := p.enclosingClassKeyword
p.enclosingClassKeyword = class.ClassKeyword
p.currentScope.RecursiveSetStrictMode(js_ast.ImplicitStrictModeClass)
if class.Name != nil {
p.validateDeclaredSymbolName(class.Name.Loc, p.symbols[class.Name.Ref.InnerIndex].OriginalName)
}
// Create the "__super" symbol if necessary. This will cause us to replace
// all "super()" call expressions with a call to this symbol, which will
// then be inserted into the "constructor" method.
result.superCtorRef = ast.InvalidRef
if classLoweringInfo.shimSuperCtorCalls {
result.superCtorRef = p.newSymbol(ast.SymbolOther, "__super")
p.currentScope.Generated = append(p.currentScope.Generated, result.superCtorRef)
p.recordDeclaredSymbol(result.superCtorRef)
}
oldSuperCtorRef := p.superCtorRef
p.superCtorRef = result.superCtorRef
// Insert an immutable inner name that spans the whole class to match
// JavaScript's semantics specifically the "CreateImmutableBinding" here:
// https://262.ecma-international.org/6.0/#sec-runtime-semantics-classdefinitionevaluation
// The class body (and extends clause) "captures" the original value of the
// class name. This matters for class statements because the symbol can be
// re-assigned to something else later. The captured values must be the
// original value of the name, not the re-assigned value. Use "const" for
// this symbol to match JavaScript run-time semantics. You are not allowed
// to assign to this symbol (it throws a TypeError).
if class.Name != nil {
name := p.symbols[class.Name.Ref.InnerIndex].OriginalName
result.innerClassNameRef = p.newSymbol(ast.SymbolConst, "_"+name)
p.currentScope.Members[name] = js_ast.ScopeMember{Loc: class.Name.Loc, Ref: result.innerClassNameRef}
} else {
name := "_this"
if defaultNameRef != ast.InvalidRef {
name = "_" + p.source.IdentifierName + "_default"
}
result.innerClassNameRef = p.newSymbol(ast.SymbolConst, name)
}
p.recordDeclaredSymbol(result.innerClassNameRef)
if class.ExtendsOrNil.Data != nil {
class.ExtendsOrNil = p.visitExpr(class.ExtendsOrNil)
}
// A scope is needed for private identifiers
p.pushScopeForVisitPass(js_ast.ScopeClassBody, class.BodyLoc)
result.bodyScope = p.currentScope
for i := range class.Properties {
property := &class.Properties[i]
if property.Kind == js_ast.PropertyClassStaticBlock {
oldFnOrArrowData := p.fnOrArrowDataVisit
oldFnOnlyDataVisit := p.fnOnlyDataVisit
p.fnOrArrowDataVisit = fnOrArrowDataVisit{}
p.fnOnlyDataVisit = fnOnlyDataVisit{
isThisNested: true,
isNewTargetAllowed: true,
isInStaticClassContext: true,
innerClassNameRef: &result.innerClassNameRef,
}
if classLoweringInfo.lowerAllStaticFields {
// Need to lower "this" and "super" since they won't be valid outside the class body
p.fnOnlyDataVisit.shouldReplaceThisWithInnerClassNameRef = true
p.fnOrArrowDataVisit.shouldLowerSuperPropertyAccess = true
}
p.pushScopeForVisitPass(js_ast.ScopeClassStaticInit, property.ClassStaticBlock.Loc)
// Make it an error to use "arguments" in a static class block
p.currentScope.ForbidArguments = true
property.ClassStaticBlock.Block.Stmts = p.visitStmts(property.ClassStaticBlock.Block.Stmts, stmtsFnBody)
p.popScope()
p.fnOrArrowDataVisit = oldFnOrArrowData
p.fnOnlyDataVisit = oldFnOnlyDataVisit
continue
}
property.Decorators = p.visitDecorators(property.Decorators, result.bodyScope)
// Visit the property key
if private, ok := property.Key.Data.(*js_ast.EPrivateIdentifier); ok {
// Special-case private identifiers here
p.recordDeclaredSymbol(private.Ref)
} else {
// It's forbidden to reference the class name in a computed key
if property.Flags.Has(js_ast.PropertyIsComputed) && class.Name != nil {
p.symbols[result.innerClassNameRef.InnerIndex].Kind = ast.SymbolClassInComputedPropertyKey
}
key, _ := p.visitExprInOut(property.Key, exprIn{
shouldMangleStringsAsProps: true,
})
property.Key = key
// Re-allow using the class name after visiting a computed key
if property.Flags.Has(js_ast.PropertyIsComputed) && class.Name != nil {
p.symbols[result.innerClassNameRef.InnerIndex].Kind = ast.SymbolConst
}
if p.options.minifySyntax {
if inlined, ok := key.Data.(*js_ast.EInlinedEnum); ok {
switch inlined.Value.Data.(type) {
case *js_ast.EString, *js_ast.ENumber:
key.Data = inlined.Value.Data
property.Key.Data = key.Data
}
}
switch k := key.Data.(type) {
case *js_ast.ENumber, *js_ast.ENameOfSymbol:
// "class { [123] }" => "class { 123 }"
property.Flags &= ^js_ast.PropertyIsComputed
case *js_ast.EString:
if numberValue, ok := js_ast.StringToEquivalentNumberValue(k.Value); ok && numberValue >= 0 {
// "class { '123' }" => "class { 123 }"
property.Key.Data = &js_ast.ENumber{Value: numberValue}
property.Flags &= ^js_ast.PropertyIsComputed
} else if property.Flags.Has(js_ast.PropertyIsComputed) {
// "class {['x'] = y}" => "class {'x' = y}"
isInvalidConstructor := false
if helpers.UTF16EqualsString(k.Value, "constructor") {
if !property.Kind.IsMethodDefinition() {
// "constructor" is an invalid name for both instance and static fields
isInvalidConstructor = true
} else if !property.Flags.Has(js_ast.PropertyIsStatic) {
// Calling an instance method "constructor" is problematic so avoid that too
isInvalidConstructor = true
}
}
// A static property must not be called "prototype"
isInvalidPrototype := property.Flags.Has(js_ast.PropertyIsStatic) && helpers.UTF16EqualsString(k.Value, "prototype")
if !isInvalidConstructor && !isInvalidPrototype {
property.Flags &= ^js_ast.PropertyIsComputed
}
}
}
}
}
// Make it an error to use "arguments" in a class body
p.currentScope.ForbidArguments = true
// The value of "this" and "super" is shadowed inside property values
oldFnOnlyDataVisit := p.fnOnlyDataVisit
oldShouldLowerSuperPropertyAccess := p.fnOrArrowDataVisit.shouldLowerSuperPropertyAccess
p.fnOrArrowDataVisit.shouldLowerSuperPropertyAccess = false
p.fnOnlyDataVisit.shouldReplaceThisWithInnerClassNameRef = false
p.fnOnlyDataVisit.isThisNested = true
p.fnOnlyDataVisit.isNewTargetAllowed = true
p.fnOnlyDataVisit.isInStaticClassContext = property.Flags.Has(js_ast.PropertyIsStatic)
p.fnOnlyDataVisit.innerClassNameRef = &result.innerClassNameRef
// We need to explicitly assign the name to the property initializer if it
// will be transformed such that it is no longer an inline initializer.
nameToKeep := ""
isLoweredPrivateMethod := false
if private, ok := property.Key.Data.(*js_ast.EPrivateIdentifier); ok {
if !property.Kind.IsMethodDefinition() || p.privateSymbolNeedsToBeLowered(private) {
nameToKeep = p.symbols[private.Ref.InnerIndex].OriginalName
}
// Lowered private methods (both instance and static) are initialized
// outside of the class body, so we must rewrite "super" property
// accesses inside them. Lowered private instance fields are initialized
// inside the constructor where "super" is valid, so those don't need to
// be rewritten.
if property.Kind.IsMethodDefinition() && p.privateSymbolNeedsToBeLowered(private) {
isLoweredPrivateMethod = true
}
} else if !property.Kind.IsMethodDefinition() && !property.Flags.Has(js_ast.PropertyIsComputed) {
if str, ok := property.Key.Data.(*js_ast.EString); ok {
nameToKeep = helpers.UTF16ToString(str.Value)
}
}
// Handle methods
if property.ValueOrNil.Data != nil {
p.propMethodDecoratorScope = result.bodyScope
// Propagate the name to keep from the method into the initializer
if nameToKeep != "" {
p.nameToKeep = nameToKeep
p.nameToKeepIsFor = property.ValueOrNil.Data
}
// Propagate whether we're in a derived class constructor
if class.ExtendsOrNil.Data != nil && !property.Flags.Has(js_ast.PropertyIsComputed) {
if str, ok := property.Key.Data.(*js_ast.EString); ok && helpers.UTF16EqualsString(str.Value, "constructor") {
p.propDerivedCtorValue = property.ValueOrNil.Data
}
}
property.ValueOrNil, _ = p.visitExprInOut(property.ValueOrNil, exprIn{
isMethod: true,
isLoweredPrivateMethod: isLoweredPrivateMethod,
})
}
// Handle initialized fields
if property.InitializerOrNil.Data != nil {
if property.Flags.Has(js_ast.PropertyIsStatic) && classLoweringInfo.lowerAllStaticFields {
// Need to lower "this" and "super" since they won't be valid outside the class body
p.fnOnlyDataVisit.shouldReplaceThisWithInnerClassNameRef = true
p.fnOrArrowDataVisit.shouldLowerSuperPropertyAccess = true
}
// Propagate the name to keep from the field into the initializer
if nameToKeep != "" {
p.nameToKeep = nameToKeep
p.nameToKeepIsFor = property.InitializerOrNil.Data
}
property.InitializerOrNil = p.visitExpr(property.InitializerOrNil)
}
// Restore "this" so it will take the inherited value in property keys
p.fnOnlyDataVisit = oldFnOnlyDataVisit
p.fnOrArrowDataVisit.shouldLowerSuperPropertyAccess = oldShouldLowerSuperPropertyAccess
// Restore the ability to use "arguments" in decorators and computed properties
p.currentScope.ForbidArguments = false
}
// Check for and warn about duplicate keys in class bodies
if !p.suppressWarningsAboutWeirdCode {
p.warnAboutDuplicateProperties(class.Properties, duplicatePropertiesInClass)
}
// Analyze side effects before adding the name keeping call
result.canBeRemovedIfUnused = p.astHelpers.ClassCanBeRemovedIfUnused(*class)
// Implement name keeping using a static block at the start of the class body
if p.options.keepNames && nameToKeep != "" {
propertyPreventsKeepNames := false
for _, prop := range class.Properties {
// A static property called "name" shadows the automatically-generated name
if prop.Flags.Has(js_ast.PropertyIsStatic) {
if str, ok := prop.Key.Data.(*js_ast.EString); ok && helpers.UTF16EqualsString(str.Value, "name") {
propertyPreventsKeepNames = true
break
}
}
}
if !propertyPreventsKeepNames {
var this js_ast.Expr
if classLoweringInfo.lowerAllStaticFields {
p.recordUsage(result.innerClassNameRef)
this = js_ast.Expr{Loc: class.BodyLoc, Data: &js_ast.EIdentifier{Ref: result.innerClassNameRef}}
} else {
this = js_ast.Expr{Loc: class.BodyLoc, Data: js_ast.EThisShared}
}
properties := make([]js_ast.Property, 0, 1+len(class.Properties))
properties = append(properties, js_ast.Property{
Kind: js_ast.PropertyClassStaticBlock,
ClassStaticBlock: &js_ast.ClassStaticBlock{Loc: class.BodyLoc, Block: js_ast.SBlock{Stmts: []js_ast.Stmt{
p.keepClassOrFnSymbolName(class.BodyLoc, this, nameToKeep),
}}},
})
class.Properties = append(properties, class.Properties...)
}
}
p.enclosingClassKeyword = oldEnclosingClassKeyword
p.superCtorRef = oldSuperCtorRef
p.popScope()
if p.symbols[result.innerClassNameRef.InnerIndex].UseCountEstimate == 0 {
// Don't generate a shadowing name if one isn't needed
result.innerClassNameRef = ast.InvalidRef
} else if class.Name == nil {
// If there was originally no class name but something inside needed one
// (e.g. there was a static property initializer that referenced "this"),
// populate the class name. If this is an "export default class" statement,
// use the existing default name so that things will work as expected if
// this is turned into a regular class statement later on.
classNameRef := defaultNameRef
if classNameRef == ast.InvalidRef {
classNameRef = p.newSymbol(ast.SymbolOther, "_this")
p.currentScope.Generated = append(p.currentScope.Generated, classNameRef)
p.recordDeclaredSymbol(classNameRef)
}
class.Name = &ast.LocRef{Loc: nameScopeLoc, Ref: classNameRef}
}
p.popScope()
// Sanity check that the class lowering info hasn't changed before and after
// visiting. The class transform relies on this because lowering assumes that
// must be able to expect that visiting has done certain things.
if classLoweringInfo != p.computeClassLoweringInfo(class) {
panic("Internal error")
}
return
}
func isSimpleParameterList(args []js_ast.Arg, hasRestArg bool) bool {
if hasRestArg {
return false
}
for _, arg := range args {
if _, ok := arg.Binding.Data.(*js_ast.BIdentifier); !ok || arg.DefaultOrNil.Data != nil {
return false
}
}
return true
}
func fnBodyContainsUseStrict(body []js_ast.Stmt) (logger.Loc, bool) {
for _, stmt := range body {
switch s := stmt.Data.(type) {
case *js_ast.SComment:
continue
case *js_ast.SDirective:
if helpers.UTF16EqualsString(s.Value, "use strict") {
return stmt.Loc, true
}
default:
return logger.Loc{}, false
}
}
return logger.Loc{}, false
}
type visitArgsOpts struct {
body []js_ast.Stmt
decoratorScope *js_ast.Scope
hasRestArg bool
// This is true if the function is an arrow function or a method
isUniqueFormalParameters bool
}
func (p *parser) visitArgs(args []js_ast.Arg, opts visitArgsOpts) {
var duplicateArgCheck map[string]logger.Range
useStrictLoc, hasUseStrict := fnBodyContainsUseStrict(opts.body)
hasSimpleArgs := isSimpleParameterList(args, opts.hasRestArg)
// Section 15.2.1 Static Semantics: Early Errors: "It is a Syntax Error if
// FunctionBodyContainsUseStrict of FunctionBody is true and
// IsSimpleParameterList of FormalParameters is false."
if hasUseStrict && !hasSimpleArgs {
p.log.AddError(&p.tracker, p.source.RangeOfString(useStrictLoc),
"Cannot use a \"use strict\" directive in a function with a non-simple parameter list")
}
// Section 15.1.1 Static Semantics: Early Errors: "Multiple occurrences of
// the same BindingIdentifier in a FormalParameterList is only allowed for
// functions which have simple parameter lists and which are not defined in
// strict mode code."
if opts.isUniqueFormalParameters || hasUseStrict || !hasSimpleArgs || p.isStrictMode() {
duplicateArgCheck = make(map[string]logger.Range)
}
for i := range args {
arg := &args[i]
arg.Decorators = p.visitDecorators(arg.Decorators, opts.decoratorScope)
p.visitBinding(arg.Binding, bindingOpts{
duplicateArgCheck: duplicateArgCheck,
})
if arg.DefaultOrNil.Data != nil {
arg.DefaultOrNil = p.visitExpr(arg.DefaultOrNil)
}
}
}
func (p *parser) isDotOrIndexDefineMatch(expr js_ast.Expr, parts []string) bool {
switch e := expr.Data.(type) {
case *js_ast.EDot:
if len(parts) > 1 {
// Intermediates must be dot expressions
last := len(parts) - 1
return parts[last] == e.Name && p.isDotOrIndexDefineMatch(e.Target, parts[:last])
}
case *js_ast.EIndex:
if len(parts) > 1 {
if str, ok := e.Index.Data.(*js_ast.EString); ok {
// Intermediates must be dot expressions
last := len(parts) - 1
return parts[last] == helpers.UTF16ToString(str.Value) && p.isDotOrIndexDefineMatch(e.Target, parts[:last])
}
}
case *js_ast.EThis:
// Allow matching on top-level "this"
if !p.fnOnlyDataVisit.isThisNested {
return len(parts) == 1 && parts[0] == "this"
}
case *js_ast.EImportMeta:
// Allow matching on "import.meta"
return len(parts) == 2 && parts[0] == "import" && parts[1] == "meta"
case *js_ast.EIdentifier:
// The last expression must be an identifier
if len(parts) == 1 {
// The name must match
name := p.loadNameFromRef(e.Ref)
if name != parts[0] {
return false
}
result := p.findSymbol(expr.Loc, name)
// The "findSymbol" function also marks this symbol as used. But that's
// never what we want here because we're just peeking to see what kind of
// symbol it is to see if it's a match. If it's not a match, it will be
// re-resolved again later and marked as used there. So we don't want to
// mark it as used twice.
p.ignoreUsage(result.ref)
// We must not be in a "with" statement scope
if result.isInsideWithScope {
return false
}
// The last symbol must be unbound or injected
return p.symbols[result.ref.InnerIndex].Kind.IsUnboundOrInjected()
}
}
return false
}
func (p *parser) instantiateDefineExpr(loc logger.Loc, expr config.DefineExpr, opts identifierOpts) js_ast.Expr {
if expr.Constant != nil {
return js_ast.Expr{Loc: loc, Data: expr.Constant}
}
if expr.InjectedDefineIndex.IsValid() {
ref := p.injectedDefineSymbols[expr.InjectedDefineIndex.GetIndex()]
p.recordUsage(ref)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: ref}}
}
parts := expr.Parts
if len(parts) == 0 {
return js_ast.Expr{}
}
// Check both user-specified defines and known globals
if opts.matchAgainstDefines {
// Make sure define resolution is not recursive
opts.matchAgainstDefines = false
// Substitute user-specified defines
if defines, ok := p.options.defines.DotDefines[parts[len(parts)-1]]; ok {
for _, define := range defines {
if define.DefineExpr != nil && helpers.StringArraysEqual(define.KeyParts, parts) {
return p.instantiateDefineExpr(loc, *define.DefineExpr, opts)
}
}
}
}
// Check injected dot names
if names, ok := p.injectedDotNames[parts[len(parts)-1]]; ok {
for _, name := range names {
if helpers.StringArraysEqual(name.parts, parts) {
return p.instantiateInjectDotName(loc, name, opts.assignTarget)
}
}
}
// Generate an identifier for the first part
var value js_ast.Expr
firstPart := parts[0]
parts = parts[1:]
switch firstPart {
case "NaN":
value = js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: math.NaN()}}
case "Infinity":
value = js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: math.Inf(1)}}
case "null":
value = js_ast.Expr{Loc: loc, Data: js_ast.ENullShared}
case "undefined":
value = js_ast.Expr{Loc: loc, Data: js_ast.EUndefinedShared}
case "this":
if thisValue, ok := p.valueForThis(loc, false /* shouldLog */, js_ast.AssignTargetNone, false, false); ok {
value = thisValue
} else {
value = js_ast.Expr{Loc: loc, Data: js_ast.EThisShared}
}
default:
if firstPart == "import" && len(parts) > 0 && parts[0] == "meta" {
if importMeta, ok := p.valueForImportMeta(loc); ok {
value = importMeta
} else {
value = js_ast.Expr{Loc: loc, Data: &js_ast.EImportMeta{}}
}
parts = parts[1:]
break
}
result := p.findSymbol(loc, firstPart)
value = p.handleIdentifier(loc, &js_ast.EIdentifier{
Ref: result.ref,
MustKeepDueToWithStmt: result.isInsideWithScope,
// Enable tree shaking
CanBeRemovedIfUnused: true,
}, opts)
}
// Build up a chain of property access expressions for subsequent parts
for _, part := range parts {
if expr, ok := p.maybeRewritePropertyAccess(loc, js_ast.AssignTargetNone, false, value, part, loc, false, false, false); ok {
value = expr
} else if p.isMangledProp(part) {
value = js_ast.Expr{Loc: loc, Data: &js_ast.EIndex{
Target: value,
Index: js_ast.Expr{Loc: loc, Data: &js_ast.ENameOfSymbol{Ref: p.symbolForMangledProp(part)}},
}}
} else {
value = js_ast.Expr{Loc: loc, Data: &js_ast.EDot{
Target: value,
Name: part,
NameLoc: loc,
// Enable tree shaking
CanBeRemovedIfUnused: true,
}}
}
}
return value
}
func (p *parser) instantiateInjectDotName(loc logger.Loc, name injectedDotName, assignTarget js_ast.AssignTarget) js_ast.Expr {
// Note: We don't need to "ignoreRef" on the underlying identifier
// because we have only parsed it but not visited it yet
ref := p.injectedDefineSymbols[name.injectedDefineIndex]
p.recordUsage(ref)
if assignTarget != js_ast.AssignTargetNone {
if where, ok := p.injectedSymbolSources[ref]; ok {
r := js_lexer.RangeOfIdentifier(p.source, loc)
tracker := logger.MakeLineColumnTracker(&where.source)
joined := strings.Join(name.parts, ".")
p.log.AddErrorWithNotes(&p.tracker, r,
fmt.Sprintf("Cannot assign to %q because it's an import from an injected file", joined),
[]logger.MsgData{tracker.MsgData(js_lexer.RangeOfIdentifier(where.source, where.loc),
fmt.Sprintf("The symbol %q was exported from %q here:",
joined, where.source.PrettyPaths.Select(p.options.logPathStyle)))})
}
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: ref}}
}
func (p *parser) checkForUnrepresentableIdentifier(loc logger.Loc, name string) {
if p.options.asciiOnly && p.options.unsupportedJSFeatures.Has(compat.UnicodeEscapes) &&
helpers.ContainsNonBMPCodePoint(name) {
if p.unrepresentableIdentifiers == nil {
p.unrepresentableIdentifiers = make(map[string]bool)
}
if !p.unrepresentableIdentifiers[name] {
p.unrepresentableIdentifiers[name] = true
where := config.PrettyPrintTargetEnvironment(p.options.originalTargetEnv, p.options.unsupportedJSFeatureOverridesMask)
r := js_lexer.RangeOfIdentifier(p.source, loc)
p.log.AddError(&p.tracker, r, fmt.Sprintf("%q cannot be escaped in %s but you "+
"can set the charset to \"utf8\" to allow unescaped Unicode characters", name, where))
}
}
}
type typeofStringOrder uint8
const (
onlyCheckOriginalOrder typeofStringOrder = iota
checkBothOrders
)
func (p *parser) warnAboutTypeofAndString(a js_ast.Expr, b js_ast.Expr, order typeofStringOrder) {
if order == checkBothOrders {
if _, ok := a.Data.(*js_ast.EString); ok {
a, b = b, a
}
}
if typeof, ok := a.Data.(*js_ast.EUnary); ok && typeof.Op == js_ast.UnOpTypeof {
if str, ok := b.Data.(*js_ast.EString); ok {
value := helpers.UTF16ToString(str.Value)
switch value {
case "undefined", "object", "boolean", "number", "bigint", "string", "symbol", "function", "unknown":
default:
// Warn about typeof comparisons with values that will never be
// returned. Here's an example of code with this problem:
// https://github.com/olifolkerd/tabulator/issues/2962
r := p.source.RangeOfString(b.Loc)
text := fmt.Sprintf("The \"typeof\" operator will never evaluate to %q", value)
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
var notes []logger.MsgData
if value == "null" {
notes = append(notes, logger.MsgData{
Text: "The expression \"typeof x\" actually evaluates to \"object\" in JavaScript, not \"null\". " +
"You need to use \"x === null\" to test for null.",
})
}
p.log.AddIDWithNotes(logger.MsgID_JS_ImpossibleTypeof, kind, &p.tracker, r, text, notes)
}
}
}
}
func (p *parser) warnAboutEqualityCheck(op string, value js_ast.Expr, afterOpLoc logger.Loc) bool {
switch e := value.Data.(type) {
case *js_ast.ENumber:
// "0 === -0" is true in JavaScript. Here's an example of code with this
// problem: https://github.com/mrdoob/three.js/pull/11183
if e.Value == 0 && math.Signbit(e.Value) {
r := logger.Range{Loc: value.Loc, Len: 0}
if int(r.Loc.Start) < len(p.source.Contents) && p.source.Contents[r.Loc.Start] == '-' {
zeroRange := p.source.RangeOfNumber(logger.Loc{Start: r.Loc.Start + 1})
r.Len = zeroRange.Len + 1
}
text := fmt.Sprintf("Comparison with -0 using the %q operator will also match 0", op)
if op == "case" {
text = "Comparison with -0 using a case clause will also match 0"
}
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddIDWithNotes(logger.MsgID_JS_EqualsNegativeZero, kind, &p.tracker, r, text,
[]logger.MsgData{{Text: "Floating-point equality is defined such that 0 and -0 are equal, so \"x === -0\" returns true for both 0 and -0. " +
"You need to use \"Object.is(x, -0)\" instead to test for -0."}})
return true
}
// "NaN === NaN" is false in JavaScript
if math.IsNaN(e.Value) {
text := fmt.Sprintf("Comparison with NaN using the %q operator here is always %v", op, op[0] == '!')
if op == "case" {
text = "This case clause will never be evaluated because equality with NaN is always false"
}
r := p.source.RangeOfOperatorBefore(afterOpLoc, op)
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddIDWithNotes(logger.MsgID_JS_EqualsNaN, kind, &p.tracker, r, text,
[]logger.MsgData{{Text: "Floating-point equality is defined such that NaN is never equal to anything, so \"x === NaN\" always returns false. " +
"You need to use \"Number.isNaN(x)\" instead to test for NaN."}})
return true
}
case *js_ast.EArray, *js_ast.EArrow, *js_ast.EClass,
*js_ast.EFunction, *js_ast.EObject, *js_ast.ERegExp:
// This warning only applies to strict equality because loose equality can
// cause string conversions. For example, "x == []" is true if x is the
// empty string. Here's an example of code with this problem:
// https://github.com/aws/aws-sdk-js/issues/3325
if len(op) > 2 {
text := fmt.Sprintf("Comparison using the %q operator here is always %v", op, op[0] == '!')
if op == "case" {
text = "This case clause will never be evaluated because the comparison is always false"
}
r := p.source.RangeOfOperatorBefore(afterOpLoc, op)
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddIDWithNotes(logger.MsgID_JS_EqualsNewObject, kind, &p.tracker, r, text,
[]logger.MsgData{{Text: "Equality with a new object is always false in JavaScript because the equality operator tests object identity. " +
"You need to write code to compare the contents of the object instead. " +
"For example, use \"Array.isArray(x) && x.length === 0\" instead of \"x === []\" to test for an empty array."}})
return true
}
}
return false
}
// EDot nodes represent a property access. This function may return an
// expression to replace the property access with. It assumes that the
// target of the EDot expression has already been visited.
func (p *parser) maybeRewritePropertyAccess(
loc logger.Loc,
assignTarget js_ast.AssignTarget,
isDeleteTarget bool,
target js_ast.Expr,
name string,
nameLoc logger.Loc,
isCallTarget bool,
isTemplateTag bool,
preferQuotedKey bool,
) (js_ast.Expr, bool) {
if id, ok := target.Data.(*js_ast.EIdentifier); ok {
// Rewrite property accesses on explicit namespace imports as an identifier.
// This lets us replace them easily in the printer to rebind them to
// something else without paying the cost of a whole-tree traversal during
// module linking just to rewrite these EDot expressions.
if p.options.mode == config.ModeBundle {
if importItems, ok := p.importItemsForNamespace[id.Ref]; ok {
// Cache translation so each property access resolves to the same import
item, ok := importItems.entries[name]
if !ok {
// Replace non-default imports with "undefined" for JSON import assertions
if record := &p.importRecords[importItems.importRecordIndex]; (record.Flags&ast.AssertTypeJSON) != 0 && name != "default" {
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddIDWithNotes(logger.MsgID_JS_AssertTypeJSON, kind, &p.tracker, js_lexer.RangeOfIdentifier(p.source, nameLoc),
fmt.Sprintf("Non-default import %q is undefined with a JSON import assertion", name),
p.notesForAssertTypeJSON(record, name))
p.ignoreUsage(id.Ref)
return js_ast.Expr{Loc: loc, Data: js_ast.EUndefinedShared}, true
}
// Generate a new import item symbol in the module scope
item = ast.LocRef{Loc: nameLoc, Ref: p.newSymbol(ast.SymbolImport, name)}
p.moduleScope.Generated = append(p.moduleScope.Generated, item.Ref)
// Link the namespace import and the import item together
importItems.entries[name] = item
p.isImportItem[item.Ref] = true
symbol := &p.symbols[item.Ref.InnerIndex]
if p.options.mode == config.ModePassThrough {
// Make sure the printer prints this as a property access
symbol.NamespaceAlias = &ast.NamespaceAlias{
NamespaceRef: id.Ref,
Alias: name,
}
} else {
// Mark this as generated in case it's missing. We don't want to
// generate errors for missing import items that are automatically
// generated.
symbol.ImportItemStatus = ast.ImportItemGenerated
}
}
// Undo the usage count for the namespace itself. This is used later
// to detect whether the namespace symbol has ever been "captured"
// or whether it has just been used to read properties off of.
//
// The benefit of doing this is that if both this module and the
// imported module end up in the same module group and the namespace
// symbol has never been captured, then we don't need to generate
// any code for the namespace at all.
p.ignoreUsage(id.Ref)
// Track how many times we've referenced this symbol
p.recordUsage(item.Ref)
return p.handleIdentifier(nameLoc, &js_ast.EIdentifier{Ref: item.Ref}, identifierOpts{
assignTarget: assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
preferQuotedKey: preferQuotedKey,
// If this expression is used as the target of a call expression, make
// sure the value of "this" is preserved.
wasOriginallyIdentifier: false,
}), true
}
// Rewrite "module.require()" to "require()" for Webpack compatibility.
// See https://github.com/webpack/webpack/pull/7750 for more info.
if isCallTarget && id.Ref == p.moduleRef && name == "require" {
p.ignoreUsage(p.moduleRef)
// This uses "require" instead of a reference to our "__require"
// function so that the code coming up that detects calls to
// "require" will recognize it.
p.recordUsage(p.requireRef)
return js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: p.requireRef}}, true
}
}
}
// Attempt to simplify statically-determined object literal property accesses
if !isCallTarget && !isTemplateTag && p.options.minifySyntax && assignTarget == js_ast.AssignTargetNone {
if object, ok := target.Data.(*js_ast.EObject); ok {
var replace js_ast.Expr
hasProtoNull := false
isUnsafe := false
// Check that doing this is safe
for _, prop := range object.Properties {
// "{ ...a }.a" must be preserved
// "new ({ a() {} }.a)" must throw
// "{ get a() {} }.a" must be preserved
// "{ set a(b) {} }.a = 1" must be preserved
// "{ a: 1, [String.fromCharCode(97)]: 2 }.a" must be 2
if prop.Kind == js_ast.PropertySpread || prop.Flags.Has(js_ast.PropertyIsComputed) || prop.Kind.IsMethodDefinition() {
isUnsafe = true
break
}
// Do not attempt to compare against numeric keys
key, ok := prop.Key.Data.(*js_ast.EString)
if !ok {
isUnsafe = true
break
}
// The "__proto__" key has special behavior
if helpers.UTF16EqualsString(key.Value, "__proto__") {
if _, ok := prop.ValueOrNil.Data.(*js_ast.ENull); ok {
// Replacing "{__proto__: null}.a" with undefined should be safe
hasProtoNull = true
}
}
// This entire object literal must have no side effects
if !p.astHelpers.ExprCanBeRemovedIfUnused(prop.ValueOrNil) {
isUnsafe = true
break
}
// Note that we need to take the last value if there are duplicate keys
if helpers.UTF16EqualsString(key.Value, name) {
replace = prop.ValueOrNil
}
}
if !isUnsafe {
// If the key was found, return the value for that key. Note
// that "{__proto__: null}.__proto__" is undefined, not null.
if replace.Data != nil && name != "__proto__" {
return replace, true
}
// We can only return "undefined" when a key is missing if the prototype is null
if hasProtoNull {
return js_ast.Expr{Loc: target.Loc, Data: js_ast.EUndefinedShared}, true
}
}
}
}
// Handle references to namespaces or namespace members
if target.Data == p.tsNamespaceTarget && assignTarget == js_ast.AssignTargetNone && !isDeleteTarget {
if ns, ok := p.tsNamespaceMemberData.(*js_ast.TSNamespaceMemberNamespace); ok {
if member, ok := ns.ExportedMembers[name]; ok {
switch m := member.Data.(type) {
case *js_ast.TSNamespaceMemberEnumNumber:
p.ignoreUsageOfIdentifierInDotChain(target)
return p.wrapInlinedEnum(js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: m.Value}}, name), true
case *js_ast.TSNamespaceMemberEnumString:
p.ignoreUsageOfIdentifierInDotChain(target)
return p.wrapInlinedEnum(js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: m.Value}}, name), true
case *js_ast.TSNamespaceMemberNamespace:
// If this isn't a constant, return a clone of this property access
// but with the namespace member data associated with it so that
// more property accesses off of this property access are recognized.
if preferQuotedKey || !js_ast.IsIdentifier(name) {
p.tsNamespaceTarget = &js_ast.EIndex{
Target: target,
Index: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EString{Value: helpers.StringToUTF16(name)}},
}
} else {
p.tsNamespaceTarget = p.dotOrMangledPropVisit(target, name, nameLoc)
}
p.tsNamespaceMemberData = member.Data
return js_ast.Expr{Loc: loc, Data: p.tsNamespaceTarget}, true
}
}
}
}
// Symbol uses due to a property access off of an imported symbol are tracked
// specially. This lets us do tree shaking for cross-file TypeScript enums.
if p.options.mode == config.ModeBundle && !p.isControlFlowDead {
if id, ok := target.Data.(*js_ast.EImportIdentifier); ok {
// Remove the normal symbol use
symbolUses := p.currentPart.SymbolUses
use := symbolUses[id.Ref]
use.CountEstimate--
if use.CountEstimate == 0 {
delete(symbolUses, id.Ref)
} else {
symbolUses[id.Ref] = use
}
// Add a special symbol use instead
importSymbolPropertyUses := p.currentPart.ImportSymbolPropertyUses
if importSymbolPropertyUses == nil {
importSymbolPropertyUses = make(map[ast.Ref]map[string]js_ast.SymbolUse)
p.currentPart.ImportSymbolPropertyUses = importSymbolPropertyUses
}
properties := importSymbolPropertyUses[id.Ref]
if properties == nil {
properties = make(map[string]js_ast.SymbolUse)
importSymbolPropertyUses[id.Ref] = properties
}
use = properties[name]
use.CountEstimate++
properties[name] = use
}
}
// Minify "foo".length
if p.options.minifySyntax && assignTarget == js_ast.AssignTargetNone {
switch t := target.Data.(type) {
case *js_ast.EString:
if name == "length" {
return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: float64(len(t.Value))}}, true
}
case *js_ast.EInlinedEnum:
if s, ok := t.Value.Data.(*js_ast.EString); ok && name == "length" {
return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: float64(len(s.Value))}}, true
}
}
}
return js_ast.Expr{}, false
}
type exprIn struct {
isMethod bool
isLoweredPrivateMethod bool
// This tells us if there are optional chain expressions (EDot, EIndex, or
// ECall) that are chained on to this expression. Because of the way the AST
// works, chaining expressions on to this expression means they are our
// parent expressions.
//
// Some examples:
//
// a?.b.c // EDot
// a?.b[c] // EIndex
// a?.b() // ECall
//
// Note that this is false if our parent is a node with a OptionalChain
// value of OptionalChainStart. That means it's the start of a new chain, so
// it's not considered part of this one.
//
// Some examples:
//
// a?.b?.c // EDot
// a?.b?.[c] // EIndex
// a?.b?.() // ECall
//
// Also note that this is false if our parent is a node with a OptionalChain
// value of OptionalChainNone. That means it's outside parentheses, which
// means it's no longer part of the chain.
//
// Some examples:
//
// (a?.b).c // EDot
// (a?.b)[c] // EIndex
// (a?.b)() // ECall
//
hasChainParent bool
// If our parent is an ECall node with an OptionalChain value of
// OptionalChainStart, then we will need to store the value for the "this" of
// that call somewhere if the current expression is an optional chain that
// ends in a property access. That's because the value for "this" will be
// used twice: once for the inner optional chain and once for the outer
// optional chain.
//
// Example:
//
// // Original
// a?.b?.();
//
// // Lowered
// var _a;
// (_a = a == null ? void 0 : a.b) == null ? void 0 : _a.call(a);
//
// In the example above we need to store "a" as the value for "this" so we
// can substitute it back in when we call "_a" if "_a" is indeed present.
// See also "thisArgFunc" and "thisArgWrapFunc" in "exprOut".
storeThisArgForParentOptionalChain bool
// If true, string literals that match the current property mangling pattern
// should be turned into ENameOfSymbol expressions, which will cause us to
// rename them in the linker.
shouldMangleStringsAsProps bool
// Certain substitutions of identifiers are disallowed for assignment targets.
// For example, we shouldn't transform "undefined = 1" into "void 0 = 1". This
// isn't something real-world code would do but it matters for conformance
// tests.
assignTarget js_ast.AssignTarget
}
type exprOut struct {
// If our parent is an ECall node with an OptionalChain value of
// OptionalChainContinue, then we may need to return the value for "this"
// from this node or one of this node's children so that the parent that is
// the end of the optional chain can use it.
//
// Example:
//
// // Original
// a?.b?.().c();
//
// // Lowered
// var _a;
// (_a = a == null ? void 0 : a.b) == null ? void 0 : _a.call(a).c();
//
// The value "_a" for "this" must be passed all the way up to the call to
// ".c()" which is where the optional chain is lowered. From there it must
// be substituted as the value for "this" in the call to ".b?.()". See also
// "storeThisArgForParentOptionalChain" in "exprIn".
thisArgFunc func() js_ast.Expr
thisArgWrapFunc func(js_ast.Expr) js_ast.Expr
// True if the child node is an optional chain node (EDot, EIndex, or ECall
// with an IsOptionalChain value of true)
childContainsOptionalChain bool
// If true and this is used as a call target, the whole call expression
// must be replaced with undefined.
callMustBeReplacedWithUndefined bool
methodCallMustBeReplacedWithUndefined bool
}
func (p *parser) visitExpr(expr js_ast.Expr) js_ast.Expr {
expr, _ = p.visitExprInOut(expr, exprIn{})
return expr
}
func (p *parser) valueForThis(
loc logger.Loc,
shouldLog bool,
assignTarget js_ast.AssignTarget,
isCallTarget bool,
isDeleteTarget bool,
) (js_ast.Expr, bool) {
// Substitute "this" if we're inside a static class context
if p.fnOnlyDataVisit.shouldReplaceThisWithInnerClassNameRef {
p.recordUsage(*p.fnOnlyDataVisit.innerClassNameRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: *p.fnOnlyDataVisit.innerClassNameRef}}, true
}
// Is this a top-level use of "this"?
if !p.fnOnlyDataVisit.isThisNested {
// Substitute user-specified defines
if data, ok := p.options.defines.IdentifierDefines["this"]; ok {
if data.DefineExpr != nil {
return p.instantiateDefineExpr(loc, *data.DefineExpr, identifierOpts{
assignTarget: assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
}), true
}
}
// Otherwise, replace top-level "this" with either "undefined" or "exports"
if p.isFileConsideredToHaveESMExports {
// Warn about "this" becoming undefined, but only once per file
if shouldLog && !p.messageAboutThisIsUndefined && !p.fnOnlyDataVisit.silenceMessageAboutThisBeingUndefined {
p.messageAboutThisIsUndefined = true
kind := logger.Debug
data := p.tracker.MsgData(js_lexer.RangeOfIdentifier(p.source, loc),
"Top-level \"this\" will be replaced with undefined since this file is an ECMAScript module")
data.Location.Suggestion = "undefined"
_, notes := p.whyESModule()
p.log.AddMsgID(logger.MsgID_JS_ThisIsUndefinedInESM, logger.Msg{Kind: kind, Data: data, Notes: notes})
}
// In an ES6 module, "this" is supposed to be undefined. Instead of
// doing this at runtime using "fn.call(undefined)", we do it at
// compile time using expression substitution here.
return js_ast.Expr{Loc: loc, Data: js_ast.EUndefinedShared}, true
} else if p.options.mode != config.ModePassThrough {
// In a CommonJS module, "this" is supposed to be the same as "exports".
// Instead of doing this at runtime using "fn.call(module.exports)", we
// do it at compile time using expression substitution here.
p.recordUsage(p.exportsRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: p.exportsRef}}, true
}
}
return js_ast.Expr{}, false
}
func (p *parser) valueForImportMeta(loc logger.Loc) (js_ast.Expr, bool) {
if p.options.unsupportedJSFeatures.Has(compat.ImportMeta) ||
(p.options.mode != config.ModePassThrough && !p.options.outputFormat.KeepESMImportExportSyntax()) {
// Generate the variable if it doesn't exist yet
if p.importMetaRef == ast.InvalidRef {
p.importMetaRef = p.newSymbol(ast.SymbolOther, "import_meta")
p.moduleScope.Generated = append(p.moduleScope.Generated, p.importMetaRef)
}
// Replace "import.meta" with a reference to the symbol
p.recordUsage(p.importMetaRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: p.importMetaRef}}, true
}
return js_ast.Expr{}, false
}
func locAfterOp(e *js_ast.EBinary) logger.Loc {
if e.Left.Loc.Start < e.Right.Loc.Start {
return e.Right.Loc
} else {
// Handle the case when we have transposed the operands
return e.Left.Loc
}
}
// This function exists to tie all of these checks together in one place
func isEvalOrArguments(name string) bool {
return name == "eval" || name == "arguments"
}
func (p *parser) reportPrivateNameUsage(name string) {
if p.parseExperimentalDecoratorNesting > 0 {
if p.lowerAllOfThesePrivateNames == nil {
p.lowerAllOfThesePrivateNames = make(map[string]bool)
}
p.lowerAllOfThesePrivateNames[name] = true
}
}
func (p *parser) isValidAssignmentTarget(expr js_ast.Expr) bool {
switch e := expr.Data.(type) {
case *js_ast.EIdentifier:
if p.isStrictMode() {
if name := p.loadNameFromRef(e.Ref); isEvalOrArguments(name) {
return false
}
}
return true
case *js_ast.EDot:
return e.OptionalChain == js_ast.OptionalChainNone
case *js_ast.EIndex:
return e.OptionalChain == js_ast.OptionalChainNone
// Don't worry about recursive checking for objects and arrays. This will
// already be handled naturally by passing down the assign target flag.
case *js_ast.EObject:
return !e.IsParenthesized
case *js_ast.EArray:
return !e.IsParenthesized
}
return false
}
func containsClosingScriptTag(text string) bool {
for {
i := strings.Index(text, "</")
if i < 0 {
break
}
text = text[i+2:]
if len(text) >= 6 && strings.EqualFold(text[:6], "script") {
return true
}
}
return false
}
func (p *parser) isUnsupportedRegularExpression(loc logger.Loc, value string) (pattern string, flags string, isUnsupported bool) {
var what string
var r logger.Range
end := strings.LastIndexByte(value, '/')
pattern = value[1:end]
flags = value[end+1:]
isUnicode := strings.IndexByte(flags, 'u') >= 0
parenDepth := 0
i := 0
// Do a simple scan for unsupported features assuming the regular expression
// is valid. This doesn't do a full validation of the regular expression
// because regular expression grammar is complicated. If it contains a syntax
// error that we don't catch, then we will just generate output code with a
// syntax error. Garbage in, garbage out.
pattern:
for i < len(pattern) {
c := pattern[i]
i++
switch c {
case '[':
class:
for i < len(pattern) {
c := pattern[i]
i++
switch c {
case ']':
break class
case '\\':
i++ // Skip the escaped character
}
}
case '(':
tail := pattern[i:]
if strings.HasPrefix(tail, "?<=") || strings.HasPrefix(tail, "?<!") {
if p.options.unsupportedJSFeatures.Has(compat.RegexpLookbehindAssertions) {
what = "Lookbehind assertions in regular expressions are not available"
r = logger.Range{Loc: logger.Loc{Start: loc.Start + int32(i) + 1}, Len: 3}
isUnsupported = true
break pattern
}
} else if strings.HasPrefix(tail, "?<") {
if p.options.unsupportedJSFeatures.Has(compat.RegexpNamedCaptureGroups) {
if end := strings.IndexByte(tail, '>'); end >= 0 {
what = "Named capture groups in regular expressions are not available"
r = logger.Range{Loc: logger.Loc{Start: loc.Start + int32(i) + 1}, Len: int32(end) + 1}
isUnsupported = true
break pattern
}
}
}
parenDepth++
case ')':
if parenDepth == 0 {
r := logger.Range{Loc: logger.Loc{Start: loc.Start + int32(i)}, Len: 1}
p.log.AddError(&p.tracker, r, "Unexpected \")\" in regular expression")
return
}
parenDepth--
case '\\':
tail := pattern[i:]
if isUnicode && (strings.HasPrefix(tail, "p{") || strings.HasPrefix(tail, "P{")) {
if p.options.unsupportedJSFeatures.Has(compat.RegexpUnicodePropertyEscapes) {
if end := strings.IndexByte(tail, '}'); end >= 0 {
what = "Unicode property escapes in regular expressions are not available"
r = logger.Range{Loc: logger.Loc{Start: loc.Start + int32(i)}, Len: int32(end) + 2}
isUnsupported = true
break pattern
}
}
}
i++ // Skip the escaped character
}
}
if !isUnsupported {
for i, c := range flags {
switch c {
case 'g', 'i', 'm':
continue // These are part of ES5 and are always supported
case 's':
if !p.options.unsupportedJSFeatures.Has(compat.RegexpDotAllFlag) {
continue // This is part of ES2018
}
case 'y', 'u':
if !p.options.unsupportedJSFeatures.Has(compat.RegexpStickyAndUnicodeFlags) {
continue // These are part of ES2018
}
case 'd':
if !p.options.unsupportedJSFeatures.Has(compat.RegexpMatchIndices) {
continue // This is part of ES2022
}
case 'v':
if !p.options.unsupportedJSFeatures.Has(compat.RegexpSetNotation) {
continue // This is from a proposal: https://github.com/tc39/proposal-regexp-v-flag
}
default:
// Unknown flags are never supported
}
r = logger.Range{Loc: logger.Loc{Start: loc.Start + int32(end+1) + int32(i)}, Len: 1}
what = fmt.Sprintf("The regular expression flag \"%c\" is not available", c)
isUnsupported = true
break
}
}
if isUnsupported {
where := config.PrettyPrintTargetEnvironment(p.options.originalTargetEnv, p.options.unsupportedJSFeatureOverridesMask)
p.log.AddIDWithNotes(logger.MsgID_JS_UnsupportedRegExp, logger.Debug, &p.tracker, r, fmt.Sprintf("%s in %s", what, where), []logger.MsgData{{
Text: "This regular expression literal has been converted to a \"new RegExp()\" constructor " +
"to avoid generating code with a syntax error. However, you will need to include a " +
"polyfill for \"RegExp\" for your code to have the correct behavior at run-time."}})
}
return
}
// This function takes "exprIn" as input from the caller and produces "exprOut"
// for the caller to pass along extra data. This is mostly for optional chaining.
func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprOut) {
if in.assignTarget != js_ast.AssignTargetNone && !p.isValidAssignmentTarget(expr) {
p.log.AddError(&p.tracker, logger.Range{Loc: expr.Loc}, "Invalid assignment target")
}
// Note: Anything added before or after this switch statement will be bypassed
// when visiting nested "EBinary" nodes due to stack overflow mitigations for
// deeply-nested ASTs. If anything like that is added, care must be taken that
// it doesn't affect these mitigations by ensuring that the mitigations are not
// applied in those cases (e.g. by adding an additional conditional check).
switch e := expr.Data.(type) {
case *js_ast.ENull, *js_ast.ESuper, *js_ast.EBoolean, *js_ast.EUndefined, *js_ast.EJSXText:
case *js_ast.EBigInt:
if p.options.unsupportedJSFeatures.Has(compat.Bigint) {
// For ease of implementation, the actual reference of the "BigInt"
// symbol is deferred to print time. That means we don't have to
// special-case the "BigInt" constructor in side-effect computations
// and future big integer constant folding (of which there isn't any
// at the moment).
p.markSyntaxFeature(compat.Bigint, p.source.RangeOfNumber(expr.Loc))
p.recordUsage(p.makeBigIntRef())
}
case *js_ast.ENameOfSymbol:
e.Ref = p.symbolForMangledProp(p.loadNameFromRef(e.Ref))
case *js_ast.ERegExp:
// "/pattern/flags" => "new RegExp('pattern', 'flags')"
if pattern, flags, ok := p.isUnsupportedRegularExpression(expr.Loc, e.Value); ok {
args := []js_ast.Expr{{
Loc: logger.Loc{Start: expr.Loc.Start + 1},
Data: &js_ast.EString{Value: helpers.StringToUTF16(pattern)},
}}
if flags != "" {
args = append(args, js_ast.Expr{
Loc: logger.Loc{Start: expr.Loc.Start + int32(len(pattern)) + 2},
Data: &js_ast.EString{Value: helpers.StringToUTF16(flags)},
})
}
regExpRef := p.makeRegExpRef()
p.recordUsage(regExpRef)
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENew{
Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: regExpRef}},
Args: args,
CloseParenLoc: logger.Loc{Start: expr.Loc.Start + int32(len(e.Value))},
}}, exprOut{}
}
case *js_ast.ENewTarget:
if !p.fnOnlyDataVisit.isNewTargetAllowed {
p.log.AddError(&p.tracker, e.Range, "Cannot use \"new.target\" here:")
}
case *js_ast.EString:
if e.LegacyOctalLoc.Start > 0 {
if e.PreferTemplate {
p.log.AddError(&p.tracker, p.source.RangeOfLegacyOctalEscape(e.LegacyOctalLoc),
"Legacy octal escape sequences cannot be used in template literals")
} else if p.isStrictMode() {
p.markStrictModeFeature(legacyOctalEscape, p.source.RangeOfLegacyOctalEscape(e.LegacyOctalLoc), "")
}
}
if in.shouldMangleStringsAsProps && p.options.mangleQuoted && !e.PreferTemplate {
if name := helpers.UTF16ToString(e.Value); p.isMangledProp(name) {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENameOfSymbol{
Ref: p.symbolForMangledProp(name),
HasPropertyKeyComment: e.HasPropertyKeyComment,
}}, exprOut{}
}
}
case *js_ast.ENumber:
if p.legacyOctalLiterals != nil && p.isStrictMode() {
if r, ok := p.legacyOctalLiterals[expr.Data]; ok {
p.markStrictModeFeature(legacyOctalLiteral, r, "")
}
}
case *js_ast.EThis:
isDeleteTarget := e == p.deleteTarget
isCallTarget := e == p.callTarget
p.fnOnlyDataVisit.hasThisUsage = true
if value, ok := p.valueForThis(expr.Loc, true /* shouldLog */, in.assignTarget, isDeleteTarget, isCallTarget); ok {
return value, exprOut{}
}
// Capture "this" inside arrow functions that will be lowered into normal
// function expressions for older language environments
if p.fnOrArrowDataVisit.isArrow && p.options.unsupportedJSFeatures.Has(compat.Arrow) && p.fnOnlyDataVisit.isThisNested {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: p.captureThis()}}, exprOut{}
}
case *js_ast.EImportMeta:
isDeleteTarget := e == p.deleteTarget
isCallTarget := e == p.callTarget
// Check both user-specified defines and known globals
if defines, ok := p.options.defines.DotDefines["meta"]; ok {
for _, define := range defines {
if p.isDotOrIndexDefineMatch(expr, define.KeyParts) {
// Substitute user-specified defines
if define.DefineExpr != nil {
return p.instantiateDefineExpr(expr.Loc, *define.DefineExpr, identifierOpts{
assignTarget: in.assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
}), exprOut{}
}
}
}
}
// Check injected dot names
if names, ok := p.injectedDotNames["meta"]; ok {
for _, name := range names {
if p.isDotOrIndexDefineMatch(expr, name.parts) {
// Note: We don't need to "ignoreRef" on the underlying identifier
// because we have only parsed it but not visited it yet
return p.instantiateInjectDotName(expr.Loc, name, in.assignTarget), exprOut{}
}
}
}
// Warn about "import.meta" if it's not replaced by a define
if p.options.unsupportedJSFeatures.Has(compat.ImportMeta) {
r := logger.Range{Loc: expr.Loc, Len: e.RangeLen}
p.markSyntaxFeature(compat.ImportMeta, r)
} else if p.options.mode != config.ModePassThrough && !p.options.outputFormat.KeepESMImportExportSyntax() {
r := logger.Range{Loc: expr.Loc, Len: e.RangeLen}
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode || p.fnOrArrowDataVisit.tryBodyCount > 0 {
kind = logger.Debug
}
p.log.AddIDWithNotes(logger.MsgID_JS_EmptyImportMeta, kind, &p.tracker, r, fmt.Sprintf(
"\"import.meta\" is not available with the %q output format and will be empty", p.options.outputFormat.String()),
[]logger.MsgData{{Text: "You need to set the output format to \"esm\" for \"import.meta\" to work correctly."}})
}
// Convert "import.meta" to a variable if it's not supported in the output format
if importMeta, ok := p.valueForImportMeta(expr.Loc); ok {
return importMeta, exprOut{}
}
case *js_ast.ESpread:
e.Value = p.visitExpr(e.Value)
case *js_ast.EIdentifier:
isCallTarget := e == p.callTarget
isDeleteTarget := e == p.deleteTarget
name := p.loadNameFromRef(e.Ref)
if p.isStrictMode() && js_lexer.StrictModeReservedWords[name] {
p.markStrictModeFeature(reservedWord, js_lexer.RangeOfIdentifier(p.source, expr.Loc), name)
}
result := p.findSymbol(expr.Loc, name)
e.MustKeepDueToWithStmt = result.isInsideWithScope
e.Ref = result.ref
// Handle referencing a class name within that class's computed property
// key. This is not allowed, and must fail at run-time:
//
// class Foo {
// static foo = 'bar'
// static [Foo.foo] = 'foo'
// }
//
if p.symbols[result.ref.InnerIndex].Kind == ast.SymbolClassInComputedPropertyKey {
p.log.AddID(logger.MsgID_JS_ClassNameWillThrow, logger.Warning, &p.tracker, js_lexer.RangeOfIdentifier(p.source, expr.Loc),
fmt.Sprintf("Accessing class %q before initialization will throw", name))
return p.callRuntime(expr.Loc, "__earlyAccess", []js_ast.Expr{{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(name)}}}), exprOut{}
}
// Handle assigning to a constant
if in.assignTarget != js_ast.AssignTargetNone {
switch p.symbols[result.ref.InnerIndex].Kind {
case ast.SymbolConst:
r := js_lexer.RangeOfIdentifier(p.source, expr.Loc)
notes := []logger.MsgData{p.tracker.MsgData(js_lexer.RangeOfIdentifier(p.source, result.declareLoc),
fmt.Sprintf("The symbol %q was declared a constant here:", name))}
// Make this an error when bundling because we may need to convert this
// "const" into a "var" during bundling. Also make this an error when
// the constant is inlined because we will otherwise generate code with
// a syntax error.
if _, isInlinedConstant := p.constValues[result.ref]; isInlinedConstant || p.options.mode == config.ModeBundle ||
(p.currentScope.Parent == nil && p.willWrapModuleInTryCatchForUsing) {
p.log.AddErrorWithNotes(&p.tracker, r,
fmt.Sprintf("Cannot assign to %q because it is a constant", name), notes)
} else {
p.log.AddIDWithNotes(logger.MsgID_JS_AssignToConstant, logger.Warning, &p.tracker, r,
fmt.Sprintf("This assignment will throw because %q is a constant", name), notes)
}
case ast.SymbolInjected:
if where, ok := p.injectedSymbolSources[result.ref]; ok {
r := js_lexer.RangeOfIdentifier(p.source, expr.Loc)
tracker := logger.MakeLineColumnTracker(&where.source)
p.log.AddErrorWithNotes(&p.tracker, r,
fmt.Sprintf("Cannot assign to %q because it's an import from an injected file", name),
[]logger.MsgData{tracker.MsgData(js_lexer.RangeOfIdentifier(where.source, where.loc),
fmt.Sprintf("The symbol %q was exported from %q here:",
name, where.source.PrettyPaths.Select(p.options.logPathStyle)))})
}
}
}
// Substitute user-specified defines for unbound or injected symbols
methodCallMustBeReplacedWithUndefined := false
if p.symbols[e.Ref.InnerIndex].Kind.IsUnboundOrInjected() && !result.isInsideWithScope && e != p.deleteTarget {
if data, ok := p.options.defines.IdentifierDefines[name]; ok {
if data.DefineExpr != nil {
new := p.instantiateDefineExpr(expr.Loc, *data.DefineExpr, identifierOpts{
assignTarget: in.assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
})
if in.assignTarget == js_ast.AssignTargetNone || defineValueCanBeUsedInAssignTarget(new.Data) {
p.ignoreUsage(e.Ref)
return new, exprOut{}
} else {
p.logAssignToDefine(js_lexer.RangeOfIdentifier(p.source, expr.Loc), name, js_ast.Expr{})
}
}
// Copy the side effect flags over in case this expression is unused
if data.Flags.Has(config.CanBeRemovedIfUnused) {
e.CanBeRemovedIfUnused = true
}
if data.Flags.Has(config.CallCanBeUnwrappedIfUnused) && !p.options.ignoreDCEAnnotations {
e.CallCanBeUnwrappedIfUnused = true
}
if data.Flags.Has(config.MethodCallsMustBeReplacedWithUndefined) {
methodCallMustBeReplacedWithUndefined = true
}
}
}
return p.handleIdentifier(expr.Loc, e, identifierOpts{
assignTarget: in.assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
wasOriginallyIdentifier: true,
}), exprOut{
methodCallMustBeReplacedWithUndefined: methodCallMustBeReplacedWithUndefined,
}
case *js_ast.EJSXElement:
propsLoc := expr.Loc
// Resolving the location index to a specific line and column in
// development mode is not too expensive because we seek from the
// previous JSX element. It amounts to at most a single additional
// scan over the source code. Note that this has to happen before
// we visit anything about this JSX element to make sure that we
// only ever need to scan forward, not backward.
var jsxSourceLine int
var jsxSourceColumn int
if p.options.jsx.Development && p.options.jsx.AutomaticRuntime {
for p.jsxSourceLoc < int(propsLoc.Start) {
r, size := utf8.DecodeRuneInString(p.source.Contents[p.jsxSourceLoc:])
p.jsxSourceLoc += size
if r == '\n' || r == '\r' || r == '\u2028' || r == '\u2029' {
if r == '\r' && p.jsxSourceLoc < len(p.source.Contents) && p.source.Contents[p.jsxSourceLoc] == '\n' {
p.jsxSourceLoc++ // Handle Windows-style CRLF newlines
}
p.jsxSourceLine++
p.jsxSourceColumn = 0
} else {
// Babel and TypeScript count columns in UTF-16 code units
if r < 0xFFFF {
p.jsxSourceColumn++
} else {
p.jsxSourceColumn += 2
}
}
}
jsxSourceLine = p.jsxSourceLine
jsxSourceColumn = p.jsxSourceColumn
}
if e.TagOrNil.Data != nil {
propsLoc = e.TagOrNil.Loc
e.TagOrNil = p.visitExpr(e.TagOrNil)
p.warnAboutImportNamespaceCall(e.TagOrNil, exprKindJSXTag)
}
// Visit properties
hasSpread := false
for i, property := range e.Properties {
if property.Kind == js_ast.PropertySpread {
hasSpread = true
} else {
if mangled, ok := property.Key.Data.(*js_ast.ENameOfSymbol); ok {
mangled.Ref = p.symbolForMangledProp(p.loadNameFromRef(mangled.Ref))
} else {
property.Key = p.visitExpr(property.Key)
}
}
if property.ValueOrNil.Data != nil {
property.ValueOrNil = p.visitExpr(property.ValueOrNil)
}
if property.InitializerOrNil.Data != nil {
property.InitializerOrNil = p.visitExpr(property.InitializerOrNil)
}
e.Properties[i] = property
}
// "{a, ...{b, c}, d}" => "{a, b, c, d}"
if p.options.minifySyntax && hasSpread {
e.Properties = js_ast.MangleObjectSpread(e.Properties)
}
// Visit children
if len(e.NullableChildren) > 0 {
for i, childOrNil := range e.NullableChildren {
if childOrNil.Data != nil {
e.NullableChildren[i] = p.visitExpr(childOrNil)
}
}
}
if p.options.jsx.Preserve {
// If the tag is an identifier, mark it as needing to be upper-case
switch tag := e.TagOrNil.Data.(type) {
case *js_ast.EIdentifier:
p.symbols[tag.Ref.InnerIndex].Flags |= ast.MustStartWithCapitalLetterForJSX
case *js_ast.EImportIdentifier:
p.symbols[tag.Ref.InnerIndex].Flags |= ast.MustStartWithCapitalLetterForJSX
}
} else {
// Remove any nil children in the array (in place) before iterating over it
children := e.NullableChildren
{
end := 0
for _, childOrNil := range children {
if childOrNil.Data != nil {
children[end] = childOrNil
end++
}
}
children = children[:end]
}
// A missing tag is a fragment
if e.TagOrNil.Data == nil {
if p.options.jsx.AutomaticRuntime {
e.TagOrNil = p.importJSXSymbol(expr.Loc, JSXImportFragment)
} else {
e.TagOrNil = p.instantiateDefineExpr(expr.Loc, p.options.jsx.Fragment, identifierOpts{
wasOriginallyIdentifier: true,
matchAgainstDefines: true, // Allow defines to rewrite the JSX fragment factory
})
}
}
shouldUseCreateElement := !p.options.jsx.AutomaticRuntime
if !shouldUseCreateElement {
// Even for runtime="automatic", <div {...props} key={key} /> is special cased to createElement
// See https://github.com/babel/babel/blob/e482c763466ba3f44cb9e3467583b78b7f030b4a/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts#L352
seenPropsSpread := false
for _, property := range e.Properties {
if seenPropsSpread && property.Kind == js_ast.PropertyField {
if str, ok := property.Key.Data.(*js_ast.EString); ok && helpers.UTF16EqualsString(str.Value, "key") {
shouldUseCreateElement = true
break
}
} else if property.Kind == js_ast.PropertySpread {
seenPropsSpread = true
}
}
}
if shouldUseCreateElement {
// Arguments to createElement()
args := []js_ast.Expr{e.TagOrNil}
if len(e.Properties) > 0 {
args = append(args, p.lowerObjectSpread(propsLoc, &js_ast.EObject{
Properties: e.Properties,
IsSingleLine: e.IsTagSingleLine,
}))
} else {
args = append(args, js_ast.Expr{Loc: propsLoc, Data: js_ast.ENullShared})
}
if len(children) > 0 {
args = append(args, children...)
}
// Call createElement()
var target js_ast.Expr
kind := js_ast.NormalCall
if p.options.jsx.AutomaticRuntime {
target = p.importJSXSymbol(expr.Loc, JSXImportCreateElement)
} else {
target = p.instantiateDefineExpr(expr.Loc, p.options.jsx.Factory, identifierOpts{
wasOriginallyIdentifier: true,
matchAgainstDefines: true, // Allow defines to rewrite the JSX factory
})
if js_ast.IsPropertyAccess(target) {
kind = js_ast.TargetWasOriginallyPropertyAccess
}
p.warnAboutImportNamespaceCall(target, exprKindCall)
}
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
Target: target,
Args: args,
CloseParenLoc: e.CloseLoc,
IsMultiLine: !e.IsTagSingleLine,
Kind: kind,
// Enable tree shaking
CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations && !p.options.jsx.SideEffects,
}}, exprOut{}
} else {
// Arguments to jsx()
args := []js_ast.Expr{e.TagOrNil}
// Props argument
properties := make([]js_ast.Property, 0, len(e.Properties)+1)
// For jsx(), "key" is passed in as a separate argument, so filter it out
// from the props here. Also, check for __source and __self, which might have
// been added by some upstream plugin. Their presence here would represent a
// configuration error.
hasKey := false
keyProperty := js_ast.Expr{Loc: expr.Loc, Data: js_ast.EUndefinedShared}
for _, property := range e.Properties {
if str, ok := property.Key.Data.(*js_ast.EString); ok {
propName := helpers.UTF16ToString(str.Value)
switch propName {
case "key":
if boolean, ok := property.ValueOrNil.Data.(*js_ast.EBoolean); ok && boolean.Value && property.Flags.Has(js_ast.PropertyWasShorthand) {
r := js_lexer.RangeOfIdentifier(p.source, property.Loc)
msg := logger.Msg{
Kind: logger.Error,
Data: p.tracker.MsgData(r, "Please provide an explicit value for \"key\":"),
Notes: []logger.MsgData{{Text: "Using \"key\" as a shorthand for \"key={true}\" is not allowed when using React's \"automatic\" JSX transform."}},
}
msg.Data.Location.Suggestion = "key={true}"
p.log.AddMsg(msg)
} else {
keyProperty = property.ValueOrNil
hasKey = true
}
continue
case "__source", "__self":
r := js_lexer.RangeOfIdentifier(p.source, property.Loc)
p.log.AddErrorWithNotes(&p.tracker, r,
fmt.Sprintf("Duplicate \"%s\" prop found:", propName),
[]logger.MsgData{{Text: "Both \"__source\" and \"__self\" are set automatically by esbuild when using React's \"automatic\" JSX transform. " +
"This duplicate prop may have come from a plugin."}})
continue
}
}
properties = append(properties, property)
}
isStaticChildren := len(children) > 1
// Children are passed in as an explicit prop
if len(children) > 0 {
childrenValue := children[0]
if len(children) > 1 {
childrenValue.Data = &js_ast.EArray{Items: children}
} else if _, ok := childrenValue.Data.(*js_ast.ESpread); ok {
// TypeScript considers spread children to be static, but Babel considers
// it to be an error ("Spread children are not supported in React.").
// We'll follow TypeScript's behavior here because spread children may be
// valid with non-React source runtimes.
childrenValue.Data = &js_ast.EArray{Items: []js_ast.Expr{childrenValue}}
isStaticChildren = true
}
properties = append(properties, js_ast.Property{
Key: js_ast.Expr{
Data: &js_ast.EString{Value: helpers.StringToUTF16("children")},
Loc: childrenValue.Loc,
},
ValueOrNil: childrenValue,
Kind: js_ast.PropertyField,
Loc: childrenValue.Loc,
})
}
args = append(args, p.lowerObjectSpread(propsLoc, &js_ast.EObject{
Properties: properties,
IsSingleLine: e.IsTagSingleLine,
}))
// "key"
if hasKey || p.options.jsx.Development {
args = append(args, keyProperty)
}
if p.options.jsx.Development {
// "isStaticChildren"
args = append(args, js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EBoolean{Value: isStaticChildren}})
// "__source"
args = append(args, js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EObject{
Properties: []js_ast.Property{
{
Kind: js_ast.PropertyField,
Key: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16("fileName")}},
ValueOrNil: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(p.source.PrettyPaths.Select(p.options.codePathStyle))}},
},
{
Kind: js_ast.PropertyField,
Key: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16("lineNumber")}},
ValueOrNil: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: float64(jsxSourceLine + 1)}}, // 1-based lines
},
{
Kind: js_ast.PropertyField,
Key: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16("columnNumber")}},
ValueOrNil: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: float64(jsxSourceColumn + 1)}}, // 1-based columns
},
},
}})
// "__self"
__self := js_ast.Expr{Loc: expr.Loc, Data: js_ast.EThisShared}
{
if p.fnOnlyDataVisit.shouldReplaceThisWithInnerClassNameRef {
// Substitute "this" if we're inside a static class context
p.recordUsage(*p.fnOnlyDataVisit.innerClassNameRef)
__self.Data = &js_ast.EIdentifier{Ref: *p.fnOnlyDataVisit.innerClassNameRef}
} else if !p.fnOnlyDataVisit.isThisNested && p.options.mode != config.ModePassThrough {
// Replace top-level "this" with "undefined" if there's an output format
__self.Data = js_ast.EUndefinedShared
} else if p.fnOrArrowDataVisit.isDerivedClassCtor {
// We can't use "this" here in case it comes before "super()"
__self.Data = js_ast.EUndefinedShared
}
}
if _, ok := __self.Data.(*js_ast.EUndefined); !ok {
// Omit "__self" entirely if it's undefined
args = append(args, __self)
}
}
jsx := JSXImportJSX
if isStaticChildren {
jsx = JSXImportJSXS
}
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
Target: p.importJSXSymbol(expr.Loc, jsx),
Args: args,
CloseParenLoc: e.CloseLoc,
IsMultiLine: !e.IsTagSingleLine,
// Enable tree shaking
CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations && !p.options.jsx.SideEffects,
}}, exprOut{}
}
}
case *js_ast.ETemplate:
if e.LegacyOctalLoc.Start > 0 {
p.log.AddError(&p.tracker, p.source.RangeOfLegacyOctalEscape(e.LegacyOctalLoc),
"Legacy octal escape sequences cannot be used in template literals")
}
var tagThisFunc func() js_ast.Expr
var tagWrapFunc func(js_ast.Expr) js_ast.Expr
if e.TagOrNil.Data != nil {
// Capture the value for "this" if the tag is a lowered optional chain.
// We'll need to manually apply this value later to preserve semantics.
tagIsLoweredOptionalChain := false
if p.options.unsupportedJSFeatures.Has(compat.OptionalChain) {
switch target := e.TagOrNil.Data.(type) {
case *js_ast.EDot:
tagIsLoweredOptionalChain = target.OptionalChain != js_ast.OptionalChainNone
case *js_ast.EIndex:
tagIsLoweredOptionalChain = target.OptionalChain != js_ast.OptionalChainNone
}
}
p.templateTag = e.TagOrNil.Data
tag, tagOut := p.visitExprInOut(e.TagOrNil, exprIn{storeThisArgForParentOptionalChain: tagIsLoweredOptionalChain})
e.TagOrNil = tag
tagThisFunc = tagOut.thisArgFunc
tagWrapFunc = tagOut.thisArgWrapFunc
// Copy the call side effect flag over if this is a known target
if id, ok := tag.Data.(*js_ast.EIdentifier); ok && p.symbols[id.Ref.InnerIndex].Flags.Has(ast.CallCanBeUnwrappedIfUnused) {
e.CanBeUnwrappedIfUnused = true
}
// The value of "this" must be manually preserved for private member
// accesses inside template tag expressions such as "this.#foo``".
// The private member "this.#foo" must see the value of "this".
if target, loc, private := p.extractPrivateIndex(e.TagOrNil); private != nil {
// "foo.#bar`123`" => "__privateGet(_a = foo, #bar).bind(_a)`123`"
targetFunc, targetWrapFunc := p.captureValueWithPossibleSideEffects(target.Loc, 2, target, valueCouldBeMutated)
e.TagOrNil = targetWrapFunc(js_ast.Expr{Loc: target.Loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: target.Loc, Data: &js_ast.EDot{
Target: p.lowerPrivateGet(targetFunc(), loc, private),
Name: "bind",
NameLoc: target.Loc,
}},
Args: []js_ast.Expr{targetFunc()},
Kind: js_ast.TargetWasOriginallyPropertyAccess,
}})
}
}
for i, part := range e.Parts {
e.Parts[i].Value = p.visitExpr(part.Value)
}
// When mangling, inline string values into the template literal. Note that
// it may no longer be a template literal after this point (it may turn into
// a plain string literal instead).
if p.shouldFoldTypeScriptConstantExpressions || p.options.minifySyntax {
expr = js_ast.InlinePrimitivesIntoTemplate(expr.Loc, e)
}
shouldLowerTemplateLiteral := p.options.unsupportedJSFeatures.Has(compat.TemplateLiteral)
// If the tag was originally an optional chaining property access, then
// we'll need to lower this template literal as well to preserve the value
// for "this".
if tagThisFunc != nil {
shouldLowerTemplateLiteral = true
}
// Lower tagged template literals that include "</script"
// since we won't be able to escape it without lowering it
if !shouldLowerTemplateLiteral && !p.options.unsupportedJSFeatures.Has(compat.InlineScript) && e.TagOrNil.Data != nil {
if containsClosingScriptTag(e.HeadRaw) {
shouldLowerTemplateLiteral = true
} else {
for _, part := range e.Parts {
if containsClosingScriptTag(part.TailRaw) {
shouldLowerTemplateLiteral = true
break
}
}
}
}
// Convert template literals to older syntax if this is still a template literal
if shouldLowerTemplateLiteral {
if e, ok := expr.Data.(*js_ast.ETemplate); ok {
return p.lowerTemplateLiteral(expr.Loc, e, tagThisFunc, tagWrapFunc), exprOut{}
}
}
case *js_ast.EBinary:
// The handling of binary expressions is convoluted because we're using
// iteration on the heap instead of recursion on the call stack to avoid
// stack overflow for deeply-nested ASTs. See the comment before the
// definition of "binaryExprVisitor" for details.
v := binaryExprVisitor{
e: e,
loc: expr.Loc,
in: in,
}
// Everything uses a single stack to reduce allocation overhead. This stack
// should almost always be very small, and almost all visits should reuse
// existing memory without allocating anything.
stackBottom := len(p.binaryExprStack)
// Iterate down into the AST along the left node of the binary operation.
// Continue iterating until we encounter something that's not a binary node.
for {
// Check whether this node is a special case. If it is, a result will be
// provided which ends our iteration. Otherwise, the visitor object will
// be prepared for visiting.
if result := v.checkAndPrepare(p); result.Data != nil {
expr = result
break
}
// Grab the arguments to our nested "visitExprInOut" call for the left
// node. We only care about deeply-nested left nodes because most binary
// operators in JavaScript are left-associative and the problematic edge
// cases we're trying to avoid crashing on have lots of left-associative
// binary operators chained together without parentheses (e.g. "1+2+...").
left := v.e.Left
leftIn := v.leftIn
leftBinary, ok := left.Data.(*js_ast.EBinary)
// Stop iterating if iteration doesn't apply to the left node. This checks
// the assignment target because "visitExprInOut" has additional behavior
// in that case that we don't want to miss (before the top-level "switch"
// statement).
if !ok || leftIn.assignTarget != js_ast.AssignTargetNone {
v.e.Left, _ = p.visitExprInOut(left, leftIn)
expr = v.visitRightAndFinish(p)
break
}
// Note that we only append to the stack (and therefore allocate memory
// on the heap) when there are nested binary expressions. A single binary
// expression doesn't add anything to the stack.
p.binaryExprStack = append(p.binaryExprStack, v)
v = binaryExprVisitor{
e: leftBinary,
loc: left.Loc,
in: leftIn,
}
}
// Process all binary operations from the deepest-visited node back toward
// our original top-level binary operation.
for {
n := len(p.binaryExprStack) - 1
if n < stackBottom {
break
}
v := p.binaryExprStack[n]
p.binaryExprStack = p.binaryExprStack[:n]
v.e.Left = expr
expr = v.visitRightAndFinish(p)
}
return expr, exprOut{}
case *js_ast.EDot:
isDeleteTarget := e == p.deleteTarget
isCallTarget := e == p.callTarget
isTemplateTag := e == p.templateTag
// Check both user-specified defines and known globals
if defines, ok := p.options.defines.DotDefines[e.Name]; ok {
for _, define := range defines {
if p.isDotOrIndexDefineMatch(expr, define.KeyParts) {
// Substitute user-specified defines
if define.DefineExpr != nil {
new := p.instantiateDefineExpr(expr.Loc, *define.DefineExpr, identifierOpts{
assignTarget: in.assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
})
if in.assignTarget == js_ast.AssignTargetNone || defineValueCanBeUsedInAssignTarget(new.Data) {
// Note: We don't need to "ignoreRef" on the underlying identifier
// because we have only parsed it but not visited it yet
return new, exprOut{}
} else {
r := logger.Range{Loc: expr.Loc, Len: js_lexer.RangeOfIdentifier(p.source, e.NameLoc).End() - expr.Loc.Start}
p.logAssignToDefine(r, "", expr)
}
}
// Copy the side effect flags over in case this expression is unused
if define.Flags.Has(config.CanBeRemovedIfUnused) {
e.CanBeRemovedIfUnused = true
}
if define.Flags.Has(config.CallCanBeUnwrappedIfUnused) && !p.options.ignoreDCEAnnotations {
e.CallCanBeUnwrappedIfUnused = true
}
if define.Flags.Has(config.IsSymbolInstance) {
e.IsSymbolInstance = true
}
break
}
}
}
// Check injected dot names
if names, ok := p.injectedDotNames[e.Name]; ok {
for _, name := range names {
if p.isDotOrIndexDefineMatch(expr, name.parts) {
// Note: We don't need to "ignoreRef" on the underlying identifier
// because we have only parsed it but not visited it yet
return p.instantiateInjectDotName(expr.Loc, name, in.assignTarget), exprOut{}
}
}
}
// Track ".then().catch()" chains
if isCallTarget && p.thenCatchChain.nextTarget == e {
if e.Name == "catch" {
p.thenCatchChain = thenCatchChain{
nextTarget: e.Target.Data,
hasCatch: true,
catchLoc: e.NameLoc,
}
} else if e.Name == "then" {
p.thenCatchChain = thenCatchChain{
nextTarget: e.Target.Data,
hasCatch: p.thenCatchChain.hasCatch || p.thenCatchChain.hasMultipleArgs,
catchLoc: p.thenCatchChain.catchLoc,
}
}
}
p.dotOrIndexTarget = e.Target.Data
target, out := p.visitExprInOut(e.Target, exprIn{
hasChainParent: e.OptionalChain == js_ast.OptionalChainContinue,
})
e.Target = target
// Lower "super.prop" if necessary
if e.OptionalChain == js_ast.OptionalChainNone && in.assignTarget == js_ast.AssignTargetNone &&
!isCallTarget && p.shouldLowerSuperPropertyAccess(e.Target) {
// "super.foo" => "__superGet('foo')"
key := js_ast.Expr{Loc: e.NameLoc, Data: &js_ast.EString{Value: helpers.StringToUTF16(e.Name)}}
value := p.lowerSuperPropertyGet(expr.Loc, key)
if isTemplateTag {
value.Data = &js_ast.ECall{
Target: js_ast.Expr{Loc: value.Loc, Data: &js_ast.EDot{
Target: value,
Name: "bind",
NameLoc: value.Loc,
}},
Args: []js_ast.Expr{{Loc: value.Loc, Data: js_ast.EThisShared}},
Kind: js_ast.TargetWasOriginallyPropertyAccess,
}
}
return value, exprOut{}
}
// Lower optional chaining if we're the top of the chain
containsOptionalChain := e.OptionalChain == js_ast.OptionalChainStart ||
(e.OptionalChain == js_ast.OptionalChainContinue && out.childContainsOptionalChain)
if containsOptionalChain && !in.hasChainParent {
return p.lowerOptionalChain(expr, in, out)
}
// Also erase "console.log.call(console, 123)" and "console.log.bind(console)"
if out.callMustBeReplacedWithUndefined {
if e.Name == "call" || e.Name == "apply" {
out.methodCallMustBeReplacedWithUndefined = true
} else if p.options.unsupportedJSFeatures.Has(compat.Arrow) {
e.Target.Data = &js_ast.EFunction{}
} else {
e.Target.Data = &js_ast.EArrow{}
}
}
// Potentially rewrite this property access
out = exprOut{
childContainsOptionalChain: containsOptionalChain,
callMustBeReplacedWithUndefined: out.methodCallMustBeReplacedWithUndefined,
thisArgFunc: out.thisArgFunc,
thisArgWrapFunc: out.thisArgWrapFunc,
}
if !in.hasChainParent {
out.thisArgFunc = nil
out.thisArgWrapFunc = nil
}
if e.OptionalChain == js_ast.OptionalChainNone {
if value, ok := p.maybeRewritePropertyAccess(expr.Loc, in.assignTarget,
isDeleteTarget, e.Target, e.Name, e.NameLoc, isCallTarget, isTemplateTag, false); ok {
return value, out
}
}
return js_ast.Expr{Loc: expr.Loc, Data: e}, out
case *js_ast.EIndex:
isCallTarget := e == p.callTarget
isTemplateTag := e == p.templateTag
isDeleteTarget := e == p.deleteTarget
// Check both user-specified defines and known globals
if str, ok := e.Index.Data.(*js_ast.EString); ok {
if defines, ok := p.options.defines.DotDefines[helpers.UTF16ToString(str.Value)]; ok {
for _, define := range defines {
if p.isDotOrIndexDefineMatch(expr, define.KeyParts) {
// Substitute user-specified defines
if define.DefineExpr != nil {
new := p.instantiateDefineExpr(expr.Loc, *define.DefineExpr, identifierOpts{
assignTarget: in.assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
})
if in.assignTarget == js_ast.AssignTargetNone || defineValueCanBeUsedInAssignTarget(new.Data) {
// Note: We don't need to "ignoreRef" on the underlying identifier
// because we have only parsed it but not visited it yet
return new, exprOut{}
} else {
r := logger.Range{Loc: expr.Loc}
afterIndex := logger.Loc{Start: p.source.RangeOfString(e.Index.Loc).End()}
if closeBracket := p.source.RangeOfOperatorAfter(afterIndex, "]"); closeBracket.Len > 0 {
r.Len = closeBracket.End() - r.Loc.Start
}
p.logAssignToDefine(r, "", expr)
}
}
// Copy the side effect flags over in case this expression is unused
if define.Flags.Has(config.CanBeRemovedIfUnused) {
e.CanBeRemovedIfUnused = true
}
if define.Flags.Has(config.CallCanBeUnwrappedIfUnused) && !p.options.ignoreDCEAnnotations {
e.CallCanBeUnwrappedIfUnused = true
}
if define.Flags.Has(config.IsSymbolInstance) {
e.IsSymbolInstance = true
}
break
}
}
}
}
// "a['b']" => "a.b"
if p.options.minifySyntax {
if str, ok := e.Index.Data.(*js_ast.EString); ok && js_ast.IsIdentifierUTF16(str.Value) {
dot := p.dotOrMangledPropParse(e.Target, js_lexer.MaybeSubstring{String: helpers.UTF16ToString(str.Value)}, e.Index.Loc, e.OptionalChain, wasOriginallyIndex)
if isCallTarget {
p.callTarget = dot
}
if isTemplateTag {
p.templateTag = dot
}
if isDeleteTarget {
p.deleteTarget = dot
}
return p.visitExprInOut(js_ast.Expr{Loc: expr.Loc, Data: dot}, in)
}
}
p.dotOrIndexTarget = e.Target.Data
target, out := p.visitExprInOut(e.Target, exprIn{
hasChainParent: e.OptionalChain == js_ast.OptionalChainContinue,
})
e.Target = target
// Special-case private identifiers
if private, ok := e.Index.Data.(*js_ast.EPrivateIdentifier); ok {
name := p.loadNameFromRef(private.Ref)
result := p.findSymbol(e.Index.Loc, name)
private.Ref = result.ref
// Unlike regular identifiers, there are no unbound private identifiers
kind := p.symbols[result.ref.InnerIndex].Kind
if !kind.IsPrivate() {
r := logger.Range{Loc: e.Index.Loc, Len: int32(len(name))}
p.log.AddError(&p.tracker, r, fmt.Sprintf("Private name %q must be declared in an enclosing class", name))
} else {
var r logger.Range
var text string
if in.assignTarget != js_ast.AssignTargetNone && (kind == ast.SymbolPrivateMethod || kind == ast.SymbolPrivateStaticMethod) {
r = logger.Range{Loc: e.Index.Loc, Len: int32(len(name))}
text = fmt.Sprintf("Writing to read-only method %q will throw", name)
} else if in.assignTarget != js_ast.AssignTargetNone && (kind == ast.SymbolPrivateGet || kind == ast.SymbolPrivateStaticGet) {
r = logger.Range{Loc: e.Index.Loc, Len: int32(len(name))}
text = fmt.Sprintf("Writing to getter-only property %q will throw", name)
} else if in.assignTarget != js_ast.AssignTargetReplace && (kind == ast.SymbolPrivateSet || kind == ast.SymbolPrivateStaticSet) {
r = logger.Range{Loc: e.Index.Loc, Len: int32(len(name))}
text = fmt.Sprintf("Reading from setter-only property %q will throw", name)
}
if text != "" {
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddID(logger.MsgID_JS_PrivateNameWillThrow, kind, &p.tracker, r, text)
}
}
// Lower private member access only if we're sure the target isn't needed
// for the value of "this" for a call expression. All other cases will be
// taken care of by the enclosing call expression.
if p.privateSymbolNeedsToBeLowered(private) && e.OptionalChain == js_ast.OptionalChainNone &&
in.assignTarget == js_ast.AssignTargetNone && !isCallTarget && !isTemplateTag {
// "foo.#bar" => "__privateGet(foo, #bar)"
return p.lowerPrivateGet(e.Target, e.Index.Loc, private), exprOut{}
}
} else {
e.Index, _ = p.visitExprInOut(e.Index, exprIn{
shouldMangleStringsAsProps: true,
})
}
// Lower "super[prop]" if necessary
if e.OptionalChain == js_ast.OptionalChainNone && in.assignTarget == js_ast.AssignTargetNone &&
!isCallTarget && p.shouldLowerSuperPropertyAccess(e.Target) {
// "super[foo]" => "__superGet(foo)"
value := p.lowerSuperPropertyGet(expr.Loc, e.Index)
if isTemplateTag {
value.Data = &js_ast.ECall{
Target: js_ast.Expr{Loc: value.Loc, Data: &js_ast.EDot{
Target: value,
Name: "bind",
NameLoc: value.Loc,
}},
Args: []js_ast.Expr{{Loc: value.Loc, Data: js_ast.EThisShared}},
Kind: js_ast.TargetWasOriginallyPropertyAccess,
}
}
return value, exprOut{}
}
// Lower optional chaining if we're the top of the chain
containsOptionalChain := e.OptionalChain == js_ast.OptionalChainStart ||
(e.OptionalChain == js_ast.OptionalChainContinue && out.childContainsOptionalChain)
if containsOptionalChain && !in.hasChainParent {
return p.lowerOptionalChain(expr, in, out)
}
// Potentially rewrite this property access
out = exprOut{
childContainsOptionalChain: containsOptionalChain,
callMustBeReplacedWithUndefined: out.methodCallMustBeReplacedWithUndefined,
thisArgFunc: out.thisArgFunc,
thisArgWrapFunc: out.thisArgWrapFunc,
}
if !in.hasChainParent {
out.thisArgFunc = nil
out.thisArgWrapFunc = nil
}
if str, ok := e.Index.Data.(*js_ast.EString); ok && e.OptionalChain == js_ast.OptionalChainNone {
preferQuotedKey := !p.options.minifySyntax
if value, ok := p.maybeRewritePropertyAccess(expr.Loc, in.assignTarget, isDeleteTarget,
e.Target, helpers.UTF16ToString(str.Value), e.Index.Loc, isCallTarget, isTemplateTag, preferQuotedKey); ok {
return value, out
}
}
// Create an error for assigning to an import namespace when bundling. Even
// though this is a run-time error, we make it a compile-time error when
// bundling because scope hoisting means these will no longer be run-time
// errors.
if p.options.mode == config.ModeBundle && (in.assignTarget != js_ast.AssignTargetNone || isDeleteTarget) {
if id, ok := e.Target.Data.(*js_ast.EIdentifier); ok && p.symbols[id.Ref.InnerIndex].Kind == ast.SymbolImport {
r := js_lexer.RangeOfIdentifier(p.source, e.Target.Loc)
p.log.AddErrorWithNotes(&p.tracker, r,
fmt.Sprintf("Cannot assign to property on import %q", p.symbols[id.Ref.InnerIndex].OriginalName),
[]logger.MsgData{{Text: "Imports are immutable in JavaScript. " +
"To modify the value of this import, you must export a setter function in the " +
"imported file and then import and call that function here instead."}})
}
}
if p.options.minifySyntax {
switch index := e.Index.Data.(type) {
case *js_ast.EString:
// "a['x' + 'y']" => "a.xy" (this is done late to allow for constant folding)
if js_ast.IsIdentifierUTF16(index.Value) {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EDot{
Target: e.Target,
Name: helpers.UTF16ToString(index.Value),
NameLoc: e.Index.Loc,
OptionalChain: e.OptionalChain,
CanBeRemovedIfUnused: e.CanBeRemovedIfUnused,
CallCanBeUnwrappedIfUnused: e.CallCanBeUnwrappedIfUnused,
}}, out
}
// "a['123']" => "a[123]" (this is done late to allow "'123'" to be mangled)
if numberValue, ok := js_ast.StringToEquivalentNumberValue(index.Value); ok {
e.Index.Data = &js_ast.ENumber{Value: numberValue}
}
case *js_ast.ENumber:
// "'abc'[1]" => "'b'"
if target, ok := e.Target.Data.(*js_ast.EString); ok {
if intValue := math.Floor(index.Value); index.Value == intValue && intValue >= 0 && intValue < float64(len(target.Value)) {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: []uint16{target.Value[int(intValue)]}}}, out
}
}
}
}
return js_ast.Expr{Loc: expr.Loc, Data: e}, out
case *js_ast.EUnary:
switch e.Op {
case js_ast.UnOpTypeof:
e.Value, _ = p.visitExprInOut(e.Value, exprIn{assignTarget: e.Op.UnaryAssignTarget()})
// Compile-time "typeof" evaluation
if typeof, ok := js_ast.TypeofWithoutSideEffects(e.Value.Data); ok {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(typeof)}}, exprOut{}
}
case js_ast.UnOpDelete:
// Warn about code that tries to do "delete super.foo"
var superPropLoc logger.Loc
switch e2 := e.Value.Data.(type) {
case *js_ast.EDot:
if _, ok := e2.Target.Data.(*js_ast.ESuper); ok {
superPropLoc = e2.Target.Loc
}
case *js_ast.EIndex:
if _, ok := e2.Target.Data.(*js_ast.ESuper); ok {
superPropLoc = e2.Target.Loc
}
case *js_ast.EIdentifier:
p.markStrictModeFeature(deleteBareName, js_lexer.RangeOfIdentifier(p.source, e.Value.Loc), "")
}
if superPropLoc.Start != 0 {
r := js_lexer.RangeOfIdentifier(p.source, superPropLoc)
text := "Attempting to delete a property of \"super\" will throw a ReferenceError"
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddID(logger.MsgID_JS_DeleteSuperProperty, kind, &p.tracker, r, text)
}
p.deleteTarget = e.Value.Data
value, out := p.visitExprInOut(e.Value, exprIn{hasChainParent: true})
e.Value = value
// Lower optional chaining if present since we're guaranteed to be the
// end of the chain
if out.childContainsOptionalChain {
return p.lowerOptionalChain(expr, in, out)
}
default:
e.Value, _ = p.visitExprInOut(e.Value, exprIn{assignTarget: e.Op.UnaryAssignTarget()})
// Post-process the unary expression
switch e.Op {
case js_ast.UnOpNot:
if p.options.minifySyntax {
e.Value = p.astHelpers.SimplifyBooleanExpr(e.Value)
}
if boolean, sideEffects, ok := js_ast.ToBooleanWithSideEffects(e.Value.Data); ok && sideEffects == js_ast.NoSideEffects {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EBoolean{Value: !boolean}}, exprOut{}
}
if p.options.minifySyntax {
if result, ok := js_ast.MaybeSimplifyNot(e.Value); ok {
return result, exprOut{}
}
}
case js_ast.UnOpVoid:
var shouldRemove bool
if p.options.minifySyntax {
shouldRemove = p.astHelpers.ExprCanBeRemovedIfUnused(e.Value)
} else {
// This special case was added for a very obscure reason. There's a
// custom dialect of JavaScript called Svelte that uses JavaScript
// syntax with different semantics. Specifically variable accesses
// have side effects (!). And someone wants to use "void x" instead
// of just "x" to trigger the side effect for some reason.
//
// Arguably this should not be supported, because you shouldn't be
// running esbuild on weird kinda-JavaScript-but-not languages and
// expecting it to work correctly. But this one special case seems
// harmless enough. This is definitely not fully supported though.
//
// More info: https://github.com/evanw/esbuild/issues/4041
shouldRemove = isUnsightlyPrimitive(e.Value.Data)
}
if shouldRemove {
return js_ast.Expr{Loc: expr.Loc, Data: js_ast.EUndefinedShared}, exprOut{}
}
case js_ast.UnOpPos:
if number, ok := js_ast.ToNumberWithoutSideEffects(e.Value.Data); ok {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: number}}, exprOut{}
}
case js_ast.UnOpNeg:
if number, ok := js_ast.ToNumberWithoutSideEffects(e.Value.Data); ok {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: -number}}, exprOut{}
}
case js_ast.UnOpCpl:
if p.shouldFoldTypeScriptConstantExpressions || p.options.minifySyntax {
// Minification folds complement operations since they are unlikely to result in larger output
if number, ok := js_ast.ToNumberWithoutSideEffects(e.Value.Data); ok {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: float64(^js_ast.ToInt32(number))}}, exprOut{}
}
}
////////////////////////////////////////////////////////////////////////////////
// All assignment operators below here
case js_ast.UnOpPreDec, js_ast.UnOpPreInc, js_ast.UnOpPostDec, js_ast.UnOpPostInc:
if target, loc, private := p.extractPrivateIndex(e.Value); private != nil {
return p.lowerPrivateSetUnOp(target, loc, private, e.Op), exprOut{}
}
if property := p.extractSuperProperty(e.Value); property.Data != nil {
e.Value = p.callSuperPropertyWrapper(expr.Loc, property)
}
}
}
// "-(a, b)" => "a, -b"
if p.options.minifySyntax && e.Op != js_ast.UnOpDelete && e.Op != js_ast.UnOpTypeof {
if comma, ok := e.Value.Data.(*js_ast.EBinary); ok && comma.Op == js_ast.BinOpComma {
return js_ast.JoinWithComma(comma.Left, js_ast.Expr{
Loc: comma.Right.Loc,
Data: &js_ast.EUnary{
Op: e.Op,
Value: comma.Right,
},
}), exprOut{}
}
}
case *js_ast.EIf:
e.Test = p.visitExpr(e.Test)
if p.options.minifySyntax {
e.Test = p.astHelpers.SimplifyBooleanExpr(e.Test)
}
// Propagate these flags into the branches
childIn := exprIn{
shouldMangleStringsAsProps: in.shouldMangleStringsAsProps,
}
// Fold constants
if boolean, sideEffects, ok := js_ast.ToBooleanWithSideEffects(e.Test.Data); !ok {
e.Yes, _ = p.visitExprInOut(e.Yes, childIn)
e.No, _ = p.visitExprInOut(e.No, childIn)
} else {
// Mark the control flow as dead if the branch is never taken
if boolean {
// "true ? live : dead"
e.Yes, _ = p.visitExprInOut(e.Yes, childIn)
old := p.isControlFlowDead
p.isControlFlowDead = true
e.No, _ = p.visitExprInOut(e.No, childIn)
p.isControlFlowDead = old
if p.options.minifySyntax {
// "(a, true) ? b : c" => "a, b"
if sideEffects == js_ast.CouldHaveSideEffects {
return js_ast.JoinWithComma(p.astHelpers.SimplifyUnusedExpr(e.Test, p.options.unsupportedJSFeatures), e.Yes), exprOut{}
}
return e.Yes, exprOut{}
}
} else {
// "false ? dead : live"
old := p.isControlFlowDead
p.isControlFlowDead = true
e.Yes, _ = p.visitExprInOut(e.Yes, childIn)
p.isControlFlowDead = old
e.No, _ = p.visitExprInOut(e.No, childIn)
if p.options.minifySyntax {
// "(a, false) ? b : c" => "a, c"
if sideEffects == js_ast.CouldHaveSideEffects {
return js_ast.JoinWithComma(p.astHelpers.SimplifyUnusedExpr(e.Test, p.options.unsupportedJSFeatures), e.No), exprOut{}
}
return e.No, exprOut{}
}
}
}
if p.options.minifySyntax {
return p.astHelpers.MangleIfExpr(expr.Loc, e, p.options.unsupportedJSFeatures), exprOut{}
}
case *js_ast.EAwait:
// Silently remove unsupported top-level "await" in dead code branches
if p.fnOrArrowDataVisit.isOutsideFnOrArrow {
if p.isControlFlowDead && (p.options.unsupportedJSFeatures.Has(compat.TopLevelAwait) || !p.options.outputFormat.KeepESMImportExportSyntax()) {
return p.visitExprInOut(e.Value, in)
} else {
p.liveTopLevelAwaitKeyword = logger.Range{Loc: expr.Loc, Len: 5}
p.markSyntaxFeature(compat.TopLevelAwait, logger.Range{Loc: expr.Loc, Len: 5})
}
}
p.awaitTarget = e.Value.Data
e.Value = p.visitExpr(e.Value)
// "await" expressions turn into "yield" expressions when lowering
return p.maybeLowerAwait(expr.Loc, e), exprOut{}
case *js_ast.EYield:
if e.ValueOrNil.Data != nil {
e.ValueOrNil = p.visitExpr(e.ValueOrNil)
}
// "yield* x" turns into "yield* __yieldStar(x)" when lowering async generator functions
if e.IsStar && p.options.unsupportedJSFeatures.Has(compat.AsyncGenerator) && p.fnOrArrowDataVisit.isGenerator {
e.ValueOrNil = p.callRuntime(expr.Loc, "__yieldStar", []js_ast.Expr{e.ValueOrNil})
}
case *js_ast.EArray:
if in.assignTarget != js_ast.AssignTargetNone {
if e.CommaAfterSpread.Start != 0 {
p.log.AddError(&p.tracker, logger.Range{Loc: e.CommaAfterSpread, Len: 1}, "Unexpected \",\" after rest pattern")
}
p.markSyntaxFeature(compat.Destructuring, logger.Range{Loc: expr.Loc, Len: 1})
}
hasSpread := false
for i, item := range e.Items {
switch e2 := item.Data.(type) {
case *js_ast.EMissing:
case *js_ast.ESpread:
e2.Value, _ = p.visitExprInOut(e2.Value, exprIn{assignTarget: in.assignTarget})
hasSpread = true
case *js_ast.EBinary:
if in.assignTarget != js_ast.AssignTargetNone && e2.Op == js_ast.BinOpAssign {
e2.Left, _ = p.visitExprInOut(e2.Left, exprIn{assignTarget: js_ast.AssignTargetReplace})
// Propagate the name to keep from the binding into the initializer
if id, ok := e2.Left.Data.(*js_ast.EIdentifier); ok {
p.nameToKeep = p.symbols[id.Ref.InnerIndex].OriginalName
p.nameToKeepIsFor = e2.Right.Data
}
e2.Right = p.visitExpr(e2.Right)
} else {
item, _ = p.visitExprInOut(item, exprIn{assignTarget: in.assignTarget})
}
default:
item, _ = p.visitExprInOut(item, exprIn{assignTarget: in.assignTarget})
}
e.Items[i] = item
}
// "[1, ...[2, 3], 4]" => "[1, 2, 3, 4]"
if p.options.minifySyntax && hasSpread && in.assignTarget == js_ast.AssignTargetNone {
e.Items = js_ast.InlineSpreadsOfArrayLiterals(e.Items)
}
case *js_ast.EObject:
if in.assignTarget != js_ast.AssignTargetNone {
if e.CommaAfterSpread.Start != 0 {
p.log.AddError(&p.tracker, logger.Range{Loc: e.CommaAfterSpread, Len: 1}, "Unexpected \",\" after rest pattern")
}
p.markSyntaxFeature(compat.Destructuring, logger.Range{Loc: expr.Loc, Len: 1})
}
hasSpread := false
protoRange := logger.Range{}
innerClassNameRef := ast.InvalidRef
for i := range e.Properties {
property := &e.Properties[i]
if property.Kind != js_ast.PropertySpread {
key := property.Key
if mangled, ok := key.Data.(*js_ast.ENameOfSymbol); ok {
mangled.Ref = p.symbolForMangledProp(p.loadNameFromRef(mangled.Ref))
} else {
key, _ = p.visitExprInOut(property.Key, exprIn{
shouldMangleStringsAsProps: true,
})
property.Key = key
}
// Forbid duplicate "__proto__" properties according to the specification
if !property.Flags.Has(js_ast.PropertyIsComputed) && !property.Flags.Has(js_ast.PropertyWasShorthand) &&
property.Kind == js_ast.PropertyField && in.assignTarget == js_ast.AssignTargetNone {
if str, ok := key.Data.(*js_ast.EString); ok && helpers.UTF16EqualsString(str.Value, "__proto__") {
r := js_lexer.RangeOfIdentifier(p.source, key.Loc)
if protoRange.Len > 0 {
p.log.AddErrorWithNotes(&p.tracker, r,
"Cannot specify the \"__proto__\" property more than once per object",
[]logger.MsgData{p.tracker.MsgData(protoRange, "The earlier \"__proto__\" property is here:")})
} else {
protoRange = r
}
}
}
// "{['x']: y}" => "{x: y}"
if p.options.minifySyntax && property.Flags.Has(js_ast.PropertyIsComputed) {
if inlined, ok := key.Data.(*js_ast.EInlinedEnum); ok {
switch inlined.Value.Data.(type) {
case *js_ast.EString, *js_ast.ENumber:
key.Data = inlined.Value.Data
property.Key.Data = key.Data
}
}
switch k := key.Data.(type) {
case *js_ast.ENumber, *js_ast.ENameOfSymbol:
property.Flags &= ^js_ast.PropertyIsComputed
case *js_ast.EString:
if !helpers.UTF16EqualsString(k.Value, "__proto__") {
property.Flags &= ^js_ast.PropertyIsComputed
}
}
}
} else {
hasSpread = true
}
// Extract the initializer for expressions like "({ a: b = c } = d)"
if in.assignTarget != js_ast.AssignTargetNone && property.InitializerOrNil.Data == nil && property.ValueOrNil.Data != nil {
if binary, ok := property.ValueOrNil.Data.(*js_ast.EBinary); ok && binary.Op == js_ast.BinOpAssign {
property.InitializerOrNil = binary.Right
property.ValueOrNil = binary.Left
}
}
if property.ValueOrNil.Data != nil {
oldIsInStaticClassContext := p.fnOnlyDataVisit.isInStaticClassContext
oldInnerClassNameRef := p.fnOnlyDataVisit.innerClassNameRef
// If this is an async method and async methods are unsupported,
// generate a temporary variable in case this async method contains a
// "super" property reference. If that happens, the "super" expression
// must be lowered which will need a reference to this object literal.
if property.Kind == js_ast.PropertyMethod && p.options.unsupportedJSFeatures.Has(compat.AsyncAwait) {
if fn, ok := property.ValueOrNil.Data.(*js_ast.EFunction); ok && fn.Fn.IsAsync {
if innerClassNameRef == ast.InvalidRef {
innerClassNameRef = p.generateTempRef(tempRefNeedsDeclareMayBeCapturedInsideLoop, "")
}
p.fnOnlyDataVisit.isInStaticClassContext = true
p.fnOnlyDataVisit.innerClassNameRef = &innerClassNameRef
}
}
// Propagate the name to keep from the property into the value
if str, ok := property.Key.Data.(*js_ast.EString); ok {
p.nameToKeep = helpers.UTF16ToString(str.Value)
p.nameToKeepIsFor = property.ValueOrNil.Data
}
property.ValueOrNil, _ = p.visitExprInOut(property.ValueOrNil, exprIn{
isMethod: property.Kind.IsMethodDefinition(),
assignTarget: in.assignTarget,
})
p.fnOnlyDataVisit.innerClassNameRef = oldInnerClassNameRef
p.fnOnlyDataVisit.isInStaticClassContext = oldIsInStaticClassContext
}
if property.InitializerOrNil.Data != nil {
// Propagate the name to keep from the binding into the initializer
if id, ok := property.ValueOrNil.Data.(*js_ast.EIdentifier); ok {
p.nameToKeep = p.symbols[id.Ref.InnerIndex].OriginalName
p.nameToKeepIsFor = property.InitializerOrNil.Data
}
property.InitializerOrNil = p.visitExpr(property.InitializerOrNil)
}
// "{ '123': 4 }" => "{ 123: 4 }" (this is done late to allow "'123'" to be mangled)
if p.options.minifySyntax {
if str, ok := property.Key.Data.(*js_ast.EString); ok {
if numberValue, ok := js_ast.StringToEquivalentNumberValue(str.Value); ok && numberValue >= 0 {
property.Key.Data = &js_ast.ENumber{Value: numberValue}
}
}
}
}
// Check for and warn about duplicate keys in object literals
if !p.suppressWarningsAboutWeirdCode {
p.warnAboutDuplicateProperties(e.Properties, duplicatePropertiesInObject)
}
if in.assignTarget == js_ast.AssignTargetNone {
// "{a, ...{b, c}, d}" => "{a, b, c, d}"
if p.options.minifySyntax && hasSpread {
e.Properties = js_ast.MangleObjectSpread(e.Properties)
}
// Object expressions represent both object literals and binding patterns.
// Only lower object spread if we're an object literal, not a binding pattern.
value := p.lowerObjectSpread(expr.Loc, e)
// If we generated and used the temporary variable for a lowered "super"
// property reference inside a lowered "async" method, then initialize
// the temporary with this object literal.
if innerClassNameRef != ast.InvalidRef && p.symbols[innerClassNameRef.InnerIndex].UseCountEstimate > 0 {
p.recordUsage(innerClassNameRef)
value = js_ast.Assign(js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: innerClassNameRef}}, value)
}
return value, exprOut{}
}
case *js_ast.EImportCall:
isAwaitTarget := e == p.awaitTarget
isThenCatchTarget := e == p.thenCatchChain.nextTarget && p.thenCatchChain.hasCatch
e.Expr = p.visitExpr(e.Expr)
var assertOrWith *ast.ImportAssertOrWith
var flags ast.ImportRecordFlags
if e.OptionsOrNil.Data != nil {
e.OptionsOrNil = p.visitExpr(e.OptionsOrNil)
// If there's an additional argument, this can't be split because the
// additional argument requires evaluation and our AST nodes can't be
// reused in different places in the AST (e.g. function scopes must be
// unique). Also the additional argument may have side effects and we
// don't currently account for that.
why := "the second argument was not an object literal"
whyLoc := e.OptionsOrNil.Loc
// However, make a special case for an additional argument that contains
// only an "assert" or a "with" clause. In that case we can split this
// AST node.
if object, ok := e.OptionsOrNil.Data.(*js_ast.EObject); ok {
if len(object.Properties) == 1 {
if prop := object.Properties[0]; prop.Kind == js_ast.PropertyField && !prop.Flags.Has(js_ast.PropertyIsComputed) {
if str, ok := prop.Key.Data.(*js_ast.EString); ok && (helpers.UTF16EqualsString(str.Value, "assert") || helpers.UTF16EqualsString(str.Value, "with")) {
keyword := ast.WithKeyword
if helpers.UTF16EqualsString(str.Value, "assert") {
keyword = ast.AssertKeyword
}
if value, ok := prop.ValueOrNil.Data.(*js_ast.EObject); ok {
entries := []ast.AssertOrWithEntry{}
for _, p := range value.Properties {
if p.Kind == js_ast.PropertyField && !p.Flags.Has(js_ast.PropertyIsComputed) {
if key, ok := p.Key.Data.(*js_ast.EString); ok {
if value, ok := p.ValueOrNil.Data.(*js_ast.EString); ok {
entries = append(entries, ast.AssertOrWithEntry{
Key: key.Value,
KeyLoc: p.Key.Loc,
Value: value.Value,
ValueLoc: p.ValueOrNil.Loc,
PreferQuotedKey: p.Flags.Has(js_ast.PropertyPreferQuotedKey),
})
if keyword == ast.AssertKeyword && helpers.UTF16EqualsString(key.Value, "type") && helpers.UTF16EqualsString(value.Value, "json") {
flags |= ast.AssertTypeJSON
}
continue
} else {
why = fmt.Sprintf("the value for the property %q was not a string literal",
helpers.UTF16ToString(key.Value))
whyLoc = p.ValueOrNil.Loc
}
} else {
why = "this property was not a string literal"
whyLoc = p.Key.Loc
}
} else {
why = "this property was invalid"
whyLoc = p.Key.Loc
}
entries = nil
break
}
if entries != nil {
if keyword == ast.AssertKeyword {
p.maybeWarnAboutAssertKeyword(prop.Key.Loc)
}
assertOrWith = &ast.ImportAssertOrWith{
Entries: entries,
Keyword: keyword,
KeywordLoc: prop.Key.Loc,
InnerOpenBraceLoc: prop.ValueOrNil.Loc,
InnerCloseBraceLoc: value.CloseBraceLoc,
OuterOpenBraceLoc: e.OptionsOrNil.Loc,
OuterCloseBraceLoc: object.CloseBraceLoc,
}
why = ""
}
} else {
why = "the value for \"assert\" was not an object literal"
whyLoc = prop.ValueOrNil.Loc
}
} else {
why = "this property was not called \"assert\" or \"with\""
whyLoc = prop.Key.Loc
}
} else {
why = "this property was invalid"
whyLoc = prop.Key.Loc
}
} else {
why = "the second argument was not an object literal with a single property called \"assert\" or \"with\""
whyLoc = e.OptionsOrNil.Loc
}
}
// Handle the case that isn't just an import assertion or attribute clause
if why != "" {
// Only warn when bundling
if p.options.mode == config.ModeBundle {
text := "This \"import()\" was not recognized because " + why
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddID(logger.MsgID_JS_UnsupportedDynamicImport, kind, &p.tracker, logger.Range{Loc: whyLoc}, text)
}
// If import assertions and/attributes are both not supported in the
// target platform, then "import()" cannot accept a second argument
// and keeping them would be a syntax error, so we need to get rid of
// them. We can't just not print them because they may have important
// side effects. Attempt to discard them without changing side effects
// and generate an error if that isn't possible.
if p.options.unsupportedJSFeatures.Has(compat.ImportAssertions) && p.options.unsupportedJSFeatures.Has(compat.ImportAttributes) {
if p.astHelpers.ExprCanBeRemovedIfUnused(e.OptionsOrNil) {
e.OptionsOrNil = js_ast.Expr{}
} else {
p.markSyntaxFeature(compat.ImportAttributes, logger.Range{Loc: e.OptionsOrNil.Loc})
}
}
// Stop now so we don't try to split "?:" expressions below and
// potentially end up with an AST node reused multiple times
break
}
}
return p.maybeTransposeIfExprChain(e.Expr, func(arg js_ast.Expr) js_ast.Expr {
// The argument must be a string
if str, ok := arg.Data.(*js_ast.EString); ok {
// Ignore calls to import() if the control flow is provably dead here.
// We don't want to spend time scanning the required files if they will
// never be used.
if p.isControlFlowDead {
return js_ast.Expr{Loc: arg.Loc, Data: js_ast.ENullShared}
}
importRecordIndex := p.addImportRecord(ast.ImportDynamic, e.Phase, p.source.RangeOfString(arg.Loc), helpers.UTF16ToString(str.Value), assertOrWith, flags)
if isAwaitTarget && p.fnOrArrowDataVisit.tryBodyCount != 0 {
record := &p.importRecords[importRecordIndex]
record.Flags |= ast.HandlesImportErrors
record.ErrorHandlerLoc = p.fnOrArrowDataVisit.tryCatchLoc
} else if isThenCatchTarget {
record := &p.importRecords[importRecordIndex]
record.Flags |= ast.HandlesImportErrors
record.ErrorHandlerLoc = p.thenCatchChain.catchLoc
}
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, importRecordIndex)
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EImportString{
ImportRecordIndex: importRecordIndex,
CloseParenLoc: e.CloseParenLoc,
}}
}
// Handle glob patterns
if p.options.mode == config.ModeBundle {
if value := p.handleGlobPattern(arg, ast.ImportDynamic, e.Phase, "globImport", assertOrWith); value.Data != nil {
return value
}
}
// Use a debug log so people can see this if they want to
r := js_lexer.RangeOfIdentifier(p.source, expr.Loc)
p.log.AddID(logger.MsgID_JS_UnsupportedDynamicImport, logger.Debug, &p.tracker, r,
"This \"import\" expression will not be bundled because the argument is not a string literal")
// We need to convert this into a call to "require()" if ES6 syntax is
// not supported in the current output format. The full conversion:
//
// Before:
// import(foo)
//
// After:
// Promise.resolve().then(() => __toESM(require(foo)))
//
// This is normally done by the printer since we don't know during the
// parsing stage whether this module is external or not. However, it's
// guaranteed to be external if the argument isn't a string. We handle
// this case here instead of in the printer because both the printer
// and the linker currently need an import record to handle this case
// correctly, and you need a string literal to get an import record.
if p.options.unsupportedJSFeatures.Has(compat.DynamicImport) {
var then js_ast.Expr
value := p.callRuntime(arg.Loc, "__toESM", []js_ast.Expr{{Loc: expr.Loc, Data: &js_ast.ECall{
Target: p.valueToSubstituteForRequire(expr.Loc),
Args: []js_ast.Expr{arg},
CloseParenLoc: e.CloseParenLoc,
}}})
body := js_ast.FnBody{Loc: expr.Loc, Block: js_ast.SBlock{Stmts: []js_ast.Stmt{{Loc: expr.Loc, Data: &js_ast.SReturn{ValueOrNil: value}}}}}
if p.options.unsupportedJSFeatures.Has(compat.Arrow) {
then = js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EFunction{Fn: js_ast.Fn{Body: body}}}
} else {
then = js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EArrow{Body: body, PreferExpr: true}}
}
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EDot{
Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EDot{
Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: p.makePromiseRef()}},
Name: "resolve",
NameLoc: expr.Loc,
}},
Kind: js_ast.TargetWasOriginallyPropertyAccess,
}},
Name: "then",
NameLoc: expr.Loc,
}},
Args: []js_ast.Expr{then},
Kind: js_ast.TargetWasOriginallyPropertyAccess,
}}
}
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EImportCall{
Expr: arg,
OptionsOrNil: e.OptionsOrNil,
CloseParenLoc: e.CloseParenLoc,
}}
}), exprOut{}
case *js_ast.ECall:
p.callTarget = e.Target.Data
// Track ".then().catch()" chains
p.thenCatchChain = thenCatchChain{
nextTarget: e.Target.Data,
hasMultipleArgs: len(e.Args) >= 2,
hasCatch: p.thenCatchChain.nextTarget == e && p.thenCatchChain.hasCatch,
catchLoc: p.thenCatchChain.catchLoc,
}
if p.thenCatchChain.hasMultipleArgs {
p.thenCatchChain.catchLoc = e.Args[1].Loc
}
wasIdentifierBeforeVisit := false
isParenthesizedOptionalChain := false
switch e2 := e.Target.Data.(type) {
case *js_ast.EIdentifier:
wasIdentifierBeforeVisit = true
case *js_ast.EDot:
isParenthesizedOptionalChain = e.OptionalChain == js_ast.OptionalChainNone && e2.OptionalChain != js_ast.OptionalChainNone
case *js_ast.EIndex:
isParenthesizedOptionalChain = e.OptionalChain == js_ast.OptionalChainNone && e2.OptionalChain != js_ast.OptionalChainNone
}
target, out := p.visitExprInOut(e.Target, exprIn{
hasChainParent: e.OptionalChain == js_ast.OptionalChainContinue,
// Signal to our child if this is an ECall at the start of an optional
// chain. If so, the child will need to stash the "this" context for us
// that we need for the ".call(this, ...args)".
storeThisArgForParentOptionalChain: e.OptionalChain == js_ast.OptionalChainStart || isParenthesizedOptionalChain,
})
e.Target = target
p.warnAboutImportNamespaceCall(target, exprKindCall)
// Automatically mark immediately-invoked function expressions for eager compilation
if fn, ok := target.Data.(*js_ast.EFunction); ok {
fn.IsParenthesized = true
}
hasSpread := false
oldIsControlFlowDead := p.isControlFlowDead
// If we're removing this call, don't count any arguments as symbol uses
if out.callMustBeReplacedWithUndefined {
if js_ast.IsPropertyAccess(e.Target) {
p.isControlFlowDead = true
} else {
out.callMustBeReplacedWithUndefined = false
}
}
// Visit the arguments
for i, arg := range e.Args {
arg = p.visitExpr(arg)
if _, ok := arg.Data.(*js_ast.ESpread); ok {
hasSpread = true
}
e.Args[i] = arg
}
// Mark side-effect free IIFEs with "/* @__PURE__ */"
if !e.CanBeUnwrappedIfUnused {
switch target := e.Target.Data.(type) {
case *js_ast.EArrow:
if !target.IsAsync && p.iifeCanBeRemovedIfUnused(target.Args, target.Body) {
e.CanBeUnwrappedIfUnused = true
}
case *js_ast.EFunction:
if !target.Fn.IsAsync && !target.Fn.IsGenerator && p.iifeCanBeRemovedIfUnused(target.Fn.Args, target.Fn.Body) {
e.CanBeUnwrappedIfUnused = true
}
}
}
// Our hack for reading Yarn PnP files is implemented here:
if p.options.decodeHydrateRuntimeStateYarnPnP {
if id, ok := e.Target.Data.(*js_ast.EIdentifier); ok && p.symbols[id.Ref.InnerIndex].OriginalName == "hydrateRuntimeState" && len(e.Args) >= 1 {
switch arg := e.Args[0].Data.(type) {
case *js_ast.EObject:
// "hydrateRuntimeState(<object literal>)"
if arg := e.Args[0]; isValidJSON(arg) {
p.manifestForYarnPnP = arg
}
case *js_ast.ECall:
// "hydrateRuntimeState(JSON.parse(<something>))"
if len(arg.Args) == 1 {
if dot, ok := arg.Target.Data.(*js_ast.EDot); ok && dot.Name == "parse" {
if id, ok := dot.Target.Data.(*js_ast.EIdentifier); ok {
if symbol := &p.symbols[id.Ref.InnerIndex]; symbol.Kind == ast.SymbolUnbound && symbol.OriginalName == "JSON" {
arg := arg.Args[0]
switch a := arg.Data.(type) {
case *js_ast.EString:
// "hydrateRuntimeState(JSON.parse(<string literal>))"
source := logger.Source{KeyPath: p.source.KeyPath, Contents: helpers.UTF16ToString(a.Value)}
stringInJSTable := logger.GenerateStringInJSTable(p.source.Contents, arg.Loc, source.Contents)
log := logger.NewStringInJSLog(p.log, &p.tracker, stringInJSTable)
p.manifestForYarnPnP, _ = ParseJSON(log, source, JSONOptions{})
remapExprLocsInJSON(&p.manifestForYarnPnP, stringInJSTable)
case *js_ast.EIdentifier:
// "hydrateRuntimeState(JSON.parse(<identifier>))"
if data, ok := p.stringLocalsForYarnPnP[a.Ref]; ok {
source := logger.Source{KeyPath: p.source.KeyPath, Contents: helpers.UTF16ToString(data.value)}
stringInJSTable := logger.GenerateStringInJSTable(p.source.Contents, data.loc, source.Contents)
log := logger.NewStringInJSLog(p.log, &p.tracker, stringInJSTable)
p.manifestForYarnPnP, _ = ParseJSON(log, source, JSONOptions{})
remapExprLocsInJSON(&p.manifestForYarnPnP, stringInJSTable)
}
}
}
}
}
}
}
}
}
// Stop now if this call must be removed
if out.callMustBeReplacedWithUndefined {
p.isControlFlowDead = oldIsControlFlowDead
return js_ast.Expr{Loc: expr.Loc, Data: js_ast.EUndefinedShared}, exprOut{}
}
if p.options.minifySyntax {
// "foo(1, ...[2, 3], 4)" => "foo(1, 2, 3, 4)"
if hasSpread {
e.Args = js_ast.InlineSpreadsOfArrayLiterals(e.Args)
}
// "(() => x)()" => "x"
if value, ok := p.maybeInlineIIFE(expr.Loc, e); ok {
return value, exprOut{}
}
}
switch t := target.Data.(type) {
case *js_ast.EImportIdentifier:
// If this function is inlined, allow it to be tree-shaken
if p.options.minifySyntax && !p.isControlFlowDead {
p.convertSymbolUseToCall(t.Ref, len(e.Args) == 1 && !hasSpread)
}
case *js_ast.EIdentifier:
// Detect if this is a direct eval. Note that "(1 ? eval : 0)(x)" will
// become "eval(x)" after we visit the target due to dead code elimination,
// but that doesn't mean it should become a direct eval.
//
// Note that "eval?.(x)" is considered an indirect eval. There was debate
// about this after everyone implemented it as a direct eval, but the
// language committee said it was indirect and everyone had to change it:
// https://github.com/tc39/ecma262/issues/2062.
if e.OptionalChain == js_ast.OptionalChainNone {
symbol := p.symbols[t.Ref.InnerIndex]
if wasIdentifierBeforeVisit && symbol.OriginalName == "eval" {
e.Kind = js_ast.DirectEval
// Pessimistically assume that if this looks like a CommonJS module
// (e.g. no "export" keywords), a direct call to "eval" means that
// code could potentially access "module" or "exports".
if p.options.mode == config.ModeBundle && !p.isFileConsideredToHaveESMExports {
p.recordUsage(p.moduleRef)
p.recordUsage(p.exportsRef)
}
// Mark this scope and all parent scopes as containing a direct eval.
// This will prevent us from renaming any symbols.
for s := p.currentScope; s != nil; s = s.Parent {
s.ContainsDirectEval = true
}
// Warn when direct eval is used in an ESM file. There is no way we
// can guarantee that this will work correctly for top-level imported
// and exported symbols due to scope hoisting. Except don't warn when
// this code is in a 3rd-party library because there's nothing people
// will be able to do about the warning.
text := "Using direct eval with a bundler is not recommended and may cause problems"
kind := logger.Debug
if p.options.mode == config.ModeBundle && p.isFileConsideredESM && !p.suppressWarningsAboutWeirdCode {
kind = logger.Warning
}
p.log.AddIDWithNotes(logger.MsgID_JS_DirectEval, kind, &p.tracker, js_lexer.RangeOfIdentifier(p.source, e.Target.Loc), text,
[]logger.MsgData{{Text: "You can read more about direct eval and bundling here: https://esbuild.github.io/link/direct-eval"}})
} else if symbol.Flags.Has(ast.CallCanBeUnwrappedIfUnused) {
// Automatically add a "/* @__PURE__ */" comment to file-local calls
// of functions declared with a "/* @__NO_SIDE_EFFECTS__ */" comment
t.CallCanBeUnwrappedIfUnused = true
}
}
// Handle certain special cases
if len(e.Args) <= 1 && !hasSpread {
if symbol := &p.symbols[t.Ref.InnerIndex]; symbol.Kind == ast.SymbolUnbound {
switch symbol.OriginalName {
case "Symbol":
// Calling the "Symbol()" constructor with a primitive will never throw
if len(e.Args) == 0 || js_ast.KnownPrimitiveType(e.Args[0].Data) != js_ast.PrimitiveUnknown {
e.CanBeUnwrappedIfUnused = true
}
}
// Optimize references to global constructors
if p.options.minifySyntax && t.CanBeRemovedIfUnused {
// Note: We construct expressions by assigning to "expr.Data" so
// that the source map position for the constructor is preserved
switch symbol.OriginalName {
case "Boolean":
if len(e.Args) == 0 {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EBoolean{Value: false}}, exprOut{}
} else {
expr.Data = &js_ast.EUnary{Value: p.astHelpers.SimplifyBooleanExpr(e.Args[0]), Op: js_ast.UnOpNot}
return js_ast.Not(expr), exprOut{}
}
case "Number":
if len(e.Args) == 0 {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: 0}}, exprOut{}
} else {
arg := e.Args[0]
switch js_ast.KnownPrimitiveType(arg.Data) {
case js_ast.PrimitiveNumber:
return arg, exprOut{}
case
js_ast.PrimitiveUndefined, // NaN
js_ast.PrimitiveNull, // 0
js_ast.PrimitiveBoolean, // 0 or 1
js_ast.PrimitiveString: // StringToNumber
if number, ok := js_ast.ToNumberWithoutSideEffects(arg.Data); ok {
expr.Data = &js_ast.ENumber{Value: number}
} else {
expr.Data = &js_ast.EUnary{Value: arg, Op: js_ast.UnOpPos}
}
return expr, exprOut{}
}
}
case "String":
if len(e.Args) == 0 {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: nil}}, exprOut{}
} else {
arg := e.Args[0]
switch js_ast.KnownPrimitiveType(arg.Data) {
case js_ast.PrimitiveString:
return arg, exprOut{}
}
}
case "BigInt":
if len(e.Args) == 1 {
arg := e.Args[0]
switch js_ast.KnownPrimitiveType(arg.Data) {
case js_ast.PrimitiveBigInt:
return arg, exprOut{}
}
}
}
}
}
}
// Copy the call side effect flag over if this is a known target
if t.CallCanBeUnwrappedIfUnused {
e.CanBeUnwrappedIfUnused = true
}
// If this function is inlined, allow it to be tree-shaken
if p.options.minifySyntax && !p.isControlFlowDead {
p.convertSymbolUseToCall(t.Ref, len(e.Args) == 1 && !hasSpread)
}
case *js_ast.EDot:
if len(e.Args) == 1 {
switch t.Name {
case "resolve":
// Recognize "require.resolve()" calls
if t.OptionalChain == js_ast.OptionalChainNone && p.options.mode != config.ModePassThrough {
if id, ok := t.Target.Data.(*js_ast.EIdentifier); ok && id.Ref == p.requireRef {
p.ignoreUsage(p.requireRef)
return p.maybeTransposeIfExprChain(e.Args[0], func(arg js_ast.Expr) js_ast.Expr {
if str, ok := e.Args[0].Data.(*js_ast.EString); ok {
// Ignore calls to require.resolve() if the control flow is provably
// dead here. We don't want to spend time scanning the required files
// if they will never be used.
if p.isControlFlowDead {
return js_ast.Expr{Loc: expr.Loc, Data: js_ast.ENullShared}
}
importRecordIndex := p.addImportRecord(ast.ImportRequireResolve, ast.EvaluationPhase, p.source.RangeOfString(e.Args[0].Loc), helpers.UTF16ToString(str.Value), nil, 0)
if p.fnOrArrowDataVisit.tryBodyCount != 0 {
record := &p.importRecords[importRecordIndex]
record.Flags |= ast.HandlesImportErrors
record.ErrorHandlerLoc = p.fnOrArrowDataVisit.tryCatchLoc
}
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, importRecordIndex)
// Create a new expression to represent the operation
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ERequireResolveString{
ImportRecordIndex: importRecordIndex,
CloseParenLoc: e.CloseParenLoc,
}}
}
// Otherwise just return a clone of the "require.resolve()" call
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: e.Target.Loc, Data: &js_ast.EDot{
Target: p.valueToSubstituteForRequire(t.Target.Loc),
Name: t.Name,
NameLoc: t.NameLoc,
}},
Args: []js_ast.Expr{arg},
Kind: e.Kind,
CloseParenLoc: e.CloseParenLoc,
}}
}), exprOut{}
}
}
case "for":
// Calling "Symbol.for()" with a primitive will never throw
if id, ok := t.Target.Data.(*js_ast.EIdentifier); ok {
if symbol := &p.symbols[id.Ref.InnerIndex]; symbol.Kind == ast.SymbolUnbound && symbol.OriginalName == "Symbol" {
if js_ast.KnownPrimitiveType(e.Args[0].Data) != js_ast.PrimitiveUnknown {
e.CanBeUnwrappedIfUnused = true
}
}
}
case "create":
// Recognize "Object.create()" calls
if id, ok := t.Target.Data.(*js_ast.EIdentifier); ok {
if symbol := &p.symbols[id.Ref.InnerIndex]; symbol.Kind == ast.SymbolUnbound && symbol.OriginalName == "Object" {
switch e.Args[0].Data.(type) {
case *js_ast.ENull, *js_ast.EObject:
// Mark "Object.create(null)" and "Object.create({})" as pure
e.CanBeUnwrappedIfUnused = true
}
}
}
case "escape":
// Recognize "RegExp.escape()" calls
if id, ok := t.Target.Data.(*js_ast.EIdentifier); ok {
if symbol := &p.symbols[id.Ref.InnerIndex]; symbol.Kind == ast.SymbolUnbound && symbol.OriginalName == "RegExp" {
if js_ast.KnownPrimitiveType(e.Args[0].Data) == js_ast.PrimitiveString {
// Mark "RegExp.escape" with a string literal as pure
e.CanBeUnwrappedIfUnused = true
}
}
}
}
}
if p.options.minifySyntax {
switch t.Name {
case "charCodeAt":
// Recognize "'string'.charCodeAt()" calls
if str, ok := t.Target.Data.(*js_ast.EString); ok && len(e.Args) <= 1 {
index := 0
hasIndex := false
if len(e.Args) == 0 {
hasIndex = true
} else if num, ok := e.Args[0].Data.(*js_ast.ENumber); ok && num.Value == math.Trunc(num.Value) && math.Abs(num.Value) <= 0x7FFF_FFFF {
index = int(num.Value)
hasIndex = true
}
if hasIndex {
if index >= 0 && index < len(str.Value) {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: float64(str.Value[index])}}, exprOut{}
} else {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: math.NaN()}}, exprOut{}
}
}
}
case "fromCharCode":
// Recognize "String.fromCharCode()" calls
if id, ok := t.Target.Data.(*js_ast.EIdentifier); ok {
if symbol := &p.symbols[id.Ref.InnerIndex]; symbol.Kind == ast.SymbolUnbound && symbol.OriginalName == "String" {
charCodes := make([]uint16, 0, len(e.Args))
for _, arg := range e.Args {
arg, ok := js_ast.ToNumberWithoutSideEffects(arg.Data)
if !ok {
break
}
charCodes = append(charCodes, uint16(js_ast.ToInt32(arg)))
}
if len(charCodes) == len(e.Args) {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: charCodes}}, exprOut{}
}
}
}
case "toString":
switch target := t.Target.Data.(type) {
case *js_ast.ENumber:
radix := 0
if len(e.Args) == 0 {
radix = 10
} else if len(e.Args) == 1 {
if num, ok := e.Args[0].Data.(*js_ast.ENumber); ok && num.Value == math.Trunc(num.Value) && num.Value >= 2 && num.Value <= 36 {
radix = int(num.Value)
}
}
if radix != 0 {
if str, ok := js_ast.TryToStringOnNumberSafely(target.Value, radix); ok {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(str)}}, exprOut{}
}
}
case *js_ast.ERegExp:
if len(e.Args) == 0 {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(target.Value)}}, exprOut{}
}
case *js_ast.EBoolean:
if len(e.Args) == 0 {
if target.Value {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16("true")}}, exprOut{}
} else {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16("false")}}, exprOut{}
}
}
case *js_ast.EString:
if len(e.Args) == 0 {
return t.Target, exprOut{}
}
}
}
}
// Copy the call side effect flag over if this is a known target
if t.CallCanBeUnwrappedIfUnused {
e.CanBeUnwrappedIfUnused = true
}
case *js_ast.EIndex:
// Copy the call side effect flag over if this is a known target
if t.CallCanBeUnwrappedIfUnused {
e.CanBeUnwrappedIfUnused = true
}
case *js_ast.ESuper:
// If we're shimming "super()" calls, replace this call with "__super()"
if p.superCtorRef != ast.InvalidRef {
p.recordUsage(p.superCtorRef)
target.Data = &js_ast.EIdentifier{Ref: p.superCtorRef}
e.Target.Data = target.Data
}
}
// Handle parenthesized optional chains
if isParenthesizedOptionalChain && out.thisArgFunc != nil && out.thisArgWrapFunc != nil {
return p.lowerParenthesizedOptionalChain(expr.Loc, e, out), exprOut{}
}
// Lower optional chaining if we're the top of the chain
containsOptionalChain := e.OptionalChain == js_ast.OptionalChainStart ||
(e.OptionalChain == js_ast.OptionalChainContinue && out.childContainsOptionalChain)
if containsOptionalChain && !in.hasChainParent {
return p.lowerOptionalChain(expr, in, out)
}
// If this is a plain call expression (instead of an optional chain), lower
// private member access in the call target now if there is one
if !containsOptionalChain {
if target, loc, private := p.extractPrivateIndex(e.Target); private != nil {
// "foo.#bar(123)" => "__privateGet(_a = foo, #bar).call(_a, 123)"
targetFunc, targetWrapFunc := p.captureValueWithPossibleSideEffects(target.Loc, 2, target, valueCouldBeMutated)
return targetWrapFunc(js_ast.Expr{Loc: target.Loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: target.Loc, Data: &js_ast.EDot{
Target: p.lowerPrivateGet(targetFunc(), loc, private),
Name: "call",
NameLoc: target.Loc,
}},
Args: append([]js_ast.Expr{targetFunc()}, e.Args...),
CanBeUnwrappedIfUnused: e.CanBeUnwrappedIfUnused,
Kind: js_ast.TargetWasOriginallyPropertyAccess,
}}), exprOut{}
}
p.maybeLowerSuperPropertyGetInsideCall(e)
}
// Track calls to require() so we can use them while bundling
if p.options.mode != config.ModePassThrough && e.OptionalChain == js_ast.OptionalChainNone {
if id, ok := e.Target.Data.(*js_ast.EIdentifier); ok && id.Ref == p.requireRef {
// Heuristic: omit warnings inside try/catch blocks because presumably
// the try/catch statement is there to handle the potential run-time
// error from the unbundled require() call failing.
omitWarnings := p.fnOrArrowDataVisit.tryBodyCount != 0
if p.options.mode != config.ModePassThrough {
// There must be one argument
if len(e.Args) == 1 {
p.ignoreUsage(p.requireRef)
return p.maybeTransposeIfExprChain(e.Args[0], func(arg js_ast.Expr) js_ast.Expr {
// The argument must be a string
if str, ok := arg.Data.(*js_ast.EString); ok {
// Ignore calls to require() if the control flow is provably dead here.
// We don't want to spend time scanning the required files if they will
// never be used.
if p.isControlFlowDead {
return js_ast.Expr{Loc: expr.Loc, Data: js_ast.ENullShared}
}
importRecordIndex := p.addImportRecord(ast.ImportRequire, ast.EvaluationPhase, p.source.RangeOfString(arg.Loc), helpers.UTF16ToString(str.Value), nil, 0)
if p.fnOrArrowDataVisit.tryBodyCount != 0 {
record := &p.importRecords[importRecordIndex]
record.Flags |= ast.HandlesImportErrors
record.ErrorHandlerLoc = p.fnOrArrowDataVisit.tryCatchLoc
}
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, importRecordIndex)
// Currently "require" is not converted into "import" for ESM
if p.options.mode != config.ModeBundle && p.options.outputFormat == config.FormatESModule && !omitWarnings {
r := js_lexer.RangeOfIdentifier(p.source, e.Target.Loc)
p.log.AddID(logger.MsgID_JS_UnsupportedRequireCall, logger.Warning, &p.tracker, r, "Converting \"require\" to \"esm\" is currently not supported")
}
// Create a new expression to represent the operation
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ERequireString{
ImportRecordIndex: importRecordIndex,
CloseParenLoc: e.CloseParenLoc,
}}
}
// Handle glob patterns
if p.options.mode == config.ModeBundle {
if value := p.handleGlobPattern(arg, ast.ImportRequire, ast.EvaluationPhase, "globRequire", nil); value.Data != nil {
return value
}
}
// Use a debug log so people can see this if they want to
r := js_lexer.RangeOfIdentifier(p.source, e.Target.Loc)
p.log.AddID(logger.MsgID_JS_UnsupportedRequireCall, logger.Debug, &p.tracker, r,
"This call to \"require\" will not be bundled because the argument is not a string literal")
// Otherwise just return a clone of the "require()" call
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
Target: p.valueToSubstituteForRequire(e.Target.Loc),
Args: []js_ast.Expr{arg},
CloseParenLoc: e.CloseParenLoc,
}}
}), exprOut{}
} else {
// Use a debug log so people can see this if they want to
r := js_lexer.RangeOfIdentifier(p.source, e.Target.Loc)
p.log.AddIDWithNotes(logger.MsgID_JS_UnsupportedRequireCall, logger.Debug, &p.tracker, r,
fmt.Sprintf("This call to \"require\" will not be bundled because it has %d arguments", len(e.Args)),
[]logger.MsgData{{Text: "To be bundled by esbuild, a \"require\" call must have exactly 1 argument."}})
}
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
Target: p.valueToSubstituteForRequire(e.Target.Loc),
Args: e.Args,
CloseParenLoc: e.CloseParenLoc,
}}, exprOut{}
}
}
}
out = exprOut{
childContainsOptionalChain: containsOptionalChain,
thisArgFunc: out.thisArgFunc,
thisArgWrapFunc: out.thisArgWrapFunc,
}
if !in.hasChainParent {
out.thisArgFunc = nil
out.thisArgWrapFunc = nil
}
return expr, out
case *js_ast.ENew:
hasSpread := false
e.Target = p.visitExpr(e.Target)
p.warnAboutImportNamespaceCall(e.Target, exprKindNew)
for i, arg := range e.Args {
arg = p.visitExpr(arg)
if _, ok := arg.Data.(*js_ast.ESpread); ok {
hasSpread = true
}
e.Args[i] = arg
}
// "new foo(1, ...[2, 3], 4)" => "new foo(1, 2, 3, 4)"
if p.options.minifySyntax && hasSpread {
e.Args = js_ast.InlineSpreadsOfArrayLiterals(e.Args)
}
p.maybeMarkKnownGlobalConstructorAsPure(e)
case *js_ast.EArrow:
// Check for a propagated name to keep from the parent context
var nameToKeep string
if p.nameToKeepIsFor == e {
nameToKeep = p.nameToKeep
}
// Prepare for suspicious logical operator checking
if e.PreferExpr && len(e.Args) == 1 && e.Args[0].DefaultOrNil.Data == nil && len(e.Body.Block.Stmts) == 1 {
if _, ok := e.Args[0].Binding.Data.(*js_ast.BIdentifier); ok {
if stmt, ok := e.Body.Block.Stmts[0].Data.(*js_ast.SReturn); ok {
if binary, ok := stmt.ValueOrNil.Data.(*js_ast.EBinary); ok && (binary.Op == js_ast.BinOpLogicalAnd || binary.Op == js_ast.BinOpLogicalOr) {
p.suspiciousLogicalOperatorInsideArrow = binary
}
}
}
}
asyncArrowNeedsToBeLowered := e.IsAsync && p.options.unsupportedJSFeatures.Has(compat.AsyncAwait)
oldFnOrArrowData := p.fnOrArrowDataVisit
p.fnOrArrowDataVisit = fnOrArrowDataVisit{
isArrow: true,
isAsync: e.IsAsync,
shouldLowerSuperPropertyAccess: oldFnOrArrowData.shouldLowerSuperPropertyAccess || asyncArrowNeedsToBeLowered,
}
// Mark if we're inside an async arrow function. This value should be true
// even if we're inside multiple arrow functions and the closest inclosing
// arrow function isn't async, as long as at least one enclosing arrow
// function within the current enclosing function is async.
oldInsideAsyncArrowFn := p.fnOnlyDataVisit.isInsideAsyncArrowFn
if e.IsAsync {
p.fnOnlyDataVisit.isInsideAsyncArrowFn = true
}
p.pushScopeForVisitPass(js_ast.ScopeFunctionArgs, expr.Loc)
p.visitArgs(e.Args, visitArgsOpts{
hasRestArg: e.HasRestArg,
body: e.Body.Block.Stmts,
isUniqueFormalParameters: true,
})
p.pushScopeForVisitPass(js_ast.ScopeFunctionBody, e.Body.Loc)
e.Body.Block.Stmts = p.visitStmtsAndPrependTempRefs(e.Body.Block.Stmts, prependTempRefsOpts{kind: stmtsFnBody})
p.popScope()
p.lowerFunction(&e.IsAsync, nil, &e.Args, e.Body.Loc, &e.Body.Block, &e.PreferExpr, &e.HasRestArg, true /* isArrow */)
p.popScope()
if p.options.minifySyntax && len(e.Body.Block.Stmts) == 1 {
if s, ok := e.Body.Block.Stmts[0].Data.(*js_ast.SReturn); ok && s.ValueOrNil.Data != nil {
// "() => { return x }" => "() => x"
e.PreferExpr = true
}
}
p.fnOnlyDataVisit.isInsideAsyncArrowFn = oldInsideAsyncArrowFn
p.fnOrArrowDataVisit = oldFnOrArrowData
// Convert arrow functions to function expressions when lowering
if p.options.unsupportedJSFeatures.Has(compat.Arrow) {
expr.Data = &js_ast.EFunction{Fn: js_ast.Fn{
Args: e.Args,
Body: e.Body,
ArgumentsRef: ast.InvalidRef,
IsAsync: e.IsAsync,
HasRestArg: e.HasRestArg,
}}
}
// Optionally preserve the name
if p.options.keepNames && nameToKeep != "" {
expr = p.keepExprSymbolName(expr, nameToKeep)
}
case *js_ast.EFunction:
// Check for a propagated name to keep from the parent context
var nameToKeep string
if p.nameToKeepIsFor == e {
nameToKeep = p.nameToKeep
}
p.visitFn(&e.Fn, expr.Loc, visitFnOpts{
isMethod: in.isMethod,
isDerivedClassCtor: e == p.propDerivedCtorValue,
isLoweredPrivateMethod: in.isLoweredPrivateMethod,
})
name := e.Fn.Name
// Remove unused function names when minifying
if p.options.minifySyntax && !p.currentScope.ContainsDirectEval &&
name != nil && p.symbols[name.Ref.InnerIndex].UseCountEstimate == 0 {
e.Fn.Name = nil
}
// Optionally preserve the name for functions, but not for methods
if p.options.keepNames && (!in.isMethod || in.isLoweredPrivateMethod) {
if name != nil {
expr = p.keepExprSymbolName(expr, p.symbols[name.Ref.InnerIndex].OriginalName)
} else if nameToKeep != "" {
expr = p.keepExprSymbolName(expr, nameToKeep)
}
}
case *js_ast.EClass:
// Check for a propagated name to keep from the parent context
var nameToKeep string
if p.nameToKeepIsFor == e {
nameToKeep = p.nameToKeep
}
result := p.visitClass(expr.Loc, &e.Class, ast.InvalidRef, nameToKeep)
// Lower class field syntax for browsers that don't support it
_, expr = p.lowerClass(js_ast.Stmt{}, expr, result, nameToKeep)
// We may be able to determine that a class is side-effect before lowering
// but not after lowering (e.g. due to "--keep-names" mutating the object).
// If that's the case, add a special annotation so this doesn't prevent
// tree-shaking from happening.
if result.canBeRemovedIfUnused {
expr.Data = &js_ast.EAnnotation{
Value: expr,
Flags: js_ast.CanBeRemovedIfUnusedFlag,
}
}
default:
// Note: EPrivateIdentifier should have already been handled
panic(fmt.Sprintf("Unexpected expression of type %T", expr.Data))
}
return expr, exprOut{}
}
// This exists to handle very deeply-nested ASTs. For example, the "grapheme-splitter"
// package contains this monstrosity:
//
// if (
// (0x0300 <= code && code <= 0x036F) ||
// (0x0483 <= code && code <= 0x0487) ||
// (0x0488 <= code && code <= 0x0489) ||
// (0x0591 <= code && code <= 0x05BD) ||
// ... many hundreds of lines later ...
// ) {
// return;
// }
//
// If "checkAndPrepare" returns non-nil, then the return value is the final
// expression. Otherwise, the final expression can be obtained by manually
// visiting the left child and then calling "visitRightAndFinish":
//
// if result := v.checkAndPrepare(p); result.Data != nil {
// return result
// }
// v.e.Left, _ = p.visitExprInOut(v.e.Left, v.leftIn)
// return v.visitRightAndFinish(p)
//
// This code is convoluted this way so that we can use our own stack on the
// heap instead of the call stack when there are additional levels of nesting.
// Before this transformation, the code previously looked something like this:
//
// ... The code in "checkAndPrepare" ...
// e.Left, _ = p.visitExprInOut(e.Left, in)
// ... The code in "visitRightAndFinish" ...
//
// If this code is still confusing, it may be helpful to look back in git
// history at the commit that introduced this transformation.
//
// Go normally has growable call stacks so this code transformation normally
// doesn't do anything, but WebAssembly doesn't allow stack pointer manipulation
// so Go's WebAssembly implementation doesn't support growable call stacks and
// is therefore vulnerable to stack overflow. So this code transformation is
// only really relevant for esbuild's WebAssembly-based API.
type binaryExprVisitor struct {
// Inputs
e *js_ast.EBinary
loc logger.Loc
in exprIn
// Input for visiting the left child
leftIn exprIn
// "Local variables" passed from "checkAndPrepare" to "visitRightAndFinish"
isStmtExpr bool
oldSilenceWarningAboutThisBeingUndefined bool
}
func (v *binaryExprVisitor) checkAndPrepare(p *parser) js_ast.Expr {
e := v.e
// Special-case EPrivateIdentifier to allow it here
if private, ok := e.Left.Data.(*js_ast.EPrivateIdentifier); ok && e.Op == js_ast.BinOpIn {
name := p.loadNameFromRef(private.Ref)
result := p.findSymbol(e.Left.Loc, name)
private.Ref = result.ref
// Unlike regular identifiers, there are no unbound private identifiers
symbol := &p.symbols[result.ref.InnerIndex]
if !symbol.Kind.IsPrivate() {
r := logger.Range{Loc: e.Left.Loc, Len: int32(len(name))}
p.log.AddError(&p.tracker, r, fmt.Sprintf("Private name %q must be declared in an enclosing class", name))
}
e.Right = p.visitExpr(e.Right)
if p.privateSymbolNeedsToBeLowered(private) {
return p.lowerPrivateBrandCheck(e.Right, v.loc, private)
}
return js_ast.Expr{Loc: v.loc, Data: e}
}
v.isStmtExpr = e == p.stmtExprValue
v.oldSilenceWarningAboutThisBeingUndefined = p.fnOnlyDataVisit.silenceMessageAboutThisBeingUndefined
if _, ok := e.Left.Data.(*js_ast.EThis); ok && e.Op == js_ast.BinOpLogicalAnd {
p.fnOnlyDataVisit.silenceMessageAboutThisBeingUndefined = true
}
v.leftIn = exprIn{
assignTarget: e.Op.BinaryAssignTarget(),
shouldMangleStringsAsProps: e.Op == js_ast.BinOpIn,
}
return js_ast.Expr{}
}
func (v *binaryExprVisitor) visitRightAndFinish(p *parser) js_ast.Expr {
e := v.e
// Mark the control flow as dead if the branch is never taken
switch e.Op {
case js_ast.BinOpLogicalOr:
if boolean, _, ok := js_ast.ToBooleanWithSideEffects(e.Left.Data); ok && boolean {
// "true || dead"
old := p.isControlFlowDead
p.isControlFlowDead = true
e.Right = p.visitExpr(e.Right)
p.isControlFlowDead = old
} else {
e.Right = p.visitExpr(e.Right)
}
case js_ast.BinOpLogicalAnd:
if boolean, _, ok := js_ast.ToBooleanWithSideEffects(e.Left.Data); ok && !boolean {
// "false && dead"
old := p.isControlFlowDead
p.isControlFlowDead = true
e.Right = p.visitExpr(e.Right)
p.isControlFlowDead = old
} else {
e.Right = p.visitExpr(e.Right)
}
case js_ast.BinOpNullishCoalescing:
if isNullOrUndefined, _, ok := js_ast.ToNullOrUndefinedWithSideEffects(e.Left.Data); ok && !isNullOrUndefined {
// "notNullOrUndefined ?? dead"
old := p.isControlFlowDead
p.isControlFlowDead = true
e.Right = p.visitExpr(e.Right)
p.isControlFlowDead = old
} else {
e.Right = p.visitExpr(e.Right)
}
case js_ast.BinOpComma:
e.Right, _ = p.visitExprInOut(e.Right, exprIn{
shouldMangleStringsAsProps: v.in.shouldMangleStringsAsProps,
})
case js_ast.BinOpAssign, js_ast.BinOpLogicalOrAssign, js_ast.BinOpLogicalAndAssign, js_ast.BinOpNullishCoalescingAssign:
// Check for a propagated name to keep from the parent context
if id, ok := e.Left.Data.(*js_ast.EIdentifier); ok {
p.nameToKeep = p.symbols[id.Ref.InnerIndex].OriginalName
p.nameToKeepIsFor = e.Right.Data
}
e.Right = p.visitExpr(e.Right)
default:
e.Right = p.visitExpr(e.Right)
}
p.fnOnlyDataVisit.silenceMessageAboutThisBeingUndefined = v.oldSilenceWarningAboutThisBeingUndefined
// Always put constants consistently on the same side for equality
// comparisons to help improve compression. In theory, dictionary-based
// compression methods may already have a dictionary entry for code that
// is similar to previous code. Note that we can only reorder expressions
// that do not have any side effects.
//
// Constants are currently ordered on the right instead of the left because
// it results in slightly smalller gzip size on our primary benchmark
// (although slightly larger uncompressed size). The size difference is
// less than 0.1% so it really isn't that important an optimization.
if p.options.minifySyntax {
switch e.Op {
case js_ast.BinOpLooseEq, js_ast.BinOpLooseNe, js_ast.BinOpStrictEq, js_ast.BinOpStrictNe:
// "1 === x" => "x === 1"
if js_ast.IsPrimitiveLiteral(e.Left.Data) && !js_ast.IsPrimitiveLiteral(e.Right.Data) {
e.Left, e.Right = e.Right, e.Left
}
}
}
if p.shouldFoldTypeScriptConstantExpressions || (p.options.minifySyntax && js_ast.ShouldFoldBinaryOperatorWhenMinifying(e)) {
if result := js_ast.FoldBinaryOperator(v.loc, e); result.Data != nil {
return result
}
}
// Post-process the binary expression
switch e.Op {
case js_ast.BinOpComma:
// "(1, 2)" => "2"
// "(sideEffects(), 2)" => "(sideEffects(), 2)"
if p.options.minifySyntax {
e.Left = p.astHelpers.SimplifyUnusedExpr(e.Left, p.options.unsupportedJSFeatures)
if e.Left.Data == nil {
return e.Right
}
}
case js_ast.BinOpLooseEq:
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data, js_ast.LooseEquality); ok {
return js_ast.Expr{Loc: v.loc, Data: &js_ast.EBoolean{Value: result}}
}
afterOpLoc := locAfterOp(e)
if !p.warnAboutEqualityCheck("==", e.Left, afterOpLoc) {
p.warnAboutEqualityCheck("==", e.Right, afterOpLoc)
}
p.warnAboutTypeofAndString(e.Left, e.Right, checkBothOrders)
if p.options.minifySyntax {
// "x == void 0" => "x == null"
if _, ok := e.Left.Data.(*js_ast.EUndefined); ok {
e.Left.Data = js_ast.ENullShared
} else if _, ok := e.Right.Data.(*js_ast.EUndefined); ok {
e.Right.Data = js_ast.ENullShared
}
if result, ok := js_ast.MaybeSimplifyEqualityComparison(v.loc, e, p.options.unsupportedJSFeatures); ok {
return result
}
}
case js_ast.BinOpStrictEq:
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data, js_ast.StrictEquality); ok {
return js_ast.Expr{Loc: v.loc, Data: &js_ast.EBoolean{Value: result}}
}
afterOpLoc := locAfterOp(e)
if !p.warnAboutEqualityCheck("===", e.Left, afterOpLoc) {
p.warnAboutEqualityCheck("===", e.Right, afterOpLoc)
}
p.warnAboutTypeofAndString(e.Left, e.Right, checkBothOrders)
if p.options.minifySyntax {
// "typeof x === 'undefined'" => "typeof x == 'undefined'"
if js_ast.CanChangeStrictToLoose(e.Left, e.Right) {
e.Op = js_ast.BinOpLooseEq
}
if result, ok := js_ast.MaybeSimplifyEqualityComparison(v.loc, e, p.options.unsupportedJSFeatures); ok {
return result
}
}
case js_ast.BinOpLooseNe:
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data, js_ast.LooseEquality); ok {
return js_ast.Expr{Loc: v.loc, Data: &js_ast.EBoolean{Value: !result}}
}
afterOpLoc := locAfterOp(e)
if !p.warnAboutEqualityCheck("!=", e.Left, afterOpLoc) {
p.warnAboutEqualityCheck("!=", e.Right, afterOpLoc)
}
p.warnAboutTypeofAndString(e.Left, e.Right, checkBothOrders)
if p.options.minifySyntax {
// "x != void 0" => "x != null"
if _, ok := e.Left.Data.(*js_ast.EUndefined); ok {
e.Left.Data = js_ast.ENullShared
} else if _, ok := e.Right.Data.(*js_ast.EUndefined); ok {
e.Right.Data = js_ast.ENullShared
}
if result, ok := js_ast.MaybeSimplifyEqualityComparison(v.loc, e, p.options.unsupportedJSFeatures); ok {
return result
}
}
case js_ast.BinOpStrictNe:
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data, js_ast.StrictEquality); ok {
return js_ast.Expr{Loc: v.loc, Data: &js_ast.EBoolean{Value: !result}}
}
afterOpLoc := locAfterOp(e)
if !p.warnAboutEqualityCheck("!==", e.Left, afterOpLoc) {
p.warnAboutEqualityCheck("!==", e.Right, afterOpLoc)
}
p.warnAboutTypeofAndString(e.Left, e.Right, checkBothOrders)
if p.options.minifySyntax {
// "typeof x !== 'undefined'" => "typeof x != 'undefined'"
if js_ast.CanChangeStrictToLoose(e.Left, e.Right) {
e.Op = js_ast.BinOpLooseNe
}
if result, ok := js_ast.MaybeSimplifyEqualityComparison(v.loc, e, p.options.unsupportedJSFeatures); ok {
return result
}
}
case js_ast.BinOpNullishCoalescing:
if isNullOrUndefined, sideEffects, ok := js_ast.ToNullOrUndefinedWithSideEffects(e.Left.Data); ok {
// Warn about potential bugs
if !js_ast.IsPrimitiveLiteral(e.Left.Data) {
// "return props.flag === flag ?? true" is "return (props.flag === flag) ?? true" not "return props.flag === (flag ?? true)"
var which string
var leftIsNullOrUndefined string
var leftIsReturned string
if !isNullOrUndefined {
which = "left"
leftIsNullOrUndefined = "never"
leftIsReturned = "always"
} else {
which = "right"
leftIsNullOrUndefined = "always"
leftIsReturned = "never"
}
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
rOp := p.source.RangeOfOperatorBefore(e.Right.Loc, "??")
rLeft := logger.Range{Loc: e.Left.Loc, Len: p.source.LocBeforeWhitespace(rOp.Loc).Start - e.Left.Loc.Start}
p.log.AddIDWithNotes(logger.MsgID_JS_SuspiciousNullishCoalescing, kind, &p.tracker, rOp,
fmt.Sprintf("The \"??\" operator here will always return the %s operand", which), []logger.MsgData{
p.tracker.MsgData(rLeft, fmt.Sprintf(
"The left operand of the \"??\" operator here will %s be null or undefined, so it will %s be returned. This usually indicates a bug in your code:",
leftIsNullOrUndefined, leftIsReturned))})
}
if !isNullOrUndefined {
return e.Left
} else if sideEffects == js_ast.NoSideEffects {
return e.Right
}
}
if p.options.minifySyntax {
// "a ?? (b ?? c)" => "a ?? b ?? c"
if right, ok := e.Right.Data.(*js_ast.EBinary); ok && right.Op == js_ast.BinOpNullishCoalescing {
e.Left = js_ast.JoinWithLeftAssociativeOp(js_ast.BinOpNullishCoalescing, e.Left, right.Left)
e.Right = right.Right
}
}
if p.options.unsupportedJSFeatures.Has(compat.NullishCoalescing) {
return p.lowerNullishCoalescing(v.loc, e.Left, e.Right)
}
case js_ast.BinOpLogicalOr:
if boolean, sideEffects, ok := js_ast.ToBooleanWithSideEffects(e.Left.Data); ok {
// Warn about potential bugs
if e == p.suspiciousLogicalOperatorInsideArrow {
if arrowLoc := p.source.RangeOfOperatorBefore(v.loc, "=>"); arrowLoc.Loc.Start+2 == p.source.LocBeforeWhitespace(v.loc).Start {
// "return foo => 1 || foo <= 0"
var which string
if boolean {
which = "left"
} else {
which = "right"
}
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
note := p.tracker.MsgData(arrowLoc,
"The \"=>\" symbol creates an arrow function expression in JavaScript. Did you mean to use the greater-than-or-equal-to operator \">=\" here instead?")
note.Location.Suggestion = ">="
rOp := p.source.RangeOfOperatorBefore(e.Right.Loc, "||")
p.log.AddIDWithNotes(logger.MsgID_JS_SuspiciousLogicalOperator, kind, &p.tracker, rOp,
fmt.Sprintf("The \"||\" operator here will always return the %s operand", which), []logger.MsgData{note})
}
}
if boolean {
return e.Left
} else if sideEffects == js_ast.NoSideEffects {
return e.Right
}
}
if p.options.minifySyntax {
// "a || (b || c)" => "a || b || c"
if right, ok := e.Right.Data.(*js_ast.EBinary); ok && right.Op == js_ast.BinOpLogicalOr {
e.Left = js_ast.JoinWithLeftAssociativeOp(js_ast.BinOpLogicalOr, e.Left, right.Left)
e.Right = right.Right
}
// "a === null || a === undefined" => "a == null"
if left, right, ok := js_ast.IsBinaryNullAndUndefined(e.Left, e.Right, js_ast.BinOpStrictEq); ok {
e.Op = js_ast.BinOpLooseEq
e.Left = left
e.Right = right
}
}
case js_ast.BinOpLogicalAnd:
if boolean, sideEffects, ok := js_ast.ToBooleanWithSideEffects(e.Left.Data); ok {
// Warn about potential bugs
if e == p.suspiciousLogicalOperatorInsideArrow {
if arrowLoc := p.source.RangeOfOperatorBefore(v.loc, "=>"); arrowLoc.Loc.Start+2 == p.source.LocBeforeWhitespace(v.loc).Start {
// "return foo => 0 && foo <= 1"
var which string
if !boolean {
which = "left"
} else {
which = "right"
}
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
note := p.tracker.MsgData(arrowLoc,
"The \"=>\" symbol creates an arrow function expression in JavaScript. Did you mean to use the greater-than-or-equal-to operator \">=\" here instead?")
note.Location.Suggestion = ">="
rOp := p.source.RangeOfOperatorBefore(e.Right.Loc, "&&")
p.log.AddIDWithNotes(logger.MsgID_JS_SuspiciousLogicalOperator, kind, &p.tracker, rOp,
fmt.Sprintf("The \"&&\" operator here will always return the %s operand", which), []logger.MsgData{note})
}
}
if !boolean {
return e.Left
} else if sideEffects == js_ast.NoSideEffects {
return e.Right
}
}
if p.options.minifySyntax {
// "a && (b && c)" => "a && b && c"
if right, ok := e.Right.Data.(*js_ast.EBinary); ok && right.Op == js_ast.BinOpLogicalAnd {
e.Left = js_ast.JoinWithLeftAssociativeOp(js_ast.BinOpLogicalAnd, e.Left, right.Left)
e.Right = right.Right
}
// "a !== null && a !== undefined" => "a != null"
if left, right, ok := js_ast.IsBinaryNullAndUndefined(e.Left, e.Right, js_ast.BinOpStrictNe); ok {
e.Op = js_ast.BinOpLooseNe
e.Left = left
e.Right = right
}
}
case js_ast.BinOpAdd:
// "'abc' + 'xyz'" => "'abcxyz'"
if result := js_ast.FoldStringAddition(e.Left, e.Right, js_ast.StringAdditionNormal); result.Data != nil {
return result
}
if left, ok := e.Left.Data.(*js_ast.EBinary); ok && left.Op == js_ast.BinOpAdd {
// "x + 'abc' + 'xyz'" => "x + 'abcxyz'"
if result := js_ast.FoldStringAddition(left.Right, e.Right, js_ast.StringAdditionWithNestedLeft); result.Data != nil {
return js_ast.Expr{Loc: v.loc, Data: &js_ast.EBinary{Op: left.Op, Left: left.Left, Right: result}}
}
}
case js_ast.BinOpPow:
// Lower the exponentiation operator for browsers that don't support it
if p.options.unsupportedJSFeatures.Has(compat.ExponentOperator) {
return p.callRuntime(v.loc, "__pow", []js_ast.Expr{e.Left, e.Right})
}
////////////////////////////////////////////////////////////////////////////////
// All assignment operators below here
case js_ast.BinOpAssign:
if target, loc, private := p.extractPrivateIndex(e.Left); private != nil {
return p.lowerPrivateSet(target, loc, private, e.Right)
}
if property := p.extractSuperProperty(e.Left); property.Data != nil {
return p.lowerSuperPropertySet(e.Left.Loc, property, e.Right)
}
// Lower assignment destructuring patterns for browsers that don't
// support them. Note that assignment expressions are used to represent
// initializers in binding patterns, so only do this if we're not
// ourselves the target of an assignment. Example: "[a = b] = c"
if v.in.assignTarget == js_ast.AssignTargetNone {
mode := objRestMustReturnInitExpr
if v.isStmtExpr {
mode = objRestReturnValueIsUnused
}
if result, ok := p.lowerAssign(e.Left, e.Right, mode); ok {
return result
}
// If CommonJS-style exports are disabled, then references to them are
// treated as global variable references. This is consistent with how
// they work in node and the browser, so it's the correct interpretation.
//
// However, people sometimes try to use both types of exports within the
// same module and expect it to work. We warn about this when module
// format conversion is enabled.
//
// Only warn about this for uses in assignment position since there are
// some legitimate other uses. For example, some people do "typeof module"
// to check for a CommonJS environment, and we shouldn't warn on that.
if p.options.mode != config.ModePassThrough && p.isFileConsideredToHaveESMExports && !p.isControlFlowDead {
if dot, ok := e.Left.Data.(*js_ast.EDot); ok {
var name string
var loc logger.Loc
switch target := dot.Target.Data.(type) {
case *js_ast.EIdentifier:
if symbol := &p.symbols[target.Ref.InnerIndex]; symbol.Kind == ast.SymbolUnbound &&
((symbol.OriginalName == "module" && dot.Name == "exports") || symbol.OriginalName == "exports") &&
!symbol.Flags.Has(ast.DidWarnAboutCommonJSInESM) {
// "module.exports = ..."
// "exports.something = ..."
name = symbol.OriginalName
loc = dot.Target.Loc
symbol.Flags |= ast.DidWarnAboutCommonJSInESM
}
case *js_ast.EDot:
if target.Name == "exports" {
if id, ok := target.Target.Data.(*js_ast.EIdentifier); ok {
if symbol := &p.symbols[id.Ref.InnerIndex]; symbol.Kind == ast.SymbolUnbound &&
symbol.OriginalName == "module" && !symbol.Flags.Has(ast.DidWarnAboutCommonJSInESM) {
// "module.exports.foo = ..."
name = symbol.OriginalName
loc = target.Target.Loc
symbol.Flags |= ast.DidWarnAboutCommonJSInESM
}
}
}
}
if name != "" {
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
why, notes := p.whyESModule()
if why == whyESMTypeModulePackageJSON {
text := "Node's package format requires that CommonJS files in a \"type\": \"module\" package use the \".cjs\" file extension."
if p.options.ts.Parse {
text += " If you are using TypeScript, you can use the \".cts\" file extension with esbuild instead."
}
notes = append(notes, logger.MsgData{Text: text})
}
p.log.AddIDWithNotes(logger.MsgID_JS_CommonJSVariableInESM, kind, &p.tracker, js_lexer.RangeOfIdentifier(p.source, loc),
fmt.Sprintf("The CommonJS %q variable is treated as a global variable in an ECMAScript module and may not work as expected", name),
notes)
}
}
}
}
case js_ast.BinOpAddAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpAdd, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpSubAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpSub, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpMulAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpMul, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpDivAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpDiv, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpRemAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpRem, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpPowAssign:
// Lower the exponentiation operator for browsers that don't support it
if p.options.unsupportedJSFeatures.Has(compat.ExponentOperator) {
return p.lowerExponentiationAssignmentOperator(v.loc, e)
}
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpPow, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpShlAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpShl, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpShrAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpShr, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpUShrAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpUShr, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpBitwiseOrAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpBitwiseOr, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpBitwiseAndAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpBitwiseAnd, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpBitwiseXorAssign:
if result := p.maybeLowerSetBinOp(e.Left, js_ast.BinOpBitwiseXor, e.Right); result.Data != nil {
return result
}
case js_ast.BinOpNullishCoalescingAssign:
if value, ok := p.lowerNullishCoalescingAssignmentOperator(v.loc, e); ok {
return value
}
case js_ast.BinOpLogicalAndAssign:
if value, ok := p.lowerLogicalAssignmentOperator(v.loc, e, js_ast.BinOpLogicalAnd); ok {
return value
}
case js_ast.BinOpLogicalOrAssign:
if value, ok := p.lowerLogicalAssignmentOperator(v.loc, e, js_ast.BinOpLogicalOr); ok {
return value
}
}
// "(a, b) + c" => "a, b + c"
if p.options.minifySyntax && e.Op != js_ast.BinOpComma {
if comma, ok := e.Left.Data.(*js_ast.EBinary); ok && comma.Op == js_ast.BinOpComma {
return js_ast.JoinWithComma(comma.Left, js_ast.Expr{
Loc: comma.Right.Loc,
Data: &js_ast.EBinary{
Op: e.Op,
Left: comma.Right,
Right: e.Right,
},
})
}
}
return js_ast.Expr{Loc: v.loc, Data: e}
}
func remapExprLocsInJSON(expr *js_ast.Expr, table []logger.StringInJSTableEntry) {
expr.Loc = logger.RemapStringInJSLoc(table, expr.Loc)
switch e := expr.Data.(type) {
case *js_ast.EArray:
e.CloseBracketLoc = logger.RemapStringInJSLoc(table, e.CloseBracketLoc)
for i := range e.Items {
remapExprLocsInJSON(&e.Items[i], table)
}
case *js_ast.EObject:
e.CloseBraceLoc = logger.RemapStringInJSLoc(table, e.CloseBraceLoc)
for i := range e.Properties {
remapExprLocsInJSON(&e.Properties[i].Key, table)
remapExprLocsInJSON(&e.Properties[i].ValueOrNil, table)
}
}
}
func (p *parser) handleGlobPattern(expr js_ast.Expr, kind ast.ImportKind, phase ast.ImportPhase, prefix string, assertOrWith *ast.ImportAssertOrWith) js_ast.Expr {
pattern, approximateRange := p.globPatternFromExpr(expr)
if pattern == nil {
return js_ast.Expr{}
}
var last helpers.GlobPart
var parts []helpers.GlobPart
for _, part := range pattern {
if part.isWildcard {
if last.Wildcard == helpers.GlobNone {
if !strings.HasSuffix(last.Prefix, "/") {
// "`a${b}c`" => "a*c"
last.Wildcard = helpers.GlobAllExceptSlash
} else {
// "`a/${b}c`" => "a/**/*c"
last.Wildcard = helpers.GlobAllIncludingSlash
parts = append(parts, last)
last = helpers.GlobPart{Prefix: "/", Wildcard: helpers.GlobAllExceptSlash}
}
}
} else if part.text != "" {
if last.Wildcard != helpers.GlobNone {
parts = append(parts, last)
last = helpers.GlobPart{}
}
last.Prefix += part.text
}
}
parts = append(parts, last)
// Don't handle this if it's a string constant
if len(parts) == 1 && parts[0].Wildcard == helpers.GlobNone {
return js_ast.Expr{}
}
// We currently only support relative globs
if prefix := parts[0].Prefix; !strings.HasPrefix(prefix, "./") && !strings.HasPrefix(prefix, "../") {
return js_ast.Expr{}
}
ref := ast.InvalidRef
// Don't generate duplicate glob imports
outer:
for _, globPattern := range p.globPatternImports {
// Check the kind and phase
if globPattern.kind != kind || globPattern.phase != phase {
continue
}
// Check the parts
if len(globPattern.parts) != len(parts) {
continue
}
for i := range parts {
if globPattern.parts[i] != parts[i] {
continue outer
}
}
// Check the import assertions/attributes
if assertOrWith == nil {
if globPattern.assertOrWith != nil {
continue
}
} else {
if globPattern.assertOrWith == nil {
continue
}
if assertOrWith.Keyword != globPattern.assertOrWith.Keyword {
continue
}
a := assertOrWith.Entries
b := globPattern.assertOrWith.Entries
if len(a) != len(b) {
continue
}
for i := range a {
ai := a[i]
bi := b[i]
if !helpers.UTF16EqualsUTF16(ai.Key, bi.Key) || !helpers.UTF16EqualsUTF16(ai.Value, bi.Value) {
continue outer
}
}
}
// If we get here, then these are the same glob pattern
ref = globPattern.ref
break
}
// If there's no duplicate glob import, then generate a new glob import
if ref == ast.InvalidRef && prefix != "" {
sb := strings.Builder{}
sb.WriteString(prefix)
for _, part := range parts {
gap := true
for _, c := range part.Prefix {
if !js_ast.IsIdentifierContinue(c) {
gap = true
} else {
if gap {
sb.WriteByte('_')
gap = false
}
sb.WriteRune(c)
}
}
}
name := sb.String()
ref = p.newSymbol(ast.SymbolOther, name)
p.moduleScope.Generated = append(p.moduleScope.Generated, ref)
p.globPatternImports = append(p.globPatternImports, globPatternImport{
assertOrWith: assertOrWith,
parts: parts,
name: name,
approximateRange: approximateRange,
ref: ref,
kind: kind,
phase: phase,
})
}
p.recordUsage(ref)
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: ref}},
Args: []js_ast.Expr{expr},
}}
}
type globPart struct {
text string
isWildcard bool
}
func (p *parser) globPatternFromExpr(expr js_ast.Expr) ([]globPart, logger.Range) {
switch e := expr.Data.(type) {
case *js_ast.EString:
return []globPart{{text: helpers.UTF16ToString(e.Value)}}, p.source.RangeOfString(expr.Loc)
case *js_ast.ETemplate:
if e.TagOrNil.Data != nil {
break
}
pattern := make([]globPart, 0, 1+2*len(e.Parts))
pattern = append(pattern, globPart{text: helpers.UTF16ToString(e.HeadCooked)})
for _, part := range e.Parts {
if partPattern, _ := p.globPatternFromExpr(part.Value); partPattern != nil {
pattern = append(pattern, partPattern...)
} else {
pattern = append(pattern, globPart{isWildcard: true})
}
pattern = append(pattern, globPart{text: helpers.UTF16ToString(part.TailCooked)})
}
if len(e.Parts) == 0 {
return pattern, p.source.RangeOfString(expr.Loc)
}
text := p.source.Contents
templateRange := logger.Range{Loc: e.HeadLoc}
for i := e.Parts[len(e.Parts)-1].TailLoc.Start; i < int32(len(text)); i++ {
c := text[i]
if c == '`' {
templateRange.Len = i + 1 - templateRange.Loc.Start
break
} else if c == '\\' {
i += 1
}
}
return pattern, templateRange
case *js_ast.EBinary:
if e.Op != js_ast.BinOpAdd {
break
}
pattern, leftRange := p.globPatternFromExpr(e.Left)
if pattern == nil {
break
}
if rightPattern, rightRange := p.globPatternFromExpr(e.Right); rightPattern != nil {
pattern = append(pattern, rightPattern...)
leftRange.Len = rightRange.End() - leftRange.Loc.Start
return pattern, leftRange
}
pattern = append(pattern, globPart{isWildcard: true})
// Try to extend the left range by the right operand in some common cases
switch right := e.Right.Data.(type) {
case *js_ast.EIdentifier:
leftRange.Len = js_lexer.RangeOfIdentifier(p.source, e.Right.Loc).End() - leftRange.Loc.Start
case *js_ast.ECall:
if right.CloseParenLoc.Start > 0 {
leftRange.Len = right.CloseParenLoc.Start + 1 - leftRange.Loc.Start
}
}
return pattern, leftRange
}
return nil, logger.Range{}
}
func (p *parser) convertSymbolUseToCall(ref ast.Ref, isSingleNonSpreadArgCall bool) {
// Remove the normal symbol use
symbolUses := p.currentPart.SymbolUses
use := symbolUses[ref]
use.CountEstimate--
if use.CountEstimate == 0 {
delete(symbolUses, ref)
} else {
symbolUses[ref] = use
}
// Add a special symbol use instead
symbolCallUses := p.currentPart.SymbolCallUses
if symbolCallUses == nil {
symbolCallUses = make(map[ast.Ref]js_ast.SymbolCallUse)
p.currentPart.SymbolCallUses = symbolCallUses
}
callUse := symbolCallUses[ref]
callUse.CallCountEstimate++
if isSingleNonSpreadArgCall {
callUse.SingleArgNonSpreadCallCountEstimate++
}
symbolCallUses[ref] = callUse
}
func (p *parser) warnAboutImportNamespaceCall(target js_ast.Expr, kind importNamespaceCallKind) {
if p.options.outputFormat != config.FormatPreserve {
if id, ok := target.Data.(*js_ast.EIdentifier); ok && p.importItemsForNamespace[id.Ref].entries != nil {
key := importNamespaceCall{
ref: id.Ref,
kind: kind,
}
if p.importNamespaceCCMap == nil {
p.importNamespaceCCMap = make(map[importNamespaceCall]bool)
}
// Don't log a warning for the same identifier more than once
if _, ok := p.importNamespaceCCMap[key]; ok {
return
}
p.importNamespaceCCMap[key] = true
r := js_lexer.RangeOfIdentifier(p.source, target.Loc)
var notes []logger.MsgData
name := p.symbols[id.Ref.InnerIndex].OriginalName
if member, ok := p.moduleScope.Members[name]; ok && member.Ref == id.Ref {
if star := p.source.RangeOfOperatorBefore(member.Loc, "*"); star.Len > 0 {
if as := p.source.RangeOfOperatorBefore(member.Loc, "as"); as.Len > 0 && as.Loc.Start > star.Loc.Start {
note := p.tracker.MsgData(
logger.Range{Loc: star.Loc, Len: js_lexer.RangeOfIdentifier(p.source, member.Loc).End() - star.Loc.Start},
fmt.Sprintf("Consider changing %q to a default import instead:", name))
note.Location.Suggestion = name
notes = append(notes, note)
}
}
}
if p.options.ts.Parse {
notes = append(notes, logger.MsgData{
Text: "Make sure to enable TypeScript's \"esModuleInterop\" setting so that TypeScript's type checker generates an error when you try to do this. " +
"You can read more about this setting here: https://www.typescriptlang.org/tsconfig#esModuleInterop",
})
}
var verb string
var where string
var noun string
switch kind {
case exprKindCall:
verb = "Calling"
noun = "function"
case exprKindNew:
verb = "Constructing"
noun = "constructor"
case exprKindJSXTag:
verb = "Using"
where = " in a JSX expression"
noun = "component"
}
p.log.AddIDWithNotes(logger.MsgID_JS_CallImportNamespace, logger.Warning, &p.tracker, r, fmt.Sprintf(
"%s %q%s will crash at run-time because it's an import namespace object, not a %s",
verb,
p.symbols[id.Ref.InnerIndex].OriginalName,
where,
noun,
), notes)
}
}
}
func (p *parser) maybeMarkKnownGlobalConstructorAsPure(e *js_ast.ENew) {
if id, ok := e.Target.Data.(*js_ast.EIdentifier); ok {
if symbol := p.symbols[id.Ref.InnerIndex]; symbol.Kind == ast.SymbolUnbound {
switch symbol.OriginalName {
case "WeakSet", "WeakMap":
n := len(e.Args)
if n == 0 {
// "new WeakSet()" is pure
e.CanBeUnwrappedIfUnused = true
break
}
if n == 1 {
switch arg := e.Args[0].Data.(type) {
case *js_ast.ENull, *js_ast.EUndefined:
// "new WeakSet(null)" is pure
// "new WeakSet(void 0)" is pure
e.CanBeUnwrappedIfUnused = true
case *js_ast.EArray:
if len(arg.Items) == 0 {
// "new WeakSet([])" is pure
e.CanBeUnwrappedIfUnused = true
} else {
// "new WeakSet([x])" is impure because an exception is thrown if "x" is not an object
}
default:
// "new WeakSet(x)" is impure because the iterator for "x" could have side effects
}
}
case "Date":
n := len(e.Args)
if n == 0 {
// "new Date()" is pure
e.CanBeUnwrappedIfUnused = true
break
}
if n == 1 {
switch js_ast.KnownPrimitiveType(e.Args[0].Data) {
case js_ast.PrimitiveNull, js_ast.PrimitiveUndefined, js_ast.PrimitiveBoolean, js_ast.PrimitiveNumber, js_ast.PrimitiveString:
// "new Date('')" is pure
// "new Date(0)" is pure
// "new Date(null)" is pure
// "new Date(true)" is pure
// "new Date(false)" is pure
// "new Date(undefined)" is pure
e.CanBeUnwrappedIfUnused = true
default:
// "new Date(x)" is impure because converting "x" to a string could have side effects
}
}
case "Set":
n := len(e.Args)
if n == 0 {
// "new Set()" is pure
e.CanBeUnwrappedIfUnused = true
break
}
if n == 1 {
switch e.Args[0].Data.(type) {
case *js_ast.EArray, *js_ast.ENull, *js_ast.EUndefined:
// "new Set([a, b, c])" is pure
// "new Set(null)" is pure
// "new Set(void 0)" is pure
e.CanBeUnwrappedIfUnused = true
default:
// "new Set(x)" is impure because the iterator for "x" could have side effects
}
}
case "Map":
n := len(e.Args)
if n == 0 {
// "new Map()" is pure
e.CanBeUnwrappedIfUnused = true
break
}
if n == 1 {
switch arg := e.Args[0].Data.(type) {
case *js_ast.ENull, *js_ast.EUndefined:
// "new Map(null)" is pure
// "new Map(void 0)" is pure
e.CanBeUnwrappedIfUnused = true
case *js_ast.EArray:
allEntriesAreArrays := true
for _, item := range arg.Items {
if _, ok := item.Data.(*js_ast.EArray); !ok {
// "new Map([x])" is impure because "x[0]" could have side effects
allEntriesAreArrays = false
break
}
}
// "new Map([[a, b], [c, d]])" is pure
if allEntriesAreArrays {
e.CanBeUnwrappedIfUnused = true
}
default:
// "new Map(x)" is impure because the iterator for "x" could have side effects
}
}
}
}
}
}
type identifierOpts struct {
assignTarget js_ast.AssignTarget
isCallTarget bool
isDeleteTarget bool
preferQuotedKey bool
wasOriginallyIdentifier bool
matchAgainstDefines bool
}
func (p *parser) handleIdentifier(loc logger.Loc, e *js_ast.EIdentifier, opts identifierOpts) js_ast.Expr {
ref := e.Ref
// Substitute inlined constants
if p.options.minifySyntax && !p.currentScope.ContainsDirectEval {
if value, ok := p.constValues[ref]; ok {
p.ignoreUsage(ref)
return js_ast.ConstValueToExpr(loc, value)
}
}
// Capture the "arguments" variable if necessary
if p.fnOnlyDataVisit.argumentsRef != nil && ref == *p.fnOnlyDataVisit.argumentsRef {
isInsideUnsupportedArrow := p.fnOrArrowDataVisit.isArrow && p.options.unsupportedJSFeatures.Has(compat.Arrow)
isInsideUnsupportedAsyncArrow := p.fnOnlyDataVisit.isInsideAsyncArrowFn && p.options.unsupportedJSFeatures.Has(compat.AsyncAwait)
if isInsideUnsupportedArrow || isInsideUnsupportedAsyncArrow {
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: p.captureArguments()}}
}
}
// Create an error for assigning to an import namespace
if (opts.assignTarget != js_ast.AssignTargetNone ||
(opts.isDeleteTarget && p.symbols[ref.InnerIndex].ImportItemStatus == ast.ImportItemGenerated)) &&
p.symbols[ref.InnerIndex].Kind == ast.SymbolImport {
r := js_lexer.RangeOfIdentifier(p.source, loc)
// Try to come up with a setter name to try to make this message more understandable
var setterHint string
originalName := p.symbols[ref.InnerIndex].OriginalName
if js_ast.IsIdentifier(originalName) && originalName != "_" {
if len(originalName) == 1 || (len(originalName) > 1 && originalName[0] < utf8.RuneSelf) {
setterHint = fmt.Sprintf(" (e.g. \"set%s%s\")", strings.ToUpper(originalName[:1]), originalName[1:])
} else {
setterHint = fmt.Sprintf(" (e.g. \"set_%s\")", originalName)
}
}
notes := []logger.MsgData{{Text: "Imports are immutable in JavaScript. " +
fmt.Sprintf("To modify the value of this import, you must export a setter function in the "+
"imported file%s and then import and call that function here instead.", setterHint)}}
if p.options.mode == config.ModeBundle {
p.log.AddErrorWithNotes(&p.tracker, r, fmt.Sprintf("Cannot assign to import %q", originalName), notes)
} else {
kind := logger.Warning
if p.suppressWarningsAboutWeirdCode {
kind = logger.Debug
}
p.log.AddIDWithNotes(logger.MsgID_JS_AssignToImport, kind, &p.tracker, r,
fmt.Sprintf("This assignment will throw because %q is an import", originalName), notes)
}
}
// Substitute an EImportIdentifier now if this has a namespace alias
if opts.assignTarget == js_ast.AssignTargetNone && !opts.isDeleteTarget {
symbol := &p.symbols[ref.InnerIndex]
if nsAlias := symbol.NamespaceAlias; nsAlias != nil {
data := p.dotOrMangledPropVisit(
js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: nsAlias.NamespaceRef}},
symbol.OriginalName, loc)
// Handle references to namespaces or namespace members
if tsMemberData, ok := p.refToTSNamespaceMemberData[nsAlias.NamespaceRef]; ok {
if ns, ok := tsMemberData.(*js_ast.TSNamespaceMemberNamespace); ok {
if member, ok := ns.ExportedMembers[nsAlias.Alias]; ok {
switch m := member.Data.(type) {
case *js_ast.TSNamespaceMemberEnumNumber:
return p.wrapInlinedEnum(js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: m.Value}}, nsAlias.Alias)
case *js_ast.TSNamespaceMemberEnumString:
return p.wrapInlinedEnum(js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: m.Value}}, nsAlias.Alias)
case *js_ast.TSNamespaceMemberNamespace:
p.tsNamespaceTarget = data
p.tsNamespaceMemberData = member.Data
}
}
}
}
return js_ast.Expr{Loc: loc, Data: data}
}
}
// Substitute an EImportIdentifier now if this is an import item
if p.isImportItem[ref] {
return js_ast.Expr{Loc: loc, Data: &js_ast.EImportIdentifier{
Ref: ref,
PreferQuotedKey: opts.preferQuotedKey,
WasOriginallyIdentifier: opts.wasOriginallyIdentifier,
}}
}
// Handle references to namespaces or namespace members
if tsMemberData, ok := p.refToTSNamespaceMemberData[ref]; ok {
switch m := tsMemberData.(type) {
case *js_ast.TSNamespaceMemberEnumNumber:
return p.wrapInlinedEnum(js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: m.Value}}, p.symbols[ref.InnerIndex].OriginalName)
case *js_ast.TSNamespaceMemberEnumString:
return p.wrapInlinedEnum(js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: m.Value}}, p.symbols[ref.InnerIndex].OriginalName)
case *js_ast.TSNamespaceMemberNamespace:
p.tsNamespaceTarget = e
p.tsNamespaceMemberData = tsMemberData
}
}
// Substitute a namespace export reference now if appropriate
if p.options.ts.Parse {
if nsRef, ok := p.isExportedInsideNamespace[ref]; ok {
name := p.symbols[ref.InnerIndex].OriginalName
// Otherwise, create a property access on the namespace
p.recordUsage(nsRef)
propertyAccess := p.dotOrMangledPropVisit(js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: nsRef}}, name, loc)
if p.tsNamespaceTarget == e {
p.tsNamespaceTarget = propertyAccess
}
return js_ast.Expr{Loc: loc, Data: propertyAccess}
}
}
// Swap references to the global "require" function with our "__require" stub
if ref == p.requireRef && !opts.isCallTarget {
if p.options.mode == config.ModeBundle && p.source.Index != runtime.SourceIndex && e != p.dotOrIndexTarget {
p.log.AddID(logger.MsgID_JS_IndirectRequire, logger.Debug, &p.tracker, js_lexer.RangeOfIdentifier(p.source, loc),
"Indirect calls to \"require\" will not be bundled")
}
return p.valueToSubstituteForRequire(loc)
}
// Mark any mutated symbols as mutable
if opts.assignTarget != js_ast.AssignTargetNone {
p.symbols[e.Ref.InnerIndex].Flags |= ast.CouldPotentiallyBeMutated
}
return js_ast.Expr{Loc: loc, Data: e}
}
type visitFnOpts struct {
isMethod bool
isDerivedClassCtor bool
isLoweredPrivateMethod bool
}
func (p *parser) visitFn(fn *js_ast.Fn, scopeLoc logger.Loc, opts visitFnOpts) {
var decoratorScope *js_ast.Scope
oldFnOrArrowData := p.fnOrArrowDataVisit
oldFnOnlyData := p.fnOnlyDataVisit
p.fnOrArrowDataVisit = fnOrArrowDataVisit{
isAsync: fn.IsAsync,
isGenerator: fn.IsGenerator,
isDerivedClassCtor: opts.isDerivedClassCtor,
shouldLowerSuperPropertyAccess: (fn.IsAsync && p.options.unsupportedJSFeatures.Has(compat.AsyncAwait)) || opts.isLoweredPrivateMethod,
}
p.fnOnlyDataVisit = fnOnlyDataVisit{
isThisNested: true,
isNewTargetAllowed: true,
argumentsRef: &fn.ArgumentsRef,
}
if opts.isMethod {
decoratorScope = p.propMethodDecoratorScope
p.fnOnlyDataVisit.innerClassNameRef = oldFnOnlyData.innerClassNameRef
p.fnOnlyDataVisit.isInStaticClassContext = oldFnOnlyData.isInStaticClassContext
}
if fn.Name != nil {
p.recordDeclaredSymbol(fn.Name.Ref)
}
p.pushScopeForVisitPass(js_ast.ScopeFunctionArgs, scopeLoc)
p.visitArgs(fn.Args, visitArgsOpts{
hasRestArg: fn.HasRestArg,
body: fn.Body.Block.Stmts,
isUniqueFormalParameters: fn.IsUniqueFormalParameters,
decoratorScope: decoratorScope,
})
p.pushScopeForVisitPass(js_ast.ScopeFunctionBody, fn.Body.Loc)
if fn.Name != nil {
p.validateDeclaredSymbolName(fn.Name.Loc, p.symbols[fn.Name.Ref.InnerIndex].OriginalName)
}
fn.Body.Block.Stmts = p.visitStmtsAndPrependTempRefs(fn.Body.Block.Stmts, prependTempRefsOpts{fnBodyLoc: &fn.Body.Loc, kind: stmtsFnBody})
p.popScope()
p.lowerFunction(&fn.IsAsync, &fn.IsGenerator, &fn.Args, fn.Body.Loc, &fn.Body.Block, nil, &fn.HasRestArg, false /* isArrow */)
p.popScope()
p.fnOrArrowDataVisit = oldFnOrArrowData
p.fnOnlyDataVisit = oldFnOnlyData
}
func (p *parser) recordExport(loc logger.Loc, alias string, ref ast.Ref) {
if name, ok := p.namedExports[alias]; ok {
// Duplicate exports are an error
p.log.AddErrorWithNotes(&p.tracker, js_lexer.RangeOfIdentifier(p.source, loc),
fmt.Sprintf("Multiple exports with the same name %q", alias),
[]logger.MsgData{p.tracker.MsgData(js_lexer.RangeOfIdentifier(p.source, name.AliasLoc),
fmt.Sprintf("The name %q was originally exported here:", alias))})
} else {
p.namedExports[alias] = js_ast.NamedExport{AliasLoc: loc, Ref: ref}
}
}
type importsExportsScanResult struct {
stmts []js_ast.Stmt
keptImportEquals bool
removedImportEquals bool
}
// Returns true if this is an unused TypeScript import-equals statement
func (p *parser) checkForUnusedTSImportEquals(s *js_ast.SLocal, result *importsExportsScanResult) bool {
if s.WasTSImportEquals && !s.IsExport {
decl := s.Decls[0]
// Skip to the underlying reference
value := s.Decls[0].ValueOrNil
for {
if dot, ok := value.Data.(*js_ast.EDot); ok {
value = dot.Target
} else {
break
}
}
// Is this an identifier reference and not a require() call?
valueRef := ast.InvalidRef
switch v := value.Data.(type) {
case *js_ast.EIdentifier:
valueRef = v.Ref
case *js_ast.EImportIdentifier:
valueRef = v.Ref
}
if valueRef != ast.InvalidRef {
// Is this import statement unused?
if ref := decl.Binding.Data.(*js_ast.BIdentifier).Ref; p.symbols[ref.InnerIndex].UseCountEstimate == 0 {
// Also don't count the referenced identifier
p.ignoreUsage(valueRef)
// Import-equals statements can come in any order. Removing one
// could potentially cause another one to be removable too.
// Continue iterating until a fixed point has been reached to make
// sure we get them all.
result.removedImportEquals = true
return true
} else {
result.keptImportEquals = true
}
}
}
return false
}
func (p *parser) scanForUnusedTSImportEquals(stmts []js_ast.Stmt) (result importsExportsScanResult) {
stmtsEnd := 0
for _, stmt := range stmts {
if s, ok := stmt.Data.(*js_ast.SLocal); ok && p.checkForUnusedTSImportEquals(s, &result) {
// Remove unused import-equals statements, since those likely
// correspond to types instead of values
continue
}
// Filter out statements we skipped over
stmts[stmtsEnd] = stmt
stmtsEnd++
}
result.stmts = stmts[:stmtsEnd]
return
}
func (p *parser) scanForImportsAndExports(stmts []js_ast.Stmt) (result importsExportsScanResult) {
unusedImportFlags := p.options.ts.Config.UnusedImportFlags()
stmtsEnd := 0
for _, stmt := range stmts {
switch s := stmt.Data.(type) {
case *js_ast.SImport:
record := &p.importRecords[s.ImportRecordIndex]
// We implement TypeScript's "preserveValueImports" tsconfig.json setting
// to support the use case of compiling partial modules for compile-to-
// JavaScript languages such as Svelte. These languages try to reference
// imports in ways that are impossible for TypeScript and esbuild to know
// about when they are only given a partial module to compile. Here is an
// example of some Svelte code that contains a TypeScript snippet:
//
// <script lang="ts">
// import Counter from './Counter.svelte';
// export let name: string = 'world';
// </script>
// <main>
// <h1>Hello {name}!</h1>
// <Counter />
// </main>
//
// Tools that use esbuild to compile TypeScript code inside a Svelte
// file like this only give esbuild the contents of the <script> tag.
// The "preserveValueImports" setting avoids removing unused import
// names, which means additional code appended after the TypeScript-
// to-JavaScript conversion can still access those unused imports.
//
// There are two scenarios where we don't do this:
//
// * If we're bundling, then we know we aren't being used to compile
// a partial module. The parser is seeing the entire code for the
// module so it's safe to remove unused imports. And also we don't
// want the linker to generate errors about missing imports if the
// imported file is also in the bundle.
//
// * If identifier minification is enabled, then using esbuild as a
// partial-module transform library wouldn't work anyway because
// the names wouldn't match. And that means we're minifying so the
// user is expecting the output to be as small as possible. So we
// should omit unused imports.
//
keepUnusedImports := p.options.ts.Parse && (unusedImportFlags&config.TSUnusedImport_KeepValues) != 0 &&
p.options.mode != config.ModeBundle && !p.options.minifyIdentifiers
// Forbid non-default imports for JSON import assertions
if (record.Flags&ast.AssertTypeJSON) != 0 && p.options.mode == config.ModeBundle && s.Items != nil {
for _, item := range *s.Items {
if p.options.ts.Parse && p.tsUseCounts[item.Name.Ref.InnerIndex] == 0 && (unusedImportFlags&config.TSUnusedImport_KeepValues) == 0 {
// Do not count imports that TypeScript interprets as type annotations
continue
}
if item.Alias != "default" {
p.log.AddErrorWithNotes(&p.tracker, js_lexer.RangeOfIdentifier(p.source, item.AliasLoc),
fmt.Sprintf("Cannot use non-default import %q with a JSON import assertion", item.Alias),
p.notesForAssertTypeJSON(record, item.Alias))
}
}
}
// TypeScript always trims unused imports. This is important for
// correctness since some imports might be fake (only in the type
// system and used for type-only imports).
if (p.options.minifySyntax || p.options.ts.Parse) && !keepUnusedImports {
foundImports := false
isUnusedInTypeScript := true
// Remove the default name if it's unused
if s.DefaultName != nil {
foundImports = true
symbol := p.symbols[s.DefaultName.Ref.InnerIndex]
// TypeScript has a separate definition of unused
if p.options.ts.Parse && (p.tsUseCounts[s.DefaultName.Ref.InnerIndex] != 0 || (p.options.ts.Config.UnusedImportFlags()&config.TSUnusedImport_KeepValues) != 0) {
isUnusedInTypeScript = false
}
// Remove the symbol if it's never used outside a dead code region
if symbol.UseCountEstimate == 0 && (p.options.ts.Parse || !p.moduleScope.ContainsDirectEval) {
s.DefaultName = nil
}
}
// Remove the star import if it's unused
if s.StarNameLoc != nil {
foundImports = true
symbol := p.symbols[s.NamespaceRef.InnerIndex]
// TypeScript has a separate definition of unused
if p.options.ts.Parse && (p.tsUseCounts[s.NamespaceRef.InnerIndex] != 0 || (p.options.ts.Config.UnusedImportFlags()&config.TSUnusedImport_KeepValues) != 0) {
isUnusedInTypeScript = false
}
// Remove the symbol if it's never used outside a dead code region
if symbol.UseCountEstimate == 0 && (p.options.ts.Parse || !p.moduleScope.ContainsDirectEval) {
// Make sure we don't remove this if it was used for a property
// access while bundling
if importItems, ok := p.importItemsForNamespace[s.NamespaceRef]; ok && len(importItems.entries) == 0 {
s.StarNameLoc = nil
}
}
}
// Remove items if they are unused
if s.Items != nil {
foundImports = true
itemsEnd := 0
for _, item := range *s.Items {
symbol := p.symbols[item.Name.Ref.InnerIndex]
// TypeScript has a separate definition of unused
if p.options.ts.Parse && (p.tsUseCounts[item.Name.Ref.InnerIndex] != 0 || (p.options.ts.Config.UnusedImportFlags()&config.TSUnusedImport_KeepValues) != 0) {
isUnusedInTypeScript = false
}
// Remove the symbol if it's never used outside a dead code region
if symbol.UseCountEstimate != 0 || (!p.options.ts.Parse && p.moduleScope.ContainsDirectEval) {
(*s.Items)[itemsEnd] = item
itemsEnd++
}
}
// Filter the array by taking a slice
if itemsEnd == 0 {
s.Items = nil
} else {
*s.Items = (*s.Items)[:itemsEnd]
}
}
// Omit this statement if we're parsing TypeScript and all imports are
// unused. Note that this is distinct from the case where there were
// no imports at all (e.g. "import 'foo'"). In that case we want to keep
// the statement because the user is clearly trying to import the module
// for side effects.
//
// This culling is important for correctness when parsing TypeScript
// because a) the TypeScript compiler does ths and we want to match it
// and b) this may be a fake module that only exists in the type system
// and doesn't actually exist in reality.
//
// We do not want to do this culling in JavaScript though because the
// module may have side effects even if all imports are unused.
if p.options.ts.Parse && foundImports && isUnusedInTypeScript && (unusedImportFlags&config.TSUnusedImport_KeepStmt) == 0 {
// Ignore import records with a pre-filled source index. These are
// for injected files and we definitely do not want to trim these.
if !record.SourceIndex.IsValid() && !record.CopySourceIndex.IsValid() {
record.Flags |= ast.IsUnused
continue
}
}
}
if p.options.mode != config.ModePassThrough {
if s.StarNameLoc != nil {
// "importItemsForNamespace" has property accesses off the namespace
if importItems, ok := p.importItemsForNamespace[s.NamespaceRef]; ok && len(importItems.entries) > 0 {
// Sort keys for determinism
sorted := make([]string, 0, len(importItems.entries))
for alias := range importItems.entries {
sorted = append(sorted, alias)
}
sort.Strings(sorted)
// Create named imports for these property accesses. This will
// cause missing imports to generate useful warnings.
//
// It will also improve bundling efficiency for internal imports
// by still converting property accesses off the namespace into
// bare identifiers even if the namespace is still needed.
for _, alias := range sorted {
name := importItems.entries[alias]
p.namedImports[name.Ref] = js_ast.NamedImport{
Alias: alias,
AliasLoc: name.Loc,
NamespaceRef: s.NamespaceRef,
ImportRecordIndex: s.ImportRecordIndex,
}
// Make sure the printer prints this as a property access
p.symbols[name.Ref.InnerIndex].NamespaceAlias = &ast.NamespaceAlias{
NamespaceRef: s.NamespaceRef,
Alias: alias,
}
// Also record these automatically-generated top-level namespace alias symbols
p.currentPart.DeclaredSymbols = append(p.currentPart.DeclaredSymbols, js_ast.DeclaredSymbol{
Ref: name.Ref,
IsTopLevel: true,
})
}
}
}
if s.DefaultName != nil {
p.namedImports[s.DefaultName.Ref] = js_ast.NamedImport{
Alias: "default",
AliasLoc: s.DefaultName.Loc,
NamespaceRef: s.NamespaceRef,
ImportRecordIndex: s.ImportRecordIndex,
}
}
if s.StarNameLoc != nil {
p.namedImports[s.NamespaceRef] = js_ast.NamedImport{
AliasIsStar: true,
AliasLoc: *s.StarNameLoc,
NamespaceRef: ast.InvalidRef,
ImportRecordIndex: s.ImportRecordIndex,
}
}
if s.Items != nil {
for _, item := range *s.Items {
p.namedImports[item.Name.Ref] = js_ast.NamedImport{
Alias: item.Alias,
AliasLoc: item.AliasLoc,
NamespaceRef: s.NamespaceRef,
ImportRecordIndex: s.ImportRecordIndex,
}
}
}
}
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, s.ImportRecordIndex)
if s.StarNameLoc != nil {
record.Flags |= ast.ContainsImportStar
}
if s.DefaultName != nil {
record.Flags |= ast.ContainsDefaultAlias
} else if s.Items != nil {
for _, item := range *s.Items {
if item.Alias == "default" {
record.Flags |= ast.ContainsDefaultAlias
} else if item.Alias == "__esModule" {
record.Flags |= ast.ContainsESModuleAlias
}
}
}
case *js_ast.SFunction:
if s.IsExport {
p.recordExport(s.Fn.Name.Loc, p.symbols[s.Fn.Name.Ref.InnerIndex].OriginalName, s.Fn.Name.Ref)
}
case *js_ast.SClass:
if s.IsExport {
p.recordExport(s.Class.Name.Loc, p.symbols[s.Class.Name.Ref.InnerIndex].OriginalName, s.Class.Name.Ref)
}
case *js_ast.SLocal:
if s.IsExport {
js_ast.ForEachIdentifierBindingInDecls(s.Decls, func(loc logger.Loc, b *js_ast.BIdentifier) {
p.recordExport(loc, p.symbols[b.Ref.InnerIndex].OriginalName, b.Ref)
})
}
// Remove unused import-equals statements, since those likely
// correspond to types instead of values
if p.checkForUnusedTSImportEquals(s, &result) {
continue
}
case *js_ast.SExportDefault:
p.recordExport(s.DefaultName.Loc, "default", s.DefaultName.Ref)
case *js_ast.SExportClause:
for _, item := range s.Items {
p.recordExport(item.AliasLoc, item.Alias, item.Name.Ref)
}
case *js_ast.SExportStar:
record := &p.importRecords[s.ImportRecordIndex]
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, s.ImportRecordIndex)
if s.Alias != nil {
// "export * as ns from 'path'"
p.namedImports[s.NamespaceRef] = js_ast.NamedImport{
AliasIsStar: true,
AliasLoc: s.Alias.Loc,
NamespaceRef: ast.InvalidRef,
ImportRecordIndex: s.ImportRecordIndex,
IsExported: true,
}
p.recordExport(s.Alias.Loc, s.Alias.OriginalName, s.NamespaceRef)
record.Flags |= ast.ContainsImportStar
} else {
// "export * from 'path'"
p.exportStarImportRecords = append(p.exportStarImportRecords, s.ImportRecordIndex)
}
case *js_ast.SExportFrom:
record := &p.importRecords[s.ImportRecordIndex]
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, s.ImportRecordIndex)
for _, item := range s.Items {
// Note that the imported alias is not item.Alias, which is the
// exported alias. This is somewhat confusing because each
// SExportFrom statement is basically SImport + SExportClause in one.
p.namedImports[item.Name.Ref] = js_ast.NamedImport{
Alias: item.OriginalName,
AliasLoc: item.Name.Loc,
NamespaceRef: s.NamespaceRef,
ImportRecordIndex: s.ImportRecordIndex,
IsExported: true,
}
p.recordExport(item.Name.Loc, item.Alias, item.Name.Ref)
if item.OriginalName == "default" {
record.Flags |= ast.ContainsDefaultAlias
} else if item.OriginalName == "__esModule" {
record.Flags |= ast.ContainsESModuleAlias
}
}
// Forbid non-default imports for JSON import assertions
if (record.Flags&ast.AssertTypeJSON) != 0 && p.options.mode == config.ModeBundle {
for _, item := range s.Items {
if item.OriginalName != "default" {
p.log.AddErrorWithNotes(&p.tracker, js_lexer.RangeOfIdentifier(p.source, item.Name.Loc),
fmt.Sprintf("Cannot use non-default import %q with a JSON import assertion", item.OriginalName),
p.notesForAssertTypeJSON(record, item.OriginalName))
}
}
}
// TypeScript always trims unused re-exports. This is important for
// correctness since some re-exports might be fake (only in the type
// system and used for type-only stuff).
if p.options.ts.Parse && len(s.Items) == 0 && (unusedImportFlags&config.TSUnusedImport_KeepStmt) == 0 {
continue
}
}
// Filter out statements we skipped over
stmts[stmtsEnd] = stmt
stmtsEnd++
}
result.stmts = stmts[:stmtsEnd]
return
}
func (p *parser) appendPart(parts []js_ast.Part, stmts []js_ast.Stmt) []js_ast.Part {
part := js_ast.Part{
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
}
p.currentPart = &part
part.Stmts = p.visitStmtsAndPrependTempRefs(stmts, prependTempRefsOpts{})
// Sanity check
if p.currentScope != p.moduleScope {
panic("Internal error: Scope stack imbalance")
}
// Insert any relocated variable statements now
if len(p.relocatedTopLevelVars) > 0 {
alreadyDeclared := make(map[ast.Ref]bool)
for _, local := range p.relocatedTopLevelVars {
// Follow links because "var" declarations may be merged due to hoisting
for {
link := p.symbols[local.Ref.InnerIndex].Link
if link == ast.InvalidRef {
break
}
local.Ref = link
}
// Only declare a given relocated variable once
if !alreadyDeclared[local.Ref] {
alreadyDeclared[local.Ref] = true
part.Stmts = append(part.Stmts, js_ast.Stmt{Loc: local.Loc, Data: &js_ast.SLocal{
Decls: []js_ast.Decl{{
Binding: js_ast.Binding{Loc: local.Loc, Data: &js_ast.BIdentifier{Ref: local.Ref}},
}},
}})
}
}
p.relocatedTopLevelVars = nil
}
if len(part.Stmts) > 0 {
var flags js_ast.StmtsCanBeRemovedIfUnusedFlags
if p.options.mode == config.ModePassThrough {
// Exports are tracked separately, so export clauses can normally always
// be removed. Except we should keep them if we're not doing any format
// conversion because exports are not re-emitted in that case.
flags |= js_ast.KeepExportClauses
}
part.CanBeRemovedIfUnused = p.astHelpers.StmtsCanBeRemovedIfUnused(part.Stmts, flags)
parts = append(parts, part)
}
// Reset the state for this part so we don't accidentally mutate it
p.currentPart = nil
return parts
}
func newParser(log logger.Log, source logger.Source, lexer js_lexer.Lexer, options *Options) *parser {
if options.defines == nil {
defaultDefines := config.ProcessDefines(nil)
options.defines = &defaultDefines
}
p := &parser{
log: log,
source: source,
tracker: logger.MakeLineColumnTracker(&source),
lexer: lexer,
allowIn: true,
options: *options,
runtimeImports: make(map[string]ast.LocRef),
promiseRef: ast.InvalidRef,
regExpRef: ast.InvalidRef,
bigIntRef: ast.InvalidRef,
afterArrowBodyLoc: logger.Loc{Start: -1},
firstJSXElementLoc: logger.Loc{Start: -1},
importMetaRef: ast.InvalidRef,
superCtorRef: ast.InvalidRef,
// For lowering private methods
weakMapRef: ast.InvalidRef,
weakSetRef: ast.InvalidRef,
privateGetters: make(map[ast.Ref]ast.Ref),
privateSetters: make(map[ast.Ref]ast.Ref),
// These are for TypeScript
refToTSNamespaceMemberData: make(map[ast.Ref]js_ast.TSNamespaceMemberData),
emittedNamespaceVars: make(map[ast.Ref]bool),
isExportedInsideNamespace: make(map[ast.Ref]ast.Ref),
localTypeNames: make(map[string]bool),
// These are for handling ES6 imports and exports
importItemsForNamespace: make(map[ast.Ref]namespaceImportItems),
isImportItem: make(map[ast.Ref]bool),
namedImports: make(map[ast.Ref]js_ast.NamedImport),
namedExports: make(map[string]js_ast.NamedExport),
// For JSX runtime imports
jsxRuntimeImports: make(map[string]ast.LocRef),
jsxLegacyImports: make(map[string]ast.LocRef),
// Add "/* @__KEY__ */" comments when mangling properties to support
// running esbuild (or other tools like Terser) again on the output.
// This checks both "--mangle-props" and "--reserve-props" so that
// you can turn this on with just "--reserve-props=." if you want to.
shouldAddKeyComment: options.mangleProps != nil || options.reserveProps != nil,
suppressWarningsAboutWeirdCode: helpers.IsInsideNodeModules(source.KeyPath.Text),
}
if len(options.dropLabels) > 0 {
p.dropLabelsMap = make(map[string]struct{})
for _, name := range options.dropLabels {
p.dropLabelsMap[name] = struct{}{}
}
}
if !options.minifyWhitespace {
p.exprComments = make(map[logger.Loc][]string)
}
p.astHelpers = js_ast.MakeHelperContext(func(ref ast.Ref) bool {
return p.symbols[ref.InnerIndex].Kind == ast.SymbolUnbound
})
p.pushScopeForParsePass(js_ast.ScopeEntry, logger.Loc{Start: locModuleScope})
return p
}
var defaultJSXFactory = []string{"React", "createElement"}
var defaultJSXFragment = []string{"React", "Fragment"}
const defaultJSXImportSource = "react"
func Parse(log logger.Log, source logger.Source, options Options) (result js_ast.AST, ok bool) {
ok = true
defer func() {
r := recover()
if _, isLexerPanic := r.(js_lexer.LexerPanic); isLexerPanic {
ok = false
} else if r != nil {
panic(r)
}
}()
// Default options for JSX elements
if len(options.jsx.Factory.Parts) == 0 {
options.jsx.Factory = config.DefineExpr{Parts: defaultJSXFactory}
}
if len(options.jsx.Fragment.Parts) == 0 && options.jsx.Fragment.Constant == nil {
options.jsx.Fragment = config.DefineExpr{Parts: defaultJSXFragment}
}
if len(options.jsx.ImportSource) == 0 {
options.jsx.ImportSource = defaultJSXImportSource
}
p := newParser(log, source, js_lexer.NewLexer(log, source, options.ts), &options)
// Consume a leading hashbang comment
hashbang := ""
if p.lexer.Token == js_lexer.THashbang {
hashbang = p.lexer.Identifier.String
p.lexer.Next()
}
// Allow top-level await
p.fnOrArrowDataParse.await = allowExpr
p.fnOrArrowDataParse.isTopLevel = true
// Parse the file in the first pass, but do not bind symbols
stmts := p.parseStmtsUpTo(js_lexer.TEndOfFile, parseStmtOpts{
isModuleScope: true,
allowDirectivePrologue: true,
})
p.prepareForVisitPass()
// Insert a "use strict" directive if "alwaysStrict" is active
var directives []string
if tsAlwaysStrict := p.options.tsAlwaysStrict; tsAlwaysStrict != nil && tsAlwaysStrict.Value {
directives = append(directives, "use strict")
}
// Strip off all leading directives
{
totalCount := 0
keptCount := 0
for _, stmt := range stmts {
switch s := stmt.Data.(type) {
case *js_ast.SComment:
stmts[keptCount] = stmt
keptCount++
totalCount++
continue
case *js_ast.SDirective:
if p.isStrictMode() && s.LegacyOctalLoc.Start > 0 {
p.markStrictModeFeature(legacyOctalEscape, p.source.RangeOfLegacyOctalEscape(s.LegacyOctalLoc), "")
}
directive := helpers.UTF16ToString(s.Value)
// Remove duplicate directives
found := false
for _, existing := range directives {
if existing == directive {
found = true
break
}
}
if !found {
directives = append(directives, directive)
}
// Remove this directive from the statement list
totalCount++
continue
}
// Stop when the directive prologue ends
break
}
if keptCount < totalCount {
stmts = append(stmts[:keptCount], stmts[totalCount:]...)
}
}
// Add an empty part for the namespace export that we can fill in later
nsExportPart := js_ast.Part{
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
CanBeRemovedIfUnused: true,
}
var before = []js_ast.Part{nsExportPart}
var parts []js_ast.Part
var after []js_ast.Part
// Insert any injected import statements now that symbols have been declared
for _, file := range p.options.injectedFiles {
exportsNoConflict := make([]string, 0, len(file.Exports))
symbols := make(map[string]ast.LocRef)
if file.DefineName != "" {
ref := p.newSymbol(ast.SymbolOther, file.DefineName)
p.moduleScope.Generated = append(p.moduleScope.Generated, ref)
symbols["default"] = ast.LocRef{Ref: ref}
exportsNoConflict = append(exportsNoConflict, "default")
p.injectedDefineSymbols = append(p.injectedDefineSymbols, ref)
} else {
nextExport:
for _, export := range file.Exports {
// Skip injecting this symbol if it's already declared locally (i.e. it's not a reference to a global)
if _, ok := p.moduleScope.Members[export.Alias]; ok {
continue
}
parts := strings.Split(export.Alias, ".")
// The key must be a dot-separated identifier list
for _, part := range parts {
if !js_ast.IsIdentifier(part) {
continue nextExport
}
}
ref := p.newSymbol(ast.SymbolInjected, export.Alias)
symbols[export.Alias] = ast.LocRef{Ref: ref}
if len(parts) == 1 {
// Handle the identifier case by generating an injected symbol directly
p.moduleScope.Members[export.Alias] = js_ast.ScopeMember{Ref: ref}
} else {
// Handle the dot case using a map. This map is similar to the map
// "options.defines.DotDefines" but is kept separate instead of being
// implemented using the same mechanism because we allow you to use
// "define" to rewrite something to an injected symbol (i.e. we allow
// two levels of mappings). This was historically necessary to be able
// to map a dot name to an injected symbol because we previously didn't
// support dot names as injected symbols. But now dot names as injected
// symbols has been implemented, so supporting two levels of mappings
// is only for backward-compatibility.
if p.injectedDotNames == nil {
p.injectedDotNames = make(map[string][]injectedDotName)
}
tail := parts[len(parts)-1]
p.injectedDotNames[tail] = append(p.injectedDotNames[tail], injectedDotName{parts: parts, injectedDefineIndex: uint32(len(p.injectedDefineSymbols))})
p.injectedDefineSymbols = append(p.injectedDefineSymbols, ref)
}
exportsNoConflict = append(exportsNoConflict, export.Alias)
if p.injectedSymbolSources == nil {
p.injectedSymbolSources = make(map[ast.Ref]injectedSymbolSource)
}
p.injectedSymbolSources[ref] = injectedSymbolSource{
source: file.Source,
loc: export.Loc,
}
}
}
if file.IsCopyLoader {
before, _ = p.generateImportStmt(file.Source.KeyPath.Text, logger.Range{}, exportsNoConflict, before, symbols, nil, &file.Source.Index)
} else {
before, _ = p.generateImportStmt(file.Source.KeyPath.Text, logger.Range{}, exportsNoConflict, before, symbols, &file.Source.Index, nil)
}
}
// When "using" declarations appear at the top level, we change all TDZ
// variables in the top-level scope into "var" so that they aren't harmed
// when they are moved into the try/catch statement that lowering will
// generate.
//
// This is necessary because exported function declarations must be hoisted
// outside of the try/catch statement because they can be evaluated before
// this module is evaluated due to ESM cross-file function hoisting. And
// these function bodies might reference anything else in this scope, which
// must still work when those things are moved inside a try/catch statement.
//
// Before:
//
// using foo = get()
// export function fn() {
// return [foo, new Bar]
// }
// class Bar {}
//
// After ("fn" is hoisted, "Bar" is converted to "var"):
//
// export function fn() {
// return [foo, new Bar]
// }
// try {
// var foo = get();
// var Bar = class {};
// } catch (_) {
// ...
// } finally {
// ...
// }
//
// This is also necessary because other code might be appended to the code
// that we're processing and expect to be able to access top-level variables.
p.willWrapModuleInTryCatchForUsing = p.shouldLowerUsingDeclarations(stmts)
// Bind symbols in a second pass over the AST. I started off doing this in a
// single pass, but it turns out it's pretty much impossible to do this
// correctly while handling arrow functions because of the grammar
// ambiguities.
//
// Note that top-level lowered "using" declarations disable tree-shaking
// because we only do tree-shaking on top-level statements and lowering
// a top-level "using" declaration moves all top-level statements into a
// nested scope.
if !p.options.treeShaking || p.willWrapModuleInTryCatchForUsing {
// When tree shaking is disabled, everything comes in a single part
parts = p.appendPart(parts, stmts)
} else {
var preprocessedEnums map[int][]js_ast.Part
if p.scopesInOrderForEnum != nil {
// Preprocess TypeScript enums to improve code generation. Otherwise
// uses of an enum before that enum has been declared won't be inlined:
//
// console.log(Foo.FOO) // We want "FOO" to be inlined here
// const enum Foo { FOO = 0 }
//
// The TypeScript compiler itself contains code with this pattern, so
// it's important to implement this optimization.
for i, stmt := range stmts {
if _, ok := stmt.Data.(*js_ast.SEnum); ok {
if preprocessedEnums == nil {
preprocessedEnums = make(map[int][]js_ast.Part)
}
oldScopesInOrder := p.scopesInOrder
p.scopesInOrder = p.scopesInOrderForEnum[stmt.Loc]
preprocessedEnums[i] = p.appendPart(nil, []js_ast.Stmt{stmt})
p.scopesInOrder = oldScopesInOrder
}
}
}
// When tree shaking is enabled, each top-level statement is potentially a separate part
for i, stmt := range stmts {
switch s := stmt.Data.(type) {
case *js_ast.SLocal:
// Split up top-level multi-declaration variable statements
for _, decl := range s.Decls {
clone := *s
clone.Decls = []js_ast.Decl{decl}
parts = p.appendPart(parts, []js_ast.Stmt{{Loc: stmt.Loc, Data: &clone}})
}
case *js_ast.SImport, *js_ast.SExportFrom, *js_ast.SExportStar:
if p.options.mode != config.ModePassThrough {
// Move imports (and import-like exports) to the top of the file to
// ensure that if they are converted to a require() call, the effects
// will take place before any other statements are evaluated.
before = p.appendPart(before, []js_ast.Stmt{stmt})
} else {
// If we aren't doing any format conversion, just keep these statements
// inline where they were. Exports are sorted so order doesn't matter:
// https://262.ecma-international.org/6.0/#sec-module-namespace-exotic-objects.
// However, this is likely an aesthetic issue that some people will
// complain about. In addition, there are code transformation tools
// such as TypeScript and Babel with bugs where the order of exports
// in the file is incorrectly preserved instead of sorted, so preserving
// the order of exports ourselves here may be preferable.
parts = p.appendPart(parts, []js_ast.Stmt{stmt})
}
case *js_ast.SExportEquals:
// TypeScript "export = value;" becomes "module.exports = value;". This
// must happen at the end after everything is parsed because TypeScript
// moves this statement to the end when it generates code.
after = p.appendPart(after, []js_ast.Stmt{stmt})
case *js_ast.SEnum:
parts = append(parts, preprocessedEnums[i]...)
p.scopesInOrder = p.scopesInOrder[len(p.scopesInOrderForEnum[stmt.Loc]):]
default:
parts = p.appendPart(parts, []js_ast.Stmt{stmt})
}
}
}
// Insert a variable for "import.meta" at the top of the file if it was used.
// We don't need to worry about "use strict" directives because this only
// happens when bundling, in which case we are flatting the module scopes of
// all modules together anyway so such directives are meaningless.
if p.importMetaRef != ast.InvalidRef {
importMetaStmt := js_ast.Stmt{Data: &js_ast.SLocal{
Kind: p.selectLocalKind(js_ast.LocalConst),
Decls: []js_ast.Decl{{
Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: p.importMetaRef}},
ValueOrNil: js_ast.Expr{Data: &js_ast.EObject{}},
}},
}}
before = append(before, js_ast.Part{
Stmts: []js_ast.Stmt{importMetaStmt},
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
DeclaredSymbols: []js_ast.DeclaredSymbol{{Ref: p.importMetaRef, IsTopLevel: true}},
CanBeRemovedIfUnused: true,
})
}
// Pop the module scope to apply the "ContainsDirectEval" rules
p.popScope()
result = p.toAST(before, parts, after, hashbang, directives)
result.SourceMapComment = p.lexer.SourceMappingURL
return
}
type HelperCall struct {
Global []string
Runtime string
}
func LazyExportAST(log logger.Log, source logger.Source, options Options, expr js_ast.Expr, helperCall *HelperCall) js_ast.AST {
// Don't create a new lexer using js_lexer.NewLexer() here since that will
// actually attempt to parse the first token, which might cause a syntax
// error.
p := newParser(log, source, js_lexer.Lexer{}, &options)
p.prepareForVisitPass()
// Defer the actual code generation until linking
part := js_ast.Part{
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
}
// Optionally call a runtime API function to transform the expression
if helperCall != nil {
p.currentPart = &part
if len(helperCall.Global) > 0 {
ref := p.newSymbol(ast.SymbolUnbound, helperCall.Global[0])
p.recordUsage(ref)
target := js_ast.Expr{Data: &js_ast.EIdentifier{Ref: ref}}
kind := js_ast.NormalCall
for _, name := range helperCall.Global[1:] {
target.Data = &js_ast.EDot{Target: target, Name: name}
kind = js_ast.TargetWasOriginallyPropertyAccess
}
expr.Data = &js_ast.ECall{Target: target, Args: []js_ast.Expr{expr}, Kind: kind}
} else {
expr = p.callRuntime(expr.Loc, helperCall.Runtime, []js_ast.Expr{expr})
}
p.currentPart = nil
}
part.Stmts = []js_ast.Stmt{{Loc: expr.Loc, Data: &js_ast.SLazyExport{Value: expr}}}
// Add an empty part for the namespace export that we can fill in later
nsExportPart := js_ast.Part{
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
CanBeRemovedIfUnused: true,
}
ast := p.toAST([]js_ast.Part{nsExportPart}, []js_ast.Part{part}, nil, "", nil)
ast.HasLazyExport = true
return ast
}
func GlobResolveAST(log logger.Log, source logger.Source, importRecords []ast.ImportRecord, object *js_ast.EObject, name string) js_ast.AST {
// Don't create a new lexer using js_lexer.NewLexer() here since that will
// actually attempt to parse the first token, which might cause a syntax
// error.
p := newParser(log, source, js_lexer.Lexer{}, &Options{})
p.prepareForVisitPass()
// Add an empty part for the namespace export that we can fill in later
nsExportPart := js_ast.Part{
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
CanBeRemovedIfUnused: true,
}
if len(p.importRecords) != 0 {
panic("Internal error")
}
p.importRecords = importRecords
importRecordIndices := make([]uint32, 0, len(importRecords))
for importRecordIndex := range importRecords {
importRecordIndices = append(importRecordIndices, uint32(importRecordIndex))
}
ref := p.newSymbol(ast.SymbolOther, name)
p.moduleScope.Generated = append(p.moduleScope.Generated, ref)
part := js_ast.Part{
ImportRecordIndices: importRecordIndices,
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
}
p.currentPart = &part
part.Stmts = []js_ast.Stmt{{Data: &js_ast.SLocal{
IsExport: true,
Decls: []js_ast.Decl{{
Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: ref}},
ValueOrNil: p.callRuntime(logger.Loc{}, "__glob", []js_ast.Expr{{Data: object}}),
}},
}}}
p.currentPart = nil
p.esmExportKeyword.Len = 1
return p.toAST([]js_ast.Part{nsExportPart}, []js_ast.Part{part}, nil, "", nil)
}
func ParseDefineExpr(text string) (config.DefineExpr, js_ast.E) {
if text == "" {
return config.DefineExpr{}, nil
}
// Try a property chain
parts := strings.Split(text, ".")
for i, part := range parts {
if !js_ast.IsIdentifier(part) {
parts = nil
break
}
// Don't allow most keywords as the identifier
if i == 0 {
if token, ok := js_lexer.Keywords[part]; ok && token != js_lexer.TNull && token != js_lexer.TThis &&
(token != js_lexer.TImport || len(parts) < 2 || parts[1] != "meta") {
parts = nil
break
}
}
}
if parts != nil {
return config.DefineExpr{Parts: parts}, nil
}
// Try parsing a value
log := logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, nil)
expr, ok := ParseJSON(log, logger.Source{Contents: text}, JSONOptions{
IsForDefine: true,
})
if !ok {
return config.DefineExpr{}, nil
}
// Only primitive literals are inlined directly
switch expr.Data.(type) {
case *js_ast.ENull, *js_ast.EBoolean, *js_ast.EString, *js_ast.ENumber, *js_ast.EBigInt:
return config.DefineExpr{Constant: expr.Data}, nil
}
// If it's not a primitive, return the whole compound JSON value to be injected out-of-line
return config.DefineExpr{}, expr.Data
}
type whyESM uint8
const (
whyESMUnknown whyESM = iota
whyESMExportKeyword
whyESMImportMeta
whyESMTopLevelAwait
whyESMFileMJS
whyESMFileMTS
whyESMTypeModulePackageJSON
whyESMImportStatement
)
// Say why the current file is being considered an ES module
func (p *parser) whyESModule() (whyESM, []logger.MsgData) {
because := "This file is considered to be an ECMAScript module because"
switch {
case p.esmExportKeyword.Len > 0:
return whyESMExportKeyword, []logger.MsgData{p.tracker.MsgData(p.esmExportKeyword,
because+" of the \"export\" keyword here:")}
case p.esmImportMeta.Len > 0:
return whyESMImportMeta, []logger.MsgData{p.tracker.MsgData(p.esmImportMeta,
because+" of the use of \"import.meta\" here:")}
case p.topLevelAwaitKeyword.Len > 0:
return whyESMTopLevelAwait, []logger.MsgData{p.tracker.MsgData(p.topLevelAwaitKeyword,
because+" of the top-level \"await\" keyword here:")}
case p.options.moduleTypeData.Type == js_ast.ModuleESM_MJS:
return whyESMFileMJS, []logger.MsgData{{Text: because + " the file name ends in \".mjs\"."}}
case p.options.moduleTypeData.Type == js_ast.ModuleESM_MTS:
return whyESMFileMTS, []logger.MsgData{{Text: because + " the file name ends in \".mts\"."}}
case p.options.moduleTypeData.Type == js_ast.ModuleESM_PackageJSON:
tracker := logger.MakeLineColumnTracker(p.options.moduleTypeData.Source)
return whyESMTypeModulePackageJSON, []logger.MsgData{tracker.MsgData(p.options.moduleTypeData.Range,
because+" the enclosing \"package.json\" file sets the type of this file to \"module\":")}
// This case must come last because some code cares about the "import"
// statement keyword and some doesn't, and we don't want to give code
// that doesn't care about the "import" statement the wrong error message.
case p.esmImportStatementKeyword.Len > 0:
return whyESMImportStatement, []logger.MsgData{p.tracker.MsgData(p.esmImportStatementKeyword,
because+" of the \"import\" keyword here:")}
}
return whyESMUnknown, nil
}
func (p *parser) prepareForVisitPass() {
p.pushScopeForVisitPass(js_ast.ScopeEntry, logger.Loc{Start: locModuleScope})
p.fnOrArrowDataVisit.isOutsideFnOrArrow = true
p.moduleScope = p.currentScope
// Force-enable strict mode if that's the way TypeScript is configured
if tsAlwaysStrict := p.options.tsAlwaysStrict; tsAlwaysStrict != nil && tsAlwaysStrict.Value {
p.currentScope.StrictMode = js_ast.ImplicitStrictModeTSAlwaysStrict
}
// Determine whether or not this file is ESM
p.isFileConsideredToHaveESMExports =
p.esmExportKeyword.Len > 0 ||
p.esmImportMeta.Len > 0 ||
p.topLevelAwaitKeyword.Len > 0 ||
p.options.moduleTypeData.Type.IsESM()
p.isFileConsideredESM =
p.isFileConsideredToHaveESMExports ||
p.esmImportStatementKeyword.Len > 0
// Legacy HTML comments are not allowed in ESM files
if p.isFileConsideredESM && p.lexer.LegacyHTMLCommentRange.Len > 0 {
_, notes := p.whyESModule()
p.log.AddErrorWithNotes(&p.tracker, p.lexer.LegacyHTMLCommentRange,
"Legacy HTML single-line comments are not allowed in ECMAScript modules", notes)
}
// ECMAScript modules are always interpreted as strict mode. This has to be
// done before "hoistSymbols" because strict mode can alter hoisting (!).
if p.isFileConsideredESM {
p.moduleScope.RecursiveSetStrictMode(js_ast.ImplicitStrictModeESM)
}
p.hoistSymbols(p.moduleScope)
if p.options.mode != config.ModePassThrough {
p.requireRef = p.declareCommonJSSymbol(ast.SymbolUnbound, "require")
} else {
p.requireRef = p.newSymbol(ast.SymbolUnbound, "require")
}
// CommonJS-style exports are only enabled if this isn't using ECMAScript-
// style exports. You can still use "require" in ESM, just not "module" or
// "exports". You can also still use "import" in CommonJS.
if p.options.mode != config.ModePassThrough && !p.isFileConsideredToHaveESMExports {
// CommonJS-style exports
p.exportsRef = p.declareCommonJSSymbol(ast.SymbolHoisted, "exports")
p.moduleRef = p.declareCommonJSSymbol(ast.SymbolHoisted, "module")
} else {
// ESM-style exports
p.exportsRef = p.newSymbol(ast.SymbolHoisted, "exports")
p.moduleRef = p.newSymbol(ast.SymbolHoisted, "module")
}
// Handle "@jsx" and "@jsxFrag" pragmas now that lexing is done
if p.options.jsx.Parse {
if jsxRuntime := p.lexer.JSXRuntimePragmaComment; jsxRuntime.Text != "" {
if jsxRuntime.Text == "automatic" {
p.options.jsx.AutomaticRuntime = true
} else if jsxRuntime.Text == "classic" {
p.options.jsx.AutomaticRuntime = false
} else {
p.log.AddIDWithNotes(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxRuntime.Range,
fmt.Sprintf("Invalid JSX runtime: %q", jsxRuntime.Text),
[]logger.MsgData{{Text: "The JSX runtime can only be set to either \"classic\" or \"automatic\"."}})
}
}
if jsxFactory := p.lexer.JSXFactoryPragmaComment; jsxFactory.Text != "" {
if p.options.jsx.AutomaticRuntime {
p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFactory.Range,
"The JSX factory cannot be set when using React's \"automatic\" JSX transform")
} else if expr, _ := ParseDefineExpr(jsxFactory.Text); len(expr.Parts) > 0 {
p.options.jsx.Factory = expr
} else {
p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFactory.Range,
fmt.Sprintf("Invalid JSX factory: %s", jsxFactory.Text))
}
}
if jsxFragment := p.lexer.JSXFragmentPragmaComment; jsxFragment.Text != "" {
if p.options.jsx.AutomaticRuntime {
p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFragment.Range,
"The JSX fragment cannot be set when using React's \"automatic\" JSX transform")
} else if expr, _ := ParseDefineExpr(jsxFragment.Text); len(expr.Parts) > 0 || expr.Constant != nil {
p.options.jsx.Fragment = expr
} else {
p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFragment.Range,
fmt.Sprintf("Invalid JSX fragment: %s", jsxFragment.Text))
}
}
if jsxImportSource := p.lexer.JSXImportSourcePragmaComment; jsxImportSource.Text != "" {
if !p.options.jsx.AutomaticRuntime {
p.log.AddIDWithNotes(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxImportSource.Range,
"The JSX import source cannot be set without also enabling React's \"automatic\" JSX transform",
[]logger.MsgData{{Text: "You can enable React's \"automatic\" JSX transform for this file by using a \"@jsxRuntime automatic\" comment."}})
} else {
p.options.jsx.ImportSource = jsxImportSource.Text
}
}
}
// Force-enable strict mode if the JSX "automatic" runtime is enabled and
// there is at least one JSX element. This is because the automatically-
// generated import statement turns the file into an ES module. This behavior
// matches TypeScript which also does this. See this PR for more information:
// https://github.com/microsoft/TypeScript/pull/39199
if p.currentScope.StrictMode == js_ast.SloppyMode && p.options.jsx.AutomaticRuntime && p.firstJSXElementLoc.Start != -1 {
p.currentScope.StrictMode = js_ast.ImplicitStrictModeJSXAutomaticRuntime
}
}
func (p *parser) declareCommonJSSymbol(kind ast.SymbolKind, name string) ast.Ref {
member, ok := p.moduleScope.Members[name]
// If the code declared this symbol using "var name", then this is actually
// not a collision. For example, node will let you do this:
//
// var exports;
// module.exports.foo = 123;
// console.log(exports.foo);
//
// This works because node's implementation of CommonJS wraps the entire
// source file like this:
//
// (function(require, exports, module, __filename, __dirname) {
// var exports;
// module.exports.foo = 123;
// console.log(exports.foo);
// })
//
// Both the "exports" argument and "var exports" are hoisted variables, so
// they don't collide.
if ok && p.symbols[member.Ref.InnerIndex].Kind == ast.SymbolHoisted &&
kind == ast.SymbolHoisted && !p.isFileConsideredToHaveESMExports {
return member.Ref
}
// Create a new symbol if we didn't merge with an existing one above
ref := p.newSymbol(kind, name)
// If the variable wasn't declared, declare it now. This means any references
// to this name will become bound to this symbol after this (since we haven't
// run the visit pass yet).
if !ok {
p.moduleScope.Members[name] = js_ast.ScopeMember{Ref: ref, Loc: logger.Loc{Start: -1}}
return ref
}
// If the variable was declared, then it shadows this symbol. The code in
// this module will be unable to reference this symbol. However, we must
// still add the symbol to the scope so it gets minified (automatically-
// generated code may still reference the symbol).
p.moduleScope.Generated = append(p.moduleScope.Generated, ref)
return ref
}
// Compute a character frequency histogram for everything that's not a bound
// symbol. This is used to modify how minified names are generated for slightly
// better gzip compression. Even though it's a very small win, we still do it
// because it's simple to do and very cheap to compute.
func (p *parser) computeCharacterFrequency() *ast.CharFreq {
if !p.options.minifyIdentifiers || p.source.Index == runtime.SourceIndex {
return nil
}
// Add everything in the file to the histogram
charFreq := &ast.CharFreq{}
charFreq.Scan(p.source.Contents, 1)
// Subtract out all comments
for _, commentRange := range p.lexer.AllComments {
charFreq.Scan(p.source.TextForRange(commentRange), -1)
}
// Subtract out all import paths
for _, record := range p.importRecords {
if !record.SourceIndex.IsValid() {
charFreq.Scan(record.Path.Text, -1)
}
}
// Subtract out all symbols that will be minified
var visit func(*js_ast.Scope)
visit = func(scope *js_ast.Scope) {
for _, member := range scope.Members {
symbol := &p.symbols[member.Ref.InnerIndex]
if symbol.SlotNamespace() != ast.SlotMustNotBeRenamed {
charFreq.Scan(symbol.OriginalName, -int32(symbol.UseCountEstimate))
}
}
if scope.Label.Ref != ast.InvalidRef {
symbol := &p.symbols[scope.Label.Ref.InnerIndex]
if symbol.SlotNamespace() != ast.SlotMustNotBeRenamed {
charFreq.Scan(symbol.OriginalName, -int32(symbol.UseCountEstimate)-1)
}
}
for _, child := range scope.Children {
visit(child)
}
}
visit(p.moduleScope)
// Subtract out all properties that will be mangled
for _, ref := range p.mangledProps {
symbol := &p.symbols[ref.InnerIndex]
charFreq.Scan(symbol.OriginalName, -int32(symbol.UseCountEstimate))
}
return charFreq
}
func (p *parser) generateImportStmt(
path string,
pathRange logger.Range,
imports []string,
parts []js_ast.Part,
symbols map[string]ast.LocRef,
sourceIndex *uint32,
copySourceIndex *uint32,
) ([]js_ast.Part, uint32) {
if pathRange.Len == 0 {
isFirst := true
for _, it := range symbols {
if isFirst || it.Loc.Start < pathRange.Loc.Start {
pathRange.Loc = it.Loc
}
isFirst = false
}
}
namespaceRef := p.newSymbol(ast.SymbolOther, "import_"+js_ast.GenerateNonUniqueNameFromPath(path))
p.moduleScope.Generated = append(p.moduleScope.Generated, namespaceRef)
declaredSymbols := make([]js_ast.DeclaredSymbol, 1+len(imports))
clauseItems := make([]js_ast.ClauseItem, len(imports))
importRecordIndex := p.addImportRecord(ast.ImportStmt, ast.EvaluationPhase, pathRange, path, nil, 0)
if sourceIndex != nil {
p.importRecords[importRecordIndex].SourceIndex = ast.MakeIndex32(*sourceIndex)
}
if copySourceIndex != nil {
p.importRecords[importRecordIndex].CopySourceIndex = ast.MakeIndex32(*copySourceIndex)
}
declaredSymbols[0] = js_ast.DeclaredSymbol{Ref: namespaceRef, IsTopLevel: true}
// Create per-import information
for i, alias := range imports {
it := symbols[alias]
declaredSymbols[i+1] = js_ast.DeclaredSymbol{Ref: it.Ref, IsTopLevel: true}
clauseItems[i] = js_ast.ClauseItem{
Alias: alias,
AliasLoc: it.Loc,
Name: ast.LocRef{Loc: it.Loc, Ref: it.Ref},
}
p.isImportItem[it.Ref] = true
p.namedImports[it.Ref] = js_ast.NamedImport{
Alias: alias,
AliasLoc: it.Loc,
NamespaceRef: namespaceRef,
ImportRecordIndex: importRecordIndex,
}
}
// Append a single import to the end of the file (ES6 imports are hoisted
// so we don't need to worry about where the import statement goes)
return append(parts, js_ast.Part{
DeclaredSymbols: declaredSymbols,
ImportRecordIndices: []uint32{importRecordIndex},
Stmts: []js_ast.Stmt{{Loc: pathRange.Loc, Data: &js_ast.SImport{
NamespaceRef: namespaceRef,
Items: &clauseItems,
ImportRecordIndex: importRecordIndex,
IsSingleLine: true,
}}},
}), importRecordIndex
}
// Sort the keys for determinism
func sortedKeysOfMapStringLocRef(in map[string]ast.LocRef) []string {
keys := make([]string, 0, len(in))
for key := range in {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func (p *parser) toAST(before, parts, after []js_ast.Part, hashbang string, directives []string) js_ast.AST {
// Insert an import statement for any runtime imports we generated
if len(p.runtimeImports) > 0 && !p.options.omitRuntimeForTests {
keys := sortedKeysOfMapStringLocRef(p.runtimeImports)
sourceIndex := runtime.SourceIndex
before, _ = p.generateImportStmt("<runtime>", logger.Range{}, keys, before, p.runtimeImports, &sourceIndex, nil)
}
// Insert an import statement for any jsx runtime imports we generated
if len(p.jsxRuntimeImports) > 0 && !p.options.omitJSXRuntimeForTests {
keys := sortedKeysOfMapStringLocRef(p.jsxRuntimeImports)
// Determine the runtime source and whether it's prod or dev
path := p.options.jsx.ImportSource
if p.options.jsx.Development {
path = path + "/jsx-dev-runtime"
} else {
path = path + "/jsx-runtime"
}
before, _ = p.generateImportStmt(path, logger.Range{}, keys, before, p.jsxRuntimeImports, nil, nil)
}
// Insert an import statement for any legacy jsx imports we generated (i.e., createElement)
if len(p.jsxLegacyImports) > 0 && !p.options.omitJSXRuntimeForTests {
keys := sortedKeysOfMapStringLocRef(p.jsxLegacyImports)
path := p.options.jsx.ImportSource
before, _ = p.generateImportStmt(path, logger.Range{}, keys, before, p.jsxLegacyImports, nil, nil)
}
// Insert imports for each glob pattern
for _, glob := range p.globPatternImports {
symbols := map[string]ast.LocRef{glob.name: {Loc: glob.approximateRange.Loc, Ref: glob.ref}}
var importRecordIndex uint32
before, importRecordIndex = p.generateImportStmt(helpers.GlobPatternToString(glob.parts), glob.approximateRange, []string{glob.name}, before, symbols, nil, nil)
record := &p.importRecords[importRecordIndex]
record.AssertOrWith = glob.assertOrWith
record.Phase = glob.phase
record.GlobPattern = &ast.GlobPattern{
Parts: glob.parts,
ExportAlias: glob.name,
Kind: glob.kind,
}
}
// Generated imports are inserted before other code instead of appending them
// to the end of the file. Appending them should work fine because JavaScript
// import statements are "hoisted" to run before the importing file. However,
// some buggy JavaScript toolchains such as the TypeScript compiler convert
// ESM into CommonJS by replacing "import" statements inline without doing
// any hoisting, which is incorrect. See the following issue for more info:
// https://github.com/microsoft/TypeScript/issues/16166. Since JSX-related
// imports are present in the generated code when bundling is disabled, and
// could therefore be processed by these buggy tools, it's more robust to put
// them at the top even though it means potentially reallocating almost the
// entire array of parts.
if len(before) > 0 {
parts = append(before, parts...)
}
parts = append(parts, after...)
// Handle import paths after the whole file has been visited because we need
// symbol usage counts to be able to remove unused type-only imports in
// TypeScript code.
keptImportEquals := false
removedImportEquals := false
partsEnd := 0
for partIndex, part := range parts {
p.currentPart = &part
result := p.scanForImportsAndExports(part.Stmts)
p.currentPart = nil
part.Stmts = result.stmts
keptImportEquals = keptImportEquals || result.keptImportEquals
removedImportEquals = removedImportEquals || result.removedImportEquals
if len(part.Stmts) > 0 || uint32(partIndex) == js_ast.NSExportPartIndex {
if p.moduleScope.ContainsDirectEval && len(part.DeclaredSymbols) > 0 {
// If this file contains a direct call to "eval()", all parts that
// declare top-level symbols must be kept since the eval'd code may
// reference those symbols.
part.CanBeRemovedIfUnused = false
}
parts[partsEnd] = part
partsEnd++
}
}
parts = parts[:partsEnd]
// We need to iterate multiple times if an import-equals statement was
// removed and there are more import-equals statements that may be removed.
// In the example below, a/b/c should be kept but x/y/z should be removed
// (and removal requires multiple passes):
//
// import a = foo.a
// import b = a.b
// import c = b.c
//
// import x = foo.x
// import y = x.y
// import z = y.z
//
// export let bar = c
//
// This is a smaller version of the general import/export scanning loop above.
// We only want to repeat the code that eliminates TypeScript import-equals
// statements, not the other code in the loop above.
for keptImportEquals && removedImportEquals {
keptImportEquals = false
removedImportEquals = false
partsEnd := 0
for partIndex, part := range parts {
result := p.scanForUnusedTSImportEquals(part.Stmts)
part.Stmts = result.stmts
keptImportEquals = keptImportEquals || result.keptImportEquals
removedImportEquals = removedImportEquals || result.removedImportEquals
if len(part.Stmts) > 0 || uint32(partIndex) == js_ast.NSExportPartIndex {
parts[partsEnd] = part
partsEnd++
}
}
parts = parts[:partsEnd]
}
// Do a second pass for exported items now that imported items are filled out
for _, part := range parts {
for _, stmt := range part.Stmts {
if s, ok := stmt.Data.(*js_ast.SExportClause); ok {
for _, item := range s.Items {
// Mark re-exported imports as such
if namedImport, ok := p.namedImports[item.Name.Ref]; ok {
namedImport.IsExported = true
p.namedImports[item.Name.Ref] = namedImport
}
}
}
}
}
// Analyze cross-part dependencies for tree shaking and code splitting
{
// Map locals to parts
p.topLevelSymbolToParts = make(map[ast.Ref][]uint32)
for partIndex, part := range parts {
for _, declared := range part.DeclaredSymbols {
if declared.IsTopLevel {
// If this symbol was merged, use the symbol at the end of the
// linked list in the map. This is the case for multiple "var"
// declarations with the same name, for example.
ref := declared.Ref
for p.symbols[ref.InnerIndex].Link != ast.InvalidRef {
ref = p.symbols[ref.InnerIndex].Link
}
p.topLevelSymbolToParts[ref] = append(
p.topLevelSymbolToParts[ref], uint32(partIndex))
}
}
}
// Pulling in the exports of this module always pulls in the export part
p.topLevelSymbolToParts[p.exportsRef] = append(p.topLevelSymbolToParts[p.exportsRef], js_ast.NSExportPartIndex)
}
// Make a wrapper symbol in case we need to be wrapped in a closure
wrapperRef := p.newSymbol(ast.SymbolOther, "require_"+p.source.IdentifierName)
// Assign slots to symbols in nested scopes. This is some precomputation for
// the symbol renaming pass that will happen later in the linker. It's done
// now in the parser because we want it to be done in parallel per file and
// we're already executing code in a dedicated goroutine for this file.
var nestedScopeSlotCounts ast.SlotCounts
if p.options.minifyIdentifiers {
nestedScopeSlotCounts = renamer.AssignNestedScopeSlots(p.moduleScope, p.symbols)
}
exportsKind := js_ast.ExportsNone
usesExportsRef := p.symbols[p.exportsRef.InnerIndex].UseCountEstimate > 0
usesModuleRef := p.symbols[p.moduleRef.InnerIndex].UseCountEstimate > 0
if p.esmExportKeyword.Len > 0 || p.esmImportMeta.Len > 0 || p.topLevelAwaitKeyword.Len > 0 {
exportsKind = js_ast.ExportsESM
} else if usesExportsRef || usesModuleRef || p.hasTopLevelReturn {
exportsKind = js_ast.ExportsCommonJS
} else {
// If this module has no exports, try to determine what kind of module it
// is by looking at node's "type" field in "package.json" and/or whether
// the file extension is ".mjs"/".mts" or ".cjs"/".cts".
switch {
case p.options.moduleTypeData.Type.IsCommonJS():
// ".cjs" or ".cts" or ("type: commonjs" and (".js" or ".jsx" or ".ts" or ".tsx"))
exportsKind = js_ast.ExportsCommonJS
case p.options.moduleTypeData.Type.IsESM():
// ".mjs" or ".mts" or ("type: module" and (".js" or ".jsx" or ".ts" or ".tsx"))
exportsKind = js_ast.ExportsESM
default:
// Treat unknown modules containing an import statement as ESM. Otherwise
// the bundler will treat this file as CommonJS if it's imported and ESM
// if it's not imported.
if p.esmImportStatementKeyword.Len > 0 {
exportsKind = js_ast.ExportsESM
}
}
}
return js_ast.AST{
Parts: parts,
ModuleTypeData: p.options.moduleTypeData,
ModuleScope: p.moduleScope,
CharFreq: p.computeCharacterFrequency(),
Symbols: p.symbols,
ExportsRef: p.exportsRef,
ModuleRef: p.moduleRef,
WrapperRef: wrapperRef,
Hashbang: hashbang,
Directives: directives,
NamedImports: p.namedImports,
NamedExports: p.namedExports,
TSEnums: p.tsEnums,
ConstValues: p.constValues,
ExprComments: p.exprComments,
NestedScopeSlotCounts: nestedScopeSlotCounts,
TopLevelSymbolToPartsFromParser: p.topLevelSymbolToParts,
ExportStarImportRecords: p.exportStarImportRecords,
ImportRecords: p.importRecords,
ApproximateLineCount: int32(p.lexer.ApproximateNewlineCount) + 1,
MangledProps: p.mangledProps,
ReservedProps: p.reservedProps,
ManifestForYarnPnP: p.manifestForYarnPnP,
// CommonJS features
UsesExportsRef: usesExportsRef,
UsesModuleRef: usesModuleRef,
ExportsKind: exportsKind,
// ES6 features
ExportKeyword: p.esmExportKeyword,
TopLevelAwaitKeyword: p.topLevelAwaitKeyword,
LiveTopLevelAwaitKeyword: p.liveTopLevelAwaitKeyword,
}
}