Skip to content

Commit bdd1ad7

Browse files
committed
simplify unused "a != null && a.b()" into "a?.b()"
1 parent a07a659 commit bdd1ad7

File tree

6 files changed

+264
-222
lines changed

6 files changed

+264
-222
lines changed

CHANGELOG.md

+6-7
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@
88

99
```ts
1010
// Original code
11-
let foo = (bar) => [
12-
bar === null || bar === undefined ? undefined : bar.baz,
13-
bar !== null && bar !== undefined ? bar.baz() : undefined,
14-
null !== bar && undefined !== bar ? bar[baz] : undefined,
15-
]
11+
let foo = (x) => {
12+
if (x !== null && x !== undefined) x.y()
13+
return x === null || x === undefined ? undefined : x.z
14+
}
1615

1716
// Old output (with --minify)
18-
let foo=n=>[n==null?void 0:n.baz,n!=null?n.baz():void 0,n!=null?n[baz]:void 0];
17+
let foo=n=>(n!=null&&n.y(),n==null?void 0:n.z);
1918

2019
// New output (with --minify)
21-
let foo=n=>[n?.baz,n?.baz(),n?.[baz]];
20+
let foo=n=>(n?.y(),n?.z);
2221
```
2322

2423
This only takes effect when minification is enabled and when the configured target environment is known to support the optional chaining operator. As always, make sure to set `--target=` to the appropriate language target if you are running the minified code in an environment that doesn't support the latest JavaScript features.

internal/js_ast/js_ast.go

+189-18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/evanw/esbuild/internal/ast"
99
"github.com/evanw/esbuild/internal/compat"
10+
"github.com/evanw/esbuild/internal/helpers"
1011
"github.com/evanw/esbuild/internal/logger"
1112
)
1213

@@ -2623,10 +2624,10 @@ func IsPrimitiveWithSideEffects(data E) bool {
26232624
}
26242625

26252626
// This will return a nil expression if the expression can be totally removed
2626-
func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
2627+
func SimplifyUnusedExpr(expr Expr, unsupportedFeatures compat.JSFeature, isUnbound func(Ref) bool) Expr {
26272628
switch e := expr.Data.(type) {
26282629
case *EInlinedEnum:
2629-
return SimplifyUnusedExpr(e.Value, isUnbound)
2630+
return SimplifyUnusedExpr(e.Value, unsupportedFeatures, isUnbound)
26302631

26312632
case *ENull, *EUndefined, *EMissing, *EBoolean, *ENumber, *EBigInt,
26322633
*EString, *EThis, *ERegExp, *EFunction, *EArrow, *EImportMeta:
@@ -2658,7 +2659,7 @@ func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
26582659
comma = JoinWithComma(comma, Expr{Loc: templateLoc, Data: template})
26592660
template = nil
26602661
}
2661-
comma = JoinWithComma(comma, SimplifyUnusedExpr(part.Value, isUnbound))
2662+
comma = JoinWithComma(comma, SimplifyUnusedExpr(part.Value, unsupportedFeatures, isUnbound))
26622663
continue
26632664
}
26642665

