diff --git a/go.mod b/go.mod index 797eec3037..1ad13660f9 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,6 @@ require ( github.com/fsnotify/fsnotify v1.5.4 github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 github.com/spf13/pflag v1.0.5 - github.com/tdewolff/parse/v2 v2.5.32 + github.com/tdewolff/parse/v2 v2.5.33 github.com/tdewolff/test v1.0.6 ) diff --git a/go.sum b/go.sum index b3d037bf84..0bb15eb6d2 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8 github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/tdewolff/parse/v2 v2.5.32 h1:paypOpwqGqCjzm/QpgZj8+J4O4glJpe2qCtzM9zffgY= -github.com/tdewolff/parse/v2 v2.5.32/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/parse/v2 v2.5.33 h1:D75KlhAeCSQg4Na8cWKehJdPJoZxwdpRbTZw7lZFWNQ= +github.com/tdewolff/parse/v2 v2.5.33/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= diff --git a/js/js.go b/js/js.go index 75e598a0c8..06e20734c4 100644 --- a/js/js.go +++ b/js/js.go @@ -945,41 +945,13 @@ func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) { // } //} - // change a===null||a===undefined to a==null - if expr.Op == js.OrToken || expr.Op == js.AndToken { - eqEqOp := js.EqEqToken - eqEqEqOp := js.EqEqEqToken - if expr.Op == js.AndToken { - eqEqOp = js.NotEqToken - eqEqEqOp = js.NotEqEqToken - } - - left, isBinaryX := expr.X.(*js.BinaryExpr) - right, isBinaryY := expr.Y.(*js.BinaryExpr) - if isBinaryX && isBinaryY && (left.Op == eqEqOp || left.Op == eqEqEqOp) && (right.Op == eqEqOp || right.Op == eqEqEqOp) { - leftSwitched := false - var leftVar, rightVar js.IExpr - if v, ok := left.X.(*js.Var); ok && isUndefinedOrNull(left.Y) { - leftVar = v - } else if v, ok := left.Y.(*js.Var); ok && isUndefinedOrNull(left.X) { - leftSwitched = true - leftVar = v - } - if v, ok := right.X.(*js.Var); ok && isUndefinedOrNull(right.Y) { - rightVar = v - } else if v, ok := right.Y.(*js.Var); ok && isUndefinedOrNull(right.X) { - rightVar = v - } - if leftVar != nil && leftVar == rightVar { - left.Op = eqEqOp - if leftSwitched { - left.X = &js.LiteralExpr{js.NullToken, nullBytes} - } else { - left.Y = &js.LiteralExpr{js.NullToken, nullBytes} - } - expr = left - } + if v, not, ok := isUndefinedOrNullVar(expr); ok { + // change a===null||a===undefined to a==null + op := js.EqEqToken + if not { + op = js.NotEqToken } + expr = &js.BinaryExpr{op, v, &js.LiteralExpr{js.NullToken, nullBytes}} } m.minifyExpr(expr.X, precLeft) @@ -1004,12 +976,6 @@ func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) { } } } - } else if expr.Op == js.EqEqToken || expr.Op == js.NotEqToken { - if isUndefinedOrNull(expr.X) { - expr.X = &js.LiteralExpr{js.NullToken, nullBytes} - } else if isUndefinedOrNull(expr.Y) { - expr.Y = &js.LiteralExpr{js.NullToken, nullBytes} - } } m.write(expr.Op.Bytes()) if expr.Op == js.AddToken { @@ -1084,8 +1050,10 @@ func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) { } else { m.minifyExpr(expr.X, js.OpMember) } - // 0 < len(m.prev) always - if last := m.prev[len(m.prev)-1]; '0' <= last && last <= '9' { + if expr.Optional { + m.write(questionBytes) + } else if last := m.prev[len(m.prev)-1]; '0' <= last && last <= '9' { + // 0 < len(m.prev) always isInteger := true for _, c := range m.prev[:len(m.prev)-1] { if c < '0' || '9' < c { @@ -1160,6 +1128,9 @@ func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) { } else { m.minifyExpr(expr.Tag, js.OpMember) } + if expr.Optional { + m.write(optChainBytes) + } } parentInFor := m.inFor m.inFor = false @@ -1210,6 +1181,9 @@ func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) { m.minifyExpr(expr.X, js.OpCall) parentInFor := m.inFor m.inFor = false + if expr.Optional { + m.write(optChainBytes) + } m.minifyArguments(expr.Args) m.inFor = parentInFor case *js.IndexExpr: @@ -1223,6 +1197,9 @@ func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) { } else { m.minifyExpr(expr.X, js.OpMember) } + if expr.Optional { + m.write(optChainBytes) + } if lit, ok := expr.Y.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken && 2 < len(lit.Data) { if isIdent := js.AsIdentifierName(lit.Data[1 : len(lit.Data)-1]); isIdent { m.write(dotBytes) @@ -1247,18 +1224,6 @@ func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) { m.minifyExpr(expr.X, js.OpAssign) m.write(colonBytes) m.minifyExpr(expr.Y, js.OpAssign) - case *js.OptChainExpr: - m.minifyExpr(expr.X, js.OpCall) - m.write(optChainBytes) - if callExpr, ok := expr.Y.(*js.CallExpr); ok { - m.minifyArguments(callExpr.Args) - } else if indexExpr, ok := expr.Y.(*js.IndexExpr); ok { - m.write(openBracketBytes) - m.minifyExpr(indexExpr.Y, js.OpExpr) - m.write(closeBracketBytes) - } else { - m.minifyExpr(expr.Y, js.OpPrimary) // TemplateExpr or LiteralExpr - } case *js.VarDecl: m.minifyVarDecl(expr, true) // happens in for statement or when vars were hoisted case *js.FuncDecl: diff --git a/js/js_test.go b/js/js_test.go index 55b2045acc..d2ee6833f1 100644 --- a/js/js_test.go +++ b/js/js_test.go @@ -627,6 +627,16 @@ func TestJS(t *testing.T) { {`!!(a===b||c===d)`, `a===b||c===d`}, {`!(a!==null)`, `a===null`}, {`a==void 0`, `a==null`}, + //{`if(a!==null&&a!==undefined)a.b()`, `a?.b()`}, // returns undefined instead of false + {`(a===null||a===undefined)?undefined:a()`, `a?.()`}, + {`(a===null||a===undefined)?undefined:a[0]`, `a?.[0]`}, + {"(a===null||a===undefined)?undefined:a`tpl`", "a?.`tpl`"}, + {`(a===null||a===undefined)?undefined:a.b`, `a?.b`}, + {`(a===null||a===undefined)?undefined:a.b()`, `a?.b()`}, + {`(a===null||a===undefined)?undefined:a.b[0]`, `a?.b[0]`}, + {"(a===null||a===undefined)?undefined:a.b`tpl`", "a?.b`tpl`"}, + {`(a===null||a===undefined)?undefined:a.#b`, `a?.#b`}, + {`(((a===null)||(a===undefined)))?undefined:a()`, `a?.()`}, // other {`async function g(){await x+y}`, `async function g(){await x+y}`}, diff --git a/js/util.go b/js/util.go index f71c6b054d..15344b21ad 100644 --- a/js/util.go +++ b/js/util.go @@ -83,6 +83,75 @@ var ( regExpScriptBytes = []byte("/script>") ) +func isEmptyStmt(stmt js.IStmt) bool { + if stmt == nil { + return true + } else if _, ok := stmt.(*js.EmptyStmt); ok { + return true + } else if decl, ok := stmt.(*js.VarDecl); ok && decl.TokenType == js.ErrorToken { + for _, item := range decl.List { + if item.Default != nil { + return false + } + } + return true + } else if block, ok := stmt.(*js.BlockStmt); ok { + for _, item := range block.List { + if ok := isEmptyStmt(item); !ok { + return false + } + } + return true + } + return false +} + +func isFlowStmt(stmt js.IStmt) bool { + if _, ok := stmt.(*js.ReturnStmt); ok { + return true + } else if _, ok := stmt.(*js.ThrowStmt); ok { + return true + } else if _, ok := stmt.(*js.BranchStmt); ok { + return true + } + return false +} + +func lastStmt(stmt js.IStmt) js.IStmt { + if block, ok := stmt.(*js.BlockStmt); ok && 0 < len(block.List) { + return lastStmt(block.List[len(block.List)-1]) + } + return stmt +} + +func endsInIf(istmt js.IStmt) bool { + switch stmt := istmt.(type) { + case *js.IfStmt: + if stmt.Else == nil { + _, ok := optimizeStmt(stmt).(*js.IfStmt) + return ok + } + return endsInIf(stmt.Else) + case *js.BlockStmt: + if 0 < len(stmt.List) { + return endsInIf(stmt.List[len(stmt.List)-1]) + } + case *js.LabelledStmt: + return endsInIf(stmt.Value) + case *js.WithStmt: + return endsInIf(stmt.Body) + case *js.WhileStmt: + return endsInIf(stmt.Body) + case *js.ForStmt: + return endsInIf(stmt.Body) + case *js.ForInStmt: + return endsInIf(stmt.Body) + case *js.ForOfStmt: + return endsInIf(stmt.Body) + } + return false +} + // precedence maps for the precedence inside the operation var unaryPrecMap = map[js.TokenType]js.OpPrec{ js.PostIncrToken: js.OpLHS, @@ -265,7 +334,7 @@ func exprPrec(i js.IExpr) js.OpPrec { return expr.Prec case *js.NewTargetExpr, *js.ImportMetaExpr: return js.OpMember - case *js.OptChainExpr, *js.CallExpr: + case *js.CallExpr: return js.OpCall case *js.CondExpr, *js.YieldExpr, *js.ArrowFunc: return js.OpAssign @@ -275,6 +344,62 @@ func exprPrec(i js.IExpr) js.OpPrec { return js.OpExpr // CommaExpr } +func hasSideEffects(i js.IExpr) bool { + // assume that variable usage and that the index operator themselves have no side effects + switch expr := i.(type) { + case *js.Var, *js.LiteralExpr, *js.FuncDecl, *js.ClassDecl, *js.ArrowFunc, *js.NewTargetExpr, *js.ImportMetaExpr: + return false + case *js.NewExpr, *js.CallExpr, *js.YieldExpr: + return true + case *js.GroupExpr: + return hasSideEffects(expr.X) + case *js.DotExpr: + return hasSideEffects(expr.X) + case *js.IndexExpr: + return hasSideEffects(expr.X) || hasSideEffects(expr.Y) + case *js.CondExpr: + return hasSideEffects(expr.Cond) || hasSideEffects(expr.X) || hasSideEffects(expr.Y) + case *js.CommaExpr: + for _, item := range expr.List { + if hasSideEffects(item) { + return true + } + } + case *js.ArrayExpr: + for _, item := range expr.List { + if hasSideEffects(item.Value) { + return true + } + } + return false + case *js.ObjectExpr: + for _, item := range expr.List { + if hasSideEffects(item.Value) || item.Init != nil && hasSideEffects(item.Init) || item.Name != nil && item.Name.IsComputed() && hasSideEffects(item.Name.Computed) { + return true + } + } + return false + case *js.TemplateExpr: + if hasSideEffects(expr.Tag) { + return true + } + for _, item := range expr.List { + if hasSideEffects(item.Expr) { + return true + } + } + return false + case *js.UnaryExpr: + if expr.Op == js.DeleteToken || expr.Op == js.PreIncrToken || expr.Op == js.PreDecrToken || expr.Op == js.PostIncrToken || expr.Op == js.PostDecrToken { + return true + } + return hasSideEffects(expr.X) + case *js.BinaryExpr: + return binaryOpPrecMap[expr.Op] == js.OpAssign + } + return true +} + // TODO: use in more cases func groupExpr(i js.IExpr, prec js.OpPrec) js.IExpr { precInside := exprPrec(i) @@ -314,61 +439,29 @@ func commaExpr(x, y js.IExpr) js.IExpr { return comma } -func isEmptyStmt(stmt js.IStmt) bool { - if stmt == nil { - return true - } else if _, ok := stmt.(*js.EmptyStmt); ok { - return true - } else if decl, ok := stmt.(*js.VarDecl); ok && decl.TokenType == js.ErrorToken { - for _, item := range decl.List { - if item.Default != nil { - return false - } - } - return true - } else if block, ok := stmt.(*js.BlockStmt); ok { - for _, item := range block.List { - if ok := isEmptyStmt(item); !ok { - return false - } +func innerExpr(i js.IExpr) js.IExpr { + for { + if group, ok := i.(*js.GroupExpr); ok { + i = group.X + } else { + return i } - return true - } - return false -} - -func finalExpr(expr js.IExpr) js.IExpr { - if group, ok := expr.(*js.GroupExpr); ok { - expr = group.X - } - if comma, ok := expr.(*js.CommaExpr); ok { - expr = comma.List[len(comma.List)-1] - } - if binary, ok := expr.(*js.BinaryExpr); ok && binary.Op == js.EqToken { - expr = binary.X // return first } - return expr } -func isFlowStmt(stmt js.IStmt) bool { - if _, ok := stmt.(*js.ReturnStmt); ok { - return true - } else if _, ok := stmt.(*js.ThrowStmt); ok { - return true - } else if _, ok := stmt.(*js.BranchStmt); ok { - return true +func finalExpr(i js.IExpr) js.IExpr { + i = innerExpr(i) + if comma, ok := i.(*js.CommaExpr); ok { + i = comma.List[len(comma.List)-1] } - return false -} - -func lastStmt(stmt js.IStmt) js.IStmt { - if block, ok := stmt.(*js.BlockStmt); ok && 0 < len(block.List) { - return lastStmt(block.List[len(block.List)-1]) + if binary, ok := i.(*js.BinaryExpr); ok && binary.Op == js.EqToken { + i = binary.X // return first } - return stmt + return i } func isTrue(i js.IExpr) bool { + i = innerExpr(i) if lit, ok := i.(*js.LiteralExpr); ok && lit.TokenType == js.TrueToken { return true } else if unary, ok := i.(*js.UnaryExpr); ok && unary.Op == js.NotToken { @@ -379,6 +472,7 @@ func isTrue(i js.IExpr) bool { } func isFalse(i js.IExpr) bool { + i = innerExpr(i) if lit, ok := i.(*js.LiteralExpr); ok { return lit.TokenType == js.FalseToken } else if unary, ok := i.(*js.UnaryExpr); ok && unary.Op == js.NotToken { @@ -388,27 +482,107 @@ func isFalse(i js.IExpr) bool { return false } -func toNullishExpr(condExpr *js.CondExpr) (js.IExpr, js.IExpr, bool) { - // convert conditional expression to nullish: a!=null?a:b => a??b - if binaryExpr, ok := condExpr.Cond.(*js.BinaryExpr); ok && (binaryExpr.Op == js.EqEqToken || binaryExpr.Op == js.NotEqToken) { - var left, right js.IExpr - if binaryExpr.Op == js.EqEqToken { - left = condExpr.Y - right = condExpr.X - } else { - left = condExpr.X - right = condExpr.Y +func isEqualExpr(a, b js.IExpr) bool { + a = innerExpr(a) + b = innerExpr(b) + if left, ok := a.(*js.Var); ok { + if right, ok := b.(*js.Var); ok { + return bytes.Equal(left.Name(), right.Name()) } - if lit, ok := binaryExpr.X.(*js.LiteralExpr); ((ok && lit.TokenType == js.NullToken) || isUndefined(binaryExpr.X)) && isEqualExpr(binaryExpr.Y, left) { - return left, right, true - } else if lit, ok := binaryExpr.Y.(*js.LiteralExpr); ((ok && lit.TokenType == js.NullToken) || isUndefined(binaryExpr.Y)) && isEqualExpr(binaryExpr.X, left) { - return left, right, true + } + // TODO: use reflect.DeepEqual? + return false +} + +func toNullishExpr(condExpr *js.CondExpr) (js.IExpr, bool) { + if v, not, ok := isUndefinedOrNullVar(condExpr.Cond); ok { + left, right := condExpr.X, condExpr.Y + if not { + left, right = right, left + } + if isEqualExpr(v, right) { + // convert conditional expression to nullish: a==null?b:a => a??b + return &js.BinaryExpr{js.NullishToken, groupExpr(right, binaryLeftPrecMap[js.NullishToken]), groupExpr(left, binaryRightPrecMap[js.NullishToken])}, true + } else if isUndefined(left) { + // convert conditional expression to optional expr: a==null?undefined:a.b => a?.b + expr := right + var parent js.IExpr + for { + prevExpr := expr + if callExpr, ok := expr.(*js.CallExpr); ok { + expr = callExpr.X + } else if dotExpr, ok := expr.(*js.DotExpr); ok { + expr = dotExpr.X + } else if indexExpr, ok := expr.(*js.IndexExpr); ok { + expr = indexExpr.X + } else if templateExpr, ok := expr.(*js.TemplateExpr); ok { + expr = templateExpr.Tag + } else { + break + } + parent = prevExpr + } + if parent != nil && isEqualExpr(v, expr) { + if callExpr, ok := parent.(*js.CallExpr); ok { + callExpr.Optional = true + } else if dotExpr, ok := parent.(*js.DotExpr); ok { + dotExpr.Optional = true + } else if indexExpr, ok := parent.(*js.IndexExpr); ok { + indexExpr.Optional = true + } else if templateExpr, ok := parent.(*js.TemplateExpr); ok { + templateExpr.Optional = true + } + return right, true + } + } + } + return nil, false +} + +func isUndefinedOrNullVar(i js.IExpr) (*js.Var, bool, bool) { + i = innerExpr(i) + if binary, ok := i.(*js.BinaryExpr); ok && (binary.Op == js.OrToken || binary.Op == js.AndToken) { + eqEqOp := js.EqEqToken + eqEqEqOp := js.EqEqEqToken + if binary.Op == js.AndToken { + eqEqOp = js.NotEqToken + eqEqEqOp = js.NotEqEqToken + } + + left, isBinaryX := innerExpr(binary.X).(*js.BinaryExpr) + right, isBinaryY := innerExpr(binary.Y).(*js.BinaryExpr) + if isBinaryX && isBinaryY && (left.Op == eqEqOp || left.Op == eqEqEqOp) && (right.Op == eqEqOp || right.Op == eqEqEqOp) { + var leftVar, rightVar *js.Var + if v, ok := left.X.(*js.Var); ok && isUndefinedOrNull(left.Y) { + leftVar = v + } else if v, ok := left.Y.(*js.Var); ok && isUndefinedOrNull(left.X) { + leftVar = v + } + if v, ok := right.X.(*js.Var); ok && isUndefinedOrNull(right.Y) { + rightVar = v + } else if v, ok := right.Y.(*js.Var); ok && isUndefinedOrNull(right.X) { + rightVar = v + } + if leftVar != nil && leftVar == rightVar { + return leftVar, binary.Op == js.AndToken, true + } + } + } else if ok && (binary.Op == js.EqEqToken || binary.Op == js.NotEqToken) { + var variable *js.Var + if v, ok := binary.X.(*js.Var); ok && isUndefinedOrNull(binary.Y) { + variable = v + } else if v, ok := binary.Y.(*js.Var); ok && isUndefinedOrNull(binary.X) { + variable = v + } + if variable != nil { + return variable, binary.Op == js.NotEqToken, true } } - return nil, nil, false + return nil, false, false } func isUndefinedOrNull(i js.IExpr) bool { + i = innerExpr(i) if lit, ok := i.(*js.LiteralExpr); ok { return lit.TokenType == js.NullToken } @@ -416,6 +590,7 @@ func isUndefinedOrNull(i js.IExpr) bool { } func isUndefined(i js.IExpr) bool { + i = innerExpr(i) if v, ok := i.(*js.Var); ok { if bytes.Equal(v.Name(), undefinedBytes) { // TODO: only if not defined return true @@ -474,80 +649,6 @@ func isFalsy(i js.IExpr) (bool, bool) { return false, false // unknown } -func isEqualExpr(a, b js.IExpr) bool { - if group, ok := a.(*js.GroupExpr); ok { - a = group.X - } - if group, ok := b.(*js.GroupExpr); ok { - b = group.X - } - if left, ok := a.(*js.Var); ok { - if right, ok := b.(*js.Var); ok { - return bytes.Equal(left.Name(), right.Name()) - } - } - // TODO: use reflect.DeepEqual? - return false -} - -func hasSideEffects(i js.IExpr) bool { - // assume that variable usage and that the index operator themselves have no side effects - switch expr := i.(type) { - case *js.Var, *js.LiteralExpr, *js.FuncDecl, *js.ClassDecl, *js.ArrowFunc, *js.NewTargetExpr, *js.ImportMetaExpr: - return false - case *js.NewExpr, *js.CallExpr, *js.YieldExpr: - return true - case *js.GroupExpr: - return hasSideEffects(expr.X) - case *js.DotExpr: - return hasSideEffects(expr.X) - case *js.IndexExpr: - return hasSideEffects(expr.X) || hasSideEffects(expr.Y) - case *js.OptChainExpr: - return hasSideEffects(expr.X) || hasSideEffects(expr.Y) - case *js.CondExpr: - return hasSideEffects(expr.Cond) || hasSideEffects(expr.X) || hasSideEffects(expr.Y) - case *js.CommaExpr: - for _, item := range expr.List { - if hasSideEffects(item) { - return true - } - } - case *js.ArrayExpr: - for _, item := range expr.List { - if hasSideEffects(item.Value) { - return true - } - } - return false - case *js.ObjectExpr: - for _, item := range expr.List { - if hasSideEffects(item.Value) || item.Init != nil && hasSideEffects(item.Init) || item.Name != nil && item.Name.IsComputed() && hasSideEffects(item.Name.Computed) { - return true - } - } - return false - case *js.TemplateExpr: - if hasSideEffects(expr.Tag) { - return true - } - for _, item := range expr.List { - if hasSideEffects(item.Expr) { - return true - } - } - return false - case *js.UnaryExpr: - if expr.Op == js.DeleteToken || expr.Op == js.PreIncrToken || expr.Op == js.PreDecrToken || expr.Op == js.PostIncrToken || expr.Op == js.PostDecrToken { - return true - } - return hasSideEffects(expr.X) - case *js.BinaryExpr: - return binaryOpPrecMap[expr.Op] == js.OpAssign - } - return true -} - func isBooleanExpr(expr js.IExpr) bool { if unaryExpr, ok := expr.(*js.UnaryExpr); ok { return unaryExpr.Op == js.NotToken @@ -724,9 +825,9 @@ func (m *jsMinifier) optimizeCondExpr(expr *js.CondExpr, prec js.OpPrec) js.IExp } else if isEqualExpr(expr.X, expr.Y) { // if true and false bodies are equal return groupExpr(&js.CommaExpr{[]js.IExpr{expr.Cond, expr.X}}, prec) - } else if left, right, ok := toNullishExpr(expr); ok && !m.o.NoNullishOperator { + } else if nullishExpr, ok := toNullishExpr(expr); ok && !m.o.NoNullishOperator { // no need to check whether left/right need to add groups, as the space saving is always more - return &js.BinaryExpr{js.NullishToken, groupExpr(left, binaryLeftPrecMap[js.NullishToken]), groupExpr(right, binaryRightPrecMap[js.NullishToken])} + return nullishExpr } else { // shorten when true and false bodies are true and false trueX, falseX := isTrue(expr.X), isFalse(expr.X) @@ -767,34 +868,6 @@ func (m *jsMinifier) optimizeCondExpr(expr *js.CondExpr, prec js.OpPrec) js.IExp return expr } -func endsInIf(istmt js.IStmt) bool { - switch stmt := istmt.(type) { - case *js.IfStmt: - if stmt.Else == nil { - _, ok := optimizeStmt(stmt).(*js.IfStmt) - return ok - } - return endsInIf(stmt.Else) - case *js.BlockStmt: - if 0 < len(stmt.List) { - return endsInIf(stmt.List[len(stmt.List)-1]) - } - case *js.LabelledStmt: - return endsInIf(stmt.Value) - case *js.WithStmt: - return endsInIf(stmt.Body) - case *js.WhileStmt: - return endsInIf(stmt.Body) - case *js.ForStmt: - return endsInIf(stmt.Body) - case *js.ForInStmt: - return endsInIf(stmt.Body) - case *js.ForOfStmt: - return endsInIf(stmt.Body) - } - return false -} - func isHexDigit(b byte) bool { return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F' }