Files

3017 lines
87 KiB
Go

package js_ast
import (
"math"
"strconv"
"strings"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/logger"
)
type HelperContext struct {
isUnbound func(ast.Ref) bool
}
func MakeHelperContext(isUnbound func(ast.Ref) bool) HelperContext {
return HelperContext{
isUnbound: isUnbound,
}
}
// If this returns true, then calling this expression captures the target of
// the property access as "this" when calling the function in the property.
func IsPropertyAccess(expr Expr) bool {
switch expr.Data.(type) {
case *EDot, *EIndex:
return true
}
return false
}
func IsOptionalChain(value Expr) bool {
switch e := value.Data.(type) {
case *EDot:
return e.OptionalChain != OptionalChainNone
case *EIndex:
return e.OptionalChain != OptionalChainNone
case *ECall:
return e.OptionalChain != OptionalChainNone
}
return false
}
func Assign(a Expr, b Expr) Expr {
return Expr{Loc: a.Loc, Data: &EBinary{Op: BinOpAssign, Left: a, Right: b}}
}
func AssignStmt(a Expr, b Expr) Stmt {
return Stmt{Loc: a.Loc, Data: &SExpr{Value: Assign(a, b)}}
}
// Wraps the provided expression in the "!" prefix operator. The expression
// will potentially be simplified to avoid generating unnecessary extra "!"
// operators. For example, calling this with "!!x" will return "!x" instead
// of returning "!!!x".
func Not(expr Expr) Expr {
if result, ok := MaybeSimplifyNot(expr); ok {
return result
}
return Expr{Loc: expr.Loc, Data: &EUnary{Op: UnOpNot, Value: expr}}
}
// The given "expr" argument should be the operand of a "!" prefix operator
// (i.e. the "x" in "!x"). This returns a simplified expression for the
// whole operator (i.e. the "!x") if it can be simplified, or false if not.
// It's separate from "Not()" above to avoid allocation on failure in case
// that is undesired.
//
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func MaybeSimplifyNot(expr Expr) (Expr, bool) {
switch e := expr.Data.(type) {
case *EAnnotation:
return MaybeSimplifyNot(e.Value)
case *EInlinedEnum:
if value, ok := MaybeSimplifyNot(e.Value); ok {
return value, true
}
case *ENull, *EUndefined:
return Expr{Loc: expr.Loc, Data: &EBoolean{Value: true}}, true
case *EBoolean:
return Expr{Loc: expr.Loc, Data: &EBoolean{Value: !e.Value}}, true
case *ENumber:
return Expr{Loc: expr.Loc, Data: &EBoolean{Value: e.Value == 0 || math.IsNaN(e.Value)}}, true
case *EBigInt:
if equal, ok := CheckEqualityBigInt(e.Value, "0"); ok {
return Expr{Loc: expr.Loc, Data: &EBoolean{Value: equal}}, true
}
case *EString:
return Expr{Loc: expr.Loc, Data: &EBoolean{Value: len(e.Value) == 0}}, true
case *EFunction, *EArrow, *ERegExp:
return Expr{Loc: expr.Loc, Data: &EBoolean{Value: false}}, true
case *EUnary:
// "!!!a" => "!a"
if e.Op == UnOpNot && KnownPrimitiveType(e.Value.Data) == PrimitiveBoolean {
return e.Value, true
}
case *EBinary:
// Make sure that these transformations are all safe for special values.
// For example, "!(a < b)" is not the same as "a >= b" if a and/or b are
// NaN (or undefined, or null, or possibly other problem cases too).
switch e.Op {
case BinOpLooseEq:
// "!(a == b)" => "a != b"
return Expr{Loc: expr.Loc, Data: &EBinary{Op: BinOpLooseNe, Left: e.Left, Right: e.Right}}, true
case BinOpLooseNe:
// "!(a != b)" => "a == b"
return Expr{Loc: expr.Loc, Data: &EBinary{Op: BinOpLooseEq, Left: e.Left, Right: e.Right}}, true
case BinOpStrictEq:
// "!(a === b)" => "a !== b"
return Expr{Loc: expr.Loc, Data: &EBinary{Op: BinOpStrictNe, Left: e.Left, Right: e.Right}}, true
case BinOpStrictNe:
// "!(a !== b)" => "a === b"
return Expr{Loc: expr.Loc, Data: &EBinary{Op: BinOpStrictEq, Left: e.Left, Right: e.Right}}, true
case BinOpComma:
// "!(a, b)" => "a, !b"
return Expr{Loc: expr.Loc, Data: &EBinary{Op: BinOpComma, Left: e.Left, Right: Not(e.Right)}}, true
}
}
return Expr{}, false
}
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func MaybeSimplifyEqualityComparison(loc logger.Loc, e *EBinary, unsupportedFeatures compat.JSFeature) (Expr, bool) {
value, primitive := e.Left, e.Right
// Detect when the primitive comes first and flip the order of our checks
if IsPrimitiveLiteral(value.Data) {
value, primitive = primitive, value
}
// "!x === true" => "!x"
// "!x === false" => "!!x"
// "!x !== true" => "!!x"
// "!x !== false" => "!x"
if boolean, ok := primitive.Data.(*EBoolean); ok && KnownPrimitiveType(value.Data) == PrimitiveBoolean {
if boolean.Value == (e.Op == BinOpLooseNe || e.Op == BinOpStrictNe) {
return Not(value), true
} else {
return value, true
}
}
// "typeof x != 'undefined'" => "typeof x < 'u'"
// "typeof x == 'undefined'" => "typeof x > 'u'"
if !unsupportedFeatures.Has(compat.TypeofExoticObjectIsObject) {
// Only do this optimization if we know that the "typeof" operator won't
// return something random. The only case of this happening was Internet
// Explorer returning "unknown" for some objects, which messes with this
// optimization. So we don't do this when targeting Internet Explorer.
if typeof, ok := value.Data.(*EUnary); ok && typeof.Op == UnOpTypeof {
if str, ok := primitive.Data.(*EString); ok && helpers.UTF16EqualsString(str.Value, "undefined") {
flip := value == e.Right
op := BinOpLt
if (e.Op == BinOpLooseEq || e.Op == BinOpStrictEq) != flip {
op = BinOpGt
}
primitive.Data = &EString{Value: []uint16{'u'}}
if flip {
value, primitive = primitive, value
}
return Expr{Loc: loc, Data: &EBinary{Op: op, Left: value, Right: primitive}}, true
}
}
}
return Expr{}, false
}
func IsSymbolInstance(data E) bool {
switch e := data.(type) {
case *EDot:
return e.IsSymbolInstance
case *EIndex:
return e.IsSymbolInstance
}
return false
}
func IsPrimitiveLiteral(data E) bool {
switch e := data.(type) {
case *EAnnotation:
return IsPrimitiveLiteral(e.Value.Data)
case *EInlinedEnum:
return IsPrimitiveLiteral(e.Value.Data)
case *ENull, *EUndefined, *EString, *EBoolean, *ENumber, *EBigInt:
return true
}
return false
}
type PrimitiveType uint8
const (
PrimitiveUnknown PrimitiveType = iota
PrimitiveMixed
PrimitiveNull
PrimitiveUndefined
PrimitiveBoolean
PrimitiveNumber
PrimitiveString
PrimitiveBigInt
)
// This can be used when the returned type is either one or the other
func MergedKnownPrimitiveTypes(a Expr, b Expr) PrimitiveType {
x := KnownPrimitiveType(a.Data)
if x == PrimitiveUnknown {
return PrimitiveUnknown
}
y := KnownPrimitiveType(b.Data)
if y == PrimitiveUnknown {
return PrimitiveUnknown
}
if x == y {
return x
}
return PrimitiveMixed // Definitely some kind of primitive
}
// Note: This function does not say whether the expression is side-effect free
// or not. For example, the expression "++x" always returns a primitive.
func KnownPrimitiveType(expr E) PrimitiveType {
switch e := expr.(type) {
case *EAnnotation:
return KnownPrimitiveType(e.Value.Data)
case *EInlinedEnum:
return KnownPrimitiveType(e.Value.Data)
case *ENull:
return PrimitiveNull
case *EUndefined:
return PrimitiveUndefined
case *EBoolean:
return PrimitiveBoolean
case *ENumber:
return PrimitiveNumber
case *EString:
return PrimitiveString
case *EBigInt:
return PrimitiveBigInt
case *ETemplate:
if e.TagOrNil.Data == nil {
return PrimitiveString
}
case *EIf:
return MergedKnownPrimitiveTypes(e.Yes, e.No)
case *EUnary:
switch e.Op {
case UnOpVoid:
return PrimitiveUndefined
case UnOpTypeof:
return PrimitiveString
case UnOpNot, UnOpDelete:
return PrimitiveBoolean
case UnOpPos:
return PrimitiveNumber // Cannot be bigint because that throws an exception
case UnOpNeg, UnOpCpl:
value := KnownPrimitiveType(e.Value.Data)
if value == PrimitiveBigInt {
return PrimitiveBigInt
}
if value != PrimitiveUnknown && value != PrimitiveMixed {
return PrimitiveNumber
}
return PrimitiveMixed // Can be number or bigint
case UnOpPreDec, UnOpPreInc, UnOpPostDec, UnOpPostInc:
return PrimitiveMixed // Can be number or bigint
}
case *EBinary:
switch e.Op {
case BinOpStrictEq, BinOpStrictNe, BinOpLooseEq, BinOpLooseNe,
BinOpLt, BinOpGt, BinOpLe, BinOpGe,
BinOpInstanceof, BinOpIn:
return PrimitiveBoolean
case BinOpLogicalOr, BinOpLogicalAnd:
return MergedKnownPrimitiveTypes(e.Left, e.Right)
case BinOpNullishCoalescing:
left := KnownPrimitiveType(e.Left.Data)
right := KnownPrimitiveType(e.Right.Data)
if left == PrimitiveNull || left == PrimitiveUndefined {
return right
}
if left != PrimitiveUnknown {
if left != PrimitiveMixed {
return left // Definitely not null or undefined
}
if right != PrimitiveUnknown {
return PrimitiveMixed // Definitely some kind of primitive
}
}
case BinOpAdd:
left := KnownPrimitiveType(e.Left.Data)
right := KnownPrimitiveType(e.Right.Data)
if left == PrimitiveString || right == PrimitiveString {
return PrimitiveString
}
if left == PrimitiveBigInt && right == PrimitiveBigInt {
return PrimitiveBigInt
}
if left != PrimitiveUnknown && left != PrimitiveMixed && left != PrimitiveBigInt &&
right != PrimitiveUnknown && right != PrimitiveMixed && right != PrimitiveBigInt {
return PrimitiveNumber
}
return PrimitiveMixed // Can be number or bigint or string (or an exception)
case BinOpAddAssign:
right := KnownPrimitiveType(e.Right.Data)
if right == PrimitiveString {
return PrimitiveString
}
return PrimitiveMixed // Can be number or bigint or string (or an exception)
case
BinOpSub, BinOpSubAssign,
BinOpMul, BinOpMulAssign,
BinOpDiv, BinOpDivAssign,
BinOpRem, BinOpRemAssign,
BinOpPow, BinOpPowAssign,
BinOpBitwiseAnd, BinOpBitwiseAndAssign,
BinOpBitwiseOr, BinOpBitwiseOrAssign,
BinOpBitwiseXor, BinOpBitwiseXorAssign,
BinOpShl, BinOpShlAssign,
BinOpShr, BinOpShrAssign,
BinOpUShr, BinOpUShrAssign:
return PrimitiveMixed // Can be number or bigint (or an exception)
case BinOpAssign, BinOpComma:
return KnownPrimitiveType(e.Right.Data)
}
}
return PrimitiveUnknown
}
func CanChangeStrictToLoose(a Expr, b Expr) bool {
x := KnownPrimitiveType(a.Data)
y := KnownPrimitiveType(b.Data)
return x == y && x != PrimitiveUnknown && x != PrimitiveMixed
}
// Returns true if the result of the "typeof" operator on this expression is
// statically determined and this expression has no side effects (i.e. can be
// removed without consequence).
func TypeofWithoutSideEffects(data E) (string, bool) {
switch e := data.(type) {
case *EAnnotation:
if e.Flags.Has(CanBeRemovedIfUnusedFlag) {
return TypeofWithoutSideEffects(e.Value.Data)
}
case *EInlinedEnum:
return TypeofWithoutSideEffects(e.Value.Data)
case *ENull:
return "object", true
case *EUndefined:
return "undefined", true
case *EBoolean:
return "boolean", true
case *ENumber:
return "number", true
case *EBigInt:
return "bigint", true
case *EString:
return "string", true
case *EFunction, *EArrow:
return "function", true
}
return "", false
}
// The goal of this function is to "rotate" the AST if it's possible to use the
// left-associative property of the operator to avoid unnecessary parentheses.
//
// When using this, make absolutely sure that the operator is actually
// associative. For example, the "+" operator is not associative for
// floating-point numbers.
//
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func JoinWithLeftAssociativeOp(op OpCode, a Expr, b Expr) Expr {
// "(a, b) op c" => "a, b op c"
if comma, ok := a.Data.(*EBinary); ok && comma.Op == BinOpComma {
// Don't mutate the original AST
clone := *comma
clone.Right = JoinWithLeftAssociativeOp(op, clone.Right, b)
return Expr{Loc: a.Loc, Data: &clone}
}
// "a op (b op c)" => "(a op b) op c"
// "a op (b op (c op d))" => "((a op b) op c) op d"
for {
if binary, ok := b.Data.(*EBinary); ok && binary.Op == op {
a = JoinWithLeftAssociativeOp(op, a, binary.Left)
b = binary.Right
} else {
break
}
}
// "a op b" => "a op b"
// "(a op b) op c" => "(a op b) op c"
return Expr{Loc: a.Loc, Data: &EBinary{Op: op, Left: a, Right: b}}
}
func JoinWithComma(a Expr, b Expr) Expr {
if a.Data == nil {
return b
}
if b.Data == nil {
return a
}
return Expr{Loc: a.Loc, Data: &EBinary{Op: BinOpComma, Left: a, Right: b}}
}
func JoinAllWithComma(all []Expr) (result Expr) {
for _, value := range all {
result = JoinWithComma(result, value)
}
return
}
func ConvertBindingToExpr(binding Binding, wrapIdentifier func(logger.Loc, ast.Ref) Expr) Expr {
loc := binding.Loc
switch b := binding.Data.(type) {
case *BMissing:
return Expr{Loc: loc, Data: &EMissing{}}
case *BIdentifier:
if wrapIdentifier != nil {
return wrapIdentifier(loc, b.Ref)
}
return Expr{Loc: loc, Data: &EIdentifier{Ref: b.Ref}}
case *BArray:
exprs := make([]Expr, len(b.Items))
for i, item := range b.Items {
expr := ConvertBindingToExpr(item.Binding, wrapIdentifier)
if b.HasSpread && i+1 == len(b.Items) {
expr = Expr{Loc: expr.Loc, Data: &ESpread{Value: expr}}
} else if item.DefaultValueOrNil.Data != nil {
expr = Assign(expr, item.DefaultValueOrNil)
}
exprs[i] = expr
}
return Expr{Loc: loc, Data: &EArray{
Items: exprs,
IsSingleLine: b.IsSingleLine,
}}
case *BObject:
properties := make([]Property, len(b.Properties))
for i, property := range b.Properties {
value := ConvertBindingToExpr(property.Value, wrapIdentifier)
kind := PropertyField
if property.IsSpread {
kind = PropertySpread
}
var flags PropertyFlags
if property.IsComputed {
flags |= PropertyIsComputed
}
properties[i] = Property{
Kind: kind,
Flags: flags,
Key: property.Key,
ValueOrNil: value,
InitializerOrNil: property.DefaultValueOrNil,
}
}
return Expr{Loc: loc, Data: &EObject{
Properties: properties,
IsSingleLine: b.IsSingleLine,
}}
default:
panic("Internal error")
}
}
// This will return a nil expression if the expression can be totally removed.
//
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func (ctx HelperContext) SimplifyUnusedExpr(expr Expr, unsupportedFeatures compat.JSFeature) Expr {
switch e := expr.Data.(type) {
case *EAnnotation:
if e.Flags.Has(CanBeRemovedIfUnusedFlag) {
return Expr{}
}
case *EInlinedEnum:
return ctx.SimplifyUnusedExpr(e.Value, unsupportedFeatures)
case *ENull, *EUndefined, *EMissing, *EBoolean, *ENumber, *EBigInt,
*EString, *EThis, *ERegExp, *EFunction, *EArrow, *EImportMeta:
return Expr{}
case *EDot:
if e.CanBeRemovedIfUnused {
return Expr{}
}
case *EIdentifier:
if e.MustKeepDueToWithStmt {
break
}
if e.CanBeRemovedIfUnused || !ctx.isUnbound(e.Ref) {
return Expr{}
}
case *ETemplate:
if e.TagOrNil.Data == nil {
var comma Expr
var templateLoc logger.Loc
var template *ETemplate
for _, part := range e.Parts {
// If we know this value is some kind of primitive, then we know that
// "ToString" has no side effects and can be avoided.
if KnownPrimitiveType(part.Value.Data) != PrimitiveUnknown {
if template != nil {
comma = JoinWithComma(comma, Expr{Loc: templateLoc, Data: template})
template = nil
}
comma = JoinWithComma(comma, ctx.SimplifyUnusedExpr(part.Value, unsupportedFeatures))
continue
}
// Make sure "ToString" is still evaluated on the value. We can't use
// string addition here because that may evaluate "ValueOf" instead.
if template == nil {
template = &ETemplate{}
templateLoc = part.Value.Loc
}
template.Parts = append(template.Parts, TemplatePart{Value: part.Value})
}
if template != nil {
comma = JoinWithComma(comma, Expr{Loc: templateLoc, Data: template})
}
return comma
} else if e.CanBeUnwrappedIfUnused {
// If the function call was annotated as being able to be removed if the
// result is unused, then we can remove it and just keep the arguments.
// Note that there are no implicit "ToString" operations for tagged
// template literals.
var comma Expr
for _, part := range e.Parts {
comma = JoinWithComma(comma, ctx.SimplifyUnusedExpr(part.Value, unsupportedFeatures))
}
return comma
}
case *EArray:
// Arrays with "..." spread expressions can't be unwrapped because the
// "..." triggers code evaluation via iterators. In that case, just trim
// the other items instead and leave the array expression there.
for _, spread := range e.Items {
if _, ok := spread.Data.(*ESpread); ok {
items := make([]Expr, 0, len(e.Items))
for _, item := range e.Items {
item = ctx.SimplifyUnusedExpr(item, unsupportedFeatures)
if item.Data != nil {
items = append(items, item)
}
}
// Don't mutate the original AST
clone := *e
clone.Items = items
return Expr{Loc: expr.Loc, Data: &clone}
}
}
// Otherwise, the array can be completely removed. We only need to keep any
// array items with side effects. Apply this simplification recursively.
var result Expr
for _, item := range e.Items {
result = JoinWithComma(result, ctx.SimplifyUnusedExpr(item, unsupportedFeatures))
}
return result
case *EObject:
// Objects with "..." spread expressions can't be unwrapped because the
// "..." triggers code evaluation via getters. In that case, just trim
// the other items instead and leave the object expression there.
for _, spread := range e.Properties {
if spread.Kind == PropertySpread {
properties := make([]Property, 0, len(e.Properties))
for _, property := range e.Properties {
// Spread properties must always be evaluated
if property.Kind != PropertySpread {
value := ctx.SimplifyUnusedExpr(property.ValueOrNil, unsupportedFeatures)
if value.Data != nil {
// Keep the value
property.ValueOrNil = value
} else if !property.Flags.Has(PropertyIsComputed) {
// Skip this property if the key doesn't need to be computed
continue
} else {
// Replace values without side effects with "0" because it's short
property.ValueOrNil.Data = &ENumber{}
}
}
properties = append(properties, property)
}
// Don't mutate the original AST
clone := *e
clone.Properties = properties
return Expr{Loc: expr.Loc, Data: &clone}
}
}
// Otherwise, the object can be completely removed. We only need to keep any
// object properties with side effects. Apply this simplification recursively.
var result Expr
for _, property := range e.Properties {
if property.Flags.Has(PropertyIsComputed) {
// Make sure "ToString" is still evaluated on the key
result = JoinWithComma(result, Expr{Loc: property.Key.Loc, Data: &EBinary{
Op: BinOpAdd,
Left: property.Key,
Right: Expr{Loc: property.Key.Loc, Data: &EString{}},
}})
}
result = JoinWithComma(result, ctx.SimplifyUnusedExpr(property.ValueOrNil, unsupportedFeatures))
}
return result
case *EIf:
yes := ctx.SimplifyUnusedExpr(e.Yes, unsupportedFeatures)
no := ctx.SimplifyUnusedExpr(e.No, unsupportedFeatures)
// "foo() ? 1 : 2" => "foo()"
if yes.Data == nil && no.Data == nil {
return ctx.SimplifyUnusedExpr(e.Test, unsupportedFeatures)
}
// "foo() ? 1 : bar()" => "foo() || bar()"
if yes.Data == nil {
return JoinWithLeftAssociativeOp(BinOpLogicalOr, e.Test, no)
}
// "foo() ? bar() : 2" => "foo() && bar()"
if no.Data == nil {
return JoinWithLeftAssociativeOp(BinOpLogicalAnd, e.Test, yes)
}
if yes != e.Yes || no != e.No {
return Expr{Loc: expr.Loc, Data: &EIf{Test: e.Test, Yes: yes, No: no}}
}
case *EUnary:
switch e.Op {
// These operators must not have any type conversions that can execute code
// such as "toString" or "valueOf". They must also never throw any exceptions.
case UnOpVoid, UnOpNot:
return ctx.SimplifyUnusedExpr(e.Value, unsupportedFeatures)
case UnOpNeg:
if _, ok := e.Value.Data.(*EBigInt); ok {
// Consider negated bigints to have no side effects
return Expr{}
}
case UnOpTypeof:
if _, ok := e.Value.Data.(*EIdentifier); ok && e.WasOriginallyTypeofIdentifier {
// "typeof x" must not be transformed into if "x" since doing so could
// cause an exception to be thrown. Instead we can just remove it since
// "typeof x" is special-cased in the standard to never throw.
return Expr{}
}
return ctx.SimplifyUnusedExpr(e.Value, unsupportedFeatures)
}
case *EBinary:
left := e.Left
right := e.Right
switch e.Op {
// These operators must not have any type conversions that can execute code
// such as "toString" or "valueOf". They must also never throw any exceptions.
case BinOpStrictEq, BinOpStrictNe, BinOpComma:
return JoinWithComma(ctx.SimplifyUnusedExpr(left, unsupportedFeatures), ctx.SimplifyUnusedExpr(right, unsupportedFeatures))
// We can simplify "==" and "!=" even though they can call "toString" and/or
// "valueOf" if we can statically determine that the types of both sides are
// primitives. In that case there won't be any chance for user-defined
// "toString" and/or "valueOf" to be called.
case BinOpLooseEq, BinOpLooseNe:
if MergedKnownPrimitiveTypes(left, right) != PrimitiveUnknown {
return JoinWithComma(ctx.SimplifyUnusedExpr(left, unsupportedFeatures), ctx.SimplifyUnusedExpr(right, unsupportedFeatures))
}
case BinOpLogicalAnd, BinOpLogicalOr, BinOpNullishCoalescing:
// If this is a boolean logical operation and the result is unused, then
// we know the left operand will only be used for its boolean value and
// can be simplified under that assumption
if e.Op != BinOpNullishCoalescing {
left = ctx.SimplifyBooleanExpr(left)
}
// Preserve short-circuit behavior: the left expression is only unused if
// the right expression can be completely removed. Otherwise, the left
// expression is important for the branch.
right = ctx.SimplifyUnusedExpr(right, unsupportedFeatures)
if right.Data == nil {
return ctx.SimplifyUnusedExpr(left, unsupportedFeatures)
}
// Try to take advantage of the optional chain operator to shorten code
if !unsupportedFeatures.Has(compat.OptionalChain) {
if binary, ok := left.Data.(*EBinary); ok {
// "a != null && a.b()" => "a?.b()"
// "a == null || a.b()" => "a?.b()"
if (binary.Op == BinOpLooseNe && e.Op == BinOpLogicalAnd) || (binary.Op == BinOpLooseEq && e.Op == BinOpLogicalOr) {
var test Expr
if _, ok := binary.Right.Data.(*ENull); ok {
test = binary.Left
} else if _, ok := binary.Left.Data.(*ENull); ok {
test = binary.Right
}
// Note: Technically unbound identifiers can refer to a getter on
// the global object and that getter can have side effects that can
// be observed if we run that getter once instead of twice. But this
// seems like terrible coding practice and very unlikely to come up
// in real software, so we deliberately ignore this possibility and
// optimize for size instead of for this obscure edge case.
//
// If this is ever changed, then we must also pessimize the lowering
// of "foo?.bar" to save the value of "foo" to ensure that it's only
// evaluated once. Specifically "foo?.bar" would have to expand to:
//
// var _a;
// (_a = foo) == null ? void 0 : _a.bar;
//
// instead of:
//
// foo == null ? void 0 : foo.bar;
//
// Babel does the first one while TypeScript does the second one.
// Since TypeScript doesn't handle this extreme edge case and
// TypeScript is very widely used, I think it's fine for us to not
// handle this edge case either.
if id, ok := test.Data.(*EIdentifier); ok && !id.MustKeepDueToWithStmt && TryToInsertOptionalChain(test, right) {
return right
}
}
}
}
case BinOpAdd:
if result, isStringAddition := simplifyUnusedStringAdditionChain(expr); isStringAddition {
return result
}
}
if left != e.Left || right != e.Right {
return Expr{Loc: expr.Loc, Data: &EBinary{Op: e.Op, Left: left, Right: right}}
}
case *ECall:
// A call that has been marked "__PURE__" can be removed if all arguments
// can be removed. The annotation causes us to ignore the target.
if e.CanBeUnwrappedIfUnused {
var result Expr
for _, arg := range e.Args {
if _, ok := arg.Data.(*ESpread); ok {
arg.Data = &EArray{Items: []Expr{arg}, IsSingleLine: true}
}
result = JoinWithComma(result, ctx.SimplifyUnusedExpr(arg, unsupportedFeatures))
}
return result
}
// Attempt to shorten IIFEs
if len(e.Args) == 0 {
switch target := e.Target.Data.(type) {
case *EFunction:
if len(target.Fn.Args) != 0 {
break
}
// Just delete "(function() {})()" completely
if len(target.Fn.Body.Block.Stmts) == 0 {
return Expr{}
}
case *EArrow:
if len(target.Args) != 0 {
break
}
// Just delete "(() => {})()" completely
if len(target.Body.Block.Stmts) == 0 {
return Expr{}
}
if len(target.Body.Block.Stmts) == 1 {
switch s := target.Body.Block.Stmts[0].Data.(type) {
case *SExpr:
if !target.IsAsync {
// Replace "(() => { foo() })()" with "foo()"
return s.Value
} else {
// Replace "(async () => { foo() })()" with "(async () => foo())()"
clone := *target
clone.Body.Block.Stmts[0].Data = &SReturn{ValueOrNil: s.Value}
clone.PreferExpr = true
return Expr{Loc: expr.Loc, Data: &ECall{Target: Expr{Loc: e.Target.Loc, Data: &clone}}}
}
case *SReturn:
if !target.IsAsync {
// Replace "(() => foo())()" with "foo()"
return s.ValueOrNil
}
}
}
}
}
case *ENew:
// A constructor call that has been marked "__PURE__" can be removed if all
// arguments can be removed. The annotation causes us to ignore the target.
if e.CanBeUnwrappedIfUnused {
var result Expr
for _, arg := range e.Args {
if _, ok := arg.Data.(*ESpread); ok {
arg.Data = &EArray{Items: []Expr{arg}, IsSingleLine: true}
}
result = JoinWithComma(result, ctx.SimplifyUnusedExpr(arg, unsupportedFeatures))
}
return result
}
}
return expr
}
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func simplifyUnusedStringAdditionChain(expr Expr) (Expr, bool) {
switch e := expr.Data.(type) {
case *EString:
// "'x' + y" => "'' + y"
return Expr{Loc: expr.Loc, Data: &EString{}}, true
case *EBinary:
if e.Op == BinOpAdd {
left, leftIsStringAddition := simplifyUnusedStringAdditionChain(e.Left)
if right, rightIsString := e.Right.Data.(*EString); rightIsString {
// "('' + x) + 'y'" => "'' + x"
if leftIsStringAddition {
return left, true
}
// "x + 'y'" => "x + ''"
if !leftIsStringAddition && len(right.Value) > 0 {
return Expr{Loc: expr.Loc, Data: &EBinary{
Op: BinOpAdd,
Left: left,
Right: Expr{Loc: e.Right.Loc, Data: &EString{}},
}}, true
}
}
// Don't mutate the original AST
if left != e.Left {
expr.Data = &EBinary{Op: BinOpAdd, Left: left, Right: e.Right}
}
return expr, leftIsStringAddition
}
}
return expr, false
}
func ToInt32(f float64) int32 {
// The easy way
i := int32(f)
if float64(i) == f {
return i
}
// Special-case non-finite numbers (casting them is unspecified behavior in Go)
if math.IsNaN(f) || math.IsInf(f, 0) {
return 0
}
// The hard way
i = int32(uint32(math.Mod(math.Abs(f), 4294967296)))
if math.Signbit(f) {
return -i
}
return i
}
func ToUint32(f float64) uint32 {
return uint32(ToInt32(f))
}
// If this returns true, we know the result can't be NaN
func isInt32OrUint32(data E) bool {
switch e := data.(type) {
case *EBinary:
switch e.Op {
case BinOpUShr: // This is the only bitwise operator that can't return a bigint (because it throws instead)
return true
case BinOpLogicalOr, BinOpLogicalAnd:
return isInt32OrUint32(e.Left.Data) && isInt32OrUint32(e.Right.Data)
}
case *EIf:
return isInt32OrUint32(e.Yes.Data) && isInt32OrUint32(e.No.Data)
}
return false
}
func ToNumberWithoutSideEffects(data E) (float64, bool) {
switch e := data.(type) {
case *EAnnotation:
return ToNumberWithoutSideEffects(e.Value.Data)
case *EInlinedEnum:
return ToNumberWithoutSideEffects(e.Value.Data)
case *ENull:
return 0, true
case *EUndefined, *ERegExp:
return math.NaN(), true
case *EArray:
if len(e.Items) == 0 {
// "+[]" => "0"
return 0, true
}
case *EObject:
if len(e.Properties) == 0 {
// "+{}" => "NaN"
return math.NaN(), true
}
case *EBoolean:
if e.Value {
return 1, true
} else {
return 0, true
}
case *ENumber:
return e.Value, true
case *EString:
// "+''" => "0"
if len(e.Value) == 0 {
return 0, true
}
// "+'1'" => "1"
if num, ok := StringToEquivalentNumberValue(e.Value); ok {
return num, true
}
}
return 0, false
}
func ToStringWithoutSideEffects(data E) (string, bool) {
switch e := data.(type) {
case *ENull:
return "null", true
case *EUndefined:
return "undefined", true
case *EBoolean:
if e.Value {
return "true", true
} else {
return "false", true
}
case *EBigInt:
// Only do this if there is no radix
if len(e.Value) < 2 || e.Value[0] != '0' {
return e.Value, true
}
case *ENumber:
if str, ok := TryToStringOnNumberSafely(e.Value, 10); ok {
return str, true
}
case *ERegExp:
return e.Value, true
case *EDot:
// This is dumb but some JavaScript obfuscators use this to generate string literals
if e.Name == "constructor" {
switch e.Target.Data.(type) {
case *EString:
return "function String() { [native code] }", true
case *ERegExp:
return "function RegExp() { [native code] }", true
}
}
}
return "", false
}
func extractNumericValue(data E) (float64, bool) {
switch e := data.(type) {
case *EAnnotation:
return extractNumericValue(e.Value.Data)
case *EInlinedEnum:
return extractNumericValue(e.Value.Data)
case *ENumber:
return e.Value, true
}
return 0, false
}
func extractNumericValues(left Expr, right Expr) (float64, float64, bool) {
if a, ok := extractNumericValue(left.Data); ok {
if b, ok := extractNumericValue(right.Data); ok {
return a, b, true
}
}
return 0, 0, false
}
func extractStringValue(data E) ([]uint16, bool) {
switch e := data.(type) {
case *EAnnotation:
return extractStringValue(e.Value.Data)
case *EInlinedEnum:
return extractStringValue(e.Value.Data)
case *EString:
return e.Value, true
}
return nil, false
}
func extractStringValues(left Expr, right Expr) ([]uint16, []uint16, bool) {
if a, ok := extractStringValue(left.Data); ok {
if b, ok := extractStringValue(right.Data); ok {
return a, b, true
}
}
return nil, nil, false
}
func stringCompareUCS2(a []uint16, b []uint16) int {
var n int
if len(a) < len(b) {
n = len(a)
} else {
n = len(b)
}
for i := 0; i < n; i++ {
if delta := int(a[i]) - int(b[i]); delta != 0 {
return delta
}
}
return len(a) - len(b)
}
func approximatePrintedIntCharCount(intValue float64) int {
count := 1 + (int)(math.Max(0, math.Floor(math.Log10(math.Abs(intValue)))))
if intValue < 0 {
count++
}
return count
}
func ShouldFoldBinaryOperatorWhenMinifying(binary *EBinary) bool {
switch binary.Op {
case
// Equality tests should always result in smaller code when folded
BinOpLooseEq,
BinOpLooseNe,
BinOpStrictEq,
BinOpStrictNe,
// Minification always folds right signed shift operations since they are
// unlikely to result in larger output. Note: ">>>" could result in
// bigger output such as "-1 >>> 0" becoming "4294967295".
BinOpShr,
// Minification always folds the following bitwise operations since they
// are unlikely to result in larger output.
BinOpBitwiseAnd,
BinOpBitwiseOr,
BinOpBitwiseXor,
BinOpLt,
BinOpGt,
BinOpLe,
BinOpGe:
return true
case BinOpAdd:
// Addition of small-ish integers can definitely be folded without issues
// "1 + 2" => "3"
if left, right, ok := extractNumericValues(binary.Left, binary.Right); ok &&
left == math.Trunc(left) && math.Abs(left) <= 0xFFFF_FFFF &&
right == math.Trunc(right) && math.Abs(right) <= 0xFFFF_FFFF {
return true
}
// String addition should pretty much always be more compact when folded
if _, _, ok := extractStringValues(binary.Left, binary.Right); ok {
return true
}
case BinOpSub:
// Subtraction of small-ish integers can definitely be folded without issues
// "3 - 1" => "2"
if left, right, ok := extractNumericValues(binary.Left, binary.Right); ok &&
left == math.Trunc(left) && math.Abs(left) <= 0xFFFF_FFFF &&
right == math.Trunc(right) && math.Abs(right) <= 0xFFFF_FFFF {
return true
}
case BinOpMul:
// Allow multiplication of small-ish integers to be folded
// "1 * 2" => "3"
if left, right, ok := extractNumericValues(binary.Left, binary.Right); ok &&
left == math.Trunc(left) && math.Abs(left) <= 0xFF &&
right == math.Trunc(right) && math.Abs(right) <= 0xFF {
return true
}
case BinOpDiv:
// "0/0" => "NaN"
// "1/0" => "Infinity"
// "1/-0" => "-Infinity"
if _, right, ok := extractNumericValues(binary.Left, binary.Right); ok && right == 0 {
return true
}
case BinOpShl:
// "1 << 3" => "8"
// "1 << 24" => "1 << 24" (since "1<<24" is shorter than "16777216")
if left, right, ok := extractNumericValues(binary.Left, binary.Right); ok {
leftLen := approximatePrintedIntCharCount(left)
rightLen := approximatePrintedIntCharCount(right)
resultLen := approximatePrintedIntCharCount(float64(ToInt32(left) << (ToUint32(right) & 31)))
return resultLen <= leftLen+2+rightLen
}
case BinOpUShr:
// "10 >>> 1" => "5"
// "-1 >>> 0" => "-1 >>> 0" (since "-1>>>0" is shorter than "4294967295")
if left, right, ok := extractNumericValues(binary.Left, binary.Right); ok {
leftLen := approximatePrintedIntCharCount(left)
rightLen := approximatePrintedIntCharCount(right)
resultLen := approximatePrintedIntCharCount(float64(ToUint32(left) >> (ToUint32(right) & 31)))
return resultLen <= leftLen+3+rightLen
}
case BinOpLogicalAnd, BinOpLogicalOr, BinOpNullishCoalescing:
if IsPrimitiveLiteral(binary.Left.Data) {
return true
}
}
return false
}
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func FoldBinaryOperator(loc logger.Loc, e *EBinary) Expr {
switch e.Op {
case BinOpAdd:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: left + right}}
}
if left, right, ok := extractStringValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EString{Value: joinStrings(left, right)}}
}
case BinOpSub:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: left - right}}
}
case BinOpMul:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: left * right}}
}
case BinOpDiv:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: left / right}}
}
case BinOpRem:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: math.Mod(left, right)}}
}
case BinOpPow:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: math.Pow(left, right)}}
}
case BinOpShl:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: float64(ToInt32(left) << (ToUint32(right) & 31))}}
}
case BinOpShr:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: float64(ToInt32(left) >> (ToUint32(right) & 31))}}
}
case BinOpUShr:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: float64(ToUint32(left) >> (ToUint32(right) & 31))}}
}
case BinOpBitwiseAnd:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: float64(ToInt32(left) & ToInt32(right))}}
}
case BinOpBitwiseOr:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: float64(ToInt32(left) | ToInt32(right))}}
}
case BinOpBitwiseXor:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &ENumber{Value: float64(ToInt32(left) ^ ToInt32(right))}}
}
case BinOpLt:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: left < right}}
}
if left, right, ok := extractStringValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) < 0}}
}
case BinOpGt:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: left > right}}
}
if left, right, ok := extractStringValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) > 0}}
}
case BinOpLe:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: left <= right}}
}
if left, right, ok := extractStringValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) <= 0}}
}
case BinOpGe:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: left >= right}}
}
if left, right, ok := extractStringValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) >= 0}}
}
case BinOpLooseEq, BinOpStrictEq:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: left == right}}
}
if left, right, ok := extractStringValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) == 0}}
}
case BinOpLooseNe, BinOpStrictNe:
if left, right, ok := extractNumericValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: left != right}}
}
if left, right, ok := extractStringValues(e.Left, e.Right); ok {
return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) != 0}}
}
case BinOpLogicalAnd:
if boolean, sideEffects, ok := ToBooleanWithSideEffects(e.Left.Data); ok {
if !boolean {
return e.Left
} else if sideEffects == NoSideEffects {
return e.Right
}
}
case BinOpLogicalOr:
if boolean, sideEffects, ok := ToBooleanWithSideEffects(e.Left.Data); ok {
if boolean {
return e.Left
} else if sideEffects == NoSideEffects {
return e.Right
}
}
case BinOpNullishCoalescing:
if isNullOrUndefined, sideEffects, ok := ToNullOrUndefinedWithSideEffects(e.Left.Data); ok {
if !isNullOrUndefined {
return e.Left
} else if sideEffects == NoSideEffects {
return e.Right
}
}
}
return Expr{}
}
func IsBinaryNullAndUndefined(left Expr, right Expr, op OpCode) (Expr, Expr, bool) {
if a, ok := left.Data.(*EBinary); ok && a.Op == op {
if b, ok := right.Data.(*EBinary); ok && b.Op == op {
idA, eqA := a.Left, a.Right
idB, eqB := b.Left, b.Right
// Detect when the identifier comes second and flip the order of our checks
if _, ok := eqA.Data.(*EIdentifier); ok {
idA, eqA = eqA, idA
}
if _, ok := eqB.Data.(*EIdentifier); ok {
idB, eqB = eqB, idB
}
if idA, ok := idA.Data.(*EIdentifier); ok {
if idB, ok := idB.Data.(*EIdentifier); ok && idA.Ref == idB.Ref {
// "a === null || a === void 0"
if _, ok := eqA.Data.(*ENull); ok {
if _, ok := eqB.Data.(*EUndefined); ok {
return a.Left, a.Right, true
}
}
// "a === void 0 || a === null"
if _, ok := eqA.Data.(*EUndefined); ok {
if _, ok := eqB.Data.(*ENull); ok {
return b.Left, b.Right, true
}
}
}
}
}
}
return Expr{}, Expr{}, false
}
func CheckEqualityBigInt(a string, b string) (equal bool, ok bool) {
// Equal literals are always equal
if a == b {
return true, true
}
// Unequal literals are unequal if neither has a radix. Leading zeros are
// disallowed in bigint literals without a radix, so in this case we know
// each value is in canonical form.
if (len(a) < 2 || a[0] != '0') && (len(b) < 2 || b[0] != '0') {
return false, true
}
return false, false
}
type EqualityKind uint8
const (
LooseEquality EqualityKind = iota
StrictEquality
)
// Returns "equal, ok". If "ok" is false, then nothing is known about the two
// values. If "ok" is true, the equality or inequality of the two values is
// stored in "equal".
func CheckEqualityIfNoSideEffects(left E, right E, kind EqualityKind) (equal bool, ok bool) {
if r, ok := right.(*EInlinedEnum); ok {
return CheckEqualityIfNoSideEffects(left, r.Value.Data, kind)
}
switch l := left.(type) {
case *EInlinedEnum:
return CheckEqualityIfNoSideEffects(l.Value.Data, right, kind)
case *ENull:
switch right.(type) {
case *ENull:
// "null === null" is true
return true, true
case *EUndefined:
// "null == undefined" is true
// "null === undefined" is false
return kind == LooseEquality, true
default:
if IsPrimitiveLiteral(right) {
// "null == (not null or undefined)" is false
return false, true
}
}
case *EUndefined:
switch right.(type) {
case *EUndefined:
// "undefined === undefined" is true
return true, true
case *ENull:
// "undefined == null" is true
// "undefined === null" is false
return kind == LooseEquality, true
default:
if IsPrimitiveLiteral(right) {
// "undefined == (not null or undefined)" is false
return false, true
}
}
case *EBoolean:
switch r := right.(type) {
case *EBoolean:
// "false === false" is true
// "false === true" is false
return l.Value == r.Value, true
case *ENumber:
if kind == LooseEquality {
if l.Value {
// "true == 1" is true
return r.Value == 1, true
} else {
// "false == 0" is true
return r.Value == 0, true
}
} else {
// "true === 1" is false
// "false === 0" is false
return false, true
}
case *ENull, *EUndefined:
// "(not null or undefined) == undefined" is false
return false, true
default:
if kind == StrictEquality && IsPrimitiveLiteral(right) {
// "boolean === (not boolean)" is false
return false, true
}
}
case *ENumber:
switch r := right.(type) {
case *ENumber:
// "0 === 0" is true
// "0 === 1" is false
return l.Value == r.Value, true
case *EBoolean:
if kind == LooseEquality {
if r.Value {
// "1 == true" is true
return l.Value == 1, true
} else {
// "0 == false" is true
return l.Value == 0, true
}
} else {
// "1 === true" is false
// "0 === false" is false
return false, true
}
case *ENull, *EUndefined:
// "(not null or undefined) == undefined" is false
return false, true
default:
if kind == StrictEquality && IsPrimitiveLiteral(right) {
// "number === (not number)" is false
return false, true
}
}
case *EBigInt:
switch r := right.(type) {
case *EBigInt:
// "0n === 0n" is true
// "0n === 1n" is false
return CheckEqualityBigInt(l.Value, r.Value)
case *ENull, *EUndefined:
// "(not null or undefined) == undefined" is false
return false, true
default:
if kind == StrictEquality && IsPrimitiveLiteral(right) {
// "bigint === (not bigint)" is false
return false, true
}
}
case *EString:
switch r := right.(type) {
case *EString:
// "'a' === 'a'" is true
// "'a' === 'b'" is false
return helpers.UTF16EqualsUTF16(l.Value, r.Value), true
case *ENull, *EUndefined:
// "(not null or undefined) == undefined" is false
return false, true
default:
if kind == StrictEquality && IsPrimitiveLiteral(right) {
// "string === (not string)" is false
return false, true
}
}
}
return false, false
}
func ValuesLookTheSame(left E, right E) bool {
if b, ok := right.(*EInlinedEnum); ok {
return ValuesLookTheSame(left, b.Value.Data)
}
switch a := left.(type) {
case *EInlinedEnum:
return ValuesLookTheSame(a.Value.Data, right)
case *EIdentifier:
if b, ok := right.(*EIdentifier); ok && a.Ref == b.Ref {
return true
}
case *EDot:
if b, ok := right.(*EDot); ok && a.HasSameFlagsAs(b) &&
a.Name == b.Name && ValuesLookTheSame(a.Target.Data, b.Target.Data) {
return true
}
case *EIndex:
if b, ok := right.(*EIndex); ok && a.HasSameFlagsAs(b) &&
ValuesLookTheSame(a.Target.Data, b.Target.Data) && ValuesLookTheSame(a.Index.Data, b.Index.Data) {
return true
}
case *EIf:
if b, ok := right.(*EIf); ok && ValuesLookTheSame(a.Test.Data, b.Test.Data) &&
ValuesLookTheSame(a.Yes.Data, b.Yes.Data) && ValuesLookTheSame(a.No.Data, b.No.Data) {
return true
}
case *EUnary:
if b, ok := right.(*EUnary); ok && a.Op == b.Op && ValuesLookTheSame(a.Value.Data, b.Value.Data) {
return true
}
case *EBinary:
if b, ok := right.(*EBinary); ok && a.Op == b.Op && ValuesLookTheSame(a.Left.Data, b.Left.Data) &&
ValuesLookTheSame(a.Right.Data, b.Right.Data) {
return true
}
case *ECall:
if b, ok := right.(*ECall); ok && a.HasSameFlagsAs(b) &&
len(a.Args) == len(b.Args) && ValuesLookTheSame(a.Target.Data, b.Target.Data) {
for i := range a.Args {
if !ValuesLookTheSame(a.Args[i].Data, b.Args[i].Data) {
return false
}
}
return true
}
// Special-case to distinguish between negative an non-negative zero when mangling
// "a ? -0 : 0" => "a ? -0 : 0"
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness
case *ENumber:
b, ok := right.(*ENumber)
if ok && a.Value == 0 && b.Value == 0 && math.Signbit(a.Value) != math.Signbit(b.Value) {
return false
}
}
equal, ok := CheckEqualityIfNoSideEffects(left, right, StrictEquality)
return ok && equal
}
func TryToInsertOptionalChain(test Expr, expr Expr) bool {
switch e := expr.Data.(type) {
case *EDot:
if ValuesLookTheSame(test.Data, e.Target.Data) {
e.OptionalChain = OptionalChainStart
return true
}
if TryToInsertOptionalChain(test, e.Target) {
if e.OptionalChain == OptionalChainNone {
e.OptionalChain = OptionalChainContinue
}
return true
}
case *EIndex:
if ValuesLookTheSame(test.Data, e.Target.Data) {
e.OptionalChain = OptionalChainStart
return true
}
if TryToInsertOptionalChain(test, e.Target) {
if e.OptionalChain == OptionalChainNone {
e.OptionalChain = OptionalChainContinue
}
return true
}
case *ECall:
if ValuesLookTheSame(test.Data, e.Target.Data) {
e.OptionalChain = OptionalChainStart
return true
}
if TryToInsertOptionalChain(test, e.Target) {
if e.OptionalChain == OptionalChainNone {
e.OptionalChain = OptionalChainContinue
}
return true
}
}
return false
}
func joinStrings(a []uint16, b []uint16) []uint16 {
data := make([]uint16, len(a)+len(b))
copy(data[:len(a)], a)
copy(data[len(a):], b)
return data
}
// String concatenation with numbers is required by the TypeScript compiler for
// "constant expression" handling in enums. However, we don't want to introduce
// correctness bugs by accidentally stringifying a number differently than how
// a real JavaScript VM would do it. So we are conservative and we only do this
// when we know it'll be the same result.
func TryToStringOnNumberSafely(n float64, radix int) (string, bool) {
if i := int32(n); float64(i) == n {
return strconv.FormatInt(int64(i), radix), true
}
if math.IsNaN(n) {
return "NaN", true
}
if math.IsInf(n, 1) {
return "Infinity", true
}
if math.IsInf(n, -1) {
return "-Infinity", true
}
return "", false
}
// Note: We don't know if this is string addition yet at this point
func foldAdditionPreProcess(expr Expr) Expr {
switch e := expr.Data.(type) {
case *EInlinedEnum:
// "See through" inline enum constants
expr = e.Value
case *EArray:
// "[] + x" => "'' + x"
// "[1,2] + x" => "'1,2' + x"
items := make([]string, 0, len(e.Items))
for _, item := range e.Items {
switch item.Data.(type) {
case *EUndefined, *ENull:
items = append(items, "")
continue
}
if str, ok := ToStringWithoutSideEffects(item.Data); ok {
item.Data = &EString{Value: helpers.StringToUTF16(str)}
}
str, ok := item.Data.(*EString)
if !ok {
break
}
items = append(items, helpers.UTF16ToString(str.Value))
}
if len(items) == len(e.Items) {
expr.Data = &EString{Value: helpers.StringToUTF16(strings.Join(items, ","))}
}
case *EObject:
// "{} + x" => "'[object Object]' + x"
if len(e.Properties) == 0 {
expr.Data = &EString{Value: helpers.StringToUTF16("[object Object]")}
}
}
return expr
}
type StringAdditionKind uint8
const (
StringAdditionNormal StringAdditionKind = iota
StringAdditionWithNestedLeft
)
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func FoldStringAddition(left Expr, right Expr, kind StringAdditionKind) Expr {
left = foldAdditionPreProcess(left)
right = foldAdditionPreProcess(right)
// Transforming the left operand into a string is not safe if it comes from
// a nested AST node. The following transforms are invalid:
//
// "0 + 1 + 'x'" => "0 + '1x'"
// "0 + 1 + `${x}`" => "0 + `1${x}`"
//
if kind != StringAdditionWithNestedLeft {
switch right.Data.(type) {
case *EString, *ETemplate:
if str, ok := ToStringWithoutSideEffects(left.Data); ok {
left.Data = &EString{Value: helpers.StringToUTF16(str)}
}
}
}
switch l := left.Data.(type) {
case *EString:
// "'x' + 0" => "'x' + '0'"
if str, ok := ToStringWithoutSideEffects(right.Data); ok {
right.Data = &EString{Value: helpers.StringToUTF16(str)}
}
switch r := right.Data.(type) {
case *EString:
// "'x' + 'y'" => "'xy'"
return Expr{Loc: left.Loc, Data: &EString{
Value: joinStrings(l.Value, r.Value),
PreferTemplate: l.PreferTemplate || r.PreferTemplate,
}}
case *ETemplate:
if r.TagOrNil.Data == nil {
// "'x' + `y${z}`" => "`xy${z}`"
return Expr{Loc: left.Loc, Data: &ETemplate{
HeadLoc: left.Loc,
HeadCooked: joinStrings(l.Value, r.HeadCooked),
Parts: r.Parts,
}}
}
}
// "'' + typeof x" => "typeof x"
if len(l.Value) == 0 && KnownPrimitiveType(right.Data) == PrimitiveString {
return right
}
case *ETemplate:
if l.TagOrNil.Data == nil {
// "`${x}` + 0" => "`${x}` + '0'"
if str, ok := ToStringWithoutSideEffects(right.Data); ok {
right.Data = &EString{Value: helpers.StringToUTF16(str)}
}
switch r := right.Data.(type) {
case *EString:
// "`${x}y` + 'z'" => "`${x}yz`"
n := len(l.Parts)
head := l.HeadCooked
parts := make([]TemplatePart, n)
if n == 0 {
head = joinStrings(head, r.Value)
} else {
copy(parts, l.Parts)
parts[n-1].TailCooked = joinStrings(parts[n-1].TailCooked, r.Value)
}
return Expr{Loc: left.Loc, Data: &ETemplate{
HeadLoc: l.HeadLoc,
HeadCooked: head,
Parts: parts,
}}
case *ETemplate:
if r.TagOrNil.Data == nil {
// "`${a}b` + `x${y}`" => "`${a}bx${y}`"
n := len(l.Parts)
head := l.HeadCooked
parts := make([]TemplatePart, n+len(r.Parts))
copy(parts[n:], r.Parts)
if n == 0 {
head = joinStrings(head, r.HeadCooked)
} else {
copy(parts[:n], l.Parts)
parts[n-1].TailCooked = joinStrings(parts[n-1].TailCooked, r.HeadCooked)
}
return Expr{Loc: left.Loc, Data: &ETemplate{
HeadLoc: l.HeadLoc,
HeadCooked: head,
Parts: parts,
}}
}
}
}
}
// "typeof x + ''" => "typeof x"
if r, ok := right.Data.(*EString); ok && len(r.Value) == 0 && KnownPrimitiveType(left.Data) == PrimitiveString {
return left
}
return Expr{}
}
// "`a${'b'}c`" => "`abc`"
//
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func InlinePrimitivesIntoTemplate(loc logger.Loc, e *ETemplate) Expr {
// Can't inline strings if there's a custom template tag
if e.TagOrNil.Data != nil {
return Expr{Loc: loc, Data: e}
}
headCooked := e.HeadCooked
parts := make([]TemplatePart, 0, len(e.Parts))
for _, part := range e.Parts {
if value, ok := part.Value.Data.(*EInlinedEnum); ok {
part.Value = value.Value
}
if str, ok := ToStringWithoutSideEffects(part.Value.Data); ok {
part.Value.Data = &EString{Value: helpers.StringToUTF16(str)}
}
if str, ok := part.Value.Data.(*EString); ok {
if len(parts) == 0 {
headCooked = append(append(headCooked, str.Value...), part.TailCooked...)
} else {
prevPart := &parts[len(parts)-1]
prevPart.TailCooked = append(append(prevPart.TailCooked, str.Value...), part.TailCooked...)
}
} else {
parts = append(parts, part)
}
}
// Become a plain string if there are no substitutions
if len(parts) == 0 {
return Expr{Loc: loc, Data: &EString{
Value: headCooked,
PreferTemplate: true,
}}
}
return Expr{Loc: loc, Data: &ETemplate{
HeadLoc: e.HeadLoc,
HeadCooked: headCooked,
Parts: parts,
}}
}
type SideEffects uint8
const (
CouldHaveSideEffects SideEffects = iota
NoSideEffects
)
func ToNullOrUndefinedWithSideEffects(data E) (isNullOrUndefined bool, sideEffects SideEffects, ok bool) {
switch e := data.(type) {
case *EAnnotation:
isNullOrUndefined, sideEffects, ok = ToNullOrUndefinedWithSideEffects(e.Value.Data)
if e.Flags.Has(CanBeRemovedIfUnusedFlag) {
sideEffects = NoSideEffects
}
return
case *EInlinedEnum:
return ToNullOrUndefinedWithSideEffects(e.Value.Data)
// Never null or undefined
case *EBoolean, *ENumber, *EString, *ERegExp,
*EFunction, *EArrow, *EBigInt:
return false, NoSideEffects, true
// Never null or undefined
case *EObject, *EArray, *EClass:
return false, CouldHaveSideEffects, true
// Always null or undefined
case *ENull, *EUndefined:
return true, NoSideEffects, true
case *EUnary:
switch e.Op {
case
// Always number or bigint
UnOpPos, UnOpNeg, UnOpCpl,
UnOpPreDec, UnOpPreInc, UnOpPostDec, UnOpPostInc,
// Always boolean
UnOpNot, UnOpDelete:
return false, CouldHaveSideEffects, true
// Always boolean
case UnOpTypeof:
if e.WasOriginallyTypeofIdentifier {
// Expressions such as "typeof x" never have any side effects
return false, NoSideEffects, true
}
return false, CouldHaveSideEffects, true
// Always undefined
case UnOpVoid:
return true, CouldHaveSideEffects, true
}
case *EBinary:
switch e.Op {
case
// Always string or number or bigint
BinOpAdd, BinOpAddAssign,
// Always number or bigint
BinOpSub, BinOpMul, BinOpDiv, BinOpRem, BinOpPow,
BinOpSubAssign, BinOpMulAssign, BinOpDivAssign, BinOpRemAssign, BinOpPowAssign,
BinOpShl, BinOpShr, BinOpUShr,
BinOpShlAssign, BinOpShrAssign, BinOpUShrAssign,
BinOpBitwiseOr, BinOpBitwiseAnd, BinOpBitwiseXor,
BinOpBitwiseOrAssign, BinOpBitwiseAndAssign, BinOpBitwiseXorAssign,
// Always boolean
BinOpLt, BinOpLe, BinOpGt, BinOpGe, BinOpIn, BinOpInstanceof,
BinOpLooseEq, BinOpLooseNe, BinOpStrictEq, BinOpStrictNe:
return false, CouldHaveSideEffects, true
case BinOpComma:
if isNullOrUndefined, _, ok := ToNullOrUndefinedWithSideEffects(e.Right.Data); ok {
return isNullOrUndefined, CouldHaveSideEffects, true
}
}
}
return false, NoSideEffects, false
}
func ToBooleanWithSideEffects(data E) (boolean bool, sideEffects SideEffects, ok bool) {
switch e := data.(type) {
case *EAnnotation:
boolean, sideEffects, ok = ToBooleanWithSideEffects(e.Value.Data)
if e.Flags.Has(CanBeRemovedIfUnusedFlag) {
sideEffects = NoSideEffects
}
return
case *EInlinedEnum:
return ToBooleanWithSideEffects(e.Value.Data)
case *ENull, *EUndefined:
return false, NoSideEffects, true
case *EBoolean:
return e.Value, NoSideEffects, true
case *ENumber:
return e.Value != 0 && !math.IsNaN(e.Value), NoSideEffects, true
case *EBigInt:
equal, ok := CheckEqualityBigInt(e.Value, "0")
return !equal, NoSideEffects, ok
case *EString:
return len(e.Value) > 0, NoSideEffects, true
case *EFunction, *EArrow, *ERegExp:
return true, NoSideEffects, true
case *EObject, *EArray, *EClass:
return true, CouldHaveSideEffects, true
case *EUnary:
switch e.Op {
case UnOpVoid:
return false, CouldHaveSideEffects, true
case UnOpTypeof:
// Never an empty string
if e.WasOriginallyTypeofIdentifier {
// Expressions such as "typeof x" never have any side effects
return true, NoSideEffects, true
}
return true, CouldHaveSideEffects, true
case UnOpNot:
if boolean, SideEffects, ok := ToBooleanWithSideEffects(e.Value.Data); ok {
return !boolean, SideEffects, true
}
}
case *EBinary:
switch e.Op {
case BinOpLogicalOr:
// "anything || truthy" is truthy
if boolean, _, ok := ToBooleanWithSideEffects(e.Right.Data); ok && boolean {
return true, CouldHaveSideEffects, true
}
case BinOpLogicalAnd:
// "anything && falsy" is falsy
if boolean, _, ok := ToBooleanWithSideEffects(e.Right.Data); ok && !boolean {
return false, CouldHaveSideEffects, true
}
case BinOpComma:
// "anything, truthy/falsy" is truthy/falsy
if boolean, _, ok := ToBooleanWithSideEffects(e.Right.Data); ok {
return boolean, CouldHaveSideEffects, true
}
}
}
return false, CouldHaveSideEffects, false
}
// Simplify syntax when we know it's used inside a boolean context
//
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func (ctx HelperContext) SimplifyBooleanExpr(expr Expr) Expr {
switch e := expr.Data.(type) {
case *EUnary:
if e.Op == UnOpNot {
// "!!a" => "a"
if e2, ok2 := e.Value.Data.(*EUnary); ok2 && e2.Op == UnOpNot {
return ctx.SimplifyBooleanExpr(e2.Value)
}
// "!!!a" => "!a"
return Expr{Loc: expr.Loc, Data: &EUnary{Op: UnOpNot, Value: ctx.SimplifyBooleanExpr(e.Value)}}
}
case *EBinary:
left := e.Left
right := e.Right
switch e.Op {
case BinOpStrictEq, BinOpStrictNe, BinOpLooseEq, BinOpLooseNe:
if r, ok := extractNumericValue(right.Data); ok && r == 0 && isInt32OrUint32(left.Data) {
// If the left is guaranteed to be an integer (e.g. not NaN,
// Infinity, or a non-numeric value) then a test against zero
// in a boolean context is unnecessary because the value is
// only truthy if it's not zero.
if e.Op == BinOpStrictNe || e.Op == BinOpLooseNe {
// "if ((a >>> b) !== 0)" => "if (a >>> b)"
return left
} else {
// "if ((a >>> b) === 0)" => "if (!(a >>> b))"
return Not(left)
}
}
case BinOpLogicalAnd:
// "if (!!a && !!b)" => "if (a && b)"
left = ctx.SimplifyBooleanExpr(left)
right = ctx.SimplifyBooleanExpr(right)
if boolean, SideEffects, ok := ToBooleanWithSideEffects(right.Data); ok && boolean && SideEffects == NoSideEffects {
// "if (anything && truthyNoSideEffects)" => "if (anything)"
return left
}
case BinOpLogicalOr:
// "if (!!a || !!b)" => "if (a || b)"
left = ctx.SimplifyBooleanExpr(left)
right = ctx.SimplifyBooleanExpr(right)
if boolean, SideEffects, ok := ToBooleanWithSideEffects(right.Data); ok && !boolean && SideEffects == NoSideEffects {
// "if (anything || falsyNoSideEffects)" => "if (anything)"
return left
}
}
if left != e.Left || right != e.Right {
return Expr{Loc: expr.Loc, Data: &EBinary{Op: e.Op, Left: left, Right: right}}
}
case *EIf:
// "if (a ? !!b : !!c)" => "if (a ? b : c)"
yes := ctx.SimplifyBooleanExpr(e.Yes)
no := ctx.SimplifyBooleanExpr(e.No)
if boolean, SideEffects, ok := ToBooleanWithSideEffects(yes.Data); ok && SideEffects == NoSideEffects {
if boolean {
// "if (anything1 ? truthyNoSideEffects : anything2)" => "if (anything1 || anything2)"
return JoinWithLeftAssociativeOp(BinOpLogicalOr, e.Test, no)
} else {
// "if (anything1 ? falsyNoSideEffects : anything2)" => "if (!anything1 || anything2)"
return JoinWithLeftAssociativeOp(BinOpLogicalAnd, Not(e.Test), no)
}
}
if boolean, SideEffects, ok := ToBooleanWithSideEffects(no.Data); ok && SideEffects == NoSideEffects {
if boolean {
// "if (anything1 ? anything2 : truthyNoSideEffects)" => "if (!anything1 || anything2)"
return JoinWithLeftAssociativeOp(BinOpLogicalOr, Not(e.Test), yes)
} else {
// "if (anything1 ? anything2 : falsyNoSideEffects)" => "if (anything1 && anything2)"
return JoinWithLeftAssociativeOp(BinOpLogicalAnd, e.Test, yes)
}
}
if yes != e.Yes || no != e.No {
return Expr{Loc: expr.Loc, Data: &EIf{Test: e.Test, Yes: yes, No: no}}
}
default:
// "!![]" => "true"
if boolean, sideEffects, ok := ToBooleanWithSideEffects(expr.Data); ok && (sideEffects == NoSideEffects || ctx.ExprCanBeRemovedIfUnused(expr)) {
return Expr{Loc: expr.Loc, Data: &EBoolean{Value: boolean}}
}
}
return expr
}
type StmtsCanBeRemovedIfUnusedFlags uint8
const (
KeepExportClauses StmtsCanBeRemovedIfUnusedFlags = 1 << iota
ReturnCanBeRemovedIfUnused
)
func (ctx HelperContext) StmtsCanBeRemovedIfUnused(stmts []Stmt, flags StmtsCanBeRemovedIfUnusedFlags) bool {
for _, stmt := range stmts {
switch s := stmt.Data.(type) {
case *SFunction, *SEmpty:
// These never have side effects
case *SImport:
// Let these be removed if they are unused. Note that we also need to
// check if the imported file is marked as "sideEffects: false" before we
// can remove a SImport statement. Otherwise the import must be kept for
// its side effects.
case *SClass:
if !ctx.ClassCanBeRemovedIfUnused(s.Class) {
return false
}
case *SReturn:
if (flags&ReturnCanBeRemovedIfUnused) == 0 || (s.ValueOrNil.Data != nil && !ctx.ExprCanBeRemovedIfUnused(s.ValueOrNil)) {
return false
}
case *SExpr:
if !ctx.ExprCanBeRemovedIfUnused(s.Value) {
if s.IsFromClassOrFnThatCanBeRemovedIfUnused {
// This statement was automatically generated when lowering a class
// or function that we were able to analyze as having no side effects
// before lowering. So we consider it to be removable. The assumption
// here is that we are seeing at least all of the statements from the
// class lowering operation all at once (although we may possibly be
// seeing even more statements than that). Since we're making a binary
// all-or-nothing decision about the side effects of these statements,
// we can safely consider these to be side-effect free because we
// aren't in danger of partially dropping some of the class setup code.
} else {
return false
}
}
case *SLocal:
// "await" is a side effect because it affects code timing
if s.Kind == LocalAwaitUsing {
return false
}
for _, decl := range s.Decls {
// Check that the bindings are side-effect free
switch binding := decl.Binding.Data.(type) {
case *BIdentifier:
// An identifier binding has no side effects
case *BArray:
// Destructuring the initializer has no side effects if the
// initializer is an array, since we assume the iterator is then
// the built-in side-effect free array iterator.
if _, ok := decl.ValueOrNil.Data.(*EArray); ok {
for _, item := range binding.Items {
if item.DefaultValueOrNil.Data != nil && !ctx.ExprCanBeRemovedIfUnused(item.DefaultValueOrNil) {
return false
}
switch item.Binding.Data.(type) {
case *BIdentifier, *BMissing:
// Right now we only handle an array pattern with identifier
// bindings or with empty holes (i.e. "missing" elements)
default:
return false
}
}
break
}
return false
default:
// Consider anything else to potentially have side effects
return false
}
// Check that the initializer is side-effect free
if decl.ValueOrNil.Data != nil {
if !ctx.ExprCanBeRemovedIfUnused(decl.ValueOrNil) {
return false
}
// "using" declarations are only side-effect free if they are initialized to null or undefined
if s.Kind.IsUsing() {
if t := KnownPrimitiveType(decl.ValueOrNil.Data); t != PrimitiveNull && t != PrimitiveUndefined {
return false
}
}
}
}
case *STry:
if !ctx.StmtsCanBeRemovedIfUnused(s.Block.Stmts, 0) || (s.Finally != nil && !ctx.StmtsCanBeRemovedIfUnused(s.Finally.Block.Stmts, 0)) {
return false
}
case *SExportFrom:
// Exports are tracked separately, so this isn't necessary
case *SExportClause:
if (flags & KeepExportClauses) != 0 {
return false
}
case *SExportDefault:
switch s2 := s.Value.Data.(type) {
case *SExpr:
if !ctx.ExprCanBeRemovedIfUnused(s2.Value) {
return false
}
case *SFunction:
// These never have side effects
case *SClass:
if !ctx.ClassCanBeRemovedIfUnused(s2.Class) {
return false
}
default:
panic("Internal error")
}
default:
// Assume that all statements not explicitly special-cased here have side
// effects, and cannot be removed even if unused
return false
}
}
return true
}
func (ctx HelperContext) ClassCanBeRemovedIfUnused(class Class) bool {
if len(class.Decorators) > 0 {
return false
}
// Note: This check is incorrect. Extending a non-constructible object can
// throw an error, which is a side effect:
//
// async function x() {}
// class y extends x {}
//
// But refusing to tree-shake every class with a base class is not a useful
// thing for a bundler to do. So we pretend that this edge case doesn't
// exist. At the time of writing, both Rollup and Terser don't consider this
// to be a side effect either.
if class.ExtendsOrNil.Data != nil && !ctx.ExprCanBeRemovedIfUnused(class.ExtendsOrNil) {
return false
}
for _, property := range class.Properties {
if property.Kind == PropertyClassStaticBlock {
if !ctx.StmtsCanBeRemovedIfUnused(property.ClassStaticBlock.Block.Stmts, 0) {
return false
}
continue
}
if len(property.Decorators) > 0 {
return false
}
if property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data) && !IsSymbolInstance(property.Key.Data) {
return false
}
if property.Kind.IsMethodDefinition() {
if fn, ok := property.ValueOrNil.Data.(*EFunction); ok {
for _, arg := range fn.Fn.Args {
if len(arg.Decorators) > 0 {
return false
}
}
}
}
if property.Flags.Has(PropertyIsStatic) {
if property.ValueOrNil.Data != nil && !ctx.ExprCanBeRemovedIfUnused(property.ValueOrNil) {
return false
}
if property.InitializerOrNil.Data != nil && !ctx.ExprCanBeRemovedIfUnused(property.InitializerOrNil) {
return false
}
// Legacy TypeScript static class fields are considered to have side
// effects because they use assign semantics, not define semantics, and
// that can trigger getters. For example:
//
// class Foo {
// static set foo(x) { importantSideEffect(x) }
// }
// class Bar extends Foo {
// foo = 1
// }
//
// This happens in TypeScript when "useDefineForClassFields" is disabled
// because TypeScript (and esbuild) transforms the above class into this:
//
// class Foo {
// static set foo(x) { importantSideEffect(x); }
// }
// class Bar extends Foo {
// }
// Bar.foo = 1;
//
// Note that it's not possible to analyze the base class to determine that
// these assignments are side-effect free. For example:
//
// // Some code that already ran before your code
// Object.defineProperty(Object.prototype, 'foo', {
// set(x) { imporantSideEffect(x) }
// })
//
// // Your code
// class Foo {
// static foo = 1
// }
//
if property.Kind == PropertyField && !class.UseDefineForClassFields {
return false
}
}
}
return true
}
func (ctx HelperContext) ExprCanBeRemovedIfUnused(expr Expr) bool {
switch e := expr.Data.(type) {
case *EAnnotation:
return e.Flags.Has(CanBeRemovedIfUnusedFlag)
case *EInlinedEnum:
return ctx.ExprCanBeRemovedIfUnused(e.Value)
case *ENull, *EUndefined, *EMissing, *EBoolean, *ENumber, *EBigInt,
*EString, *EThis, *ERegExp, *EFunction, *EArrow, *EImportMeta:
return true
case *EDot:
return e.CanBeRemovedIfUnused
case *EClass:
return ctx.ClassCanBeRemovedIfUnused(e.Class)
case *EIdentifier:
if e.MustKeepDueToWithStmt {
return false
}
// Unbound identifiers cannot be removed because they can have side effects.
// One possible side effect is throwing a ReferenceError if they don't exist.
// Another one is a getter with side effects on the global object:
//
// Object.defineProperty(globalThis, 'x', {
// get() {
// sideEffect();
// },
// });
//
// Be very careful about this possibility. It's tempting to treat all
// identifier expressions as not having side effects but that's wrong. We
// must make sure they have been declared by the code we are currently
// compiling before we can tell that they have no side effects.
//
// Note that we currently ignore ReferenceErrors due to TDZ access. This is
// incorrect but proper TDZ analysis is very complicated and would have to
// be very conservative, which would inhibit a lot of optimizations of code
// inside closures. This may need to be revisited if it proves problematic.
if e.CanBeRemovedIfUnused || !ctx.isUnbound(e.Ref) {
return true
}
case *EImportIdentifier:
// References to an ES6 import item are always side-effect free in an
// ECMAScript environment.
//
// They could technically have side effects if the imported module is a
// CommonJS module and the import item was translated to a property access
// (which esbuild's bundler does) and the property has a getter with side
// effects.
//
// But this is very unlikely and respecting this edge case would mean
// disabling tree shaking of all code that references an export from a
// CommonJS module. It would also likely violate the expectations of some
// developers because the code *looks* like it should be able to be tree
// shaken.
//
// So we deliberately ignore this edge case and always treat import item
// references as being side-effect free.
return true
case *EIf:
return ctx.ExprCanBeRemovedIfUnused(e.Test) &&
((ctx.isSideEffectFreeUnboundIdentifierRef(e.Yes, e.Test, true) || ctx.ExprCanBeRemovedIfUnused(e.Yes)) &&
(ctx.isSideEffectFreeUnboundIdentifierRef(e.No, e.Test, false) || ctx.ExprCanBeRemovedIfUnused(e.No)))
case *EArray:
for _, item := range e.Items {
if spread, ok := item.Data.(*ESpread); ok {
if _, ok := spread.Value.Data.(*EArray); ok {
// Spread of an inline array such as "[...[x]]" is side-effect free
item = spread.Value
}
}
if !ctx.ExprCanBeRemovedIfUnused(item) {
return false
}
}
return true
case *EObject:
for _, property := range e.Properties {
// The key must still be evaluated if it's computed or a spread
if property.Kind == PropertySpread {
return false
}
if property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data) && !IsSymbolInstance(property.Key.Data) {
return false
}
if property.ValueOrNil.Data != nil && !ctx.ExprCanBeRemovedIfUnused(property.ValueOrNil) {
return false
}
}
return true
case *ECall:
canCallBeRemoved := e.CanBeUnwrappedIfUnused
// A call that has been marked "__PURE__" can be removed if all arguments
// can be removed. The annotation causes us to ignore the target.
if canCallBeRemoved {
for _, arg := range e.Args {
if !ctx.ExprCanBeRemovedIfUnused(arg) {
return false
}
}
return true
}
case *ENew:
// A constructor call that has been marked "__PURE__" can be removed if all
// arguments can be removed. The annotation causes us to ignore the target.
if e.CanBeUnwrappedIfUnused {
for _, arg := range e.Args {
if !ctx.ExprCanBeRemovedIfUnused(arg) {
return false
}
}
return true
}
case *EUnary:
switch e.Op {
// These operators must not have any type conversions that can execute code
// such as "toString" or "valueOf". They must also never throw any exceptions.
case UnOpVoid, UnOpNot:
return ctx.ExprCanBeRemovedIfUnused(e.Value)
case UnOpNeg:
if _, ok := e.Value.Data.(*EBigInt); ok {
// Consider negated bigints to have no side effects
return true
}
// The "typeof" operator doesn't do any type conversions so it can be removed
// if the result is unused and the operand has no side effects. However, it
// has a special case where if the operand is an identifier expression such
// as "typeof x" and "x" doesn't exist, no reference error is thrown so the
// operation has no side effects.
case UnOpTypeof:
if _, ok := e.Value.Data.(*EIdentifier); ok && e.WasOriginallyTypeofIdentifier {
// Expressions such as "typeof x" never have any side effects
return true
}
return ctx.ExprCanBeRemovedIfUnused(e.Value)
}
case *EBinary:
switch e.Op {
// These operators must not have any type conversions that can execute code
// such as "toString" or "valueOf". They must also never throw any exceptions.
case BinOpStrictEq, BinOpStrictNe, BinOpComma, BinOpNullishCoalescing:
return ctx.ExprCanBeRemovedIfUnused(e.Left) && ctx.ExprCanBeRemovedIfUnused(e.Right)
// Special-case "||" to make sure "typeof x === 'undefined' || x" can be removed
case BinOpLogicalOr:
return ctx.ExprCanBeRemovedIfUnused(e.Left) &&
(ctx.isSideEffectFreeUnboundIdentifierRef(e.Right, e.Left, false) || ctx.ExprCanBeRemovedIfUnused(e.Right))
// Special-case "&&" to make sure "typeof x !== 'undefined' && x" can be removed
case BinOpLogicalAnd:
return ctx.ExprCanBeRemovedIfUnused(e.Left) &&
(ctx.isSideEffectFreeUnboundIdentifierRef(e.Right, e.Left, true) || ctx.ExprCanBeRemovedIfUnused(e.Right))
// For "==" and "!=", pretend the operator was actually "===" or "!==". If
// we know that we can convert it to "==" or "!=", then we can consider the
// operator itself to have no side effects. This matters because our mangle
// logic will convert "typeof x === 'object'" into "typeof x == 'object'"
// and since "typeof x === 'object'" is considered to be side-effect free,
// we must also consider "typeof x == 'object'" to be side-effect free.
case BinOpLooseEq, BinOpLooseNe:
return CanChangeStrictToLoose(e.Left, e.Right) && ctx.ExprCanBeRemovedIfUnused(e.Left) && ctx.ExprCanBeRemovedIfUnused(e.Right)
// Special-case "<" and ">" with string, number, or bigint arguments
case BinOpLt, BinOpGt, BinOpLe, BinOpGe:
left := KnownPrimitiveType(e.Left.Data)
switch left {
case PrimitiveString, PrimitiveNumber, PrimitiveBigInt:
return KnownPrimitiveType(e.Right.Data) == left && ctx.ExprCanBeRemovedIfUnused(e.Left) && ctx.ExprCanBeRemovedIfUnused(e.Right)
}
}
case *ETemplate:
// A template can be removed if it has no tag and every value has no side
// effects and results in some kind of primitive, since all primitives
// have a "ToString" operation with no side effects.
if e.TagOrNil.Data == nil || e.CanBeUnwrappedIfUnused {
for _, part := range e.Parts {
if !ctx.ExprCanBeRemovedIfUnused(part.Value) || KnownPrimitiveType(part.Value.Data) == PrimitiveUnknown {
return false
}
}
return true
}
}
// Assume all other expression types have side effects and cannot be removed
return false
}
func (ctx HelperContext) isSideEffectFreeUnboundIdentifierRef(value Expr, guardCondition Expr, isYesBranch bool) bool {
if id, ok := value.Data.(*EIdentifier); ok && ctx.isUnbound(id.Ref) {
if binary, ok := guardCondition.Data.(*EBinary); ok {
switch binary.Op {
case BinOpStrictEq, BinOpStrictNe, BinOpLooseEq, BinOpLooseNe:
// Pattern match for "typeof x !== <string>"
typeof, string := binary.Left, binary.Right
if _, ok := typeof.Data.(*EString); ok {
typeof, string = string, typeof
}
if typeof, ok := typeof.Data.(*EUnary); ok && typeof.Op == UnOpTypeof && typeof.WasOriginallyTypeofIdentifier {
if text, ok := string.Data.(*EString); ok {
// In "typeof x !== 'undefined' ? x : null", the reference to "x" is side-effect free
// In "typeof x === 'object' ? x : null", the reference to "x" is side-effect free
if (helpers.UTF16EqualsString(text.Value, "undefined") == isYesBranch) ==
(binary.Op == BinOpStrictNe || binary.Op == BinOpLooseNe) {
if id2, ok := typeof.Value.Data.(*EIdentifier); ok && id2.Ref == id.Ref {
return true
}
}
}
}
case BinOpLt, BinOpGt, BinOpLe, BinOpGe:
// Pattern match for "typeof x < <string>"
typeof, string := binary.Left, binary.Right
if _, ok := typeof.Data.(*EString); ok {
typeof, string = string, typeof
isYesBranch = !isYesBranch
}
if typeof, ok := typeof.Data.(*EUnary); ok && typeof.Op == UnOpTypeof && typeof.WasOriginallyTypeofIdentifier {
if text, ok := string.Data.(*EString); ok && helpers.UTF16EqualsString(text.Value, "u") {
// In "typeof x < 'u' ? x : null", the reference to "x" is side-effect free
// In "typeof x > 'u' ? x : null", the reference to "x" is side-effect free
if isYesBranch == (binary.Op == BinOpLt || binary.Op == BinOpLe) {
if id2, ok := typeof.Value.Data.(*EIdentifier); ok && id2.Ref == id.Ref {
return true
}
}
}
}
}
}
}
return false
}
func StringToEquivalentNumberValue(value []uint16) (float64, bool) {
if len(value) > 0 {
var intValue int32
isNegative := false
start := 0
if value[0] == '-' && len(value) > 1 {
isNegative = true
start++
}
for _, c := range value[start:] {
if c < '0' || c > '9' {
return 0, false
}
intValue = intValue*10 + int32(c) - '0'
}
if isNegative {
intValue = -intValue
}
if helpers.UTF16EqualsString(value, strconv.FormatInt(int64(intValue), 10)) {
return float64(intValue), true
}
}
return 0, false
}
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func InlineSpreadsOfArrayLiterals(values []Expr) (results []Expr) {
for _, value := range values {
if spread, ok := value.Data.(*ESpread); ok {
if array, ok := spread.Value.Data.(*EArray); ok {
for _, item := range array.Items {
if _, ok := item.Data.(*EMissing); ok {
results = append(results, Expr{Loc: item.Loc, Data: EUndefinedShared})
} else {
results = append(results, item)
}
}
continue
}
}
results = append(results, value)
}
return
}
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func MangleObjectSpread(properties []Property) []Property {
var result []Property
for _, property := range properties {
if property.Kind == PropertySpread {
switch v := property.ValueOrNil.Data.(type) {
case *EBoolean, *ENull, *EUndefined, *ENumber,
*EBigInt, *ERegExp, *EFunction, *EArrow:
// This value is ignored because it doesn't have any of its own properties
continue
case *EObject:
for i, p := range v.Properties {
// Getters are evaluated at iteration time. The property
// descriptor is not inlined into the caller. Since we are not
// evaluating code at compile time, just bail if we hit one
// and preserve the spread with the remaining properties.
if p.Kind == PropertyGetter || p.Kind == PropertySetter {
// Don't mutate the original AST
clone := *v
clone.Properties = v.Properties[i:]
property.ValueOrNil.Data = &clone
result = append(result, property)
break
}
// Also bail if we hit a verbatim "__proto__" key. This will
// actually set the prototype of the object being spread so
// inlining it is not correct.
if p.Kind == PropertyField && !p.Flags.Has(PropertyIsComputed) {
if str, ok := p.Key.Data.(*EString); ok && helpers.UTF16EqualsString(str.Value, "__proto__") {
// Don't mutate the original AST
clone := *v
clone.Properties = v.Properties[i:]
property.ValueOrNil.Data = &clone
result = append(result, property)
break
}
}
result = append(result, p)
}
continue
}
}
result = append(result, property)
}
return result
}
// This function intentionally avoids mutating the input AST so it can be
// called after the AST has been frozen (i.e. after parsing ends).
func (ctx HelperContext) MangleIfExpr(loc logger.Loc, e *EIf, unsupportedFeatures compat.JSFeature) Expr {
test := e.Test
yes := e.Yes
no := e.No
// "(a, b) ? c : d" => "a, b ? c : d"
if comma, ok := test.Data.(*EBinary); ok && comma.Op == BinOpComma {
return JoinWithComma(comma.Left, ctx.MangleIfExpr(comma.Right.Loc, &EIf{
Test: comma.Right,
Yes: yes,
No: no,
}, unsupportedFeatures))
}
// "!a ? b : c" => "a ? c : b"
if not, ok := test.Data.(*EUnary); ok && not.Op == UnOpNot {
test = not.Value
yes, no = no, yes
}
if ValuesLookTheSame(yes.Data, no.Data) {
// "/* @__PURE__ */ a() ? b : b" => "b"
if ctx.ExprCanBeRemovedIfUnused(test) {
return yes
}
// "a ? b : b" => "a, b"
return JoinWithComma(test, yes)
}
// "a ? true : false" => "!!a"
// "a ? false : true" => "!a"
if y, ok := yes.Data.(*EBoolean); ok {
if n, ok := no.Data.(*EBoolean); ok {
if y.Value && !n.Value {
return Not(Not(test))
}
if !y.Value && n.Value {
return Not(test)
}
}
}
if id, ok := test.Data.(*EIdentifier); ok {
// "a ? a : b" => "a || b"
if id2, ok := yes.Data.(*EIdentifier); ok && id.Ref == id2.Ref {
return JoinWithLeftAssociativeOp(BinOpLogicalOr, test, no)
}
// "a ? b : a" => "a && b"
if id2, ok := no.Data.(*EIdentifier); ok && id.Ref == id2.Ref {
return JoinWithLeftAssociativeOp(BinOpLogicalAnd, test, yes)
}
}
// "a ? b ? c : d : d" => "a && b ? c : d"
if yesIf, ok := yes.Data.(*EIf); ok && ValuesLookTheSame(yesIf.No.Data, no.Data) {
return Expr{Loc: loc, Data: &EIf{Test: JoinWithLeftAssociativeOp(BinOpLogicalAnd, test, yesIf.Test), Yes: yesIf.Yes, No: no}}
}
// "a ? b : c ? b : d" => "a || c ? b : d"
if noIf, ok := no.Data.(*EIf); ok && ValuesLookTheSame(yes.Data, noIf.Yes.Data) {
return Expr{Loc: loc, Data: &EIf{Test: JoinWithLeftAssociativeOp(BinOpLogicalOr, test, noIf.Test), Yes: yes, No: noIf.No}}
}
// "a ? c : (b, c)" => "(a || b), c"
if comma, ok := no.Data.(*EBinary); ok && comma.Op == BinOpComma && ValuesLookTheSame(yes.Data, comma.Right.Data) {
return JoinWithComma(
JoinWithLeftAssociativeOp(BinOpLogicalOr, test, comma.Left),
comma.Right,
)
}
// "a ? (b, c) : c" => "(a && b), c"
if comma, ok := yes.Data.(*EBinary); ok && comma.Op == BinOpComma && ValuesLookTheSame(comma.Right.Data, no.Data) {
return JoinWithComma(
JoinWithLeftAssociativeOp(BinOpLogicalAnd, test, comma.Left),
comma.Right,
)
}
// "a ? b || c : c" => "(a && b) || c"
if binary, ok := yes.Data.(*EBinary); ok && binary.Op == BinOpLogicalOr &&
ValuesLookTheSame(binary.Right.Data, no.Data) {
return Expr{Loc: loc, Data: &EBinary{
Op: BinOpLogicalOr,
Left: JoinWithLeftAssociativeOp(BinOpLogicalAnd, test, binary.Left),
Right: binary.Right,
}}
}
// "a ? c : b && c" => "(a || b) && c"
if binary, ok := no.Data.(*EBinary); ok && binary.Op == BinOpLogicalAnd &&
ValuesLookTheSame(yes.Data, binary.Right.Data) {
return Expr{Loc: loc, Data: &EBinary{
Op: BinOpLogicalAnd,
Left: JoinWithLeftAssociativeOp(BinOpLogicalOr, test, binary.Left),
Right: binary.Right,
}}
}
// "a ? b(c, d) : b(e, d)" => "b(a ? c : e, d)"
if y, ok := yes.Data.(*ECall); ok && len(y.Args) > 0 {
if n, ok := no.Data.(*ECall); ok && len(n.Args) == len(y.Args) &&
y.HasSameFlagsAs(n) && ValuesLookTheSame(y.Target.Data, n.Target.Data) {
// Only do this if the condition can be reordered past the call target
// without side effects. For example, if the test or the call target is
// an unbound identifier, reordering could potentially mean evaluating
// the code could throw a different ReferenceError.
if ctx.ExprCanBeRemovedIfUnused(test) && ctx.ExprCanBeRemovedIfUnused(y.Target) {
sameTailArgs := true
for i, count := 1, len(y.Args); i < count; i++ {
if !ValuesLookTheSame(y.Args[i].Data, n.Args[i].Data) {
sameTailArgs = false
break
}
}
if sameTailArgs {
yesSpread, yesIsSpread := y.Args[0].Data.(*ESpread)
noSpread, noIsSpread := n.Args[0].Data.(*ESpread)
// "a ? b(...c) : b(...e)" => "b(...a ? c : e)"
if yesIsSpread && noIsSpread {
// Don't mutate the original AST
temp := EIf{Test: test, Yes: yesSpread.Value, No: noSpread.Value}
clone := *y
clone.Args = append([]Expr{}, clone.Args...)
clone.Args[0] = Expr{Loc: loc, Data: &ESpread{Value: ctx.MangleIfExpr(loc, &temp, unsupportedFeatures)}}
return Expr{Loc: loc, Data: &clone}
}
// "a ? b(c) : b(e)" => "b(a ? c : e)"
if !yesIsSpread && !noIsSpread {
// Don't mutate the original AST
temp := EIf{Test: test, Yes: y.Args[0], No: n.Args[0]}
clone := *y
clone.Args = append([]Expr{}, clone.Args...)
clone.Args[0] = ctx.MangleIfExpr(loc, &temp, unsupportedFeatures)
return Expr{Loc: loc, Data: &clone}
}
}
}
}
}
// Try using the "??" or "?." operators
if binary, ok := test.Data.(*EBinary); ok {
var check Expr
var whenNull Expr
var whenNonNull Expr
switch binary.Op {
case BinOpLooseEq:
if _, ok := binary.Right.Data.(*ENull); ok {
// "a == null ? _ : _"
check = binary.Left
whenNull = yes
whenNonNull = no
} else if _, ok := binary.Left.Data.(*ENull); ok {
// "null == a ? _ : _"
check = binary.Right
whenNull = yes
whenNonNull = no
}
case BinOpLooseNe:
if _, ok := binary.Right.Data.(*ENull); ok {
// "a != null ? _ : _"
check = binary.Left
whenNonNull = yes
whenNull = no
} else if _, ok := binary.Left.Data.(*ENull); ok {
// "null != a ? _ : _"
check = binary.Right
whenNonNull = yes
whenNull = no
}
}
if ctx.ExprCanBeRemovedIfUnused(check) {
// "a != null ? a : b" => "a ?? b"
if !unsupportedFeatures.Has(compat.NullishCoalescing) && ValuesLookTheSame(check.Data, whenNonNull.Data) {
return JoinWithLeftAssociativeOp(BinOpNullishCoalescing, check, whenNull)
}
// "a != null ? a.b.c[d](e) : undefined" => "a?.b.c[d](e)"
if !unsupportedFeatures.Has(compat.OptionalChain) {
if _, ok := whenNull.Data.(*EUndefined); ok && TryToInsertOptionalChain(check, whenNonNull) {
return whenNonNull
}
}
}
}
// Don't mutate the original AST
if test != e.Test || yes != e.Yes || no != e.No {
return Expr{Loc: loc, Data: &EIf{Test: test, Yes: yes, No: no}}
}
return Expr{Loc: loc, Data: e}
}
func ForEachIdentifierBindingInDecls(decls []Decl, callback func(loc logger.Loc, b *BIdentifier)) {
for _, decl := range decls {
ForEachIdentifierBinding(decl.Binding, callback)
}
}
func ForEachIdentifierBinding(binding Binding, callback func(loc logger.Loc, b *BIdentifier)) {
switch b := binding.Data.(type) {
case *BMissing:
case *BIdentifier:
callback(binding.Loc, b)
case *BArray:
for _, item := range b.Items {
ForEachIdentifierBinding(item.Binding, callback)
}
case *BObject:
for _, property := range b.Properties {
ForEachIdentifierBinding(property.Value, callback)
}
default:
panic("Internal error")
}
}