@@ -2684,7 +2685,7 @@ func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
26842685
if _, ok := spread.Data.(*ESpread); ok {
26852686
end := 0
26862687
for _, item := range e.Items {
2687-
item = SimplifyUnusedExpr(item, isUnbound)
2688+
item = SimplifyUnusedExpr(item, unsupportedFeatures, isUnbound)
26882689
if item.Data != nil {
26892690
e.Items[end] = item
26902691
end++
@@ -2699,7 +2700,7 @@ func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
26992700
// array items with side effects. Apply this simplification recursively.
27002701
var result Expr
27012702
for _, item := range e.Items {
2702-
result = JoinWithComma(result, SimplifyUnusedExpr(item, isUnbound))
2703+
result = JoinWithComma(result, SimplifyUnusedExpr(item, unsupportedFeatures, isUnbound))
27032704
}
27042705
return result
27052706

@@ -2713,7 +2714,7 @@ func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
27132714
for _, property := range e.Properties {
27142715
// Spread properties must always be evaluated
27152716
if property.Kind != PropertySpread {
2716-
value := SimplifyUnusedExpr(property.ValueOrNil, isUnbound)
2717+
value := SimplifyUnusedExpr(property.ValueOrNil, unsupportedFeatures, isUnbound)
27172718
if value.Data != nil {
27182719
// Keep the value
27192720
property.ValueOrNil = value
@@ -2745,17 +2746,17 @@ func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
27452746
Right: Expr{Loc: property.Key.Loc, Data: &EString{}},
27462747
}})
27472748
}
2748-
result = JoinWithComma(result, SimplifyUnusedExpr(property.ValueOrNil, isUnbound))
2749+
result = JoinWithComma(result, SimplifyUnusedExpr(property.ValueOrNil, unsupportedFeatures, isUnbound))
27492750
}
27502751
return result
27512752

27522753
case *EIf:
2753-
e.Yes = SimplifyUnusedExpr(e.Yes, isUnbound)
2754-
e.No = SimplifyUnusedExpr(e.No, isUnbound)
2754+
e.Yes = SimplifyUnusedExpr(e.Yes, unsupportedFeatures, isUnbound)
2755+
e.No = SimplifyUnusedExpr(e.No, unsupportedFeatures, isUnbound)
27552756

27562757
// "foo() ? 1 : 2" => "foo()"
27572758
if e.Yes.Data == nil && e.No.Data == nil {
2758-
return SimplifyUnusedExpr(e.Test, isUnbound)
2759+
return SimplifyUnusedExpr(e.Test, unsupportedFeatures, isUnbound)
27592760
}
27602761

27612762
// "foo() ? 1 : bar()" => "foo() || bar()"
@@ -2773,7 +2774,7 @@ func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
27732774
// These operators must not have any type conversions that can execute code
27742775
// such as "toString" or "valueOf". They must also never throw any exceptions.
27752776
case UnOpVoid, UnOpNot:
2776-
return SimplifyUnusedExpr(e.Value, isUnbound)
2777+
return SimplifyUnusedExpr(e.Value, unsupportedFeatures, isUnbound)
27772778

27782779
case UnOpTypeof:
27792780
if _, ok := e.Value.Data.(*EIdentifier); ok {
@@ -2782,32 +2783,52 @@ func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
27822783
// "typeof x" is special-cased in the standard to never throw.
27832784
return Expr{}
27842785
}
2785-
return SimplifyUnusedExpr(e.Value, isUnbound)
2786+
return SimplifyUnusedExpr(e.Value, unsupportedFeatures, isUnbound)
27862787
}
27872788

