SandpointsGitHook/vendor/github.com/tdewolff/minify/v2/js/vars.go
2021-03-20 23:21:23 +01:00

313 lines
9.1 KiB
Go

package js
import (
"bytes"
"sort"
"github.com/tdewolff/parse/v2"
"github.com/tdewolff/parse/v2/js"
)
type renamer struct {
ast *js.AST
reserved map[string]struct{}
rename bool
}
func newRenamer(ast *js.AST, undeclared js.VarArray, rename bool) *renamer {
reserved := make(map[string]struct{}, len(js.Keywords))
for name := range js.Keywords {
reserved[name] = struct{}{}
}
return &renamer{
ast: ast,
reserved: reserved,
rename: rename,
}
}
func (r *renamer) renameScope(scope js.Scope) {
if !r.rename {
return
}
rename := []byte("`") // so that the next is 'a'
sort.Sort(js.VarsByUses(scope.Declared))
for _, v := range scope.Declared {
rename = r.next(rename)
for r.isReserved(rename, scope.Undeclared) {
rename = r.next(rename)
}
v.Data = parse.Copy(rename)
}
}
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) next(name []byte) []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.
if name[len(name)-1] == 'z' {
name[len(name)-1] = 'A'
} else if name[len(name)-1] == 'Z' {
name[len(name)-1] = '_'
} else if name[len(name)-1] == '_' {
name[len(name)-1] = '$'
} else if name[len(name)-1] == '$' {
i := len(name) - 2
for ; 0 <= i; i-- {
if name[i] == 'Z' {
continue // happens after 52*54=2808 variables
} else if name[i] == 'z' {
name[i] = 'A' // happens after 26*54=1404 variables
} else {
name[i]++
break
}
}
for j := i + 1; j < len(name); j++ {
name[j] = 'a'
}
if i < 0 {
name = append(name, 'a')
}
} else {
name[len(name)-1]++
}
return name
}
////////////////////////////////////////////////////////////////
func bindingRefs(ibinding js.IBinding) (refs []*js.Var) {
switch binding := ibinding.(type) {
case *js.Var:
refs = append(refs, binding)
case *js.BindingArray:
for _, item := range binding.List {
if item.Binding != nil {
refs = append(refs, bindingRefs(item.Binding)...)
}
}
if binding.Rest != nil {
refs = append(refs, bindingRefs(binding.Rest)...)
}
case *js.BindingObject:
for _, item := range binding.List {
if item.Value.Binding != nil {
refs = append(refs, bindingRefs(item.Value.Binding)...)
}
}
if binding.Rest != nil {
refs = append(refs, binding.Rest)
}
}
return
}
func appendBindingVars(vars *[]*js.Var, binding js.IBinding) {
if v, ok := binding.(*js.Var); ok {
*vars = append(*vars, v)
} else if array, ok := binding.(*js.BindingArray); ok {
for _, item := range array.List {
appendBindingVars(vars, item.Binding)
}
if array.Rest != nil {
appendBindingVars(vars, array.Rest)
}
} else if object, ok := binding.(*js.BindingObject); ok {
for _, item := range object.List {
appendBindingVars(vars, item.Value.Binding)
}
if object.Rest != nil {
*vars = append(*vars, object.Rest)
}
}
}
func addDefinition(decl *js.VarDecl, iDefines int, binding js.IBinding, value js.IExpr) bool {
// see if not already defined in variable declaration list
if vdef, ok := binding.(*js.Var); ok {
for i, item := range decl.List[iDefines:] {
if v, ok := item.Binding.(*js.Var); ok && v == vdef {
decl.List[iDefines+i].Default = value
if 0 < i {
decl.List[iDefines], decl.List[iDefines+i] = decl.List[iDefines+i], decl.List[iDefines]
}
return true
}
}
} else {
vars := []*js.Var{}
appendBindingVars(&vars, binding)
if len(vars) == 0 {
return false
}
locs := make([]int, len(vars))
for i, vdef := range vars {
locs[i] = -1
for loc, item := range decl.List[iDefines:] {
if v, ok := item.Binding.(*js.Var); ok && v == vdef {
locs[i] = loc
break
}
}
if locs[i] == -1 {
return false // cannot (probably) happen if we hoist variables
}
}
sort.Ints(locs)
if iDefines != locs[0] {
decl.List[iDefines], decl.List[iDefines+locs[0]] = decl.List[iDefines+locs[0]], decl.List[iDefines]
}
decl.List[iDefines].Binding = binding
decl.List[iDefines].Default = value
for i := len(locs) - 1; 1 <= i; i-- {
if locs[i] != locs[i-1] { // ignore duplicates, otherwise remove items from hoisted var declaration
decl.List = append(decl.List[:locs[i]], decl.List[locs[i]+1:]...)
}
}
return true
}
return false
}
func (m *jsMinifier) hoistVars(body *js.BlockStmt) *js.VarDecl {
// 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.
parentVarsHoisted := m.varsHoisted
m.varsHoisted = nil
if 1 < body.Scope.NumVarDecls {
iDefines := 0 // position past last variable definition in declaration
mergeStatements := true
// ignore "use strict"
declStart := 0
for {
if _, ok := body.List[declStart].(*js.DirectivePrologueStmt); ok {
declStart++
} else {
break
}
}
var decl *js.VarDecl
if varDecl, ok := body.List[declStart].(*js.VarDecl); ok && varDecl.TokenType == js.VarToken {
decl = varDecl
} else if forStmt, ok := body.List[declStart].(*js.ForStmt); ok {
// TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?)
if forStmt.Init == nil {
decl = &js.VarDecl{TokenType: js.VarToken, List: nil}
forStmt.Init = decl
} else if varDecl, ok := forStmt.Init.(*js.VarDecl); ok && varDecl.TokenType == js.VarToken {
decl = varDecl
}
mergeStatements = false
} else if whileStmt, ok := body.List[declStart].(*js.WhileStmt); ok {
// TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?)
decl = &js.VarDecl{TokenType: js.VarToken, List: nil}
var forBody js.BlockStmt
if blockStmt, ok := whileStmt.Body.(*js.BlockStmt); ok {
forBody = *blockStmt
} else {
forBody.List = []js.IStmt{whileStmt.Body}
}
body.List[declStart] = &js.ForStmt{Init: decl, Cond: whileStmt.Cond, Post: nil, Body: forBody}
mergeStatements = false
}
if decl != nil {
// original declarations
vs := []*js.Var{}
for i, item := range decl.List {
if item.Default != nil {
iDefines = i + 1
}
vs = append(vs, bindingRefs(item.Binding)...)
}
// hoist other variable declarations in this function scope but don't initialize yet
DeclaredLoop:
for _, v := range body.Scope.Declared {
if v.Decl == js.VariableDecl {
for _, vdef := range vs {
if v == vdef {
continue DeclaredLoop
}
}
//v.Uses++ // might be inaccurate as we remove non-defining variable declarations later on
decl.List = append(decl.List, js.BindingElement{Binding: v, Default: nil})
}
}
} else {
decl = &js.VarDecl{TokenType: js.VarToken, List: nil}
for _, v := range body.Scope.Declared {
if v.Decl == js.VariableDecl {
v.Uses++ // might be inaccurate as we remove non-defining variable declarations later on
decl.List = append(decl.List, js.BindingElement{Binding: v, Default: nil})
}
}
body.List = append(body.List[:declStart], append([]js.IStmt{decl}, body.List[declStart:]...)...)
}
if mergeStatements {
// pull in assignments to variables into the declaration, e.g. var a;a=5 => var a=5
// sort in order of definitions
nMerged := 0
declEnd := declStart + 1
FindDefinitionsLoop:
for k, item := range body.List[declEnd:] {
if exprStmt, ok := item.(*js.ExprStmt); ok {
if binaryExpr, ok := exprStmt.Value.(*js.BinaryExpr); ok && binaryExpr.Op == js.EqToken {
if v, ok := binaryExpr.X.(*js.Var); ok && v.Decl == js.VariableDecl {
if addDefinition(decl, iDefines, v, binaryExpr.Y) {
iDefines++
nMerged++
continue
}
}
}
} else if varDecl, ok := item.(*js.VarDecl); ok && varDecl.TokenType == js.VarToken {
for j := 0; j < len(varDecl.List); j++ {
item := varDecl.List[j]
if item.Default != nil {
if addDefinition(decl, iDefines, item.Binding, item.Default) {
iDefines++
varDecl.List = append(varDecl.List[:j], varDecl.List[j+1:]...)
j--
} else {
break FindDefinitionsLoop
}
}
// declaration has no definition, that's fine as it's already merged previously
}
body.List[declEnd+k] = varDecl // update varDecl.List
nMerged++
continue // all variable declarations were matched, keep looking
}
break // not ExprStmt nor VarDecl
}
if 0 < nMerged {
body.List = append(body.List[:declEnd], body.List[declEnd+nMerged:]...)
}
}
m.varsHoisted = decl
}
return parentVarsHoisted
}