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}