123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- 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) {
- if decl.TokenType != js.ErrorToken {
- // 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 {
- if !decl2.InForInOf {
- 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) varNameLen(v *js.Var) int {
- if !m.o.KeepVarNames {
- return 2 // assume that var name will be of length one, +1 for the comma
- }
- return len(v.Data) + 1 // +1 for the comma when added to other declaration
- }
- func (m *jsMinifier) hoistVars(body *js.BlockStmt) {
- // Hoist all variable declarations in the current module/function scope to the top
- // Find the best var declaration (that results in the shortest code), 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
- scores := make([]int, len(body.Scope.VarDecls)) // savings if hoisting target
- hoists := make([][]bool, len(body.Scope.VarDecls))
- for i, target := range body.Scope.VarDecls {
- // keep list of target variables to avoid declaring a var more than once
- var refsTarget []*js.Var
- for _, item := range target.List {
- refsTarget = append(refsTarget, bindingVars(item.Binding)...)
- }
- hoists[i] = make([]bool, len(body.Scope.VarDecls))
- for j, varDecl := range body.Scope.VarDecls {
- if i == j {
- hoists[i][j] = false
- continue
- }
- score := 4 // "var "
- hoists[i][j] = true
- if !varDecl.InForInOf {
- // variable names in for-in or for-of cannot be removed
- n := 0 // total number of vars with decls
- nArrays := 0 // of which lhs arrays
- nObjects := 0 // of which lhs objects
- nNames := 0 // length of var names and commas
- hasDefinitions := false
- for k, item := range varDecl.List {
- if item.Default != nil {
- // move arrays/objects to the front (saves a space)
- if _, ok := item.Binding.(*js.BindingObject); ok {
- if k != 0 && nArrays == 0 && nObjects == 0 {
- varDecl.List[0], varDecl.List[k] = varDecl.List[k], varDecl.List[0]
- }
- nObjects++
- } else if _, ok := item.Binding.(*js.BindingArray); ok {
- if k != 0 && nArrays == 0 && nObjects == 0 {
- varDecl.List[0], varDecl.List[k] = varDecl.List[k], varDecl.List[0]
- }
- nArrays++
- }
- refs := bindingVars(item.Binding)
- CountNamesLoop:
- for _, ref := range refs {
- for _, v := range refsTarget {
- if ref == v {
- // already exists in target
- continue CountNamesLoop
- }
- }
- // declaration separate from assignment will copy var name + comma
- nNames += m.varNameLen(ref)
- refsTarget = append(refsTarget, ref)
- }
- hasDefinitions = true
- n++
- }
- }
- if hasDefinitions {
- score -= nNames // copy var names and commas to target
- } else if varDecl.InFor {
- score-- // semicolon can be reused
- }
- if nObjects != 0 && !varDecl.InFor && nObjects == n {
- score -= 2 // required parenthesis around braces
- }
- if nArrays != 0 || nObjects != 0 {
- score-- // space after var disappears
- }
- if score < 0 {
- // don't hoist if it increases the amount of characters
- hoists[i][j] = false
- score = 0
- }
- }
- scores[i] += score
- }
- if scores[best] < scores[i] || body.Scope.VarDecls[best].InForInOf {
- // select var decl with the most savings if hoist target
- best = i
- }
- }
- if scores[best] < 0 || body.Scope.VarDecls[best].InForInOf {
- // no savings possible
- return
- }
- hoist := hoists[best]
- decl := body.Scope.VarDecls[best]
- if 10000 < len(decl.List) {
- return
- }
- // 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...)
- }
- }
- }
- }
|