Updated JS function traversal ordering (bug 767566)
This commit is contained in:
Родитель
c294750842
Коммит
d8427aaab3
|
@ -121,52 +121,62 @@ def test_identifier(traverser, name):
|
|||
def _function(traverser, node):
|
||||
"Prevents code duplication"
|
||||
|
||||
me = JSObject()
|
||||
def wrap(traverser, node):
|
||||
me = JSObject()
|
||||
|
||||
# Replace the current context with a prototypeable JS object.
|
||||
traverser._pop_context()
|
||||
traverser._push_context(me)
|
||||
traverser._debug("THIS_PUSH")
|
||||
traverser.this_stack.append(me) # Allow references to "this"
|
||||
traverser.function_collection.append([])
|
||||
|
||||
# Declare parameters in the local scope
|
||||
params = []
|
||||
for param in node["params"]:
|
||||
if param["type"] == "Identifier":
|
||||
params.append(param["name"])
|
||||
elif param["type"] == "ArrayPattern":
|
||||
for element in param["elements"]:
|
||||
# Array destructuring in function prototypes? LOL!
|
||||
if element is None or element["type"] != "Identifier":
|
||||
continue
|
||||
params.append(element["name"])
|
||||
# Replace the current context with a prototypeable JS object.
|
||||
traverser._pop_context()
|
||||
traverser._push_context(me)
|
||||
traverser._debug("THIS_PUSH")
|
||||
traverser.this_stack.append(me) # Allow references to "this"
|
||||
|
||||
local_context = traverser._peek_context(1)
|
||||
for param in params:
|
||||
var = JSWrapper(lazy=True, traverser=traverser)
|
||||
# Declare parameters in the local scope
|
||||
params = []
|
||||
for param in node["params"]:
|
||||
if param["type"] == "Identifier":
|
||||
params.append(param["name"])
|
||||
elif param["type"] == "ArrayPattern":
|
||||
for element in param["elements"]:
|
||||
# Array destructuring in function prototypes? LOL!
|
||||
if element is None or element["type"] != "Identifier":
|
||||
continue
|
||||
params.append(element["name"])
|
||||
|
||||
# We can assume that the params are static because we don't care about
|
||||
# what calls the function. We want to know whether the function solely
|
||||
# returns static values. If so, it is a static function.
|
||||
#var.dynamic = False
|
||||
local_context.set(param, var)
|
||||
local_context = traverser._peek_context(1)
|
||||
for param in params:
|
||||
var = JSWrapper(lazy=True, traverser=traverser)
|
||||
|
||||
traverser._traverse_node(node["body"])
|
||||
# We can assume that the params are static because we don't care
|
||||
# about what calls the function. We want to know whether the
|
||||
# function solely returns static values. If so, it is a static
|
||||
# function.
|
||||
local_context.set(param, var)
|
||||
|
||||
# Since we need to manually manage the "this" stack, pop off that context.
|
||||
traverser._debug("THIS_POP")
|
||||
traverser.this_stack.pop()
|
||||
traverser._traverse_node(node["body"])
|
||||
|
||||
return me
|
||||
# Since we need to manually manage the "this" stack, pop off that
|
||||
# context.
|
||||
traverser._debug("THIS_POP")
|
||||
traverser.this_stack.pop()
|
||||
|
||||
# Call all of the function collection's members to traverse all of the
|
||||
# child functions.
|
||||
func_coll = traverser.function_collection.pop()
|
||||
for func in func_coll:
|
||||
func()
|
||||
|
||||
# Put the function off for traversal at the end of the current block scope.
|
||||
traverser.function_collection[-1].append(lambda: wrap(traverser, node))
|
||||
|
||||
return JSWrapper(traverser=traverser, callable=True, dirty=True)
|
||||
|
||||
|
||||
def _define_function(traverser, node):
|
||||
"Makes a function happy"
|
||||
|
||||
me = _function(traverser, node)
|
||||
me = JSWrapper(value=me,
|
||||
traverser=traverser,
|
||||
callable=True)
|
||||
traverser._peek_context(2).set(node["id"]["name"], me)
|
||||
|
||||
return True
|
||||
|
@ -175,12 +185,7 @@ def _define_function(traverser, node):
|
|||
def _func_expr(traverser, node):
|
||||
"Represents a lambda function"
|
||||
|
||||
# Collect the result as an object
|
||||
results = _function(traverser, node)
|
||||
if not isinstance(results, JSWrapper):
|
||||
results = JSWrapper(value=results, traverser=traverser)
|
||||
results.callable = True
|
||||
return results
|
||||
return _function(traverser, node)
|
||||
|
||||
|
||||
def _define_with(traverser, node):
|
||||
|
|
|
@ -11,8 +11,8 @@ from .predefinedentities import GLOBAL_ENTITIES, BANNED_IDENTIFIERS
|
|||
DEBUG = False
|
||||
|
||||
|
||||
class Traverser:
|
||||
"Traverses the AST Tree and determines problems with a chunk of JS."
|
||||
class Traverser(object):
|
||||
"""Traverses the AST Tree and determines problems with a chunk of JS."""
|
||||
|
||||
def __init__(self, err, filename, start_line=0, context=None):
|
||||
self.err = err
|
||||
|
@ -29,6 +29,9 @@ class Traverser:
|
|||
self.can_use_this = False
|
||||
self.this_stack = []
|
||||
|
||||
# For ordering of function traversal.
|
||||
self.function_collection = []
|
||||
|
||||
# For debugging
|
||||
self.debug_level = 0
|
||||
|
||||
|
@ -54,7 +57,12 @@ class Traverser:
|
|||
|
||||
self._debug("START>>")
|
||||
try:
|
||||
self.function_collection.append([])
|
||||
self._traverse_node(data)
|
||||
|
||||
func_coll = self.function_collection.pop()
|
||||
for func in func_coll:
|
||||
func()
|
||||
except Exception:
|
||||
print "Exception in JS traversal; %s (%d;%d)" % (
|
||||
self.filename, self.line, self.position)
|
||||
|
@ -77,12 +85,12 @@ class Traverser:
|
|||
if "__traversal" in node and node["__traversal"] is not None:
|
||||
return node["__traversal"]
|
||||
|
||||
if isinstance(node, (str, unicode)):
|
||||
if isinstance(node, types.StringTypes):
|
||||
return JSWrapper(JSLiteral(node), traverser=self)
|
||||
elif "type" not in node or node["type"] not in DEFINITIONS:
|
||||
return JSWrapper(JSObject(), traverser=self, dirty=True)
|
||||
|
||||
self._debug("TRAVERSE>>%s" % (node["type"]))
|
||||
self._debug("TRAVERSE>>%s" % node["type"])
|
||||
self.debug_level += 1
|
||||
|
||||
# Extract location information if it's available
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
from js_helper import TestCase
|
||||
|
||||
|
||||
class TestFunctionTraversal(TestCase):
|
||||
"""
|
||||
Consider the following tree:
|
||||
|
||||
- body
|
||||
|-function a()
|
||||
| |- foo
|
||||
| |- bar
|
||||
|-zip
|
||||
|-function b()
|
||||
| |-abc
|
||||
| |-def
|
||||
|-zap
|
||||
|
||||
In the old traversal technique, it would be evaluated in the order:
|
||||
|
||||
body a() foo bar zip b() abc def zap
|
||||
|
||||
If the tree is considered as a graph, this would be prefix notation
|
||||
traversal.
|
||||
|
||||
This is not optimal, however, as JS commonly uses callbacks which are set
|
||||
up before delegation code. The new traversal technique should access nodes
|
||||
in the following order:
|
||||
|
||||
body zip zap a() foo bar b() abc def
|
||||
|
||||
If the tree is considered a graph, this would be a custom prefix notation
|
||||
traversal where all non-function nodes are traversed before all function
|
||||
nodes.
|
||||
"""
|
||||
|
||||
def test_function_declaration_order(self):
|
||||
"""Test that function declarations happen in the right time."""
|
||||
|
||||
self.run_script("""
|
||||
foo = "first";
|
||||
function test() {
|
||||
foo = "second";
|
||||
}
|
||||
bar = foo;
|
||||
""")
|
||||
self.assert_var_eq("bar", "first")
|
||||
self.assert_var_eq("foo", "second")
|
||||
|
||||
def test_function_expression_order(self):
|
||||
"""Test that function expressions happen in the right time."""
|
||||
|
||||
self.run_script("""
|
||||
foo = "first"
|
||||
var x = function() {
|
||||
foo = "second";
|
||||
}
|
||||
bar = foo;
|
||||
""")
|
||||
self.assert_var_eq("bar", "first")
|
||||
self.assert_var_eq("foo", "second")
|
||||
|
||||
def test_nested_functions(self):
|
||||
"""Test that nested functions are considered in the right order."""
|
||||
|
||||
self.run_script("""
|
||||
foo = "first"
|
||||
function test() {
|
||||
function a() {foo = "second"}
|
||||
foo = "third";
|
||||
}
|
||||
""")
|
||||
self.assert_var_eq("foo", "second")
|
||||
|
Загрузка…
Ссылка в новой задаче