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