1package js
2
3import (
4 "github.com/tdewolff/parse/v2/js"
5)
6
7func optimizeStmt(i js.IStmt) js.IStmt {
8 // convert if/else into expression statement, and optimize blocks
9 if ifStmt, ok := i.(*js.IfStmt); ok {
10 hasIf := !isEmptyStmt(ifStmt.Body)
11 hasElse := !isEmptyStmt(ifStmt.Else)
12 if unaryExpr, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unaryExpr.Op == js.NotToken && hasElse {
13 ifStmt.Cond = unaryExpr.X
14 ifStmt.Body, ifStmt.Else = ifStmt.Else, ifStmt.Body
15 hasIf, hasElse = hasElse, hasIf
16 }
17 if !hasIf && !hasElse {
18 return &js.ExprStmt{Value: ifStmt.Cond}
19 } else if hasIf && !hasElse {
20 ifStmt.Body = optimizeStmt(ifStmt.Body)
21 if X, isExprBody := ifStmt.Body.(*js.ExprStmt); isExprBody {
22 if unaryExpr, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unaryExpr.Op == js.NotToken {
23 left := groupExpr(unaryExpr.X, binaryLeftPrecMap[js.OrToken])
24 right := groupExpr(X.Value, binaryRightPrecMap[js.OrToken])
25 return &js.ExprStmt{&js.BinaryExpr{js.OrToken, left, right}}
26 }
27 left := groupExpr(ifStmt.Cond, binaryLeftPrecMap[js.AndToken])
28 right := groupExpr(X.Value, binaryRightPrecMap[js.AndToken])
29 return &js.ExprStmt{&js.BinaryExpr{js.AndToken, left, right}}
30 } else if X, isIfStmt := ifStmt.Body.(*js.IfStmt); isIfStmt && isEmptyStmt(X.Else) {
31 left := groupExpr(ifStmt.Cond, binaryLeftPrecMap[js.AndToken])
32 right := groupExpr(X.Cond, binaryRightPrecMap[js.AndToken])
33 ifStmt.Cond = &js.BinaryExpr{js.AndToken, left, right}
34 ifStmt.Body = X.Body
35 return ifStmt
36 }
37 } else if !hasIf && hasElse {
38 ifStmt.Else = optimizeStmt(ifStmt.Else)
39 if X, isExprElse := ifStmt.Else.(*js.ExprStmt); isExprElse {
40 left := groupExpr(ifStmt.Cond, binaryLeftPrecMap[js.OrToken])
41 right := groupExpr(X.Value, binaryRightPrecMap[js.OrToken])
42 return &js.ExprStmt{&js.BinaryExpr{js.OrToken, left, right}}
43 }
44 } else if hasIf && hasElse {
45 ifStmt.Body = optimizeStmt(ifStmt.Body)
46 ifStmt.Else = optimizeStmt(ifStmt.Else)
47 XExpr, isExprBody := ifStmt.Body.(*js.ExprStmt)
48 YExpr, isExprElse := ifStmt.Else.(*js.ExprStmt)
49 if isExprBody && isExprElse {
50 return &js.ExprStmt{condExpr(ifStmt.Cond, XExpr.Value, YExpr.Value)}
51 }
52 XReturn, isReturnBody := ifStmt.Body.(*js.ReturnStmt)
53 YReturn, isReturnElse := ifStmt.Else.(*js.ReturnStmt)
54 if isReturnBody && isReturnElse {
55 if XReturn.Value == nil && YReturn.Value == nil {
56 return &js.ReturnStmt{commaExpr(ifStmt.Cond, &js.UnaryExpr{
57 Op: js.VoidToken,
58 X: &js.LiteralExpr{js.NumericToken, zeroBytes},
59 })}
60 } else if XReturn.Value != nil && YReturn.Value != nil {
61 return &js.ReturnStmt{condExpr(ifStmt.Cond, XReturn.Value, YReturn.Value)}
62 }
63 return ifStmt
64 }
65 XThrow, isThrowBody := ifStmt.Body.(*js.ThrowStmt)
66 YThrow, isThrowElse := ifStmt.Else.(*js.ThrowStmt)
67 if isThrowBody && isThrowElse {
68 return &js.ThrowStmt{condExpr(ifStmt.Cond, XThrow.Value, YThrow.Value)}
69 }
70 }
71 } else if decl, ok := i.(*js.VarDecl); ok {
72 // TODO: remove function name in var name=function name(){}
73 //for _, item := range decl.List {
74 // if v, ok := item.Binding.(*js.Var); ok && item.Default != nil {
75 // if fun, ok := item.Default.(*js.FuncDecl); ok && fun.Name != nil && bytes.Equal(v.Data, fun.Name.Data) {
76 // scope := fun.Body.Scope
77 // for i, vorig := range scope.Declared {
78 // if fun.Name == vorig {
79 // scope.Declared = append(scope.Declared[:i], scope.Declared[i+1:]...)
80 // }
81 // }
82 // scope.AddUndeclared(v)
83 // v.Uses += fun.Name.Uses - 1
84 // fun.Name.Link = v
85 // fun.Name = nil
86 // }
87 // }
88 //}
89
90 if decl.TokenType == js.ErrorToken {
91 // convert hoisted var declaration to expression or empty (if there are no defines) statement
92 for _, item := range decl.List {
93 if item.Default != nil {
94 return &js.ExprStmt{Value: decl}
95 }
96 }
97 return &js.EmptyStmt{}
98 }
99 // TODO: remove unused declarations
100 //for i := 0; i < len(decl.List); i++ {
101 // if v, ok := decl.List[i].Binding.(*js.Var); ok && v.Uses < 2 {
102 // decl.List = append(decl.List[:i], decl.List[i+1:]...)
103 // i--
104 // }
105 //}
106 //if len(decl.List) == 0 {
107 // return &js.EmptyStmt{}
108 //}
109 return decl
110 } else if blockStmt, ok := i.(*js.BlockStmt); ok {
111 // merge body and remove braces if it is not a lexical declaration
112 blockStmt.List = optimizeStmtList(blockStmt.List, defaultBlock)
113 if len(blockStmt.List) == 1 {
114 if _, ok := blockStmt.List[0].(*js.ClassDecl); ok {
115 return &js.EmptyStmt{}
116 } else if varDecl, ok := blockStmt.List[0].(*js.VarDecl); ok && varDecl.TokenType != js.VarToken {
117 // remove let or const declaration in otherwise empty scope, but keep assignments
118 exprs := []js.IExpr{}
119 for _, item := range varDecl.List {
120 if item.Default != nil && hasSideEffects(item.Default) {
121 exprs = append(exprs, item.Default)
122 }
123 }
124 if len(exprs) == 0 {
125 return &js.EmptyStmt{}
126 } else if len(exprs) == 1 {
127 return &js.ExprStmt{exprs[0]}
128 }
129 return &js.ExprStmt{&js.CommaExpr{exprs}}
130 }
131 return optimizeStmt(blockStmt.List[0])
132 } else if len(blockStmt.List) == 0 {
133 return &js.EmptyStmt{}
134 }
135 return blockStmt
136 }
137 return i
138}
139
140func optimizeStmtList(list []js.IStmt, blockType blockType) []js.IStmt {
141 // merge expression statements as well as if/else statements followed by flow control statements
142 if len(list) == 0 {
143 return list
144 }
145 j := 0 // write index
146 for i := 0; i < len(list); i++ { // read index
147 if ifStmt, ok := list[i].(*js.IfStmt); ok && !isEmptyStmt(ifStmt.Else) {
148 // if(!a)b;else c => if(a)c; else b
149 if unary, ok := ifStmt.Cond.(*js.UnaryExpr); ok && unary.Op == js.NotToken && isFlowStmt(lastStmt(ifStmt.Else)) {
150 ifStmt.Cond = unary.X
151 ifStmt.Body, ifStmt.Else = ifStmt.Else, ifStmt.Body
152 }
153 if isFlowStmt(lastStmt(ifStmt.Body)) {
154 // if body ends in flow statement (return, throw, break, continue), we can remove the else statement and put its body in the current scope
155 if blockStmt, ok := ifStmt.Else.(*js.BlockStmt); ok {
156 blockStmt.Scope.Unscope()
157 list = append(list[:i+1], append(blockStmt.List, list[i+1:]...)...)
158 } else {
159 list = append(list[:i+1], append([]js.IStmt{ifStmt.Else}, list[i+1:]...)...)
160 }
161 ifStmt.Else = nil
162 }
163 }
164
165 list[i] = optimizeStmt(list[i])
166
167 if _, ok := list[i].(*js.EmptyStmt); ok {
168 k := i + 1
169 for ; k < len(list); k++ {
170 if _, ok := list[k].(*js.EmptyStmt); !ok {
171 break
172 }
173 }
174 list = append(list[:i], list[k:]...)
175 i--
176 continue
177 }
178
179 if 0 < i {
180 // merge expression statements with expression, return, and throw statements
181 if left, ok := list[i-1].(*js.ExprStmt); ok {
182 if right, ok := list[i].(*js.ExprStmt); ok {
183 right.Value = commaExpr(left.Value, right.Value)
184 j--
185 } else if returnStmt, ok := list[i].(*js.ReturnStmt); ok && returnStmt.Value != nil {
186 returnStmt.Value = commaExpr(left.Value, returnStmt.Value)
187 j--
188 } else if throwStmt, ok := list[i].(*js.ThrowStmt); ok {
189 throwStmt.Value = commaExpr(left.Value, throwStmt.Value)
190 j--
191 } else if forStmt, ok := list[i].(*js.ForStmt); ok {
192 if varDecl, ok := forStmt.Init.(*js.VarDecl); ok && len(varDecl.List) == 0 || forStmt.Init == nil {
193 // TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?)
194 forStmt.Init = left.Value
195 j--
196 }
197 } else if whileStmt, ok := list[i].(*js.WhileStmt); ok {
198 // TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?)
199 var body *js.BlockStmt
200 if blockStmt, ok := whileStmt.Body.(*js.BlockStmt); ok {
201 body = blockStmt
202 } else {
203 body = &js.BlockStmt{}
204 body.List = []js.IStmt{whileStmt.Body}
205 }
206 list[i] = &js.ForStmt{Init: left.Value, Cond: whileStmt.Cond, Post: nil, Body: body}
207 j--
208 } else if switchStmt, ok := list[i].(*js.SwitchStmt); ok {
209 switchStmt.Init = commaExpr(left.Value, switchStmt.Init)
210 j--
211 } else if withStmt, ok := list[i].(*js.WithStmt); ok {
212 withStmt.Cond = commaExpr(left.Value, withStmt.Cond)
213 j--
214 } else if ifStmt, ok := list[i].(*js.IfStmt); ok {
215 ifStmt.Cond = commaExpr(left.Value, ifStmt.Cond)
216 j--
217 } else if varDecl, ok := list[i].(*js.VarDecl); ok && varDecl.TokenType == js.VarToken {
218 if merge := mergeVarDeclExprStmt(varDecl, left, true); merge {
219 j--
220 }
221 }
222 } else if left, ok := list[i-1].(*js.VarDecl); ok {
223 if right, ok := list[i].(*js.VarDecl); ok && left.TokenType == right.TokenType {
224 // merge const and let declarations, or non-hoisted var declarations
225 right.List = append(left.List, right.List...)
226 j--
227
228 // remove from vardecls list of scope
229 scope := left.Scope.Func
230 for i, decl := range scope.VarDecls {
231 if left == decl {
232 scope.VarDecls = append(scope.VarDecls[:i], scope.VarDecls[i+1:]...)
233 break
234 }
235 }
236 } else if left.TokenType == js.VarToken {
237 if exprStmt, ok := list[i].(*js.ExprStmt); ok {
238 // pull in assignments to variables into the declaration, e.g. var a;a=5 => var a=5
239 if merge := mergeVarDeclExprStmt(left, exprStmt, false); merge {
240 list[i] = list[i-1]
241 j--
242 }
243 } else if forStmt, ok := list[i].(*js.ForStmt); ok {
244 // TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?)
245 if forStmt.Init == nil {
246 forStmt.Init = left
247 j--
248 } else if decl, ok := forStmt.Init.(*js.VarDecl); ok && decl.TokenType == js.ErrorToken && !hasDefines(decl) {
249 forStmt.Init = left
250 j--
251 } else if ok && (decl.TokenType == js.VarToken || decl.TokenType == js.ErrorToken) {
252 // this is the second VarDecl, so we are hoisting var declarations, which means the forInit variables are already in 'left'
253 mergeVarDecls(left, decl, false)
254 decl.TokenType = js.VarToken
255 forStmt.Init = left
256 j--
257 }
258 } else if whileStmt, ok := list[i].(*js.WhileStmt); ok {
259 // TODO: only merge statements that don't have 'in' or 'of' keywords (slow to check?)
260 var body *js.BlockStmt
261 if blockStmt, ok := whileStmt.Body.(*js.BlockStmt); ok {
262 body = blockStmt
263 } else {
264 body = &js.BlockStmt{}
265 body.List = []js.IStmt{whileStmt.Body}
266 }
267 list[i] = &js.ForStmt{Init: left, Cond: whileStmt.Cond, Post: nil, Body: body}
268 j--
269 }
270 }
271 }
272 }
273 list[j] = list[i]
274
275 // merge if/else with return/throw when followed by return/throw
276 MergeIfReturnThrow:
277 if 0 < j {
278 // separate from expression merging in case of: if(a)return b;b=c;return d
279 if ifStmt, ok := list[j-1].(*js.IfStmt); ok && isEmptyStmt(ifStmt.Body) != isEmptyStmt(ifStmt.Else) {
280 // either the if body is empty or the else body is empty. In case where both bodies have return/throw, we already rewrote that if statement to an return/throw statement
281 if returnStmt, ok := list[j].(*js.ReturnStmt); ok {
282 if returnStmt.Value == nil {
283 if left, ok := ifStmt.Body.(*js.ReturnStmt); ok && left.Value == nil {
284 list[j-1] = &js.ExprStmt{Value: ifStmt.Cond}
285 } else if left, ok := ifStmt.Else.(*js.ReturnStmt); ok && left.Value == nil {
286 list[j-1] = &js.ExprStmt{Value: ifStmt.Cond}
287 }
288 } else {
289 if left, ok := ifStmt.Body.(*js.ReturnStmt); ok && left.Value != nil {
290 returnStmt.Value = condExpr(ifStmt.Cond, left.Value, returnStmt.Value)
291 list[j-1] = returnStmt
292 j--
293 goto MergeIfReturnThrow
294 } else if left, ok := ifStmt.Else.(*js.ReturnStmt); ok && left.Value != nil {
295 returnStmt.Value = condExpr(ifStmt.Cond, returnStmt.Value, left.Value)
296 list[j-1] = returnStmt
297 j--
298 goto MergeIfReturnThrow
299 }
300 }
301 } else if throwStmt, ok := list[j].(*js.ThrowStmt); ok {
302 if left, ok := ifStmt.Body.(*js.ThrowStmt); ok {
303 throwStmt.Value = condExpr(ifStmt.Cond, left.Value, throwStmt.Value)
304 list[j-1] = throwStmt
305 j--
306 goto MergeIfReturnThrow
307 } else if left, ok := ifStmt.Else.(*js.ThrowStmt); ok {
308 throwStmt.Value = condExpr(ifStmt.Cond, throwStmt.Value, left.Value)
309 list[j-1] = throwStmt
310 j--
311 goto MergeIfReturnThrow
312 }
313 }
314 }
315 }
316 j++
317 }
318
319 // remove superfluous return or continue
320 if 0 < j {
321 if blockType == functionBlock {
322 if returnStmt, ok := list[j-1].(*js.ReturnStmt); ok {
323 if returnStmt.Value == nil || isUndefined(returnStmt.Value) {
324 j--
325 } else if commaExpr, ok := returnStmt.Value.(*js.CommaExpr); ok && isUndefined(commaExpr.List[len(commaExpr.List)-1]) {
326 // rewrite function f(){return a,void 0} => function f(){a}
327 if len(commaExpr.List) == 2 {
328 list[j-1] = &js.ExprStmt{Value: commaExpr.List[0]}
329 } else {
330 commaExpr.List = commaExpr.List[:len(commaExpr.List)-1]
331 }
332 }
333 }
334 } else if blockType == iterationBlock {
335 if branchStmt, ok := list[j-1].(*js.BranchStmt); ok && branchStmt.Type == js.ContinueToken && branchStmt.Label == nil {
336 j--
337 }
338 }
339 }
340 return list[:j]
341}