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 | |
| parent | 0130404a1dc663d4aa68d780c9bcb23a4243e68d (diff) | |
| download | jbmafp-master.tar.gz | |
Diffstat (limited to 'vendor/github.com/tdewolff/minify/v2/js')
| -rw-r--r-- | vendor/github.com/tdewolff/minify/v2/js/js.go | 1277 | ||||
| -rw-r--r-- | vendor/github.com/tdewolff/minify/v2/js/stmtlist.go | 341 | ||||
| -rw-r--r-- | vendor/github.com/tdewolff/minify/v2/js/util.go | 1361 | ||||
| -rw-r--r-- | vendor/github.com/tdewolff/minify/v2/js/vars.go | 443 |
4 files changed, 3422 insertions, 0 deletions
diff --git a/vendor/github.com/tdewolff/minify/v2/js/js.go b/vendor/github.com/tdewolff/minify/v2/js/js.go new file mode 100644 index 0000000..1f6cefe --- /dev/null +++ b/vendor/github.com/tdewolff/minify/v2/js/js.go @@ -0,0 +1,1277 @@ +// Package js minifies ECMAScript 2021 following the language specification at https://tc39.es/ecma262/. +package js + +import ( + "bytes" + "io" + + "github.com/tdewolff/minify/v2" + "github.com/tdewolff/parse/v2" + "github.com/tdewolff/parse/v2/js" +) + +type blockType int + +const ( + defaultBlock blockType = iota + functionBlock + iterationBlock +) + +// Minifier is a JS minifier. +type Minifier struct { + Precision int // number of significant digits + KeepVarNames bool + useAlphabetVarNames bool + Version int +} + +func (o *Minifier) minVersion(version int) bool { + return o.Version == 0 || version <= o.Version +} + +// Minify minifies JS data, it reads from r and writes to w. +func Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error { + return (&Minifier{}).Minify(m, w, r, params) +} + +// Minify minifies JS data, it reads from r and writes to w. +func (o *Minifier) Minify(_ *minify.M, w io.Writer, r io.Reader, _ map[string]string) error { + z := parse.NewInput(r) + ast, err := js.Parse(z, js.Options{WhileToFor: true}) + if err != nil { + return err + } + + // license comments + for _, comment := range ast.Comments { + if 3 < len(comment) && comment[2] == '!' { + w.Write(comment) + if comment[1] == '/' { + w.Write(newlineBytes) + } + } else if 2 < len(comment) && comment[0] == '#' && comment[1] == '!' { + w.Write(comment) + } + } + + m := &jsMinifier{ + o: o, + w: w, + renamer: newRenamer(!o.KeepVarNames, !o.useAlphabetVarNames), + } + m.hoistVars(&ast.BlockStmt) + ast.List = optimizeStmtList(ast.List, functionBlock) + for _, item := range ast.List { + m.writeSemicolon() + m.minifyStmt(item) + } + + if _, err := w.Write(nil); err != nil { + return err + } + return nil +} + +type expectExpr int + +const ( + expectAny expectExpr = iota + expectExprStmt // in statement + expectExprBody // in arrow function body +) + +type jsMinifier struct { + o *Minifier + w io.Writer + + prev []byte + needsSemicolon bool // write a semicolon if required + needsSpace bool // write a space if next token is an identifier + expectExpr expectExpr // avoid ambiguous syntax such as an expression starting with function + groupedStmt bool // avoid ambiguous syntax by grouping the expression statement + inFor bool + spaceBefore byte + + renamer *renamer +} + +func (m *jsMinifier) write(b []byte) { + // 0 < len(b) + if m.needsSpace && js.IsIdentifierContinue(b) || m.spaceBefore == b[0] { + m.w.Write(spaceBytes) + } + m.w.Write(b) + m.prev = b + m.needsSpace = false + m.expectExpr = expectAny + m.spaceBefore = 0 +} + +func (m *jsMinifier) writeSpaceAfterIdent() { + // space after identifier and after regular expression (to prevent confusion with its tag) + if js.IsIdentifierEnd(m.prev) || 1 < len(m.prev) && m.prev[0] == '/' { + m.w.Write(spaceBytes) + } +} + +func (m *jsMinifier) writeSpaceBeforeIdent() { + m.needsSpace = true +} + +func (m *jsMinifier) writeSpaceBefore(c byte) { + m.spaceBefore = c +} + +func (m *jsMinifier) requireSemicolon() { + m.needsSemicolon = true +} + +func (m *jsMinifier) writeSemicolon() { + if m.needsSemicolon { + m.w.Write(semicolonBytes) + m.needsSemicolon = false + m.needsSpace = false + } +} + +func (m *jsMinifier) minifyStmt(i js.IStmt) { + switch stmt := i.(type) { + case *js.ExprStmt: + m.expectExpr = expectExprStmt + m.minifyExpr(stmt.Value, js.OpExpr) + if m.groupedStmt { + m.write(closeParenBytes) + m.groupedStmt = false + } + m.requireSemicolon() + case *js.VarDecl: + m.minifyVarDecl(stmt, false) + m.requireSemicolon() + case *js.IfStmt: + hasIf := !isEmptyStmt(stmt.Body) + hasElse := !isEmptyStmt(stmt.Else) + if !hasIf && !hasElse { + break + } + + m.write(ifOpenBytes) + m.minifyExpr(stmt.Cond, js.OpExpr) + m.write(closeParenBytes) + + if !hasIf && hasElse { + m.requireSemicolon() + } else if hasIf { + if hasElse && endsInIf(stmt.Body) { + // prevent: if(a){if(b)c}else d; => if(a)if(b)c;else d; + m.write(openBraceBytes) + m.minifyStmt(stmt.Body) + m.write(closeBraceBytes) + m.needsSemicolon = false + } else { + m.minifyStmt(stmt.Body) + } + } + if hasElse { + m.writeSemicolon() + m.write(elseBytes) + m.writeSpaceBeforeIdent() + m.minifyStmt(stmt.Else) + } + case *js.BlockStmt: + m.renamer.renameScope(stmt.Scope) + m.minifyBlockStmt(stmt) + case *js.ReturnStmt: + m.write(returnBytes) + m.writeSpaceBeforeIdent() + m.minifyExpr(stmt.Value, js.OpExpr) + m.requireSemicolon() + case *js.LabelledStmt: + m.write(stmt.Label) + m.write(colonBytes) + m.minifyStmtOrBlock(stmt.Value, defaultBlock) + case *js.BranchStmt: + m.write(stmt.Type.Bytes()) + if stmt.Label != nil { + m.write(spaceBytes) + m.write(stmt.Label) + } + m.requireSemicolon() + case *js.WithStmt: + m.write(withOpenBytes) + m.minifyExpr(stmt.Cond, js.OpExpr) + m.write(closeParenBytes) + m.minifyStmtOrBlock(stmt.Body, defaultBlock) + case *js.DoWhileStmt: + m.write(doBytes) + m.writeSpaceBeforeIdent() + m.minifyStmtOrBlock(stmt.Body, iterationBlock) + m.writeSemicolon() + m.write(whileOpenBytes) + m.minifyExpr(stmt.Cond, js.OpExpr) + m.write(closeParenBytes) + case *js.WhileStmt: + m.write(whileOpenBytes) + m.minifyExpr(stmt.Cond, js.OpExpr) + m.write(closeParenBytes) + m.minifyStmtOrBlock(stmt.Body, iterationBlock) + case *js.ForStmt: + stmt.Body.List = optimizeStmtList(stmt.Body.List, iterationBlock) + m.renamer.renameScope(stmt.Body.Scope) + m.write(forOpenBytes) + m.inFor = true + if decl, ok := stmt.Init.(*js.VarDecl); ok { + m.minifyVarDecl(decl, true) + } else { + m.minifyExpr(stmt.Init, js.OpLHS) + } + m.inFor = false + m.write(semicolonBytes) + m.minifyExpr(stmt.Cond, js.OpExpr) + m.write(semicolonBytes) + m.minifyExpr(stmt.Post, js.OpExpr) + m.write(closeParenBytes) + m.minifyBlockAsStmt(stmt.Body) + case *js.ForInStmt: + stmt.Body.List = optimizeStmtList(stmt.Body.List, iterationBlock) + m.renamer.renameScope(stmt.Body.Scope) + m.write(forOpenBytes) + m.inFor = true + if decl, ok := stmt.Init.(*js.VarDecl); ok { + m.minifyVarDecl(decl, false) + } else { + m.minifyExpr(stmt.Init, js.OpLHS) + } + m.inFor = false + m.writeSpaceAfterIdent() + m.write(inBytes) + m.writeSpaceBeforeIdent() + m.minifyExpr(stmt.Value, js.OpExpr) + m.write(closeParenBytes) + m.minifyBlockAsStmt(stmt.Body) + case *js.ForOfStmt: + stmt.Body.List = optimizeStmtList(stmt.Body.List, iterationBlock) + m.renamer.renameScope(stmt.Body.Scope) + if stmt.Await { + m.write(forAwaitOpenBytes) + } else { + m.write(forOpenBytes) + } + m.inFor = true + if decl, ok := stmt.Init.(*js.VarDecl); ok { + m.minifyVarDecl(decl, false) + } else { + m.minifyExpr(stmt.Init, js.OpLHS) + } + m.inFor = false + m.writeSpaceAfterIdent() + m.write(ofBytes) + m.writeSpaceBeforeIdent() + m.minifyExpr(stmt.Value, js.OpAssign) + m.write(closeParenBytes) + m.minifyBlockAsStmt(stmt.Body) + case *js.SwitchStmt: + m.write(switchOpenBytes) + m.minifyExpr(stmt.Init, js.OpExpr) + m.write(closeParenOpenBracketBytes) + m.needsSemicolon = false + for i, _ := range stmt.List { + stmt.List[i].List = optimizeStmtList(stmt.List[i].List, defaultBlock) + } + m.renamer.renameScope(stmt.Scope) + for _, clause := range stmt.List { + m.writeSemicolon() + m.write(clause.TokenType.Bytes()) + if clause.Cond != nil { + m.writeSpaceBeforeIdent() + m.minifyExpr(clause.Cond, js.OpExpr) + } + m.write(colonBytes) + for _, item := range clause.List { + m.writeSemicolon() + m.minifyStmt(item) + } + } + m.write(closeBraceBytes) + m.needsSemicolon = false + case *js.ThrowStmt: + m.write(throwBytes) + m.writeSpaceBeforeIdent() + m.minifyExpr(stmt.Value, js.OpExpr) + m.requireSemicolon() + case *js.TryStmt: + m.write(tryBytes) + stmt.Body.List = optimizeStmtList(stmt.Body.List, defaultBlock) + m.renamer.renameScope(stmt.Body.Scope) + m.minifyBlockStmt(stmt.Body) + if stmt.Catch != nil { + m.write(catchBytes) + stmt.Catch.List = optimizeStmtList(stmt.Catch.List, defaultBlock) + if v, ok := stmt.Binding.(*js.Var); ok && v.Uses == 1 && m.o.minVersion(2019) { + stmt.Catch.Scope.Declared = stmt.Catch.Scope.Declared[1:] + stmt.Binding = nil + } + m.renamer.renameScope(stmt.Catch.Scope) + if stmt.Binding != nil { + m.write(openParenBytes) + m.minifyBinding(stmt.Binding) + m.write(closeParenBytes) + } + m.minifyBlockStmt(stmt.Catch) + } + if stmt.Finally != nil { + m.write(finallyBytes) + stmt.Finally.List = optimizeStmtList(stmt.Finally.List, defaultBlock) + m.renamer.renameScope(stmt.Finally.Scope) + m.minifyBlockStmt(stmt.Finally) + } + case *js.FuncDecl: + m.minifyFuncDecl(stmt, false) + case *js.ClassDecl: + m.minifyClassDecl(stmt) + case *js.DebuggerStmt: + m.write(debuggerBytes) + m.requireSemicolon() + case *js.EmptyStmt: + case *js.ImportStmt: + m.write(importBytes) + if stmt.Default != nil { + m.write(spaceBytes) + m.write(stmt.Default) + if len(stmt.List) != 0 { + m.write(commaBytes) + } else if stmt.Default != nil || len(stmt.List) != 0 { + m.write(spaceBytes) + } + } + if len(stmt.List) == 1 && len(stmt.List[0].Name) == 1 && stmt.List[0].Name[0] == '*' { + m.writeSpaceBeforeIdent() + m.minifyAlias(stmt.List[0]) + if stmt.Default != nil || len(stmt.List) != 0 { + m.write(spaceBytes) + } + } else if 0 < len(stmt.List) { + m.write(openBraceBytes) + for i, item := range stmt.List { + if i != 0 { + m.write(commaBytes) + } + m.minifyAlias(item) + } + m.write(closeBraceBytes) + } + if stmt.Default != nil || len(stmt.List) != 0 { + m.write(fromBytes) + } + m.write(minifyString(stmt.Module, false)) + m.requireSemicolon() + case *js.ExportStmt: + m.write(exportBytes) + if stmt.Decl != nil { + if stmt.Default { + m.write(spaceDefaultBytes) + m.writeSpaceBeforeIdent() + m.minifyExpr(stmt.Decl, js.OpAssign) + _, isHoistable := stmt.Decl.(*js.FuncDecl) + _, isClass := stmt.Decl.(*js.ClassDecl) + if !isHoistable && !isClass { + m.requireSemicolon() + } + } else { + m.writeSpaceBeforeIdent() + m.minifyStmt(stmt.Decl.(js.IStmt)) // can only be variable, function, or class decl + } + } else { + if len(stmt.List) == 1 && (len(stmt.List[0].Name) == 1 && stmt.List[0].Name[0] == '*' || stmt.List[0].Name == nil && len(stmt.List[0].Binding) == 1 && stmt.List[0].Binding[0] == '*') { + m.writeSpaceBeforeIdent() + m.minifyAlias(stmt.List[0]) + if stmt.Module != nil && stmt.List[0].Name != nil { + m.write(spaceBytes) + } + } else if 0 < len(stmt.List) { + m.write(openBraceBytes) + for i, item := range stmt.List { + if i != 0 { + m.write(commaBytes) + } + m.minifyAlias(item) + } + m.write(closeBraceBytes) + } + if stmt.Module != nil { + m.write(fromBytes) + m.write(minifyString(stmt.Module, false)) + } + m.requireSemicolon() + } + case *js.DirectivePrologueStmt: + stmt.Value[0] = '"' + stmt.Value[len(stmt.Value)-1] = '"' + m.write(stmt.Value) + m.requireSemicolon() + } +} + +func (m *jsMinifier) minifyBlockStmt(stmt *js.BlockStmt) { + m.write(openBraceBytes) + m.needsSemicolon = false + for _, item := range stmt.List { + m.writeSemicolon() + m.minifyStmt(item) + } + m.write(closeBraceBytes) + m.needsSemicolon = false +} + +func (m *jsMinifier) minifyBlockAsStmt(blockStmt *js.BlockStmt) { + // minify block when statement is expected, i.e. semicolon if empty or remove braces for single statement + // assume we already renamed the scope + hasLexicalVars := false + for _, v := range blockStmt.Scope.Declared[blockStmt.Scope.NumForDecls:] { + if v.Decl == js.LexicalDecl { + hasLexicalVars = true + break + } + } + if 1 < len(blockStmt.List) || hasLexicalVars { + m.minifyBlockStmt(blockStmt) + } else if len(blockStmt.List) == 1 { + m.minifyStmt(blockStmt.List[0]) + } else { + m.write(semicolonBytes) + m.needsSemicolon = false + } +} + +func (m *jsMinifier) minifyStmtOrBlock(i js.IStmt, blockType blockType) { + // minify stmt or a block + if blockStmt, ok := i.(*js.BlockStmt); ok { + blockStmt.List = optimizeStmtList(blockStmt.List, blockType) + m.renamer.renameScope(blockStmt.Scope) + m.minifyBlockAsStmt(blockStmt) + } else { + // optimizeStmtList can in some cases expand one stmt to two shorter stmts + list := optimizeStmtList([]js.IStmt{i}, blockType) + if len(list) == 1 { + m.minifyStmt(list[0]) + } else if len(list) == 0 { + m.write(semicolonBytes) + m.needsSemicolon = false + } else { + m.minifyBlockStmt(&js.BlockStmt{List: list, Scope: js.Scope{}}) + } + } +} + +func (m *jsMinifier) minifyAlias(alias js.Alias) { + if alias.Name != nil { + if alias.Name[0] == '"' || alias.Name[0] == '\'' { + m.write(minifyString(alias.Name, false)) + } else { + m.write(alias.Name) + } + if !bytes.Equal(alias.Name, starBytes) { + m.write(spaceBytes) + } + m.write(asSpaceBytes) + } + if alias.Binding != nil { + if alias.Binding[0] == '"' || alias.Binding[0] == '\'' { + m.write(minifyString(alias.Binding, false)) + } else { + m.write(alias.Binding) + } + } +} + +func (m *jsMinifier) minifyParams(params js.Params, removeUnused bool) { + // remove unused parameters from the end + j := len(params.List) + if removeUnused && params.Rest == nil { + for ; 0 < j; j-- { + if v, ok := params.List[j-1].Binding.(*js.Var); !ok || ok && 1 < v.Uses { + break + } + } + } + + m.write(openParenBytes) + for i, item := range params.List[:j] { + if i != 0 { + m.write(commaBytes) + } + m.minifyBindingElement(item) + } + if params.Rest != nil { + if len(params.List) != 0 { + m.write(commaBytes) + } + m.write(ellipsisBytes) + m.minifyBinding(params.Rest) + } + m.write(closeParenBytes) +} + +func (m *jsMinifier) minifyArguments(args js.Args) { + m.write(openParenBytes) + for i, item := range args.List { + if i != 0 { + m.write(commaBytes) + } + if item.Rest { + m.write(ellipsisBytes) + } + m.minifyExpr(item.Value, js.OpAssign) + } + m.write(closeParenBytes) +} + +func (m *jsMinifier) minifyVarDecl(decl *js.VarDecl, onlyDefines bool) { + if len(decl.List) == 0 { + return + } else if decl.TokenType == js.ErrorToken { + // remove 'var' when hoisting variables + first := true + for _, item := range decl.List { + if item.Default != nil || !onlyDefines { + if !first { + m.write(commaBytes) + } + m.minifyBindingElement(item) + first = false + } + } + } else { + if decl.TokenType == js.VarToken && len(decl.List) <= 10000 { + // move single var decls forward and order for GZIP optimization + start := 0 + if _, ok := decl.List[0].Binding.(*js.Var); !ok { + start++ + } + for i := 0; i < len(decl.List); i++ { + item := decl.List[i] + if v, ok := item.Binding.(*js.Var); ok && item.Default == nil && len(v.Data) == 1 { + for j := start; j < len(decl.List); j++ { + if v2, ok := decl.List[j].Binding.(*js.Var); ok && decl.List[j].Default == nil && len(v2.Data) == 1 { + if m.renamer.identOrder[v2.Data[0]] < m.renamer.identOrder[v.Data[0]] { + continue + } else if m.renamer.identOrder[v2.Data[0]] == m.renamer.identOrder[v.Data[0]] { + break + } + } + decl.List = append(decl.List[:i], decl.List[i+1:]...) + decl.List = append(decl.List[:j], append([]js.BindingElement{item}, decl.List[j:]...)...) + break + } + } + } + } + + m.write(decl.TokenType.Bytes()) + m.writeSpaceBeforeIdent() + for i, item := range decl.List { + if i != 0 { + m.write(commaBytes) + } + m.minifyBindingElement(item) + } + } +} + +func (m *jsMinifier) minifyFuncDecl(decl *js.FuncDecl, inExpr bool) { + parentRename := m.renamer.rename + m.renamer.rename = !decl.Body.Scope.HasWith && !m.o.KeepVarNames + m.hoistVars(&decl.Body) + decl.Body.List = optimizeStmtList(decl.Body.List, functionBlock) + + if decl.Async { + m.write(asyncSpaceBytes) + } + m.write(functionBytes) + if decl.Generator { + m.write(starBytes) + } + + // TODO: remove function name, really necessary? + //if decl.Name != nil && decl.Name.Uses == 1 { + // scope := decl.Body.Scope + // for i, vorig := range scope.Declared { + // if decl.Name == vorig { + // scope.Declared = append(scope.Declared[:i], scope.Declared[i+1:]...) + // } + // } + //} + + if inExpr { + m.renamer.renameScope(decl.Body.Scope) + } + if decl.Name != nil && (!inExpr || 1 < decl.Name.Uses) { + if !decl.Generator { + m.write(spaceBytes) + } + m.write(decl.Name.Data) + } + if !inExpr { + m.renamer.renameScope(decl.Body.Scope) + } + + m.minifyParams(decl.Params, true) + m.minifyBlockStmt(&decl.Body) + m.renamer.rename = parentRename +} + +func (m *jsMinifier) minifyMethodDecl(decl *js.MethodDecl) { + parentRename := m.renamer.rename + m.renamer.rename = !decl.Body.Scope.HasWith && !m.o.KeepVarNames + m.hoistVars(&decl.Body) + decl.Body.List = optimizeStmtList(decl.Body.List, functionBlock) + + if decl.Static { + m.write(staticBytes) + m.writeSpaceBeforeIdent() + } + if decl.Async { + m.write(asyncBytes) + if decl.Generator { + m.write(starBytes) + } else { + m.writeSpaceBeforeIdent() + } + } else if decl.Generator { + m.write(starBytes) + } else if decl.Get { + m.write(getBytes) + m.writeSpaceBeforeIdent() + } else if decl.Set { + m.write(setBytes) + m.writeSpaceBeforeIdent() + } + m.minifyPropertyName(decl.Name) + m.renamer.renameScope(decl.Body.Scope) + m.minifyParams(decl.Params, !decl.Set) + m.minifyBlockStmt(&decl.Body) + m.renamer.rename = parentRename +} + +func (m *jsMinifier) minifyArrowFunc(decl *js.ArrowFunc) { + parentRename := m.renamer.rename + m.renamer.rename = !decl.Body.Scope.HasWith && !m.o.KeepVarNames + m.hoistVars(&decl.Body) + decl.Body.List = optimizeStmtList(decl.Body.List, functionBlock) + + m.renamer.renameScope(decl.Body.Scope) + if decl.Async { + m.write(asyncBytes) + } + removeParens := false + if decl.Params.Rest == nil && len(decl.Params.List) == 1 && decl.Params.List[0].Default == nil { + if decl.Params.List[0].Binding == nil { + removeParens = true + } else if _, ok := decl.Params.List[0].Binding.(*js.Var); ok { + removeParens = true + } + } + if removeParens { + if decl.Async && decl.Params.List[0].Binding != nil { + // add space after async in: async a => ... + m.write(spaceBytes) + } + m.minifyBindingElement(decl.Params.List[0]) + } else { + parentInFor := m.inFor + m.inFor = false + m.minifyParams(decl.Params, true) + m.inFor = parentInFor + } + m.write(arrowBytes) + removeBraces := false + if 0 < len(decl.Body.List) { + returnStmt, isReturn := decl.Body.List[len(decl.Body.List)-1].(*js.ReturnStmt) + if isReturn && returnStmt.Value != nil { + // merge expression statements to final return statement, remove function body braces + var list []js.IExpr + removeBraces = true + for _, item := range decl.Body.List[:len(decl.Body.List)-1] { + if expr, isExpr := item.(*js.ExprStmt); isExpr { + list = append(list, expr.Value) + } else { + removeBraces = false + break + } + } + if removeBraces { + list = append(list, returnStmt.Value) + expr := list[0] + if 0 < len(list) { + if 1 < len(list) { + expr = &js.CommaExpr{list} + } + expr = &js.GroupExpr{X: expr} + } + m.expectExpr = expectExprBody + m.minifyExpr(expr, js.OpAssign) + if m.groupedStmt { + m.write(closeParenBytes) + m.groupedStmt = false + } + } + } else if isReturn && returnStmt.Value == nil { + // remove empty return + decl.Body.List = decl.Body.List[:len(decl.Body.List)-1] + } + } + if !removeBraces { + m.minifyBlockStmt(&decl.Body) + } + m.renamer.rename = parentRename +} + +func (m *jsMinifier) minifyClassDecl(decl *js.ClassDecl) { + m.write(classBytes) + if decl.Name != nil { + m.write(spaceBytes) + m.write(decl.Name.Data) + } + if decl.Extends != nil { + m.write(spaceExtendsBytes) + m.writeSpaceBeforeIdent() + m.minifyExpr(decl.Extends, js.OpLHS) + } + m.write(openBraceBytes) + m.needsSemicolon = false + for _, item := range decl.List { + m.writeSemicolon() + if item.StaticBlock != nil { + m.write(staticBytes) + m.minifyBlockStmt(item.StaticBlock) + } else if item.Method != nil { + m.minifyMethodDecl(item.Method) + } else { + if item.Static { + m.write(staticBytes) + if !item.Name.IsComputed() && item.Name.Literal.TokenType == js.IdentifierToken { + m.write(spaceBytes) + } + } + m.minifyPropertyName(item.Name) + if item.Init != nil { + m.write(equalBytes) + m.minifyExpr(item.Init, js.OpAssign) + } + m.requireSemicolon() + } + } + m.write(closeBraceBytes) + m.needsSemicolon = false +} + +func (m *jsMinifier) minifyPropertyName(name js.PropertyName) { + if name.IsComputed() { + m.write(openBracketBytes) + m.minifyExpr(name.Computed, js.OpAssign) + m.write(closeBracketBytes) + } else if name.Literal.TokenType == js.StringToken { + m.write(minifyString(name.Literal.Data, false)) + } else { + m.write(name.Literal.Data) + } +} + +func (m *jsMinifier) minifyProperty(property js.Property) { + // property.Name is always set in ObjectLiteral + if property.Spread { + m.write(ellipsisBytes) + } else if v, ok := property.Value.(*js.Var); property.Name != nil && (!ok || !property.Name.IsIdent(v.Name())) { + // add 'old-name:' before BindingName as the latter will be renamed + m.minifyPropertyName(*property.Name) + m.write(colonBytes) + } + m.minifyExpr(property.Value, js.OpAssign) + if property.Init != nil { + m.write(equalBytes) + m.minifyExpr(property.Init, js.OpAssign) + } +} + +func (m *jsMinifier) minifyBindingElement(element js.BindingElement) { + if element.Binding != nil { + parentInFor := m.inFor + m.inFor = false + m.minifyBinding(element.Binding) + m.inFor = parentInFor + if element.Default != nil { + m.write(equalBytes) + m.minifyExpr(element.Default, js.OpAssign) + } + } +} + +func (m *jsMinifier) minifyBinding(ibinding js.IBinding) { + switch binding := ibinding.(type) { + case *js.Var: + m.write(binding.Data) + case *js.BindingArray: + m.write(openBracketBytes) + for i, item := range binding.List { + if i != 0 { + m.write(commaBytes) + } + m.minifyBindingElement(item) + } + if binding.Rest != nil { + if 0 < len(binding.List) { + m.write(commaBytes) + } + m.write(ellipsisBytes) + m.minifyBinding(binding.Rest) + } + m.write(closeBracketBytes) + case *js.BindingObject: + m.write(openBraceBytes) + for i, item := range binding.List { + if i != 0 { + m.write(commaBytes) + } + // item.Key is always set + if item.Key.IsComputed() { + m.minifyPropertyName(*item.Key) + m.write(colonBytes) + } else if v, ok := item.Value.Binding.(*js.Var); !ok || !item.Key.IsIdent(v.Data) { + // add 'old-name:' before BindingName as the latter will be renamed + m.minifyPropertyName(*item.Key) + m.write(colonBytes) + } + m.minifyBindingElement(item.Value) + } + if binding.Rest != nil { + if 0 < len(binding.List) { + m.write(commaBytes) + } + m.write(ellipsisBytes) + m.write(binding.Rest.Data) + } + m.write(closeBraceBytes) + } +} + +func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) { + if cond, ok := i.(*js.CondExpr); ok { + i = m.optimizeCondExpr(cond, prec) + } else if unary, ok := i.(*js.UnaryExpr); ok { + i = optimizeUnaryExpr(unary, prec) + } + + switch expr := i.(type) { + case *js.Var: + for expr.Link != nil { + expr = expr.Link + } + data := expr.Data + if bytes.Equal(data, undefinedBytes) { // TODO: only if not defined + if js.OpUnary < prec { + m.write(groupedVoidZeroBytes) + } else { + m.write(voidZeroBytes) + } + } else if bytes.Equal(data, infinityBytes) { // TODO: only if not defined + if js.OpMul < prec { + m.write(groupedOneDivZeroBytes) + } else { + m.write(oneDivZeroBytes) + } + } else { + m.write(data) + } + case *js.LiteralExpr: + if expr.TokenType == js.DecimalToken { + m.write(decimalNumber(expr.Data, m.o.Precision)) + } else if expr.TokenType == js.BinaryToken { + m.write(binaryNumber(expr.Data, m.o.Precision)) + } else if expr.TokenType == js.OctalToken { + m.write(octalNumber(expr.Data, m.o.Precision)) + } else if expr.TokenType == js.HexadecimalToken { + m.write(hexadecimalNumber(expr.Data, m.o.Precision)) + } else if expr.TokenType == js.TrueToken { + if js.OpUnary < prec { + m.write(groupedNotZeroBytes) + } else { + m.write(notZeroBytes) + } + } else if expr.TokenType == js.FalseToken { + if js.OpUnary < prec { + m.write(groupedNotOneBytes) + } else { + m.write(notOneBytes) + } + } else if expr.TokenType == js.StringToken { + m.write(minifyString(expr.Data, true)) + } else if expr.TokenType == js.RegExpToken { + // </script>/ => < /script>/ + if 0 < len(m.prev) && m.prev[len(m.prev)-1] == '<' && bytes.HasPrefix(expr.Data, regExpScriptBytes) { + m.write(spaceBytes) + } + m.write(minifyRegExp(expr.Data)) + } else { + m.write(expr.Data) + } + case *js.BinaryExpr: + mergeBinaryExpr(expr) + if expr.X == nil { + m.minifyExpr(expr.Y, prec) + break + } + + precLeft := binaryLeftPrecMap[expr.Op] + // convert (a,b)&&c into a,b&&c but not a=(b,c)&&d into a=(b,c&&d) + if prec <= js.OpExpr { + if group, ok := expr.X.(*js.GroupExpr); ok { + if comma, ok := group.X.(*js.CommaExpr); ok && js.OpAnd <= exprPrec(comma.List[len(comma.List)-1]) { + expr.X = group.X + precLeft = js.OpExpr + } + } + } + if expr.Op == js.InstanceofToken || expr.Op == js.InToken { + group := expr.Op == js.InToken && m.inFor + if group { + m.write(openParenBytes) + } + m.minifyExpr(expr.X, precLeft) + m.writeSpaceAfterIdent() + m.write(expr.Op.Bytes()) + m.writeSpaceBeforeIdent() + m.minifyExpr(expr.Y, binaryRightPrecMap[expr.Op]) + if group { + m.write(closeParenBytes) + } + } else { + // TODO: has effect on GZIP? + //if expr.Op == js.EqEqToken || expr.Op == js.NotEqToken || expr.Op == js.EqEqEqToken || expr.Op == js.NotEqEqToken { + // // switch a==const for const==a, such as typeof a=="undefined" for "undefined"==typeof a (GZIP improvement) + // if _, ok := expr.Y.(*js.LiteralExpr); ok { + // expr.X, expr.Y = expr.Y, expr.X + // } + //} + + 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) + if expr.Op == js.GtToken && m.prev[len(m.prev)-1] == '-' { + // 0 < len(m.prev) always + m.write(spaceBytes) + } else if expr.Op == js.EqEqEqToken || expr.Op == js.NotEqEqToken { + if left, ok := expr.X.(*js.UnaryExpr); ok && left.Op == js.TypeofToken { + if right, ok := expr.Y.(*js.LiteralExpr); ok && right.TokenType == js.StringToken { + if expr.Op == js.EqEqEqToken { + expr.Op = js.EqEqToken + } else { + expr.Op = js.NotEqToken + } + } + } else if right, ok := expr.Y.(*js.UnaryExpr); ok && right.Op == js.TypeofToken { + if left, ok := expr.X.(*js.LiteralExpr); ok && left.TokenType == js.StringToken { + if expr.Op == js.EqEqEqToken { + expr.Op = js.EqEqToken + } else { + expr.Op = js.NotEqToken + } + } + } + } + m.write(expr.Op.Bytes()) + if expr.Op == js.AddToken { + // +++ => + ++ + m.writeSpaceBefore('+') + } else if expr.Op == js.SubToken { + // --- => - -- + m.writeSpaceBefore('-') + } else if expr.Op == js.DivToken { + // // => / / + m.writeSpaceBefore('/') + } + m.minifyExpr(expr.Y, binaryRightPrecMap[expr.Op]) + } + case *js.UnaryExpr: + if expr.Op == js.PostIncrToken || expr.Op == js.PostDecrToken { + m.minifyExpr(expr.X, unaryPrecMap[expr.Op]) + m.write(expr.Op.Bytes()) + } else { + isLtNot := expr.Op == js.NotToken && 0 < len(m.prev) && m.prev[len(m.prev)-1] == '<' + m.write(expr.Op.Bytes()) + if expr.Op == js.DeleteToken || expr.Op == js.VoidToken || expr.Op == js.TypeofToken || expr.Op == js.AwaitToken { + m.writeSpaceBeforeIdent() + } else if expr.Op == js.PosToken { + // +++ => + ++ + m.writeSpaceBefore('+') + } else if expr.Op == js.NegToken || isLtNot { + // --- => - -- + // <!-- => <! -- + m.writeSpaceBefore('-') + } else if expr.Op == js.NotToken { + if lit, ok := expr.X.(*js.LiteralExpr); ok && (lit.TokenType == js.StringToken || lit.TokenType == js.RegExpToken) { + // !"string" => !1 + m.write(oneBytes) + break + } else if ok && lit.TokenType == js.DecimalToken { + // !123 => !1 (except for !0) + if num := minify.Number(lit.Data, m.o.Precision); len(num) == 1 && num[0] == '0' { + m.write(zeroBytes) + } else { + m.write(oneBytes) + } + break + } + } + m.minifyExpr(expr.X, unaryPrecMap[expr.Op]) + } + case *js.DotExpr: + if group, ok := expr.X.(*js.GroupExpr); ok { + if lit, ok := group.X.(*js.LiteralExpr); ok && lit.TokenType == js.DecimalToken { + num := minify.Number(lit.Data, m.o.Precision) + isInt := true + for _, c := range num { + if c == '.' || c == 'e' || c == 'E' { + isInt = false + break + } + } + if isInt { + m.write(num) + m.write(dotBytes) + } else { + m.write(num) + } + m.write(dotBytes) + m.write(expr.Y.Data) + break + } + } + if prec < js.OpMember { + m.minifyExpr(expr.X, js.OpCall) + } else { + m.minifyExpr(expr.X, js.OpMember) + } + 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 { + isInteger = false + break + } + } + if isInteger { + // prevent previous integer + m.write(dotBytes) + } + } + m.write(dotBytes) + m.write(expr.Y.Data) + case *js.GroupExpr: + if cond, ok := expr.X.(*js.CondExpr); ok { + expr.X = m.optimizeCondExpr(cond, js.OpExpr) + } + precInside := exprPrec(expr.X) + if prec <= precInside || precInside == js.OpCoalesce && prec == js.OpBitOr { + m.minifyExpr(expr.X, prec) + } else { + parentInFor := m.inFor + m.inFor = false + m.write(openParenBytes) + m.minifyExpr(expr.X, js.OpExpr) + m.write(closeParenBytes) + m.inFor = parentInFor + } + case *js.ArrayExpr: + parentInFor := m.inFor + m.inFor = false + m.write(openBracketBytes) + for i, item := range expr.List { + if i != 0 { + m.write(commaBytes) + } + if item.Spread { + m.write(ellipsisBytes) + } + m.minifyExpr(item.Value, js.OpAssign) + } + if 0 < len(expr.List) && expr.List[len(expr.List)-1].Value == nil { + m.write(commaBytes) + } + m.write(closeBracketBytes) + m.inFor = parentInFor + case *js.ObjectExpr: + parentInFor := m.inFor + m.inFor = false + groupedStmt := m.expectExpr != expectAny + if groupedStmt { + m.write(openParenBracketBytes) + } else { + m.write(openBraceBytes) + } + for i, item := range expr.List { + if i != 0 { + m.write(commaBytes) + } + m.minifyProperty(item) + } + m.write(closeBraceBytes) + if groupedStmt { + m.groupedStmt = true + } + m.inFor = parentInFor + case *js.TemplateExpr: + if expr.Tag != nil { + if prec < js.OpMember { + m.minifyExpr(expr.Tag, js.OpCall) + } else { + m.minifyExpr(expr.Tag, js.OpMember) + } + if expr.Optional { + m.write(optChainBytes) + } + } + parentInFor := m.inFor + m.inFor = false + for _, item := range expr.List { + m.write(replaceEscapes(item.Value, '`', 1, 2)) + m.minifyExpr(item.Expr, js.OpExpr) + } + m.write(replaceEscapes(expr.Tail, '`', 1, 1)) + m.inFor = parentInFor + case *js.NewExpr: + if expr.Args == nil && js.OpLHS < prec && prec != js.OpNew { + m.write(openNewBytes) + m.writeSpaceBeforeIdent() + m.minifyExpr(expr.X, js.OpNew) + m.write(closeParenBytes) + } else { + m.write(newBytes) + m.writeSpaceBeforeIdent() + if expr.Args != nil { + m.minifyExpr(expr.X, js.OpMember) + m.minifyArguments(*expr.Args) + } else { + m.minifyExpr(expr.X, js.OpNew) + } + } + case *js.NewTargetExpr: + m.write(newTargetBytes) + m.writeSpaceBeforeIdent() + case *js.ImportMetaExpr: + if m.expectExpr == expectExprStmt { + m.write(openParenBytes) + m.groupedStmt = true + } + m.write(importMetaBytes) + m.writeSpaceBeforeIdent() + case *js.YieldExpr: + m.write(yieldBytes) + m.writeSpaceBeforeIdent() + if expr.X != nil { + if expr.Generator { + m.write(starBytes) + m.minifyExpr(expr.X, js.OpAssign) + } else if v, ok := expr.X.(*js.Var); !ok || !bytes.Equal(v.Name(), undefinedBytes) { // TODO: only if not defined + m.minifyExpr(expr.X, js.OpAssign) + } + } + case *js.CallExpr: + 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: + if m.expectExpr == expectExprStmt { + if v, ok := expr.X.(*js.Var); ok && bytes.Equal(v.Name(), letBytes) { + m.write(notBytes) + } + } + if prec < js.OpMember { + m.minifyExpr(expr.X, js.OpCall) + } 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) + m.write(lit.Data[1 : len(lit.Data)-1]) + break + } else if isNum := js.AsDecimalLiteral(lit.Data[1 : len(lit.Data)-1]); isNum { + m.write(openBracketBytes) + m.write(minify.Number(lit.Data[1:len(lit.Data)-1], 0)) + m.write(closeBracketBytes) + break + } + } + parentInFor := m.inFor + m.inFor = false + m.write(openBracketBytes) + m.minifyExpr(expr.Y, js.OpExpr) + m.write(closeBracketBytes) + m.inFor = parentInFor + case *js.CondExpr: + m.minifyExpr(expr.Cond, js.OpCoalesce) + m.write(questionBytes) + m.minifyExpr(expr.X, js.OpAssign) + m.write(colonBytes) + m.minifyExpr(expr.Y, js.OpAssign) + case *js.VarDecl: + m.minifyVarDecl(expr, true) // happens in for statement or when vars were hoisted + case *js.FuncDecl: + grouped := m.expectExpr == expectExprStmt && prec != js.OpExpr + if grouped { + m.write(openParenBytes) + } else if m.expectExpr == expectExprStmt { + m.write(notBytes) + } + parentInFor, parentGroupedStmt := m.inFor, m.groupedStmt + m.inFor, m.groupedStmt = false, false + m.minifyFuncDecl(expr, true) + m.inFor, m.groupedStmt = parentInFor, parentGroupedStmt + if grouped { + m.write(closeParenBytes) + } + case *js.ArrowFunc: + parentGroupedStmt := m.groupedStmt + m.groupedStmt = false + m.minifyArrowFunc(expr) + m.groupedStmt = parentGroupedStmt + case *js.MethodDecl: + parentGroupedStmt := m.groupedStmt + m.groupedStmt = false + m.minifyMethodDecl(expr) // only happens in object literal + m.groupedStmt = parentGroupedStmt + case *js.ClassDecl: + if m.expectExpr == expectExprStmt { + m.write(notBytes) + } + parentInFor, parentGroupedStmt := m.inFor, m.groupedStmt + m.inFor, m.groupedStmt = false, false + m.minifyClassDecl(expr) + m.inFor, m.groupedStmt = parentInFor, parentGroupedStmt + case *js.CommaExpr: + for i, item := range expr.List { + if i != 0 { + m.write(commaBytes) + } + m.minifyExpr(item, js.OpAssign) + } + } +} diff --git a/vendor/github.com/tdewolff/minify/v2/js/stmtlist.go b/vendor/github.com/tdewolff/minify/v2/js/stmtlist.go new file mode 100644 index 0000000..a1d3e2e --- /dev/null +++ b/vendor/github.com/tdewolff/minify/v2/js/stmtlist.go @@ -0,0 +1,341 @@ +package js + +import ( + "github.com/tdewolff/parse/v2/js" +) + +func optimizeStmt(i js.IStmt) js.IStmt { + // convert if/else into expression statement, and optimize blocks + if ifStmt, ok := i.(*js.IfStmt); ok { + hasIf := !isEmptyStmt(ifStmt.Body) + hasElse := !isEmptyStmt(ifStmt.Else) + if unaryExpr, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unaryExpr.Op == js.NotToken && hasElse { + ifStmt.Cond = unaryExpr.X + ifStmt.Body, ifStmt.Else = ifStmt.Else, ifStmt.Body + hasIf, hasElse = hasElse, hasIf + } + if !hasIf && !hasElse { + return &js.ExprStmt{Value: ifStmt.Cond} + } else if hasIf && !hasElse { + ifStmt.Body = optimizeStmt(ifStmt.Body) + if X, isExprBody := ifStmt.Body.(*js.ExprStmt); isExprBody { + if unaryExpr, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unaryExpr.Op == js.NotToken { + left := groupExpr(unaryExpr.X, binaryLeftPrecMap[js.OrToken]) + right := groupExpr(X.Value, binaryRightPrecMap[js.OrToken]) + return &js.ExprStmt{&js.BinaryExpr{js.OrToken, left, right}} + } + left := groupExpr(ifStmt.Cond, binaryLeftPrecMap[js.AndToken]) + right := groupExpr(X.Value, binaryRightPrecMap[js.AndToken]) + return &js.ExprStmt{&js.BinaryExpr{js.AndToken, left, right}} + } else if X, isIfStmt := ifStmt.Body.(*js.IfStmt); isIfStmt && isEmptyStmt(X.Else) { + left := groupExpr(ifStmt.Cond, binaryLeftPrecMap[js.AndToken]) + right := groupExpr(X.Cond, binaryRightPrecMap[js.AndToken]) + ifStmt.Cond = &js.BinaryExpr{js.AndToken, left, right} + ifStmt.Body = X.Body + return ifStmt + } + } else if !hasIf && hasElse { + ifStmt.Else = optimizeStmt(ifStmt.Else) + if X, isExprElse := ifStmt.Else.(*js.ExprStmt); isExprElse { + left := groupExpr(ifStmt.Cond, binaryLeftPrecMap[js.OrToken]) + right := groupExpr(X.Value, binaryRightPrecMap[js.OrToken]) + return &js.ExprStmt{&js.BinaryExpr{js.OrToken, left, right}} + } + } else if hasIf && hasElse { + ifStmt.Body = optimizeStmt(ifStmt.Body) + ifStmt.Else = optimizeStmt(ifStmt.Else) + XExpr, isExprBody := ifStmt.Body.(*js.ExprStmt) + YExpr, isExprElse := ifStmt.Else.(*js.ExprStmt) + if isExprBody && isExprElse { + return &js.ExprStmt{condExpr(ifStmt.Cond, XExpr.Value, YExpr.Value)} + } + XReturn, isReturnBody := ifStmt.Body.(*js.ReturnStmt) + YReturn, isReturnElse := ifStmt.Else.(*js.ReturnStmt) + if isReturnBody && isReturnElse { + if XReturn.Value == nil && YReturn.Value == nil { + return &js.ReturnStmt{commaExpr(ifStmt.Cond, &js.UnaryExpr{ + Op: js.VoidToken, + X: &js.LiteralExpr{js.NumericToken, zeroBytes}, + })} + } else if XReturn.Value != nil && YReturn.Value != nil { + return &js.ReturnStmt{condExpr(ifStmt.Cond, XReturn.Value, YReturn.Value)} + } + return ifStmt + } + XThrow, isThrowBody := ifStmt.Body.(*js.ThrowStmt) + YThrow, isThrowElse := ifStmt.Else.(*js.ThrowStmt) + if isThrowBody && isThrowElse { + return &js.ThrowStmt{condExpr(ifStmt.Cond, XThrow.Value, YThrow.Value)} + } + } + } else if decl, ok := i.(*js.VarDecl); ok { + // TODO: remove function name in var name=function name(){} + //for _, item := range decl.List { + // if v, ok := item.Binding.(*js.Var); ok && item.Default != nil { + // if fun, ok := item.Default.(*js.FuncDecl); ok && fun.Name != nil && bytes.Equal(v.Data, fun.Name.Data) { + // scope := fun.Body.Scope + // for i, vorig := range scope.Declared { + // if fun.Name == vorig { + // scope.Declared = append(scope.Declared[:i], scope.Declared[i+1:]...) + // } + // } + // scope.AddUndeclared(v) + // v.Uses += fun.Name.Uses - 1 + // fun.Name.Link = v + // fun.Name = nil + // } + // } + //} + + if decl.TokenType == js.ErrorToken { + // convert hoisted var declaration to expression or empty (if there are no defines) statement + for _, item := range decl.List { + if item.Default != nil { + return &js.ExprStmt{Value: decl} + } + } + return &js.EmptyStmt{} + } + // TODO: remove unused declarations + //for i := 0; i < len(decl.List); i++ { + // if v, ok := decl.List[i].Binding.(*js.Var); ok && v.Uses < 2 { + // decl.List = append(decl.List[:i], decl.List[i+1:]...) + // i-- + // } + //} + //if len(decl.List) == 0 { + // return &js.EmptyStmt{} + //} + return decl + } else if blockStmt, ok := i.(*js.BlockStmt); ok { + // merge body and remove braces if it is not a lexical declaration + blockStmt.List = optimizeStmtList(blockStmt.List, defaultBlock) + if len(blockStmt.List) == 1 { + if _, ok := blockStmt.List[0].(*js.ClassDecl); ok { + return &js.EmptyStmt{} + } else if varDecl, ok := blockStmt.List[0].(*js.VarDecl); ok && varDecl.TokenType != js.VarToken { + // remove let or const declaration in otherwise empty scope, but keep assignments + exprs := []js.IExpr{} + for _, item := range varDecl.List { + if item.Default != nil && hasSideEffects(item.Default) { + exprs = append(exprs, item.Default) + } + } + if len(exprs) == 0 { + return &js.EmptyStmt{} + } else if len(exprs) == 1 { + return &js.ExprStmt{exprs[0]} + } + return &js.ExprStmt{&js.CommaExpr{exprs}} + } + return optimizeStmt(blockStmt.List[0]) + } else if len(blockStmt.List) == 0 { + return &js.EmptyStmt{} + } + return blockStmt + } + return i +} + +func optimizeStmtList(list []js.IStmt, blockType blockType) []js.IStmt { + // merge expression statements as well as if/else statements followed by flow control statements + if len(list) == 0 { + return list + } + j := 0 // write index + for i := 0; i < len(list); i++ { // read index + if ifStmt, ok := list[i].(*js.IfStmt); ok && !isEmptyStmt(ifStmt.Else) { + // if(!a)b;else c => if(a)c; else b + if unary, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unary.Op == js.NotToken && isFlowStmt(lastStmt(ifStmt.Else)) { + ifStmt.Cond = unary.X + ifStmt.Body, ifStmt.Else = ifStmt.Else, ifStmt.Body + } + if isFlowStmt(lastStmt(ifStmt.Body)) { + // if body ends in flow statement (return, throw, break, continue), we can remove the else statement and put its body in the current scope + if blockStmt, ok := ifStmt.Else.(*js.BlockStmt); ok { + blockStmt.Scope.Unscope() + list = append(list[:i+1], append(blockStmt.List, list[i+1:]...)...) + } else { + list = append(list[:i+1], append([]js.IStmt{ifStmt.Else}, list[i+1:]...)...) + } + ifStmt.Else = nil + } + } + + list[i] = optimizeStmt(list[i]) + + if _, ok := list[i].(*js.EmptyStmt); ok { + k := i + 1 + for ; k < len(list); k++ { + if _, ok := list[k].(*js.EmptyStmt); !ok { + break + } + } + list = append(list[:i], list[k:]...) + i-- + continue + } + + if 0 < i { + // merge expression statements with expression, return, and throw statements + if left, ok := list[i-1].(*js.ExprStmt); ok { + if right, ok := list[i].(*js.ExprStmt); ok { + right.Value = commaExpr(left.Value, right.Value) + j-- + } else if returnStmt, ok := list[i].(*js.ReturnStmt); ok && returnStmt.Value != nil { + returnStmt.Value = commaExpr(left.Value, returnStmt.Value) + j-- + } else if throwStmt, ok := list[i].(*js.ThrowStmt); ok { + throwStmt.Value = commaExpr(left.Value, throwStmt.Value) + j-- + } else if forStmt, ok := list[i].(*js.ForStmt); ok { + if varDecl, ok := forStmt.Init.(*js.VarDecl); ok && len(varDecl.List) == 0 || forStmt.Init == nil { + // TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?) + forStmt.Init = left.Value + j-- + } + } else if whileStmt, ok := list[i].(*js.WhileStmt); ok { + // TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?) + var body *js.BlockStmt + if blockStmt, ok := whileStmt.Body.(*js.BlockStmt); ok { + body = blockStmt + } else { + body = &js.BlockStmt{} + body.List = []js.IStmt{whileStmt.Body} + } + list[i] = &js.ForStmt{Init: left.Value, Cond: whileStmt.Cond, Post: nil, Body: body} + j-- + } else if switchStmt, ok := list[i].(*js.SwitchStmt); ok { + switchStmt.Init = commaExpr(left.Value, switchStmt.Init) + j-- + } else if withStmt, ok := list[i].(*js.WithStmt); ok { + withStmt.Cond = commaExpr(left.Value, withStmt.Cond) + j-- + } else if ifStmt, ok := list[i].(*js.IfStmt); ok { + ifStmt.Cond = commaExpr(left.Value, ifStmt.Cond) + j-- + } else if varDecl, ok := list[i].(*js.VarDecl); ok && varDecl.TokenType == js.VarToken { + if merge := mergeVarDeclExprStmt(varDecl, left, true); merge { + j-- + } + } + } else if left, ok := list[i-1].(*js.VarDecl); ok { + if right, ok := list[i].(*js.VarDecl); ok && left.TokenType == right.TokenType { + // merge const and let declarations, or non-hoisted var declarations + right.List = append(left.List, right.List...) + j-- + + // remove from vardecls list of scope + scope := left.Scope.Func + for i, decl := range scope.VarDecls { + if left == decl { + scope.VarDecls = append(scope.VarDecls[:i], scope.VarDecls[i+1:]...) + break + } + } + } else if left.TokenType == js.VarToken { + if exprStmt, ok := list[i].(*js.ExprStmt); ok { + // pull in assignments to variables into the declaration, e.g. var a;a=5 => var a=5 + if merge := mergeVarDeclExprStmt(left, exprStmt, false); merge { + list[i] = list[i-1] + j-- + } + } else if forStmt, ok := list[i].(*js.ForStmt); ok { + // TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?) + if forStmt.Init == nil { + forStmt.Init = left + j-- + } else if decl, ok := forStmt.Init.(*js.VarDecl); ok && decl.TokenType == js.ErrorToken && !hasDefines(decl) { + forStmt.Init = left + j-- + } else if ok && (decl.TokenType == js.VarToken || decl.TokenType == js.ErrorToken) { + // this is the second VarDecl, so we are hoisting var declarations, which means the forInit variables are already in 'left' + mergeVarDecls(left, decl, false) + decl.TokenType = js.VarToken + forStmt.Init = left + j-- + } + } else if whileStmt, ok := list[i].(*js.WhileStmt); ok { + // TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?) + var body *js.BlockStmt + if blockStmt, ok := whileStmt.Body.(*js.BlockStmt); ok { + body = blockStmt + } else { + body = &js.BlockStmt{} + body.List = []js.IStmt{whileStmt.Body} + } + list[i] = &js.ForStmt{Init: left, Cond: whileStmt.Cond, Post: nil, Body: body} + j-- + } + } + } + } + list[j] = list[i] + + // merge if/else with return/throw when followed by return/throw + MergeIfReturnThrow: + if 0 < j { + // separate from expression merging in case of: if(a)return b;b=c;return d + if ifStmt, ok := list[j-1].(*js.IfStmt); ok && isEmptyStmt(ifStmt.Body) != isEmptyStmt(ifStmt.Else) { + // either the if body is empty or the else body is empty. In case where both bodies have return/throw, we already rewrote that if statement to an return/throw statement + if returnStmt, ok := list[j].(*js.ReturnStmt); ok { + if returnStmt.Value == nil { + if left, ok := ifStmt.Body.(*js.ReturnStmt); ok && left.Value == nil { + list[j-1] = &js.ExprStmt{Value: ifStmt.Cond} + } else if left, ok := ifStmt.Else.(*js.ReturnStmt); ok && left.Value == nil { + list[j-1] = &js.ExprStmt{Value: ifStmt.Cond} + } + } else { + if left, ok := ifStmt.Body.(*js.ReturnStmt); ok && left.Value != nil { + returnStmt.Value = condExpr(ifStmt.Cond, left.Value, returnStmt.Value) + list[j-1] = returnStmt + j-- + goto MergeIfReturnThrow + } else if left, ok := ifStmt.Else.(*js.ReturnStmt); ok && left.Value != nil { + returnStmt.Value = condExpr(ifStmt.Cond, returnStmt.Value, left.Value) + list[j-1] = returnStmt + j-- + goto MergeIfReturnThrow + } + } + } else if throwStmt, ok := list[j].(*js.ThrowStmt); ok { + if left, ok := ifStmt.Body.(*js.ThrowStmt); ok { + throwStmt.Value = condExpr(ifStmt.Cond, left.Value, throwStmt.Value) + list[j-1] = throwStmt + j-- + goto MergeIfReturnThrow + } else if left, ok := ifStmt.Else.(*js.ThrowStmt); ok { + throwStmt.Value = condExpr(ifStmt.Cond, throwStmt.Value, left.Value) + list[j-1] = throwStmt + j-- + goto MergeIfReturnThrow + } + } + } + } + j++ + } + + // remove superfluous return or continue + if 0 < j { + if blockType == functionBlock { + if returnStmt, ok := list[j-1].(*js.ReturnStmt); ok { + if returnStmt.Value == nil || isUndefined(returnStmt.Value) { + j-- + } else if commaExpr, ok := returnStmt.Value.(*js.CommaExpr); ok && isUndefined(commaExpr.List[len(commaExpr.List)-1]) { + // rewrite function f(){return a,void 0} => function f(){a} + if len(commaExpr.List) == 2 { + list[j-1] = &js.ExprStmt{Value: commaExpr.List[0]} + } else { + commaExpr.List = commaExpr.List[:len(commaExpr.List)-1] + } + } + } + } else if blockType == iterationBlock { + if branchStmt, ok := list[j-1].(*js.BranchStmt); ok && branchStmt.Type == js.ContinueToken && branchStmt.Label == nil { + j-- + } + } + } + return list[:j] +} 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) +} diff --git a/vendor/github.com/tdewolff/minify/v2/js/vars.go b/vendor/github.com/tdewolff/minify/v2/js/vars.go new file mode 100644 index 0000000..81457c3 --- /dev/null +++ b/vendor/github.com/tdewolff/minify/v2/js/vars.go @@ -0,0 +1,443 @@ +package js + +import ( + "bytes" + "sort" + + "github.com/tdewolff/parse/v2/js" +) + +const identStartLen = 54 +const identContinueLen = 64 + +type renamer struct { + identStart []byte + identContinue []byte + identOrder map[byte]int + reserved map[string]struct{} + rename bool +} + +func newRenamer(rename, useCharFreq bool) *renamer { + reserved := make(map[string]struct{}, len(js.Keywords)) + for name := range js.Keywords { + reserved[name] = struct{}{} + } + identStart := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$") + identContinue := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789") + if useCharFreq { + // sorted based on character frequency of a collection of JS samples + identStart = []byte("etnsoiarclduhmfpgvbjy_wOxCEkASMFTzDNLRPHIBV$WUKqYGXQZJ") + identContinue = []byte("etnsoiarcldu14023hm8f6pg57v9bjy_wOxCEkASMFTzDNLRPHIBV$WUKqYGXQZJ") + } + if len(identStart) != identStartLen || len(identContinue) != identContinueLen { + panic("bad identStart or identContinue lengths") + } + identOrder := map[byte]int{} + for i, c := range identStart { + identOrder[c] = i + } + return &renamer{ + identStart: identStart, + identContinue: identContinue, + identOrder: identOrder, + reserved: reserved, + rename: rename, + } +} + +func (r *renamer) renameScope(scope js.Scope) { + if !r.rename { + return + } + + i := 0 + // keep function argument declaration order to improve GZIP compression + sort.Sort(js.VarsByUses(scope.Declared[scope.NumFuncArgs:])) + for _, v := range scope.Declared { + v.Data = r.getName(v.Data, i) + i++ + for r.isReserved(v.Data, scope.Undeclared) { + v.Data = r.getName(v.Data, i) + i++ + } + } +} + +func (r *renamer) isReserved(name []byte, undeclared js.VarArray) bool { + if 1 < len(name) { // there are no keywords or known globals that are one character long + if _, ok := r.reserved[string(name)]; ok { + return true + } + } + for _, v := range undeclared { + for v.Link != nil { + v = v.Link + } + if bytes.Equal(v.Data, name) { + return true + } + } + return false +} + +func (r *renamer) getIndex(name []byte) int { + index := 0 +NameLoop: + for i := len(name) - 1; 0 <= i; i-- { + chars := r.identContinue + if i == 0 { + chars = r.identStart + index *= identStartLen + } else { + index *= identContinueLen + } + for j, c := range chars { + if name[i] == c { + index += j + continue NameLoop + } + } + return -1 + } + for n := 0; n < len(name)-1; n++ { + offset := identStartLen + for i := 0; i < n; i++ { + offset *= identContinueLen + } + index += offset + } + return index +} + +func (r *renamer) getName(name []byte, index int) []byte { + // Generate new names for variables where the last character is (a-zA-Z$_) and others are (a-zA-Z). + // Thus we can have 54 one-character names and 52*54=2808 two-character names for every branch leaf. + // That is sufficient for virtually all input. + + // one character + if index < identStartLen { + name[0] = r.identStart[index] + return name[:1] + } + index -= identStartLen + + // two characters or more + n := 2 + for { + offset := identStartLen + for i := 0; i < n-1; i++ { + offset *= identContinueLen + } + if index < offset { + break + } + index -= offset + n++ + } + + if cap(name) < n { + name = make([]byte, n) + } else { + name = name[:n] + } + name[0] = r.identStart[index%identStartLen] + index /= identStartLen + for i := 1; i < n; i++ { + name[i] = r.identContinue[index%identContinueLen] + index /= identContinueLen + } + return name +} + +//////////////////////////////////////////////////////////////// + +func hasDefines(v *js.VarDecl) bool { + for _, item := range v.List { + if item.Default != nil { + return true + } + } + return false +} + +func bindingVars(ibinding js.IBinding) (vs []*js.Var) { + switch binding := ibinding.(type) { + case *js.Var: + vs = append(vs, binding) + case *js.BindingArray: + for _, item := range binding.List { + if item.Binding != nil { + vs = append(vs, bindingVars(item.Binding)...) + } + } + if binding.Rest != nil { + vs = append(vs, bindingVars(binding.Rest)...) + } + case *js.BindingObject: + for _, item := range binding.List { + if item.Value.Binding != nil { + vs = append(vs, bindingVars(item.Value.Binding)...) + } + } + if binding.Rest != nil { + vs = append(vs, binding.Rest) + } + } + return +} + +func addDefinition(decl *js.VarDecl, binding js.IBinding, value js.IExpr, forward bool) { + // see if not already defined in variable declaration list + // if forward is set, binding=value comes before decl, otherwise the reverse holds true + vars := bindingVars(binding) + + // remove variables in destination +RemoveVarsLoop: + for _, vbind := range vars { + for i, item := range decl.List { + if v, ok := item.Binding.(*js.Var); ok && item.Default == nil && v == vbind { + v.Uses-- + decl.List = append(decl.List[:i], decl.List[i+1:]...) + continue RemoveVarsLoop + } + } + + if value != nil { + // variable declaration must be somewhere else, find and remove it + for _, decl2 := range decl.Scope.Func.VarDecls { + for i, item := range decl2.List { + if v, ok := item.Binding.(*js.Var); ok && item.Default == nil && v == vbind { + v.Uses-- + decl2.List = append(decl2.List[:i], decl2.List[i+1:]...) + continue RemoveVarsLoop + } + } + } + } + } + + // add declaration to destination + item := js.BindingElement{Binding: binding, Default: value} + if forward { + decl.List = append([]js.BindingElement{item}, decl.List...) + } else { + decl.List = append(decl.List, item) + } +} + +func mergeVarDecls(dst, src *js.VarDecl, forward bool) { + // Merge var declarations by moving declarations from src to dst. If forward is set, src comes first and dst after, otherwise the order is reverse. + if forward { + // reverse order so we can iterate from beginning to end, sometimes addDefinition may remove another declaration in the src list + n := len(src.List) - 1 + for j := 0; j < len(src.List)/2; j++ { + src.List[j], src.List[n-j] = src.List[n-j], src.List[j] + } + } + for j := 0; j < len(src.List); j++ { + addDefinition(dst, src.List[j].Binding, src.List[j].Default, forward) + } + src.List = src.List[:0] +} + +func mergeVarDeclExprStmt(decl *js.VarDecl, exprStmt *js.ExprStmt, forward bool) bool { + // Merge var declarations with an assignment expression. If forward is set than expr comes first and decl after, otherwise the order is reverse. + if decl2, ok := exprStmt.Value.(*js.VarDecl); ok { + // this happens when a variable declarations is converted to an expression due to hoisting + mergeVarDecls(decl, decl2, forward) + return true + } else if commaExpr, ok := exprStmt.Value.(*js.CommaExpr); ok { + n := 0 + for i := 0; i < len(commaExpr.List); i++ { + item := commaExpr.List[i] + if forward { + item = commaExpr.List[len(commaExpr.List)-i-1] + } + if src, ok := item.(*js.VarDecl); ok { + // this happens when a variable declarations is converted to an expression due to hoisting + mergeVarDecls(decl, src, forward) + n++ + continue + } else if binaryExpr, ok := item.(*js.BinaryExpr); ok && binaryExpr.Op == js.EqToken { + if v, ok := binaryExpr.X.(*js.Var); ok && v.Decl == js.VariableDecl { + addDefinition(decl, v, binaryExpr.Y, forward) + n++ + continue + } + } + break + } + merge := n == len(commaExpr.List) + if !forward { + commaExpr.List = commaExpr.List[n:] + } else { + commaExpr.List = commaExpr.List[:len(commaExpr.List)-n] + } + return merge + } else if binaryExpr, ok := exprStmt.Value.(*js.BinaryExpr); ok && binaryExpr.Op == js.EqToken { + if v, ok := binaryExpr.X.(*js.Var); ok && v.Decl == js.VariableDecl { + addDefinition(decl, v, binaryExpr.Y, forward) + return true + } + } + return false +} + +func (m *jsMinifier) countHoistLength(ibinding js.IBinding) int { + if !m.o.KeepVarNames { + return len(bindingVars(ibinding)) * 2 // assume that var name will be of length one, +1 for the comma + } + + n := 0 + for _, v := range bindingVars(ibinding) { + n += len(v.Data) + 1 // +1 for the comma when added to other declaration + } + return n +} + +func (m *jsMinifier) hoistVars(body *js.BlockStmt) { + // Hoist all variable declarations in the current module/function scope to the top. + // If the first statement is a var declaration, expand it. Otherwise prepend a new var declaration. + // Except for the first var declaration, all others are converted to expressions. This is possible because an ArrayBindingPattern and ObjectBindingPattern can be converted to an ArrayLiteral or ObjectLiteral respectively, as they are supersets of the BindingPatterns. + if 1 < len(body.Scope.VarDecls) { + // Select which variable declarations will be hoisted (convert to expression) and which not + best := 0 + score := make([]int, len(body.Scope.VarDecls)) // savings if hoisted + hoist := make([]bool, len(body.Scope.VarDecls)) + for i, varDecl := range body.Scope.VarDecls { + hoist[i] = true + score[i] = 4 // "var " + if !varDecl.InForInOf { + n := 0 + nArrays := 0 + nObjects := 0 + hasDefinitions := false + for j, item := range varDecl.List { + if item.Default != nil { + if _, ok := item.Binding.(*js.BindingObject); ok { + if j != 0 && nArrays == 0 && nObjects == 0 { + varDecl.List[0], varDecl.List[j] = varDecl.List[j], varDecl.List[0] + } + nObjects++ + } else if _, ok := item.Binding.(*js.BindingArray); ok { + if j != 0 && nArrays == 0 && nObjects == 0 { + varDecl.List[0], varDecl.List[j] = varDecl.List[j], varDecl.List[0] + } + nArrays++ + } + score[i] -= m.countHoistLength(item.Binding) // var names and commas + hasDefinitions = true + n++ + } + } + if !hasDefinitions { + score[i] = 5 - 1 // 1 for a comma + if varDecl.InFor { + score[i]-- // semicolon can be reused + } + } + if nObjects != 0 && !varDecl.InFor && nObjects == n { + score[i] -= 2 // required parenthesis around braces + } + if nArrays != 0 || nObjects != 0 { + score[i]-- // space after var disappears + } + if score[i] < score[best] || body.Scope.VarDecls[best].InForInOf { + // select var decl with the least savings if hoisted + best = i + } + if score[i] < 0 { + hoist[i] = false + } + } + } + if body.Scope.VarDecls[best].InForInOf { + // no savings possible + return + } + + decl := body.Scope.VarDecls[best] + if 10000 < len(decl.List) { + return + } + hoist[best] = false + + // get original declarations + orig := []*js.Var{} + for _, item := range decl.List { + orig = append(orig, bindingVars(item.Binding)...) + } + + // hoist other variable declarations in this function scope but don't initialize yet + j := 0 + for i, varDecl := range body.Scope.VarDecls { + if hoist[i] { + varDecl.TokenType = js.ErrorToken + for _, item := range varDecl.List { + refs := bindingVars(item.Binding) + bindingElements := make([]js.BindingElement, 0, len(refs)) + DeclaredLoop: + for _, ref := range refs { + for _, v := range orig { + if ref == v { + continue DeclaredLoop + } + } + bindingElements = append(bindingElements, js.BindingElement{Binding: ref, Default: nil}) + orig = append(orig, ref) + + s := decl.Scope + for s != nil && s != s.Func { + s.AddUndeclared(ref) + s = s.Parent + } + if item.Default != nil { + ref.Uses++ + } + } + if i < best { + // prepend + decl.List = append(decl.List[:j], append(bindingElements, decl.List[j:]...)...) + j += len(bindingElements) + } else { + // append + decl.List = append(decl.List, bindingElements...) + } + } + } + } + + // rearrange to put array/object first + var prevRefs []*js.Var + BeginArrayObject: + for i, item := range decl.List { + refs := bindingVars(item.Binding) + if _, ok := item.Binding.(*js.Var); !ok { + if i != 0 { + interferes := false + if item.Default != nil { + InterferenceLoop: + for _, ref := range refs { + for _, v := range prevRefs { + if ref == v { + interferes = true + break InterferenceLoop + } + } + } + } + if !interferes { + decl.List[0], decl.List[i] = decl.List[i], decl.List[0] + break BeginArrayObject + } + } else { + break BeginArrayObject + } + } + if item.Default != nil { + prevRefs = append(prevRefs, refs...) + } + } + } +} |
