diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2024-10-25 00:47:47 +0200 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2024-10-25 00:47:47 +0200 |
| commit | c6cc0108ca7738023b45e0eeac0fa2390532dd93 (patch) | |
| tree | 36890e6cd3091bbab8efbe686cc56f467f645bfd /vendor/github.com/tdewolff/minify/v2/js/util.go | |
| parent | 0130404a1dc663d4aa68d780c9bcb23a4243e68d (diff) | |
| download | jbmafp-master.tar.gz | |
Diffstat (limited to 'vendor/github.com/tdewolff/minify/v2/js/util.go')
| -rw-r--r-- | vendor/github.com/tdewolff/minify/v2/js/util.go | 1361 |
1 files changed, 1361 insertions, 0 deletions
diff --git a/vendor/github.com/tdewolff/minify/v2/js/util.go b/vendor/github.com/tdewolff/minify/v2/js/util.go new file mode 100644 index 0000000..6883d93 --- /dev/null +++ b/vendor/github.com/tdewolff/minify/v2/js/util.go @@ -0,0 +1,1361 @@ +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; i++ { + if c := b[i]; c == '\\' { + c = b[i+1] + if c == quote || c == '\\' || quote != '`' && (c == 'n' || c == 'r') || c == '0' && (i+2 == len(b)-1 || 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+3:i+4:i+4], b[i+2:i+4]) + n = 3 + if b[i+3] == '\\' || b[i+3] == quote || b[i+3] == '\n' || b[i+3] == '\r' || b[i+3] == 0 { + if b[i+3] == '\n' { + b[i+3] = 'n' + } else if b[i+3] == '\r' { + b[i+3] = 'r' + } + n-- + b[i+2] = '\\' + } + } 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 || 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 + } + + 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[r-2] = '\\' + n = r - l + } else { + // don't convert NULL to \0 (may be an octal number) + b[r-4] = '\\' + b[r-3] = 'x' + n = r - l - 2 + } + } 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 + n = 2 + r - l + if b[i+2] == '{' { + n += 2 + } + m := utf8.RuneLen(rune(num)) + if m == -1 { + i++ + continue + } + utf8.EncodeRune(b[i+n-m:], rune(num)) + n -= m + } + } else if '0' <= c && c <= '7' { + // octal escapes (legacy), \0 already handled + num := c - '0' + 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+n] = num + if num == 0 || num == '\\' || num == quote || num == '\n' || num == '\r' { + if num == 0 { + b[i+n] = '0' + } else if num == '\n' { + b[i+n] = 'n' + } else if num == '\r' { + b[i+n] = 'r' + } + n-- + b[i+n] = '\\' + } + } else if c == 'n' { + b[i+1] = '\n' // only for template literals + } else if c == 'r' { + b[i+1] = '\r' // only for template literals + } else if c == 't' { + b[i+1] = '\t' + } else if c == 'f' { + b[i+1] = '\f' + } else if c == 'v' { + b[i+1] = '\v' + } else if c == 'b' { + b[i+1] = '\b' + } + // 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) +} |