27882789
case *EBinary:
27892790
switch e.Op {
27902791
// These operators must not have any type conversions that can execute code
27912792
// such as "toString" or "valueOf". They must also never throw any exceptions.
27922793
case BinOpStrictEq, BinOpStrictNe, BinOpComma:
2793-
return JoinWithComma(SimplifyUnusedExpr(e.Left, isUnbound), SimplifyUnusedExpr(e.Right, isUnbound))
2794+
return JoinWithComma(SimplifyUnusedExpr(e.Left, unsupportedFeatures, isUnbound), SimplifyUnusedExpr(e.Right, unsupportedFeatures, isUnbound))
27942795

27952796
// We can simplify "==" and "!=" even though they can call "toString" and/or
27962797
// "valueOf" if we can statically determine that the types of both sides are
27972798
// primitives. In that case there won't be any chance for user-defined
27982799
// "toString" and/or "valueOf" to be called.
27992800
case BinOpLooseEq, BinOpLooseNe:
28002801
if IsPrimitiveWithSideEffects(e.Left.Data) && IsPrimitiveWithSideEffects(e.Right.Data) {
2801-
return JoinWithComma(SimplifyUnusedExpr(e.Left, isUnbound), SimplifyUnusedExpr(e.Right, isUnbound))
2802+
return JoinWithComma(SimplifyUnusedExpr(e.Left, unsupportedFeatures, isUnbound), SimplifyUnusedExpr(e.Right, unsupportedFeatures, isUnbound))
28022803
}
28032804

28042805
case BinOpLogicalAnd, BinOpLogicalOr, BinOpNullishCoalescing:
28052806
// Preserve short-circuit behavior: the left expression is only unused if
28062807
// the right expression can be completely removed. Otherwise, the left
28072808
// expression is important for the branch.
2808-
e.Right = SimplifyUnusedExpr(e.Right, isUnbound)
2809+
e.Right = SimplifyUnusedExpr(e.Right, unsupportedFeatures, isUnbound)
28092810
if e.Right.Data == nil {
2810-
return SimplifyUnusedExpr(e.Left, isUnbound)
2811+
return SimplifyUnusedExpr(e.Left, unsupportedFeatures, isUnbound)
2812+
}
2813+
2814+
// Try to take advantage of the optional chain operator to shorten code
2815+
if !unsupportedFeatures.Has(compat.OptionalChain) {
2816+
if binary, ok := e.Left.Data.(*EBinary); ok {
2817+
// "a != null && a.b()" => "a?.b()"
2818+
// "a == null || a.b()" => "a?.b()"
2819+
if (binary.Op == BinOpLooseNe && e.Op == BinOpLogicalAnd) || (binary.Op == BinOpLooseEq && e.Op == BinOpLogicalOr) {
2820+
var test Expr
2821+
if _, ok := binary.Right.Data.(*ENull); ok {
2822+
test = binary.Left
2823+
} else if _, ok := binary.Left.Data.(*ENull); ok {
2824+
test = binary.Right
2825+
}
2826+
if id, ok := test.Data.(*EIdentifier); ok && !id.MustKeepDueToWithStmt &&
2827+
(id.CanBeRemovedIfUnused || !isUnbound(id.Ref)) && TryToInsertOptionalChain(test, e.Right) {
2828+
return e.Right
2829+
}
2830+
}
2831+
}
28112832
}
28122833

28132834
case BinOpAdd:
@@ -2822,7 +2843,7 @@ func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
28222843
if e.CanBeUnwrappedIfUnused {
28232844
expr = Expr{}
28242845
for _, arg := range e.Args {
2825-
expr = JoinWithComma(expr, SimplifyUnusedExpr(arg, isUnbound))
2846+
expr = JoinWithComma(expr, SimplifyUnusedExpr(arg, unsupportedFeatures, isUnbound))
28262847
}
28272848
}
28282849

@@ -2877,7 +2898,7 @@ func SimplifyUnusedExpr(expr Expr, isUnbound func(Ref) bool) Expr {
28772898
if e.CanBeUnwrappedIfUnused {
28782899
expr = Expr{}
28792900
for _, arg := range e.Args {
2880-
expr = JoinWithComma(expr, SimplifyUnusedExpr(arg, isUnbound))
2901+
expr = JoinWithComma(expr, SimplifyUnusedExpr(arg, unsupportedFeatures, isUnbound))
28812902
}
28822903
}
28832904
}
@@ -2980,3 +3001,153 @@ func ExtractNumericValues(left Expr, right Expr) (float64, float64, bool) {
29803001
}
29813002
return 0, 0, false
29823003
}
3004+
3005+
// Returns "equal, ok". If "ok" is false, then nothing is known about the two
3006+
// values. If "ok" is true, the equality or inequality of the two values is
3007+
// stored in "equal".
3008+
func CheckEqualityIfNoSideEffects(left E, right E) (bool, bool) {
3009+
if r, ok := right.(*EInlinedEnum); ok {
3010+
return CheckEqualityIfNoSideEffects(left, r.Value.Data)
3011+
}
3012+
3013+
switch l := left.(type) {
3014+
case *EInlinedEnum:
3015+
return CheckEqualityIfNoSideEffects(l.Value.Data, right)
3016+
3017+
case *ENull:
3018+
_, ok := right.(*ENull)
3019+
return ok, ok
3020+
3021+
case *EUndefined:
3022+
_, ok := right.(*EUndefined)
3023+
return ok, ok
3024+
3025+
case *EBoolean:
3026+
r, ok := right.(*EBoolean)
3027+
return ok && l.Value == r.Value, ok
3028+
3029+
case *ENumber:
3030+
r, ok := right.(*ENumber)
3031+
return ok && l.Value == r.Value, ok
3032+
3033+
case *EBigInt:
3034+
r, ok := right.(*EBigInt)
3035+
return ok && l.Value == r.Value, ok
3036+
3037+
case *EString:
3038+
r, ok := right.(*EString)
3039+
return ok && helpers.UTF16EqualsUTF16(l.Value, r.Value), ok
3040+
}
3041+
3042+
return false, false
3043+
}
3044+
3045+
func ValuesLookTheSame(left E, right E) bool {
3046+
if b, ok := right.(*EInlinedEnum); ok {
3047+
return ValuesLookTheSame(left, b.Value.Data)
3048+
}
3049+
3050+
switch a := left.(type) {
3051+
case *EInlinedEnum:
3052+
return ValuesLookTheSame(a.Value.Data, right)
3053+
3054+
case *EIdentifier:
3055+
if b, ok := right.(*EIdentifier); ok && a.Ref == b.Ref {
3056+
return true
3057+
}
3058+
3059+
case *EDot:
3060+
if b, ok := right.(*EDot); ok && a.HasSameFlagsAs(b) &&
3061+
a.Name == b.Name && ValuesLookTheSame(a.Target.Data, b.Target.Data) {
3062+
return true
3063+
}
3064+
3065+
case *EIndex:
3066+
if b, ok := right.(*EIndex); ok && a.HasSameFlagsAs(b) &&
3067+
ValuesLookTheSame(a.Target.Data, b.Target.Data) && ValuesLookTheSame(a.Index.Data, b.Index.Data) {
3068+
return true
3069+
}
3070+
3071+
case *EIf:
3072+
if b, ok := right.(*EIf); ok && ValuesLookTheSame(a.Test.Data, b.Test.Data) &&
3073+
ValuesLookTheSame(a.Yes.Data, b.Yes.Data) && ValuesLookTheSame(a.No.Data, b.No.Data) {
3074+
return true
3075+
}
3076+
3077+
case *EUnary:
3078+
if b, ok := right.(*EUnary); ok && a.Op == b.Op && ValuesLookTheSame(a.Value.Data, b.Value.Data) {
3079+
return true
3080+
}
3081+
3082+
case *EBinary:
3083+
if b, ok := right.(*EBinary); ok && a.Op == b.Op && ValuesLookTheSame(a.Left.Data, b.Left.Data) &&
3084+
ValuesLookTheSame(a.Right.Data, b.Right.Data) {
3085+
return true
3086+
}
3087+
3088+
case *ECall:
3089+
if b, ok := right.(*ECall); ok && a.HasSameFlagsAs(b) &&
3090+
len(a.Args) == len(b.Args) && ValuesLookTheSame(a.Target.Data, b.Target.Data) {
3091+
for i := range a.Args {
3092+
if !ValuesLookTheSame(a.Args[i].Data, b.Args[i].Data) {
3093+
return false
3094+
}
3095+
}
3096+
return true
3097+
}
3098+
3099+
// Special-case to distinguish between negative an non-negative zero when mangling
3100+
// "a ? -0 : 0" => "a ? -0 : 0"
3101+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness
3102+
case *ENumber:
3103+
b, ok := right.(*ENumber)
3104+
if ok && a.Value == 0 && b.Value == 0 && math.Signbit(a.Value) != math.Signbit(b.Value) {
3105+
return false
3106+
}
3107+
}
3108+
3109+
equal, ok := CheckEqualityIfNoSideEffects(left, right)
3110+
return ok && equal
3111+
}
3112+
3113+
func TryToInsertOptionalChain(test Expr, expr Expr) bool {
3114+
switch e := expr.Data.(type) {
3115+
case *EDot:
3116+
if ValuesLookTheSame(test.Data, e.Target.Data) {
3117+
e.OptionalChain = OptionalChainStart
3118+
return true
3119+
}
3120+
if TryToInsertOptionalChain(test, e.Target) {
3121+
if e.OptionalChain == OptionalChainNone {
3122+
e.OptionalChain = OptionalChainContinue
3123+
}
3124+
return true
3125+
}
3126+
3127+
case *EIndex:
3128+
if ValuesLookTheSame(test.Data, e.Target.Data) {
3129+
e.OptionalChain = OptionalChainStart
3130+
return true
3131+
}
3132+
if TryToInsertOptionalChain(test, e.Target) {
3133+
if e.OptionalChain == OptionalChainNone {
3134+
e.OptionalChain = OptionalChainContinue
3135+
}
3136+
return true
3137+
}
3138+
3139+
case *ECall:
3140+
if ValuesLookTheSame(test.Data, e.Target.Data) {
3141+
e.OptionalChain = OptionalChainStart
3142+
return true
3143+
}
3144+
if TryToInsertOptionalChain(test, e.Target) {
3145+
if e.OptionalChain == OptionalChainNone {
3146+
e.OptionalChain = OptionalChainContinue
3147+
}
3148+
return true
3149+
}
3150+
}
3151+
3152+
return false
3153+
}

0 commit comments

Comments
 (0)