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}