package js import ( "bytes" "encoding/hex" stdStrconv "strconv" "unicode/utf8" "github.com/tdewolff/minify/v2" "github.com/tdewolff/parse/v2/js" "github.com/tdewolff/parse/v2/strconv" ) var ( spaceBytes = []byte(" ") newlineBytes = []byte("\n") starBytes = []byte("*") colonBytes = []byte(":") semicolonBytes = []byte(";") commaBytes = []byte(",") dotBytes = []byte(".") ellipsisBytes = []byte("...") openBraceBytes = []byte("{") closeBraceBytes = []byte("}") openParenBytes = []byte("(") closeParenBytes = []byte(")") openBracketBytes = []byte("[") closeBracketBytes = []byte("]") openParenBracketBytes = []byte("({") closeParenOpenBracketBytes = []byte("){") notBytes = []byte("!") questionBytes = []byte("?") equalBytes = []byte("=") optChainBytes = []byte("?.") arrowBytes = []byte("=>") zeroBytes = []byte("0") oneBytes = []byte("1") letBytes = []byte("let") getBytes = []byte("get") setBytes = []byte("set") asyncBytes = []byte("async") functionBytes = []byte("function") staticBytes = []byte("static") ifOpenBytes = []byte("if(") elseBytes = []byte("else") withOpenBytes = []byte("with(") doBytes = []byte("do") whileOpenBytes = []byte("while(") forOpenBytes = []byte("for(") forAwaitOpenBytes = []byte("for await(") inBytes = []byte("in") ofBytes = []byte("of") switchOpenBytes = []byte("switch(") throwBytes = []byte("throw") tryBytes = []byte("try") catchBytes = []byte("catch") finallyBytes = []byte("finally") importBytes = []byte("import") exportBytes = []byte("export") fromBytes = []byte("from") returnBytes = []byte("return") classBytes = []byte("class") asSpaceBytes = []byte("as ") asyncSpaceBytes = []byte("async ") spaceDefaultBytes = []byte(" default") spaceExtendsBytes = []byte(" extends") yieldBytes = []byte("yield") newBytes = []byte("new") openNewBytes = []byte("(new") newTargetBytes = []byte("new.target") importMetaBytes = []byte("import.meta") nanBytes = []byte("NaN") undefinedBytes = []byte("undefined") infinityBytes = []byte("Infinity") nullBytes = []byte("null") voidZeroBytes = []byte("void 0") groupedVoidZeroBytes = []byte("(void 0)") oneDivZeroBytes = []byte("1/0") groupedOneDivZeroBytes = []byte("(1/0)") notZeroBytes = []byte("!0") groupedNotZeroBytes = []byte("(!0)") notOneBytes = []byte("!1") groupedNotOneBytes = []byte("(!1)") debuggerBytes = []byte("debugger") 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, js.PostDecrToken: js.OpLHS, js.PreIncrToken: js.OpUnary, js.PreDecrToken: js.OpUnary, js.NotToken: js.OpUnary, js.BitNotToken: js.OpUnary, js.TypeofToken: js.OpUnary, js.VoidToken: js.OpUnary, js.DeleteToken: js.OpUnary, js.PosToken: js.OpUnary, js.NegToken: js.OpUnary, js.AwaitToken: js.OpUnary, } var binaryLeftPrecMap = map[js.TokenType]js.OpPrec{ js.EqToken: js.OpLHS, js.MulEqToken: js.OpLHS, js.DivEqToken: js.OpLHS, js.ModEqToken: js.OpLHS, js.ExpEqToken: js.OpLHS, js.AddEqToken: js.OpLHS, js.SubEqToken: js.OpLHS, js.LtLtEqToken: js.OpLHS, js.GtGtEqToken: js.OpLHS, js.GtGtGtEqToken: js.OpLHS, js.BitAndEqToken: js.OpLHS, js.BitXorEqToken: js.OpLHS, js.BitOrEqToken: js.OpLHS, js.ExpToken: js.OpUpdate, js.MulToken: js.OpMul, js.DivToken: js.OpMul, js.ModToken: js.OpMul, js.AddToken: js.OpAdd, js.SubToken: js.OpAdd, js.LtLtToken: js.OpShift, js.GtGtToken: js.OpShift, js.GtGtGtToken: js.OpShift, js.LtToken: js.OpCompare, js.LtEqToken: js.OpCompare, js.GtToken: js.OpCompare, js.GtEqToken: js.OpCompare, js.InToken: js.OpCompare, js.InstanceofToken: js.OpCompare, js.EqEqToken: js.OpEquals, js.NotEqToken: js.OpEquals, js.EqEqEqToken: js.OpEquals, js.NotEqEqToken: js.OpEquals, js.BitAndToken: js.OpBitAnd, js.BitXorToken: js.OpBitXor, js.BitOrToken: js.OpBitOr, js.AndToken: js.OpAnd, js.OrToken: js.OpOr, js.NullishToken: js.OpBitOr, // or OpCoalesce js.CommaToken: js.OpExpr, } var binaryRightPrecMap = map[js.TokenType]js.OpPrec{ js.EqToken: js.OpAssign, js.MulEqToken: js.OpAssign, js.DivEqToken: js.OpAssign, js.ModEqToken: js.OpAssign, js.ExpEqToken: js.OpAssign, js.AddEqToken: js.OpAssign, js.SubEqToken: js.OpAssign, js.LtLtEqToken: js.OpAssign, js.GtGtEqToken: js.OpAssign, js.GtGtGtEqToken: js.OpAssign, js.BitAndEqToken: js.OpAssign, js.BitXorEqToken: js.OpAssign, js.BitOrEqToken: js.OpAssign, js.ExpToken: js.OpExp, js.MulToken: js.OpExp, js.DivToken: js.OpExp, js.ModToken: js.OpExp, js.AddToken: js.OpMul, js.SubToken: js.OpMul, js.LtLtToken: js.OpAdd, js.GtGtToken: js.OpAdd, js.GtGtGtToken: js.OpAdd, js.LtToken: js.OpShift, js.LtEqToken: js.OpShift, js.GtToken: js.OpShift, js.GtEqToken: js.OpShift, js.InToken: js.OpShift, js.InstanceofToken: js.OpShift, js.EqEqToken: js.OpCompare, js.NotEqToken: js.OpCompare, js.EqEqEqToken: js.OpCompare, js.NotEqEqToken: js.OpCompare, js.BitAndToken: js.OpEquals, js.BitXorToken: js.OpBitAnd, js.BitOrToken: js.OpBitXor, js.AndToken: js.OpAnd, // changes order in AST but not in execution js.OrToken: js.OpOr, // changes order in AST but not in execution js.NullishToken: js.OpBitOr, // or OpCoalesce js.CommaToken: js.OpAssign, } // precedence maps of the operation itself var unaryOpPrecMap = map[js.TokenType]js.OpPrec{ js.PostIncrToken: js.OpUpdate, js.PostDecrToken: js.OpUpdate, js.PreIncrToken: js.OpUpdate, js.PreDecrToken: js.OpUpdate, js.NotToken: js.OpUnary, js.BitNotToken: js.OpUnary, js.TypeofToken: js.OpUnary, js.VoidToken: js.OpUnary, js.DeleteToken: js.OpUnary, js.PosToken: js.OpUnary, js.NegToken: js.OpUnary, js.AwaitToken: js.OpUnary, } var binaryOpPrecMap = map[js.TokenType]js.OpPrec{ js.EqToken: js.OpAssign, js.MulEqToken: js.OpAssign, js.DivEqToken: js.OpAssign, js.ModEqToken: js.OpAssign, js.ExpEqToken: js.OpAssign, js.AddEqToken: js.OpAssign, js.SubEqToken: js.OpAssign, js.LtLtEqToken: js.OpAssign, js.GtGtEqToken: js.OpAssign, js.GtGtGtEqToken: js.OpAssign, js.BitAndEqToken: js.OpAssign, js.BitXorEqToken: js.OpAssign, js.BitOrEqToken: js.OpAssign, js.ExpToken: js.OpExp, js.MulToken: js.OpMul, js.DivToken: js.OpMul, js.ModToken: js.OpMul, js.AddToken: js.OpAdd, js.SubToken: js.OpAdd, js.LtLtToken: js.OpShift, js.GtGtToken: js.OpShift, js.GtGtGtToken: js.OpShift, js.LtToken: js.OpCompare, js.LtEqToken: js.OpCompare, js.GtToken: js.OpCompare, js.GtEqToken: js.OpCompare, js.InToken: js.OpCompare, js.InstanceofToken: js.OpCompare, js.EqEqToken: js.OpEquals, js.NotEqToken: js.OpEquals, js.EqEqEqToken: js.OpEquals, js.NotEqEqToken: js.OpEquals, js.BitAndToken: js.OpBitAnd, js.BitXorToken: js.OpBitXor, js.BitOrToken: js.OpBitOr, js.AndToken: js.OpAnd, js.OrToken: js.OpOr, js.NullishToken: js.OpCoalesce, js.CommaToken: js.OpExpr, } func exprPrec(i js.IExpr) js.OpPrec { switch expr := i.(type) { case *js.Var, *js.LiteralExpr, *js.ArrayExpr, *js.ObjectExpr, *js.FuncDecl, *js.ClassDecl: return js.OpPrimary case *js.UnaryExpr: return unaryOpPrecMap[expr.Op] case *js.BinaryExpr: return binaryOpPrecMap[expr.Op] case *js.NewExpr: if expr.Args == nil { return js.OpNew } return js.OpMember case *js.TemplateExpr: if expr.Tag == nil { return js.OpPrimary } return expr.Prec case *js.DotExpr: return expr.Prec case *js.IndexExpr: return expr.Prec case *js.NewTargetExpr, *js.ImportMetaExpr: return js.OpMember case *js.CallExpr: return js.OpCall case *js.CondExpr, *js.YieldExpr, *js.ArrowFunc: return js.OpAssign case *js.GroupExpr: return exprPrec(expr.X) } 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) if _, ok := i.(*js.GroupExpr); !ok && precInside < prec && (precInside != js.OpCoalesce || prec != js.OpBitOr) { return &js.GroupExpr{X: i} } return i } // TODO: use in more cases func condExpr(cond, x, y js.IExpr) js.IExpr { if comma, ok := cond.(*js.CommaExpr); ok { comma.List[len(comma.List)-1] = &js.CondExpr{ Cond: groupExpr(comma.List[len(comma.List)-1], js.OpCoalesce), X: groupExpr(x, js.OpAssign), Y: groupExpr(y, js.OpAssign), } return comma } return &js.CondExpr{ Cond: groupExpr(cond, js.OpCoalesce), X: groupExpr(x, js.OpAssign), Y: groupExpr(y, js.OpAssign), } } func commaExpr(x, y js.IExpr) js.IExpr { comma, ok := x.(*js.CommaExpr) if !ok { comma = &js.CommaExpr{List: []js.IExpr{x}} } if comma2, ok := y.(*js.CommaExpr); ok { comma.List = append(comma.List, comma2.List...) } else { comma.List = append(comma.List, y) } return comma } func innerExpr(i js.IExpr) js.IExpr { for { if group, ok := i.(*js.GroupExpr); ok { i = group.X } else { return i } } } func finalExpr(i js.IExpr) js.IExpr { i = innerExpr(i) if comma, ok := i.(*js.CommaExpr); ok { i = comma.List[len(comma.List)-1] } if binary, ok := i.(*js.BinaryExpr); ok && binary.Op == js.EqToken { i = binary.X // return first } 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 { ret, _ := isFalsy(unary.X) return ret } return false } 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 { ret, _ := isTruthy(unary.X) return ret } return false } 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()) } } // 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, false, false } func isUndefinedOrNull(i js.IExpr) bool { i = innerExpr(i) if lit, ok := i.(*js.LiteralExpr); ok { return lit.TokenType == js.NullToken } return isUndefined(i) } 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 } } else if unary, ok := i.(*js.UnaryExpr); ok && unary.Op == js.VoidToken { return !hasSideEffects(unary.X) } return false } // returns whether truthy and whether it could be coerced to a boolean (i.e. when returns (false,true) this means it is falsy) func isTruthy(i js.IExpr) (bool, bool) { if falsy, ok := isFalsy(i); ok { return !falsy, true } return false, false } // returns whether falsy and whether it could be coerced to a boolean (i.e. when returns (false,true) this means it is truthy) func isFalsy(i js.IExpr) (bool, bool) { negated := false group, isGroup := i.(*js.GroupExpr) unary, isUnary := i.(*js.UnaryExpr) for isGroup || isUnary && unary.Op == js.NotToken { if isGroup { i = group.X } else { i = unary.X negated = !negated } group, isGroup = i.(*js.GroupExpr) unary, isUnary = i.(*js.UnaryExpr) } if lit, ok := i.(*js.LiteralExpr); ok { tt := lit.TokenType d := lit.Data if tt == js.FalseToken || tt == js.NullToken || tt == js.StringToken && len(lit.Data) == 0 { return !negated, true // falsy } else if tt == js.TrueToken || tt == js.StringToken { return negated, true // truthy } else if tt == js.DecimalToken || tt == js.BinaryToken || tt == js.OctalToken || tt == js.HexadecimalToken || tt == js.BigIntToken { for _, c := range d { if c == 'e' || c == 'E' || c == 'n' { break } else if c != '0' && c != '.' && c != 'x' && c != 'X' && c != 'b' && c != 'B' && c != 'o' && c != 'O' { return negated, true // truthy } } return !negated, true // falsy } } else if isUndefined(i) { return !negated, true // falsy } else if v, ok := i.(*js.Var); ok && bytes.Equal(v.Name(), nanBytes) { return !negated, true // falsy } return false, false // unknown } func isBooleanExpr(expr js.IExpr) bool { if unaryExpr, ok := expr.(*js.UnaryExpr); ok { return unaryExpr.Op == js.NotToken } else if binaryExpr, ok := expr.(*js.BinaryExpr); ok { op := binaryOpPrecMap[binaryExpr.Op] if op == js.OpAnd || op == js.OpOr { return isBooleanExpr(binaryExpr.X) && isBooleanExpr(binaryExpr.Y) } return op == js.OpCompare || op == js.OpEquals } else if litExpr, ok := expr.(*js.LiteralExpr); ok { return litExpr.TokenType == js.TrueToken || litExpr.TokenType == js.FalseToken } else if groupExpr, ok := expr.(*js.GroupExpr); ok { return isBooleanExpr(groupExpr.X) } return false } func invertBooleanOp(op js.TokenType) js.TokenType { if op == js.EqEqToken { return js.NotEqToken } else if op == js.NotEqToken { return js.EqEqToken } else if op == js.EqEqEqToken { return js.NotEqEqToken } else if op == js.NotEqEqToken { return js.EqEqEqToken } return js.ErrorToken } func optimizeBooleanExpr(expr js.IExpr, invert bool, prec js.OpPrec) js.IExpr { if invert { // unary !(boolean) has already been handled if binaryExpr, ok := expr.(*js.BinaryExpr); ok && binaryOpPrecMap[binaryExpr.Op] == js.OpEquals { binaryExpr.Op = invertBooleanOp(binaryExpr.Op) return expr } else { return optimizeUnaryExpr(&js.UnaryExpr{js.NotToken, groupExpr(expr, js.OpUnary)}, prec) } } else if isBooleanExpr(expr) { return groupExpr(expr, prec) } else { return &js.UnaryExpr{js.NotToken, &js.UnaryExpr{js.NotToken, groupExpr(expr, js.OpUnary)}} } } func optimizeUnaryExpr(expr *js.UnaryExpr, prec js.OpPrec) js.IExpr { if expr.Op == js.NotToken { invert := true var expr2 js.IExpr = expr.X for { if unary, ok := expr2.(*js.UnaryExpr); ok && unary.Op == js.NotToken { invert = !invert expr2 = unary.X } else if group, ok := expr2.(*js.GroupExpr); ok { expr2 = group.X } else { break } } if !invert && isBooleanExpr(expr2) { return groupExpr(expr2, prec) } else if binary, ok := expr2.(*js.BinaryExpr); ok && invert { if binaryOpPrecMap[binary.Op] == js.OpEquals { binary.Op = invertBooleanOp(binary.Op) return groupExpr(binary, prec) } else if binary.Op == js.AndToken || binary.Op == js.OrToken { op := js.AndToken if binary.Op == js.AndToken { op = js.OrToken } precInside := binaryOpPrecMap[op] needsGroup := precInside < prec && (precInside != js.OpCoalesce || prec != js.OpBitOr) // rewrite !(a||b) to !a&&!b // rewrite !(a==0||b==0) to a!=0&&b!=0 score := 3 // savings if rewritten (group parentheses and not-token) if needsGroup { score -= 2 } score -= 2 // add two not-tokens for left and right // == and === can become != and !== var isEqX, isEqY bool if binaryExpr, ok := binary.X.(*js.BinaryExpr); ok && binaryOpPrecMap[binaryExpr.Op] == js.OpEquals { score += 1 isEqX = true } if binaryExpr, ok := binary.Y.(*js.BinaryExpr); ok && binaryOpPrecMap[binaryExpr.Op] == js.OpEquals { score += 1 isEqY = true } // add group if it wasn't already there var needsGroupX, needsGroupY bool if !isEqX && binaryLeftPrecMap[binary.Op] <= exprPrec(binary.X) && exprPrec(binary.X) < js.OpUnary { score -= 2 needsGroupX = true } if !isEqY && binaryRightPrecMap[binary.Op] <= exprPrec(binary.Y) && exprPrec(binary.Y) < js.OpUnary { score -= 2 needsGroupY = true } // remove group if op == js.OrToken { if exprPrec(binary.X) == js.OpOr { score += 2 } if exprPrec(binary.Y) == js.OpAnd { score += 2 } } if 0 < score { binary.Op = op if isEqX { binary.X.(*js.BinaryExpr).Op = invertBooleanOp(binary.X.(*js.BinaryExpr).Op) } if isEqY { binary.Y.(*js.BinaryExpr).Op = invertBooleanOp(binary.Y.(*js.BinaryExpr).Op) } if needsGroupX { binary.X = &js.GroupExpr{binary.X} } if needsGroupY { binary.Y = &js.GroupExpr{binary.Y} } if !isEqX { binary.X = &js.UnaryExpr{js.NotToken, binary.X} } if !isEqY { binary.Y = &js.UnaryExpr{js.NotToken, binary.Y} } if needsGroup { return &js.GroupExpr{binary} } return binary } } } } return expr } func (m *jsMinifier) optimizeCondExpr(expr *js.CondExpr, prec js.OpPrec) js.IExpr { // remove double negative !! in condition, or switch cases for single negative ! if unary1, ok := expr.Cond.(*js.UnaryExpr); ok && unary1.Op == js.NotToken { if unary2, ok := unary1.X.(*js.UnaryExpr); ok && unary2.Op == js.NotToken { if isBooleanExpr(unary2.X) { expr.Cond = unary2.X } } else { expr.Cond = unary1.X expr.X, expr.Y = expr.Y, expr.X } } finalCond := finalExpr(expr.Cond) if truthy, ok := isTruthy(expr.Cond); truthy && ok { // if condition is truthy return expr.X } else if !truthy && ok { // if condition is falsy return expr.Y } else if isEqualExpr(finalCond, expr.X) && (exprPrec(finalCond) < js.OpAssign || binaryLeftPrecMap[js.OrToken] <= exprPrec(finalCond)) && (exprPrec(expr.Y) < js.OpAssign || binaryRightPrecMap[js.OrToken] <= exprPrec(expr.Y)) { // if condition is equal to true body // for higher prec we need to add group parenthesis, and for lower prec we have parenthesis anyways. This only is shorter if len(expr.X) >= 3. isEqualExpr only checks for literal variables, which is a name will be minified to a one or two character name. return &js.BinaryExpr{js.OrToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.OrToken]), expr.Y} } else if isEqualExpr(finalCond, expr.Y) && (exprPrec(finalCond) < js.OpAssign || binaryLeftPrecMap[js.AndToken] <= exprPrec(finalCond)) && (exprPrec(expr.X) < js.OpAssign || binaryRightPrecMap[js.AndToken] <= exprPrec(expr.X)) { // if condition is equal to false body // for higher prec we need to add group parenthesis, and for lower prec we have parenthesis anyways. This only is shorter if len(expr.X) >= 3. isEqualExpr only checks for literal variables, which is a name will be minified to a one or two character name. return &js.BinaryExpr{js.AndToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.AndToken]), expr.X} } 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 nullishExpr, ok := toNullishExpr(expr); ok && m.o.minVersion(2020) { // no need to check whether left/right need to add groups, as the space saving is always more return nullishExpr } else { callX, isCallX := expr.X.(*js.CallExpr) callY, isCallY := expr.Y.(*js.CallExpr) if isCallX && isCallY && len(callX.Args.List) == 1 && len(callY.Args.List) == 1 && !callX.Args.List[0].Rest && !callY.Args.List[0].Rest && isEqualExpr(callX.X, callY.X) { expr.X = callX.Args.List[0].Value expr.Y = callY.Args.List[0].Value return &js.CallExpr{callX.X, js.Args{[]js.Arg{{expr, false}}}, false} // recompress the conditional expression inside } // shorten when true and false bodies are true and false trueX, falseX := isTrue(expr.X), isFalse(expr.X) trueY, falseY := isTrue(expr.Y), isFalse(expr.Y) if trueX && falseY || falseX && trueY { return optimizeBooleanExpr(expr.Cond, falseX, prec) } else if trueX || trueY { // trueX != trueY cond := optimizeBooleanExpr(expr.Cond, trueY, binaryLeftPrecMap[js.OrToken]) if trueY { return &js.BinaryExpr{js.OrToken, cond, groupExpr(expr.X, binaryRightPrecMap[js.OrToken])} } else { return &js.BinaryExpr{js.OrToken, cond, groupExpr(expr.Y, binaryRightPrecMap[js.OrToken])} } } else if falseX || falseY { // falseX != falseY cond := optimizeBooleanExpr(expr.Cond, falseX, binaryLeftPrecMap[js.AndToken]) if falseX { return &js.BinaryExpr{js.AndToken, cond, groupExpr(expr.Y, binaryRightPrecMap[js.AndToken])} } else { return &js.BinaryExpr{js.AndToken, cond, groupExpr(expr.X, binaryRightPrecMap[js.AndToken])} } } else if condExpr, ok := expr.X.(*js.CondExpr); ok && isEqualExpr(expr.Y, condExpr.Y) { // nested conditional expression with same false bodies return &js.CondExpr{&js.BinaryExpr{js.AndToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.AndToken]), groupExpr(condExpr.Cond, binaryRightPrecMap[js.AndToken])}, condExpr.X, expr.Y} } else if prec <= js.OpExpr { // regular conditional expression // convert (a,b)?c:d => a,b?c:d if group, ok := expr.Cond.(*js.GroupExpr); ok { if comma, ok := group.X.(*js.CommaExpr); ok && js.OpCoalesce <= exprPrec(comma.List[len(comma.List)-1]) { expr.Cond = comma.List[len(comma.List)-1] comma.List[len(comma.List)-1] = expr return comma // recompress the conditional expression inside } } } } return expr } func isHexDigit(b byte) bool { return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F' } func mergeBinaryExpr(expr *js.BinaryExpr) { // merge string concatenations which may be intertwined with other additions var ok bool for expr.Op == js.AddToken { if lit, ok := expr.Y.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken { left := expr strings := []*js.LiteralExpr{lit} n := len(lit.Data) - 2 for left.Op == js.AddToken { if 50 < len(strings) { return // limit recursion } if lit, ok := left.X.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken { strings = append(strings, lit) n += len(lit.Data) - 2 left.X = nil } else if newLeft, ok := left.X.(*js.BinaryExpr); ok { if lit, ok := newLeft.Y.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken { strings = append(strings, lit) n += len(lit.Data) - 2 left = newLeft continue } } break } if 1 < len(strings) { // unescaped quotes will be repaired in minifyString later on b := make([]byte, 0, n+2) b = append(b, strings[len(strings)-1].Data[:len(strings[len(strings)-1].Data)-1]...) for i := len(strings) - 2; 0 < i; i-- { b = append(b, strings[i].Data[1:len(strings[i].Data)-1]...) } b = append(b, strings[0].Data[1:]...) b[len(b)-1] = b[0] expr.X = left.X expr.Y.(*js.LiteralExpr).Data = b } } if expr, ok = expr.X.(*js.BinaryExpr); !ok { break } } } func minifyString(b []byte, allowTemplate bool) []byte { if len(b) < 3 { return []byte("\"\"") } // switch quotes if more optimal singleQuotes := 0 doubleQuotes := 0 backtickQuotes := 0 newlines := 0 dollarSigns := 0 notEscapes := false for i := 1; i < len(b)-1; i++ { if b[i] == '\'' { singleQuotes++ } else if b[i] == '"' { doubleQuotes++ } else if b[i] == '`' { backtickQuotes++ } else if b[i] == '$' { dollarSigns++ } else if b[i] == '\\' && i+1 < len(b) { if b[i+1] == 'n' || b[i+1] == 'r' { newlines++ } else if '1' <= b[i+1] && b[i+1] <= '9' || b[i+1] == '0' && i+2 < len(b) && '0' <= b[i+2] && b[i+2] <= '9' { notEscapes = true } } } quote := byte('"') // default to " for better GZIP compression quotes := singleQuotes if doubleQuotes < singleQuotes { quote = byte('"') quotes = doubleQuotes } else if singleQuotes < doubleQuotes { quote = byte('\'') } if allowTemplate && !notEscapes && backtickQuotes+dollarSigns < quotes+newlines { quote = byte('`') } b[0] = quote b[len(b)-1] = quote // strip unnecessary escapes return replaceEscapes(b, quote, 1, 1) } func replaceEscapes(b []byte, quote byte, prefix, suffix int) []byte { // strip unnecessary escapes j := 0 start := 0 for i := prefix; i < len(b)-suffix-1; i++ { if c := b[i]; c == '\\' { c = b[i+1] if c == quote || c == '\\' || quote != '`' && (c == 'n' || c == 'r') || c == '0' && (len(b)-suffix <= i+2 || b[i+2] < '0' || '7' < b[i+2]) { // keep escape sequence i++ continue } n := 1 // number of characters to skip if c == '\n' || c == '\r' || c == 0xE2 && i+3 < len(b)-1 && b[i+2] == 0x80 && (b[i+3] == 0xA8 || b[i+3] == 0xA9) { // line continuations if c == 0xE2 { n = 4 } else if c == '\r' && i+2 < len(b)-1 && b[i+2] == '\n' { n = 3 } else { n = 2 } } else if c == 'x' { if i+3 < len(b)-1 && isHexDigit(b[i+2]) && b[i+2] < '8' && isHexDigit(b[i+3]) && (!(b[i+2] == '0' && b[i+3] == '0') || i+3 == len(b) || b[i+3] != '\\' && (b[i+3] < '0' && '7' < b[i+3])) { // don't convert \x00 to \0 if it may be an octal number // hexadecimal escapes _, _ = hex.Decode(b[i:i+1:i+1], b[i+2:i+4]) n = 4 if b[i] == '\\' || b[i] == quote || b[i] == '\n' || b[i] == '\r' || b[i] == 0 { if b[i] == '\n' { b[i+1] = 'n' } else if b[i] == '\r' { b[i+1] = 'r' } else { b[i+1] = b[i] } b[i] = '\\' i++ n-- } i++ n-- } else { i++ continue } } else if c == 'u' && i+2 < len(b) { l := i + 2 if b[i+2] == '{' { l++ } r := l for ; r < len(b) && (b[i+2] == '{' || r < l+4); r++ { if b[r] < '0' || '9' < b[r] && b[r] < 'A' || 'F' < b[r] && b[r] < 'a' || 'f' < b[r] { break } } if b[i+2] == '{' && (6 < r-l || len(b) <= r || b[r] != '}') || b[i+2] != '{' && r-l != 4 { i++ continue } num, err := stdStrconv.ParseInt(string(b[l:r]), 16, 32) if err != nil || 0x10FFFF <= num { i++ continue } n = 2 + r - l if b[i+2] == '{' { n += 2 } if num == 0 { // don't convert NULL to literal NULL (gives JS parsing problems) if r == len(b) || b[r] != '\\' && (b[r] < '0' && '7' < b[r]) { b[i+1] = '0' i += 2 n -= 2 } else { // don't convert NULL to \0 (may be an octal number) b[i+1] = 'x' b[i+2] = '0' b[i+3] = '0' i += 4 n -= 4 } } else { // decode unicode character to UTF-8 and put at the end of the escape sequence // then skip the first part of the escape sequence until the decoded character m := utf8.RuneLen(rune(num)) if m == -1 { i++ continue } utf8.EncodeRune(b[i:], rune(num)) i += m n -= m } } else if '0' <= c && c <= '7' { // octal escapes (legacy), \0 already handled num := c - '0' n++ if i+2 < len(b)-1 && '0' <= b[i+2] && b[i+2] <= '7' { num = num*8 + b[i+2] - '0' n++ if num < 32 && i+3 < len(b)-1 && '0' <= b[i+3] && b[i+3] <= '7' { num = num*8 + b[i+3] - '0' n++ } } b[i] = num if num == 0 || num == '\\' || num == quote || num == '\n' || num == '\r' { if num == 0 { b[i+1] = '0' } else if num == '\n' { b[i+1] = 'n' } else if num == '\r' { b[i+1] = 'r' } else { b[i+1] = b[i] } b[i] = '\\' i++ n-- } i++ n-- } else if c == 'n' { b[i] = '\n' // only for template literals i++ } else if c == 'r' { b[i] = '\r' // only for template literals i++ } else if c == 't' { b[i] = '\t' i++ } else if c == 'f' { b[i] = '\f' i++ } else if c == 'v' { b[i] = '\v' i++ } else if c == 'b' { b[i] = '\b' i++ } // remove unnecessary escape character, anything but 0x00, 0x0A, 0x0D, \, ' or " if start != 0 { j += copy(b[j:], b[start:i]) } else { j = i } start = i + n i += n - 1 } else if c == quote || c == '$' && quote == '`' && (i+1 < len(b) && b[i+1] == '{' || i+2 < len(b) && b[i+1] == '\\' && b[i+2] == '{') { // may not be escaped properly when changing quotes if j < start { // avoid append j += copy(b[j:], b[start:i]) b[j] = '\\' j++ start = i } else { b = append(append(b[:i], '\\'), b[i:]...) i++ b[i] = c // was overwritten above } } else if c == '<' && 9 <= len(b)-1-i { if b[i+1] == '\\' && 10 <= len(b)-1-i && bytes.Equal(b[i+2:i+10], []byte("/script>")) { i += 9 } else if bytes.Equal(b[i+1:i+9], []byte("/script>")) { i++ if j < start { // avoid append j += copy(b[j:], b[start:i]) b[j] = '\\' j++ start = i } else { b = append(append(b[:i], '\\'), b[i:]...) i++ b[i] = '/' // was overwritten above } } } } if start != 0 { j += copy(b[j:], b[start:]) return b[:j] } return b } var regexpEscapeTable = [256]bool{ // ASCII false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, // $ true, true, true, true, false, false, true, true, // (, ), *, +, ., / true, true, true, true, true, true, true, true, // 0, 1, 2, 3, 4, 5, 6, 7 true, true, false, false, false, false, false, true, // 8, 9, ? false, false, true, false, true, false, false, false, // B, D false, false, false, false, false, false, false, false, true, false, false, true, false, false, false, true, // P, S, W false, false, false, true, true, true, true, false, // [, \, ], ^ false, false, true, true, true, false, true, false, // b, c, d, f false, false, false, true, false, false, true, false, // k, n true, false, true, true, true, true, true, true, // p, r, s, t, u, v, w true, false, false, true, true, true, false, false, // x, {, |, } // non-ASCII false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, } var regexpClassEscapeTable = [256]bool{ // ASCII false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, // 0, 1, 2, 3, 4, 5, 6, 7 true, true, false, false, false, false, false, false, // 8, 9 false, false, false, false, true, false, false, false, // D false, false, false, false, false, false, false, false, true, false, false, true, false, false, false, true, // P, S, W false, false, false, false, true, true, false, false, // \, ] false, false, true, true, true, false, true, false, // b, c, d, f false, false, false, false, false, false, true, false, // n true, false, true, true, true, true, true, true, // p, r, s, t, u, v, w true, false, false, false, false, false, false, false, // x // non-ASCII false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, } func minifyRegExp(b []byte) []byte { inClass := false afterDash := 0 iClass := 0 for i := 1; i < len(b)-1; i++ { if inClass { afterDash++ } if b[i] == '\\' { c := b[i+1] escape := true if inClass { escape = regexpClassEscapeTable[c] || c == '-' && 2 < afterDash && i+2 < len(b) && b[i+2] != ']' || c == '^' && i == iClass+1 } else { escape = regexpEscapeTable[c] } if !escape { b = append(b[:i], b[i+1:]...) if inClass && 2 < afterDash && c == '-' { afterDash = 0 } else if inClass && c == '^' { afterDash = 1 } } else { i++ } } else if b[i] == '[' { if b[i+1] == '^' { i++ } afterDash = 1 inClass = true iClass = i } else if inClass && b[i] == ']' { inClass = false } else if b[i] == '/' { break } else if inClass && 2 < afterDash && b[i] == '-' { afterDash = 0 } } return b } func removeUnderscores(b []byte) []byte { for i := 0; i < len(b); i++ { if b[i] == '_' { b = append(b[:i], b[i+1:]...) i-- } } return b } func decimalNumber(b []byte, prec int) []byte { b = removeUnderscores(b) return minify.Number(b, prec) } func binaryNumber(b []byte, prec int) []byte { b = removeUnderscores(b) if len(b) <= 2 || 65 < len(b) { return b } var n int64 for _, c := range b[2:] { n *= 2 n += int64(c - '0') } i := strconv.LenInt(n) - 1 b = b[:i+1] for 0 <= i { b[i] = byte('0' + n%10) n /= 10 i-- } return minify.Number(b, prec) } func octalNumber(b []byte, prec int) []byte { b = removeUnderscores(b) if len(b) <= 2 || 23 < len(b) { return b } var n int64 for _, c := range b[2:] { n *= 8 n += int64(c - '0') } i := strconv.LenInt(n) - 1 b = b[:i+1] for 0 <= i { b[i] = byte('0' + n%10) n /= 10 i-- } return minify.Number(b, prec) } func hexadecimalNumber(b []byte, prec int) []byte { b = removeUnderscores(b) if len(b) <= 2 || 12 < len(b) || len(b) == 12 && ('D' < b[2] && b[2] <= 'F' || 'd' < b[2]) { return b } var n int64 for _, c := range b[2:] { n *= 16 if c <= '9' { n += int64(c - '0') } else if c <= 'F' { n += 10 + int64(c-'A') } else { n += 10 + int64(c-'a') } } i := strconv.LenInt(n) - 1 b = b[:i+1] for 0 <= i { b[i] = byte('0' + n%10) n /= 10 i-- } return minify.Number(b, prec) }