vars.go 13 KB

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