#
# Jasy - Web Tooling Framework
# Copyright 2010-2012 Zynga Inc.
# Copyright 2013-2014 Sebastian Werner
#
import jasy.script.parse.Node as Node
import jasy.core.Console as Console
#
# Public API
#
[docs]class Error(Exception):
def __init__(self, line):
self.__line = line
[docs]def optimize(node):
Console.debug("Combining declarations...")
Console.indent()
result = __optimize(node)
Console.outdent()
return result
def __optimize(node):
# stabilize list during processing modifyable stuff
copy = node
if node.type in ("script", "block"):
copy = list(node)
for child in copy:
# None children are allowed sometimes e.g. during array_init like [1,2,,,7,8]
if child is not None:
__optimize(child)
if node.type in ("script", "block"):
__combineSiblings(node)
if node.type == "script":
__combineVarStatements(node)
#
# Merge direct variable siblings
#
def __combineSiblings(node):
"""Backwards processing and insertion into previous sibling if both are declarations."""
length = len(node)
pos = length - 1
while pos > 0:
child = node[pos]
prevChild = node[pos - 1]
# Special FOR loop optimization, emulate faked VAR
if child.type == "for" and prevChild.type == "var":
setup = getattr(child, "setup", None)
if setup and setup.type == "var":
Console.debug("Removing for-loop setup section at line %s" % setup.line)
child.remove(setup)
child = setup
# Combine declarations of VAR statements
if child.type == "var" and prevChild.type == "var":
# debug("Combining var statement at line %s" % child.line)
# Fix loop through casting node to list()
for variable in list(child):
prevChild.append(variable)
if child in node:
node.remove(child)
pos -= 1
#
# Merge var statements, convert in-place to assignments in other locations (quite complex)
#
def __combineVarStatements(node):
"""Top level method called to optimize a script node."""
if len(node.scope.declared) == 0:
return
firstVar = __findFirstVarStatement(node)
# Special case, when a node has variables, but no valid "var" block to hold them
# This happens in cases where there is a for-loop which contains a "var", but
# there are no other variable declarations anywhere. In this case we are not able
# to optimize the code further and just exit at this point
# Only size-saving when there are multiple for-in loops, but no other var statement or first
# "free" var declaration is after for-loops.
if not firstVar:
firstVar = Node.Node(None, "var")
node.insert(0, firstVar)
__patchVarStatements(node, firstVar)
__cleanFirst(firstVar)
# Remove unused "var"
if len(firstVar) == 0:
firstVar.parent.remove(firstVar)
else:
# When there is a classical for loop immediately after our
# first var statement, then we try to move the var declaration
# into there as a setup expression
firstVarParent = firstVar.parent
firstVarPos = firstVarParent.index(firstVar)
if len(firstVarParent) > firstVarPos + 1:
possibleForStatement = firstVarParent[firstVarPos + 1]
if possibleForStatement.type == "for" and not hasattr(possibleForStatement, "setup"):
possibleForStatement.append(firstVar, "setup")
def __findFirstVarStatement(node):
"""
Returns the first var statement of the given node.
Ignores inner functions.
"""
if node.type == "var":
# Ignore variable blocks which are used as an iterator in for-in loops
# In this case we return False, so that a new collector "var" is being created
if getattr(node, "rel", None) == "iterator":
return False
else:
return node
for child in node:
if child.type == "function":
continue
result = __findFirstVarStatement(child)
if result:
return result
elif result is False:
return False
return None
def __cleanFirst(first):
"""
Should remove double declared variables which have no initializer e.g.
var s=3,s,s,t,s; => var s=3,t;
"""
# Add all with initializer first
known = set()
for child in first:
if hasattr(child, "initializer"):
varName = getattr(child, "name", None)
if varName is not None:
known.add(varName)
else:
# JS 1.7 Destructing Expression
for varIdentifier in child.names:
known.add(varIdentifier.value)
# Then add all remaining ones which are not added before
# This implementation omits duplicates even if the assignments
# are listed later in the original node.
for child in list(first):
# JS 1.7 Destructing Expression always have a initializer
if not hasattr(child, "initializer"):
if child.name in known:
first.remove(child)
else:
known.add(child.name)
def __createSimpleAssignment(identifier, valueNode):
assignNode = Node.Node(None, "assign")
identNode = Node.Node(None, "identifier")
identNode.value = identifier
assignNode.append(identNode)
assignNode.append(valueNode)
return assignNode
def __createMultiAssignment(names, valueNode):
assignNode = Node.Node(None, "assign")
assignNode.append(names)
assignNode.append(valueNode)
return assignNode
def __createDeclaration(name):
declNode = Node.Node(None, "declaration")
declNode.name = name
declNode.readOnly = False
return declNode
def __createIdentifier(value):
identifier = Node.Node(None, "identifier")
identifier.value = value
return identifier
def __patchVarStatements(node, firstVarStatement):
"""Patches all variable statements in the given node (works recursively) and replace them with assignments."""
if node is firstVarStatement:
return
elif node.type == "function":
# Don't process inner functions/scopes
return
elif node.type == "var":
__rebuildAsAssignment(node, firstVarStatement)
else:
# Recursion into children
# Create a cast to list() to keep loop stable during modification
for child in list(node):
if child is not None:
__patchVarStatements(child, firstVarStatement)
def __rebuildAsAssignment(node, firstVarStatement):
"""Rebuilds the items of a var statement into a assignment list and moves declarations to the given var
statement."""
assignment = Node.Node(node.tokenizer, "semicolon")
assignmentList = Node.Node(node.tokenizer, "comma")
assignment.append(assignmentList, "expression")
# Casting to list() creates a copy during the process (keeps loop stable)
for child in list(node):
if hasattr(child, "name"):
# Cleanup initializer and move to assignment
if hasattr(child, "initializer"):
assign = __createSimpleAssignment(child.name, child.initializer)
assignmentList.append(assign)
firstVarStatement.append(child)
else:
# JS 1.7 Destructing Expression
for identifier in child.names:
firstVarStatement.append(__createDeclaration(identifier.value))
if hasattr(child, "initializer"):
assign = __createMultiAssignment(child.names, child.initializer)
assignmentList.append(assign)
node.remove(child)
# Patch parent node to contain assignment instead of declaration
if len(assignmentList) > 0:
node.parent.replace(node, assignment)
# Special process for "for-in" loops
# It is OK to be second because of assignments are not allowed at
# all in for-in loops and so the first if basically does nothing
# for these kind of statements.
elif getattr(node, "rel", None) == "iterator":
if hasattr(child, "name"):
node.parent.replace(node, __createIdentifier(child.name))
else:
# JS 1.7 Destructing Expressions
node.parent.replace(node, child.names)
# Edge case. Not yet found if this happen realistically
else:
if hasattr(node, "rel"):
Console.warn("Remove related node (%s) from parent: %s" % (node.rel, node))
node.parent.remove(node)
# Minor post-cleanup. Remove useless comma statement when only one expression is the result
if len(assignmentList) == 1:
assignment.replace(assignmentList, assignmentList[0])