1package js
  2
  3import (
  4	"bytes"
  5	"sort"
  6
  7	"github.com/tdewolff/parse/v2/js"
  8)
  9
 10const identStartLen = 54
 11const identContinueLen = 64
 12
 13type renamer struct {
 14	identStart    []byte
 15	identContinue []byte
 16	identOrder    map[byte]int
 17	reserved      map[string]struct{}
 18	rename        bool
 19}
 20
 21func newRenamer(rename, useCharFreq bool) *renamer {
 22	reserved := make(map[string]struct{}, len(js.Keywords))
 23	for name := range js.Keywords {
 24		reserved[name] = struct{}{}
 25	}
 26	identStart := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$")
 27	identContinue := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789")
 28	if useCharFreq {
 29		// sorted based on character frequency of a collection of JS samples
 30		identStart = []byte("etnsoiarclduhmfpgvbjy_wOxCEkASMFTzDNLRPHIBV$WUKqYGXQZJ")
 31		identContinue = []byte("etnsoiarcldu14023hm8f6pg57v9bjy_wOxCEkASMFTzDNLRPHIBV$WUKqYGXQZJ")
 32	}
 33	if len(identStart) != identStartLen || len(identContinue) != identContinueLen {
 34		panic("bad identStart or identContinue lengths")
 35	}
 36	identOrder := map[byte]int{}
 37	for i, c := range identStart {
 38		identOrder[c] = i
 39	}
 40	return &renamer{
 41		identStart:    identStart,
 42		identContinue: identContinue,
 43		identOrder:    identOrder,
 44		reserved:      reserved,
 45		rename:        rename,
 46	}
 47}
 48
 49func (r *renamer) renameScope(scope js.Scope) {
 50	if !r.rename {
 51		return
 52	}
 53
 54	i := 0
 55	// keep function argument declaration order to improve GZIP compression
 56	sort.Sort(js.VarsByUses(scope.Declared[scope.NumFuncArgs:]))
 57	for _, v := range scope.Declared {
 58		v.Data = r.getName(v.Data, i)
 59		i++
 60		for r.isReserved(v.Data, scope.Undeclared) {
 61			v.Data = r.getName(v.Data, i)
 62			i++
 63		}
 64	}
 65}
 66
 67func (r *renamer) isReserved(name []byte, undeclared js.VarArray) bool {
 68	if 1 < len(name) { // there are no keywords or known globals that are one character long
 69		if _, ok := r.reserved[string(name)]; ok {
 70			return true
 71		}
 72	}
 73	for _, v := range undeclared {
 74		for v.Link != nil {
 75			v = v.Link
 76		}
 77		if bytes.Equal(v.Data, name) {
 78			return true
 79		}
 80	}
 81	return false
 82}
 83
 84func (r *renamer) getIndex(name []byte) int {
 85	index := 0
 86NameLoop:
 87	for i := len(name) - 1; 0 <= i; i-- {
 88		chars := r.identContinue
 89		if i == 0 {
 90			chars = r.identStart
 91			index *= identStartLen
 92		} else {
 93			index *= identContinueLen
 94		}
 95		for j, c := range chars {
 96			if name[i] == c {
 97				index += j
 98				continue NameLoop
 99			}
100		}
101		return -1
102	}
103	for n := 0; n < len(name)-1; n++ {
104		offset := identStartLen
105		for i := 0; i < n; i++ {
106			offset *= identContinueLen
107		}
108		index += offset
109	}
110	return index
111}
112
113func (r *renamer) getName(name []byte, index int) []byte {
114	// Generate new names for variables where the last character is (a-zA-Z$_) and others are (a-zA-Z).
115	// Thus we can have 54 one-character names and 52*54=2808 two-character names for every branch leaf.
116	// That is sufficient for virtually all input.
117
118	// one character
119	if index < identStartLen {
120		name[0] = r.identStart[index]
121		return name[:1]
122	}
123	index -= identStartLen
124
125	// two characters or more
126	n := 2
127	for {
128		offset := identStartLen
129		for i := 0; i < n-1; i++ {
130			offset *= identContinueLen
131		}
132		if index < offset {
133			break
134		}
135		index -= offset
136		n++
137	}
138
139	if cap(name) < n {
140		name = make([]byte, n)
141	} else {
142		name = name[:n]
143	}
144	name[0] = r.identStart[index%identStartLen]
145	index /= identStartLen
146	for i := 1; i < n; i++ {
147		name[i] = r.identContinue[index%identContinueLen]
148		index /= identContinueLen
149	}
150	return name
151}
152
153////////////////////////////////////////////////////////////////
154
155func hasDefines(v *js.VarDecl) bool {
156	for _, item := range v.List {
157		if item.Default != nil {
158			return true
159		}
160	}
161	return false
162}
163
164func bindingVars(ibinding js.IBinding) (vs []*js.Var) {
165	switch binding := ibinding.(type) {
166	case *js.Var:
167		vs = append(vs, binding)
168	case *js.BindingArray:
169		for _, item := range binding.List {
170			if item.Binding != nil {
171				vs = append(vs, bindingVars(item.Binding)...)
172			}
173		}
174		if binding.Rest != nil {
175			vs = append(vs, bindingVars(binding.Rest)...)
176		}
177	case *js.BindingObject:
178		for _, item := range binding.List {
179			if item.Value.Binding != nil {
180				vs = append(vs, bindingVars(item.Value.Binding)...)
181			}
182		}
183		if binding.Rest != nil {
184			vs = append(vs, binding.Rest)
185		}
186	}
187	return
188}
189
190func addDefinition(decl *js.VarDecl, binding js.IBinding, value js.IExpr, forward bool) {
191	// see if not already defined in variable declaration list
192	// if forward is set, binding=value comes before decl, otherwise the reverse holds true
193	vars := bindingVars(binding)
194
195	// remove variables in destination
196RemoveVarsLoop:
197	for _, vbind := range vars {
198		for i, item := range decl.List {
199			if v, ok := item.Binding.(*js.Var); ok && item.Default == nil && v == vbind {
200				v.Uses--
201				decl.List = append(decl.List[:i], decl.List[i+1:]...)
202				continue RemoveVarsLoop
203			}
204		}
205
206		if value != nil {
207			// variable declaration must be somewhere else, find and remove it
208			for _, decl2 := range decl.Scope.Func.VarDecls {
209				for i, item := range decl2.List {
210					if v, ok := item.Binding.(*js.Var); ok && item.Default == nil && v == vbind {
211						v.Uses--
212						decl2.List = append(decl2.List[:i], decl2.List[i+1:]...)
213						continue RemoveVarsLoop
214					}
215				}
216			}
217		}
218	}
219
220	// add declaration to destination
221	item := js.BindingElement{Binding: binding, Default: value}
222	if forward {
223		decl.List = append([]js.BindingElement{item}, decl.List...)
224	} else {
225		decl.List = append(decl.List, item)
226	}
227}
228
229func mergeVarDecls(dst, src *js.VarDecl, forward bool) {
230	// 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.
231	if forward {
232		// reverse order so we can iterate from beginning to end, sometimes addDefinition may remove another declaration in the src list
233		n := len(src.List) - 1
234		for j := 0; j < len(src.List)/2; j++ {
235			src.List[j], src.List[n-j] = src.List[n-j], src.List[j]
236		}
237	}
238	for j := 0; j < len(src.List); j++ {
239		addDefinition(dst, src.List[j].Binding, src.List[j].Default, forward)
240	}
241	src.List = src.List[:0]
242}
243
244func mergeVarDeclExprStmt(decl *js.VarDecl, exprStmt *js.ExprStmt, forward bool) bool {
245	// Merge var declarations with an assignment expression. If forward is set than expr comes first and decl after, otherwise the order is reverse.
246	if decl2, ok := exprStmt.Value.(*js.VarDecl); ok {
247		// this happens when a variable declarations is converted to an expression due to hoisting
248		mergeVarDecls(decl, decl2, forward)
249		return true
250	} else if commaExpr, ok := exprStmt.Value.(*js.CommaExpr); ok {
251		n := 0
252		for i := 0; i < len(commaExpr.List); i++ {
253			item := commaExpr.List[i]
254			if forward {
255				item = commaExpr.List[len(commaExpr.List)-i-1]
256			}
257			if src, ok := item.(*js.VarDecl); ok {
258				// this happens when a variable declarations is converted to an expression due to hoisting
259				mergeVarDecls(decl, src, forward)
260				n++
261				continue
262			} else if binaryExpr, ok := item.(*js.BinaryExpr); ok && binaryExpr.Op == js.EqToken {
263				if v, ok := binaryExpr.X.(*js.Var); ok && v.Decl == js.VariableDecl {
264					addDefinition(decl, v, binaryExpr.Y, forward)
265					n++
266					continue
267				}
268			}
269			break
270		}
271		merge := n == len(commaExpr.List)
272		if !forward {
273			commaExpr.List = commaExpr.List[n:]
274		} else {
275			commaExpr.List = commaExpr.List[:len(commaExpr.List)-n]
276		}
277		return merge
278	} else if binaryExpr, ok := exprStmt.Value.(*js.BinaryExpr); ok && binaryExpr.Op == js.EqToken {
279		if v, ok := binaryExpr.X.(*js.Var); ok && v.Decl == js.VariableDecl {
280			addDefinition(decl, v, binaryExpr.Y, forward)
281			return true
282		}
283	}
284	return false
285}
286
287func (m *jsMinifier) countHoistLength(ibinding js.IBinding) int {
288	if !m.o.KeepVarNames {
289		return len(bindingVars(ibinding)) * 2 // assume that var name will be of length one, +1 for the comma
290	}
291
292	n := 0
293	for _, v := range bindingVars(ibinding) {
294		n += len(v.Data) + 1 // +1 for the comma when added to other declaration
295	}
296	return n
297}
298
299func (m *jsMinifier) hoistVars(body *js.BlockStmt) {
300	// Hoist all variable declarations in the current module/function scope to the top.
301	// If the first statement is a var declaration, expand it. Otherwise prepend a new var declaration.
302	// 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.
303	if 1 < len(body.Scope.VarDecls) {
304		// Select which variable declarations will be hoisted (convert to expression) and which not
305		best := 0
306		score := make([]int, len(body.Scope.VarDecls)) // savings if hoisted
307		hoist := make([]bool, len(body.Scope.VarDecls))
308		for i, varDecl := range body.Scope.VarDecls {
309			hoist[i] = true
310			score[i] = 4 // "var "
311			if !varDecl.InForInOf {
312				n := 0
313				nArrays := 0
314				nObjects := 0
315				hasDefinitions := false
316				for j, item := range varDecl.List {
317					if item.Default != nil {
318						if _, ok := item.Binding.(*js.BindingObject); ok {
319							if j != 0 && nArrays == 0 && nObjects == 0 {
320								varDecl.List[0], varDecl.List[j] = varDecl.List[j], varDecl.List[0]
321							}
322							nObjects++
323						} else if _, ok := item.Binding.(*js.BindingArray); ok {
324							if j != 0 && nArrays == 0 && nObjects == 0 {
325								varDecl.List[0], varDecl.List[j] = varDecl.List[j], varDecl.List[0]
326							}
327							nArrays++
328						}
329						score[i] -= m.countHoistLength(item.Binding) // var names and commas
330						hasDefinitions = true
331						n++
332					}
333				}
334				if !hasDefinitions {
335					score[i] = 5 - 1 // 1 for a comma
336					if varDecl.InFor {
337						score[i]-- // semicolon can be reused
338					}
339				}
340				if nObjects != 0 && !varDecl.InFor && nObjects == n {
341					score[i] -= 2 // required parenthesis around braces
342				}
343				if nArrays != 0 || nObjects != 0 {
344					score[i]-- // space after var disappears
345				}
346				if score[i] < score[best] || body.Scope.VarDecls[best].InForInOf {
347					// select var decl with the least savings if hoisted
348					best = i
349				}
350				if score[i] < 0 {
351					hoist[i] = false
352				}
353			}
354		}
355		if body.Scope.VarDecls[best].InForInOf {
356			// no savings possible
357			return
358		}
359
360		decl := body.Scope.VarDecls[best]
361		if 10000 < len(decl.List) {
362			return
363		}
364		hoist[best] = false
365
366		// get original declarations
367		orig := []*js.Var{}
368		for _, item := range decl.List {
369			orig = append(orig, bindingVars(item.Binding)...)
370		}
371
372		// hoist other variable declarations in this function scope but don't initialize yet
373		j := 0
374		for i, varDecl := range body.Scope.VarDecls {
375			if hoist[i] {
376				varDecl.TokenType = js.ErrorToken
377				for _, item := range varDecl.List {
378					refs := bindingVars(item.Binding)
379					bindingElements := make([]js.BindingElement, 0, len(refs))
380				DeclaredLoop:
381					for _, ref := range refs {
382						for _, v := range orig {
383							if ref == v {
384								continue DeclaredLoop
385							}
386						}
387						bindingElements = append(bindingElements, js.BindingElement{Binding: ref, Default: nil})
388						orig = append(orig, ref)
389
390						s := decl.Scope
391						for s != nil && s != s.Func {
392							s.AddUndeclared(ref)
393							s = s.Parent
394						}
395						if item.Default != nil {
396							ref.Uses++
397						}
398					}
399					if i < best {
400						// prepend
401						decl.List = append(decl.List[:j], append(bindingElements, decl.List[j:]...)...)
402						j += len(bindingElements)
403					} else {
404						// append
405						decl.List = append(decl.List, bindingElements...)
406					}
407				}
408			}
409		}
410
411		// rearrange to put array/object first
412		var prevRefs []*js.Var
413	BeginArrayObject:
414		for i, item := range decl.List {
415			refs := bindingVars(item.Binding)
416			if _, ok := item.Binding.(*js.Var); !ok {
417				if i != 0 {
418					interferes := false
419					if item.Default != nil {
420					InterferenceLoop:
421						for _, ref := range refs {
422							for _, v := range prevRefs {
423								if ref == v {
424									interferes = true
425									break InterferenceLoop
426								}
427							}
428						}
429					}
430					if !interferes {
431						decl.List[0], decl.List[i] = decl.List[i], decl.List[0]
432						break BeginArrayObject
433					}
434				} else {
435					break BeginArrayObject
436				}
437			}
438			if item.Default != nil {
439				prevRefs = append(prevRefs, refs...)
440			}
441		}
442	}
443}