Browse files

Merge pull request #14 from mozilla/lean

Rewrite JS engine for speed and size (bug 923212)
  • Loading branch information...
2 parents 24088b2 + 7e6aab9 commit 22fb874354f8ade0e0eee2c7b25ea169324f3f63 @mattbasta committed Oct 15, 2013
View
2 .gitignore
@@ -8,3 +8,5 @@
/appvalidator/constants_local.py
/src
/extras/jslibs
+.noseids
+node_modules
View
16 acorn.js
@@ -0,0 +1,16 @@
+var data = '';
+process.stdin.resume();
+process.stdin.setEncoding('utf8');
+process.stdin.on('data', function(chunk) {data += chunk;});
+process.stdin.on('end', function() {
+ try {
+ process.stdout.write(JSON.stringify(require('acorn').parse(data)));
+ } catch(e) {
+ process.stdout.write(JSON.stringify({
+ 'error': true,
+ 'error_message': e.toString(),
+ 'line_number': e.loc.line
+ }));
+ }
+ process.exit(0);
+});
View
1 appvalidator/constants.py
@@ -9,6 +9,7 @@
PACKAGE_WEBAPP = 8
PACKAGE_PACKAGED_WEBAPP = 9
+JS_DEBUG = False
SPIDERMONKEY_INSTALLATION = os.environ.get("SPIDERMONKEY_INSTALLATION")
DEFAULT_WEBAPP_MRKT_URLS = ["https://marketplace.firefox.com",
View
2 appvalidator/csp.py
@@ -1,5 +1,5 @@
-CSP_INFO = "https://developer.mozilla.org/en-US/docs/Security/CSP"
+CSP_INFO = "https://developer.mozilla.org/Apps/CSP"
MESSAGE_TITLE = "CSP Violation Detected"
MESSAGE_GENERAL_DESC = ("You can find more information about what is and is "
View
19 appvalidator/errorbundle/basebundle.py
@@ -38,25 +38,34 @@ def __init__(self, determined=True, instant=False, *args, **kwargs):
def _message(type_, message_type):
def wrap(self, *args, **kwargs):
+ arg_len = len(args)
message = {
"uid": uuid.uuid4().hex,
"id": kwargs.get("err_id") or args[0],
"message": unicodehelper.decode(
kwargs.get(message_type) or args[1]),
"description": unicodehelper.decode(
kwargs.get("description", args[2] if
- len(args) > 2 else None)),
+ arg_len > 2 else None)),
# Filename is never None.
"file": kwargs.get("filename",
- args[3] if len(args) > 3 else ""),
+ args[3] if arg_len > 3 else ""),
"line": kwargs.get("line",
- args[4] if len(args) > 4 else None),
+ args[4] if arg_len > 4 else None),
"column": kwargs.get("column",
- args[5] if len(args) > 5 else None),
+ args[5] if arg_len > 5 else None),
"tier": kwargs.get("tier", self.tier),
"context": None,
}
+ destination = getattr(self, type_)
+ # Don't show duplicate messages.
+ if any(x["id"] == message["id"] and
+ x["file"] == message["file"] and
+ x["line"] == message["line"] and
+ x["column"] == message["column"] for x in destination):
+ return self
+
context = kwargs.get("context")
if context is not None:
if isinstance(context, tuple):
@@ -66,7 +75,7 @@ def wrap(self, *args, **kwargs):
line=message["line"], column=message["column"])
# Append the message to the right stack.
- getattr(self, type_).append(message)
+ destination.append(message)
# If instant mode is turned on, output the message immediately.
if self.instant:
View
96 appvalidator/testcases/javascript/acorn.py
@@ -0,0 +1,96 @@
+import re
+import subprocess
+
+import simplejson as json
+
+from appvalidator.contextgenerator import ContextGenerator
+import appvalidator.unicodehelper as unicodehelper
+
+JS_ESCAPE = re.compile("\\\\+[ux]", re.I)
+
+
+def get_tree(code, err=None, filename=None):
+ """Retrieve the parse tree for a JS snippet."""
+
+ if not code:
+ return None
+
+ try:
+ return _get_tree(code)
+ except JSReflectException as exc:
+ str_exc = str(exc).strip("'\"")
+ if "SyntaxError" in str_exc or "ReferenceError" in str_exc:
+ err.warning(
+ err_id=("testcases_scripting", "test_js_file", "syntax_error"),
+ warning="JavaScript Compile-Time Error",
+ description=["A compile-time error in the JavaScript halted "
+ "validation of that file.",
+ "Message: %s" % str_exc.split(":", 1)[-1].strip()],
+ filename=filename,
+ line=exc.line,
+ context=ContextGenerator(code))
+ elif "InternalError: too much recursion" in str_exc:
+ err.notice(
+ err_id=("testcases_scripting", "test_js_file",
+ "recursion_error"),
+ notice="JS too deeply nested for validation",
+ description="A JS file was encountered that could not be "
+ "valiated due to limitations with Spidermonkey. "
+ "It should be manually inspected.",
+ filename=filename)
+ else:
+ err.error(
+ err_id=("testcases_scripting", "test_js_file",
+ "retrieving_tree"),
+ error="JS reflection error prevented validation",
+ description=["An error in the JavaScript file prevented it "
+ "from being properly read by the Spidermonkey JS "
+ "engine.", str(exc)],
+ filename=filename)
+
+
+class JSReflectException(Exception):
+ """An exception to indicate that tokenization has failed."""
+
+ def __init__(self, value):
+ self.value = value
+ self.line = None
+
+ def __str__(self):
+ return repr(self.value)
+
+ def line_num(self, line_num):
+ "Set the line number and return self for chaining"
+ self.line = int(line_num)
+ return self
+
+
+def _get_tree(code):
+ """Return an AST tree of the JS passed in `code`."""
+
+ if not code:
+ return
+
+ # Acceptable unicode characters still need to be stripped. Just remove the
+ # slash: a character is necessary to prevent bad identifier errors.
+ code = JS_ESCAPE.sub("u", unicodehelper.decode(code))
+
+ shell_obj = subprocess.Popen(
+ ["node", "./acorn.js"], shell=False, stdin=subprocess.PIPE,
+ stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+
+ data, stderr = shell_obj.communicate(code.encode('utf-8'))
+
+ if stderr:
+ raise RuntimeError('Error calling acorn: %s' % stderr)
+
+ if not data:
+ raise JSReflectException("Reflection failed")
+
+ parsed = json.loads(unicodehelper.decode(data), strict=False)
+
+ if parsed.get("error"):
+ raise JSReflectException(
+ parsed["error_message"]).line_num(parsed["line_number"])
+
+ return parsed
View
766 appvalidator/testcases/javascript/actions.py
@@ -1,766 +0,0 @@
-import math
-import re
-import types
-
-import spidermonkey
-import instanceactions
-import instanceproperties
-from appvalidator.python.copy import deepcopy
-from appvalidator.constants import (BUGZILLA_BUG, DESCRIPTION_TYPES,
- MAX_STR_SIZE)
-from jstypes import *
-
-
-NUMERIC_TYPES = (int, long, float, complex)
-
-# None of these operations (or their augmented assignment counterparts) should
-# be performed on non-numeric data. Any time we get non-numeric data for these
-# guys, we just return window.NaN.
-NUMERIC_OPERATORS = ("-", "*", "/", "%", "<<", ">>", ">>>", "|", "^", "&")
-NUMERIC_OPERATORS += tuple("%s=" % op for op in NUMERIC_OPERATORS)
-
-
-def get_NaN(traverser):
- # If we've cached the traverser's NaN instance, just use that.
- ncache = getattr(traverser, "NAN_CACHE", None)
- if ncache is not None:
- return ncache
-
- # Otherwise, we need to import GLOBAL_ENTITIES and build a raw copy.
- from predefinedentities import GLOBAL_ENTITIES
- ncache = traverser._build_global("NaN", GLOBAL_ENTITIES[u"NaN"])
- # Cache it so we don't need to do this again.
- traverser.NAN_CACHE = ncache
- return ncache
-
-
-def _get_member_exp_property(traverser, node):
- """Return the string value of a member expression's property."""
-
- if node["property"]["type"] == "Identifier" and not node["computed"]:
- return unicode(node["property"]["name"])
- else:
- eval_exp = traverser.traverse_node(node["property"])
- return _get_as_str(eval_exp.get_literal_value())
-
-
-def _expand_globals(traverser, node):
- """Expands a global object that has a lambda value."""
-
- if not isinstance(node, JSWrapper):
- print 'jsw', node, traverser.filename, traverser.line
- return JSWrapper(node, traverser=traverser)
-
- if node.is_global and callable(node.value.get("value")):
-
- result = node.value["value"](t=traverser)
- if isinstance(result, dict):
- output = traverser._build_global("--", result)
- elif isinstance(result, JSWrapper):
- output = result
- else:
- output = JSWrapper(result, traverser)
-
- return output
-
- return node
-
-
-def trace_member(traverser, node, instantiate=False):
- "Traces a MemberExpression and returns the appropriate object"
-
- traverser._debug("TESTING>>%s" % node["type"])
- if node["type"] == "MemberExpression":
- # x.y or x[y]
- # x = base
- base = trace_member(traverser, node["object"], instantiate)
- base = _expand_globals(traverser, base)
-
- identifier = _get_member_exp_property(traverser, node)
-
- traverser._debug("MEMBER_EXP>>PROPERTY (%s)" % identifier)
- output = base.get(
- traverser=traverser, instantiate=instantiate, name=identifier)
- return output
-
- elif node["type"] == "Identifier":
- traverser._debug("MEMBER_EXP>>ROOT:IDENTIFIER (%s)" % node["name"])
-
- # If we're supposed to instantiate the object and it doesn't already
- # exist, instantitate the object.
- if instantiate and not traverser._is_defined(node["name"]):
- output = JSWrapper(JSObject(), traverser=traverser)
- traverser.contexts[0].set(node["name"], output)
- else:
- output = traverser._seek_variable(node["name"])
-
- return _expand_globals(traverser, output)
- else:
- traverser._debug("MEMBER_EXP>>ROOT:EXPRESSION")
- # It's an expression, so just try your damndest.
- return traverser.traverse_node(node)
-
-
-def _function(traverser, node):
- "Prevents code duplication"
-
- def wrap(traverser, node):
- me = JSObject()
-
- traverser.function_collection.append([])
-
- # Replace the current context with a prototypeable JS object.
- traverser._pop_context()
- me.type_ = "default" # Treat the function as a normal object.
- traverser._push_context(me)
- traverser._debug("THIS_PUSH")
- traverser.this_stack.append(me) # Allow references to "this"
-
- # 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"])
-
- local_context = traverser._peek_context(1)
- for param in params:
- var = JSWrapper(lazy=True, traverser=traverser)
-
- # 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)
-
- traverser.traverse_node(node["body"])
-
- # 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):
- me = _function(traverser, node)
- traverser._peek_context(2).set(node["id"]["name"], me)
- return me
-
-
-# Just make it a refernce to the function that it aliases.
-_func_expr = _function
-
-
-def _define_with(traverser, node):
- object_ = traverser.traverse_node(node["object"])
- if (isinstance(object_, JSWrapper) and
- isinstance(object_.value, JSObject)):
- traverser.contexts[-1] = object_.value
- traverser.contexts.append(JSContext("block"))
-
-
-def _define_var(traverser, node):
- traverser._debug("VARIABLE_DECLARATION")
- traverser.debug_level += 1
-
- for declaration in node["declarations"]:
-
- # It could be deconstruction of variables :(
- if declaration["id"]["type"] == "ArrayPattern":
-
- vars = []
- for element in declaration["id"]["elements"]:
- # NOTE : Multi-level array destructuring sucks. Maybe implement
- # it someday if you're bored, but it's so rarely used and it's
- # so utterly complex, there's probably no need to ever code it
- # up.
- if element is None or element["type"] != "Identifier":
- vars.append(None)
- continue
- vars.append(element["name"])
-
- # The variables are not initialized
- if declaration["init"] is None:
- # Simple instantiation; no initialization
- for var in filter(None, vars):
- traverser._declare_variable(var, None)
-
- # The variables are declared inline
- elif declaration["init"]["type"] == "ArrayPattern":
- # TODO : Test to make sure len(values) == len(vars)
- for value in declaration["init"]["elements"]:
- if vars[0]:
- traverser._declare_variable(
- vars[0], JSWrapper(traverser.traverse_node(value),
- traverser=traverser))
- vars = vars[1:] # Pop off the first value
-
- # It's being assigned by a JSArray (presumably)
- elif declaration["init"]["type"] == "ArrayExpression":
- assigner = traverser.traverse_node(declaration["init"])
- for value in assigner.value.elements:
- if vars[0]:
- traverser._declare_variable(vars[0], value)
- vars = vars[1:]
-
- elif declaration["id"]["type"] == "ObjectPattern":
-
- init = traverser.traverse_node(declaration["init"])
-
- def _proc_objpattern(init_obj, properties):
- for prop in properties:
- # Get the name of the init obj's member
- if prop["key"]["type"] == "Literal":
- prop_name = prop["key"]["value"]
- elif prop["key"]["type"] == "Identifier":
- prop_name = prop["key"]["name"]
- else:
- continue
-
- if prop["value"]["type"] == "Identifier":
- traverser._declare_variable(
- prop["value"]["name"],
- init_obj.get(traverser, prop_name))
- elif prop["value"]["type"] == "ObjectPattern":
- _proc_objpattern(init_obj.get(traverser, prop_name),
- prop["value"]["properties"])
-
- if init is not None:
- _proc_objpattern(init_obj=init,
- properties=declaration["id"]["properties"])
-
- else:
- var_name = declaration["id"]["name"]
- traverser._debug("NAME>>%s" % var_name)
-
- var = traverser.traverse_node(declaration["init"])
- traverser._debug("VALUE>>%s" % (var.output()
- if var is not None
- else "None"))
-
- if not isinstance(var, JSWrapper):
- var = JSWrapper(value=var_value, traverser=traverser)
- var.const = node["kind"] == "const"
- traverser._declare_variable(var_name, var, type_=node["kind"])
-
- traverser.debug_level -= 1
-
- # The "Declarations" branch contains custom elements.
- return True
-
-
-def _define_obj(traverser, node):
- "Creates a local context object"
-
- var = JSObject()
- for prop in node["properties"]:
- var_name = ""
- key = prop["key"]
- var_name = key["value" if key["type"] == "Literal" else "name"]
- var_value = traverser.traverse_node(prop["value"])
- var.set(var_name, var_value, traverser)
-
- # TODO: Observe "kind"
-
- if not isinstance(var, JSWrapper):
- return JSWrapper(var, lazy=True, traverser=traverser)
- var.lazy = True
- return var
-
-
-def _define_array(traverser, node):
- """Instantiate an array object from the parse tree."""
- arr = JSArray()
- arr.elements = map(traverser.traverse_node, node["elements"])
- return arr
-
-
-def _define_literal(traverser, node):
- """
- Convert a literal node in the parse tree to its corresponding
- interpreted value.
- """
- value = node["value"]
- if isinstance(value, dict):
- return JSWrapper(JSObject(), traverser=traverser, dirty=True)
- return JSWrapper(value if value is not None else JSLiteral(None),
- traverser=traverser)
-
-
-def _call_expression(traverser, node):
- args = node["arguments"]
- map(traverser.traverse_node, args)
-
- member = traverser.traverse_node(node["callee"])
-
- if member.is_global and callable(member.value.get("dangerous")):
-
- dangerous = member.value["dangerous"]
- t = traverser.traverse_node
- result = dangerous(a=args, t=t, e=traverser.err)
- if result and "name" in member.value:
- ## Generate a string representation of the params
- #params = u", ".join([_get_as_str(t(p).get_literal_value()) for
- # p in args])
- traverser.err.warning(
- err_id=("testcases_javascript_actions", "_call_expression",
- "called_dangerous_global"),
- warning="`%s` called in potentially dangerous manner" %
- member.value["name"],
- description=result if isinstance(result, DESCRIPTION_TYPES) else
- "The global `%s` function was called using a set "
- "of dangerous parameters. Calls of this nature "
- "are deprecated." % member.value["name"],
- filename=traverser.filename,
- line=traverser.line,
- column=traverser.position,
- context=traverser.context)
-
- elif (node["callee"]["type"] == "MemberExpression" and
- node["callee"]["property"]["type"] == "Identifier"):
-
- # If we can identify the function being called on any member of any
- # instance, we can use that to either generate an output value or test
- # for additional conditions.
- identifier_name = node["callee"]["property"]["name"]
- if identifier_name in instanceactions.INSTANCE_DEFINITIONS:
- result = instanceactions.INSTANCE_DEFINITIONS[identifier_name](
- args, traverser, node, wrapper=member)
- return result
-
- if member.is_global and "return" in member.value:
- return member.value["return"](wrapper=member, arguments=args,
- traverser=traverser)
- return JSWrapper(JSObject(), dirty=True, traverser=traverser)
-
-
-def _expression(traverser, node):
- """
- This is a helper method that allows node definitions to point at
- `traverse_node` without needing a reference to a traverser.
- """
- return traverser.traverse_node(node["expression"])
-
-
-def _get_this(traverser, node):
- "Returns the `this` object"
- if not traverser.this_stack:
- from predefinedentities import GLOBAL_ENTITIES
- return traverser._build_global("window", GLOBAL_ENTITIES[u"window"])
- return traverser.this_stack[-1]
-
-
-def _new(traverser, node):
- "Returns a new copy of a node."
-
- # We don't actually process the arguments as part of the flow because of
- # the Angry T-Rex effect. For now, we just traverse them to ensure they
- # don't contain anything dangerous.
- args = node["arguments"]
- if isinstance(args, list):
- for arg in args:
- traverser.traverse_node(arg)
- else:
- traverser.traverse_node(args)
-
- elem = traverser.traverse_node(node["callee"])
- if not isinstance(elem, JSWrapper):
- elem = JSWrapper(elem, traverser=traverser)
- if elem.is_global:
- traverser._debug("Making overwritable")
- elem.value = deepcopy(elem.value)
- elem.value["overwritable"] = True
- elem.value["readonly"] = False
- if "new" in elem.value:
- elem = elem.value["new"](traverser, node, elem)
- return elem
-
-
-def _ident(traverser, node):
- "Initiates an object lookup on the traverser based on an identifier token"
-
- name = node["name"]
- if traverser._is_defined(name):
- return traverser._seek_variable(name)
-
- return JSWrapper(JSObject(), traverser=traverser, dirty=True)
-
-
-def _expr_assignment(traverser, node):
- """Evaluate an AssignmentExpression node."""
-
- traverser._debug("ASSIGNMENT_EXPRESSION")
- traverser.debug_level += 1
-
- traverser._debug("ASSIGNMENT>>PARSING RIGHT")
- right = traverser.traverse_node(node["right"])
- right = JSWrapper(right, traverser=traverser)
-
- # Treat direct assignment different than augmented assignment.
- if node["operator"] == "=":
-
- global_overwrite = False
- readonly_value = True
-
- node_left = node["left"]
- traverser._debug("ASSIGNMENT:DIRECT(%s)" % node_left["type"])
-
- if node_left["type"] == "Identifier":
- # Identifiers just need the ID name and a value to push.
- # Raise a global overwrite issue if the identifier is global.
- global_overwrite = traverser._is_global(node_left["name"])
-
- # Get the readonly attribute and store its value if is_global
- if global_overwrite:
- from predefinedentities import GLOBAL_ENTITIES
- global_dict = GLOBAL_ENTITIES[node_left["name"]]
- readonly_value = global_dict.get("readonly", True)
-
- traverser._declare_variable(node_left["name"], right, type_="glob")
-
- elif node_left["type"] == "MemberExpression":
- member_object = trace_member(traverser, node_left["object"],
- instantiate=True)
- global_overwrite = (member_object.is_global and
- not member_object.value.get("overwritable"))
- member_property = _get_member_exp_property(traverser, node_left)
- traverser._debug("ASSIGNMENT:MEMBER_PROPERTY(%s)" % member_property)
- traverser._debug("ASSIGNMENT:GLOB_OV::%s" % global_overwrite)
-
- # Don't do the assignment if we're facing a global.
- if not global_overwrite:
- if member_object.value is None:
- member_object.value = JSObject()
-
- if not member_object.is_global:
- member_object.value.set(member_property, right, traverser)
- else:
- # It's probably better to do nothing.
- pass
-
- elif "value" in member_object.value:
- member_object_value = _expand_globals(
- traverser, member_object).value
- if member_property in member_object_value["value"]:
-
- # If it's a global and the actual member exists, test
- # whether it can be safely overwritten.
- member = member_object_value["value"][member_property]
- if callable(member.get("value")):
- member = member["value"](t=traverser)
- readonly_value = member.get("readonly", True)
-
- traverser._debug("ASSIGNMENT:DIRECT:GLOB_OVERWRITE %s" %
- global_overwrite)
-
- if callable(readonly_value):
- # The readonly attribute supports a lambda function that accepts
- readonly_value(t=traverser, r=right, rn=node["right"])
-
- return right
-
- lit_right = right.get_literal_value()
-
- traverser._debug("ASSIGNMENT>>PARSING LEFT")
- left = traverser.traverse_node(node["left"])
- traverser._debug("ASSIGNMENT>>DONE PARSING LEFT")
- traverser.debug_level -= 1
-
- if isinstance(left, JSWrapper):
- if left.dirty:
- return left
-
- lit_left = left.get_literal_value()
- token = node["operator"]
-
- # Don't perform an operation on None. Python freaks out
- if lit_left is None:
- lit_left = 0
- if lit_right is None:
- lit_right = 0
-
- # Give them default values so we have them in scope.
- gleft, gright = 0, 0
-
- # All of the assignment operators
- operators = {"=": lambda: right,
- "+=": lambda: lit_left + lit_right,
- "-=": lambda: gleft - gright,
- "*=": lambda: gleft * gright,
- "/=": lambda: 0 if gright == 0 else (gleft / gright),
- "%=": lambda: 0 if gright == 0 else (gleft % gright),
- "<<=": lambda: int(gleft) << int(gright),
- ">>=": lambda: int(gleft) >> int(gright),
- ">>>=": lambda: float(abs(int(gleft)) >> gright),
- "|=": lambda: int(gleft) | int(gright),
- "^=": lambda: int(gleft) ^ int(gright),
- "&=": lambda: int(gleft) & int(gright)}
-
- # If we're modifying a non-numeric type with a numeric operator, return
- # NaN.
- if (not isinstance(lit_left, NUMERIC_TYPES) and
- token in NUMERIC_OPERATORS):
- left.set_value(get_NaN(traverser), traverser=traverser)
- return left
-
- # If either side of the assignment operator is a string, both sides
- # need to be casted to strings first.
- if (isinstance(lit_left, types.StringTypes) or
- isinstance(lit_right, types.StringTypes)):
- lit_left = _get_as_str(lit_left)
- lit_right = _get_as_str(lit_right)
-
- gleft, gright = _get_as_num(left), _get_as_num(right)
-
- traverser._debug("ASSIGNMENT>>OPERATION:%s" % token)
- if token not in operators:
- # We don't support that operator. (yet?)
- traverser._debug("ASSIGNMENT>>OPERATOR NOT FOUND", 1)
- return left
- elif token in ("<<=", ">>=", ">>>=") and gright < 0:
- # The user is doing weird bitshifting that will return 0 in JS but
- # not in Python.
- left.set_value(0, traverser=traverser)
- return left
- elif (token in ("<<=", ">>=", ">>>=", "|=", "^=", "&=") and
- (abs(gleft) == float('inf') or abs(gright) == float('inf'))):
- # Don't bother handling infinity for integer-converted operations.
- left.set_value(get_NaN(traverser), traverser=traverser)
- return left
-
- traverser._debug("ASSIGNMENT::L-value global? (%s)" %
- ("Y" if left.is_global else "N"), 1)
- new_value = operators[token]()
-
- # Cap the length of analyzed strings.
- if (isinstance(new_value, types.StringTypes)
- and len(new_value) > MAX_STR_SIZE):
- new_value = new_value[:MAX_STR_SIZE]
-
- traverser._debug("ASSIGNMENT::New value >> %s" % new_value, 1)
- left.set_value(new_value, traverser=traverser)
- return left
-
- # Though it would otherwise be a syntax error, we say that 4=5 should
- # evaluate out to 5.
- return right
-
-
-def _expr_binary(traverser, node):
- "Evaluates a BinaryExpression node."
-
- traverser.debug_level += 1
-
- # Select the proper operator.
- operator = node["operator"]
- traverser._debug("BIN_OPERATOR>>%s" % operator)
-
- # Traverse the left half of the binary expression.
- traverser._debug("BIN_EXP>>l-value")
- traverser.debug_level += 1
-
- if (node["left"]["type"] == "BinaryExpression" and
- "__traversal" not in node["left"]):
- # Process the left branch of the binary expression directly. This keeps
- # the recursion cap in line and speeds up processing of large chains
- # of binary expressions.
- left = _expr_binary(traverser, node["left"])
- node["left"]["__traversal"] = left
- else:
- left = traverser.traverse_node(node["left"])
-
- # Traverse the right half of the binary expression.
- traverser._debug("BIN_EXP>>r-value", -1)
-
- if (operator == "instanceof" and
- node["right"]["type"] == "Identifier" and
- node["right"]["name"] == "Function"):
- # We make an exception for instanceof's r-value if it's a dangerous
- # global, specifically Function.
- traverser.debug_level -= 1
- return JSWrapper(True, traverser=traverser)
- else:
- right = traverser.traverse_node(node["right"])
- traverser._debug("Is dirty? %r" % right.dirty, 1)
-
- traverser.debug_level -= 1
-
- # Dirty l or r values mean we can skip the expression. A dirty value
- # indicates that a lazy operation took place that introduced some
- # nondeterminacy.
- if operator != "+":
- # We don't want this to apply to concatenation.
- if left.dirty:
- return left
- elif right.dirty:
- return right
-
- # Binary expressions are only executed on literals.
- left_wrap = left
- left = left.get_literal_value()
- right_wrap = right
- right = right.get_literal_value()
-
- # Coerce the literals to numbers for numeric operations.
- gleft = _get_as_num(left)
- gright = _get_as_num(right)
-
- operators = {
- "==": lambda: left == right or gleft == gright,
- "!=": lambda: left != right,
- "===": lambda: left == right, # Be flexible.
- "!==": lambda: type(left) != type(right) or left != right,
- ">": lambda: left > right,
- "<": lambda: left < right,
- "<=": lambda: left <= right,
- ">=": lambda: left >= right,
- "<<": lambda: int(gleft) << int(gright),
- ">>": lambda: int(gleft) >> int(gright),
- ">>>": lambda: float(abs(int(gleft)) >> int(gright)),
- "+": lambda: left + right,
- "-": lambda: gleft - gright,
- "*": lambda: gleft * gright,
- "/": lambda: 0 if gright == 0 else (gleft / gright),
- "%": lambda: 0 if gright == 0 else (gleft % gright),
- "in": lambda: right_wrap.contains(left),
- # TODO : implement instanceof
- }
-
- output = None
- if (operator in (">>", "<<", ">>>") and
- (left is None or right is None or gright < 0)):
- output = False
- elif operator in operators:
- # Concatenation can be silly, so always turn undefineds into empty
- # strings and if there are strings, make everything strings.
- if operator == "+":
- if left is None:
- left = ""
- if right is None:
- right = ""
- if (isinstance(left, types.StringTypes) or
- isinstance(right, types.StringTypes)):
- left = _get_as_str(left)
- right = _get_as_str(right)
-
- # Don't even bother handling infinity if it's a numeric computation.
- if (operator in ("<<", ">>", ">>>") and
- (abs(gleft) == float('inf') or abs(gright) == float('inf'))):
- return get_NaN(traverser)
-
- output = operators[operator]()
-
- # Cap the length of analyzed strings.
- if (isinstance(output, types.StringTypes)
- and len(output) > MAX_STR_SIZE):
- output = output[:MAX_STR_SIZE]
-
- return JSWrapper(output, traverser=traverser)
-
-
-def _expr_unary(traverser, node):
- """Evaluate a UnaryExpression node."""
-
- expr = traverser.traverse_node(node["argument"])
- expr_lit = expr.get_literal_value()
- expr_num = _get_as_num(expr_lit)
-
- operators = {"-": lambda: -1 * expr_num,
- "+": lambda: expr_num,
- "!": lambda: not expr_lit,
- "~": lambda: -1 * (expr_num + 1),
- "void": lambda: None,
- "typeof": lambda: _expr_unary_typeof(expr),
- "delete": lambda: None} # We never want to empty the context
- if node["operator"] in operators:
- output = operators[node["operator"]]()
- else:
- output = None
-
- if not isinstance(output, JSWrapper):
- output = JSWrapper(output, traverser=traverser)
- return output
-
-
-def _expr_unary_typeof(wrapper):
- """Evaluate the "typeof" value for a JSWrapper object."""
- if (wrapper.callable or
- (wrapper.is_global and "return" in wrapper.value and
- "value" not in wrapper.value)):
- return "function"
-
- value = wrapper.value
- if value is None:
- return "undefined"
- elif isinstance(value, JSLiteral):
- value = value.value
- if isinstance(value, bool):
- return "boolean"
- elif isinstance(value, (int, long, float)):
- return "number"
- elif isinstance(value, types.StringTypes):
- return "string"
- return "object"
-
-
-def _get_as_num(value):
- """Return the JS numeric equivalent for a value."""
- if isinstance(value, JSWrapper):
- value = value.get_literal_value()
-
- if value is None:
- return 0
-
- try:
- if isinstance(value, types.StringTypes):
- if value.startswith("0x"):
- return int(value, 16)
- else:
- return float(value)
- elif isinstance(value, (int, float, long)):
- return value
- else:
- return int(value)
- except (ValueError, TypeError):
- return 0
-
-
-def _get_as_str(value):
- """Return the JS string equivalent for a literal value."""
- if isinstance(value, JSWrapper):
- value = value.get_literal_value()
-
- if value is None:
- return ""
-
- if isinstance(value, bool):
- return u"true" if value else u"false"
- elif isinstance(value, (int, float, long)):
- if value == float('inf'):
- return u"Infinity"
- elif value == float('-inf'):
- return u"-Infinity"
-
- # Try to see if we can shave off some trailing significant figures.
- try:
- if int(value) == value:
- return unicode(int(value))
- except (ValueError, TypeError):
- pass
- return unicode(value)
View
82 appvalidator/testcases/javascript/call_definitions.py
@@ -1,49 +1,45 @@
import math
import re
-import actions
import traverser as js_traverser
import predefinedentities
+import utils
from jstypes import *
from appvalidator.constants import BUGZILLA_BUG
# Function prototypes should implement the following:
-# wrapper : The JSWrapper instace that is being called
+# wrapper : The base object instace that is being called
# arguments : A list of argument nodes; untraversed
# traverser : The current traverser object
# Global object function definitions:
def string_global(wrapper, arguments, traverser):
- if not arguments:
- return JSWrapper("", traverser=traverser)
- arg = traverser.traverse_node(arguments[0])
- value = actions._get_as_str(arg.get_literal_value())
- return JSWrapper(value, traverser=traverser)
+ if (not arguments or
+ not arguments[0].get_literal_value()):
+ return JSObject(traverser=traverser)
+ return JSLiteral(utils.get_as_str(arguments[0].get_literal_value()),
+ traverser=traverser)
def array_global(wrapper, arguments, traverser):
- output = JSArray()
- if arguments:
- output.elements = map(traverser.traverse_node, arguments)
- return JSWrapper(output, traverser=traverser)
+ return JSArray(arguments, traverser=traverser)
def number_global(wrapper, arguments, traverser):
if not arguments:
- return JSWrapper(0, traverser=traverser)
- arg = traverser.traverse_node(arguments[0])
+ return JSLiteral(0, traverser=traverser)
try:
- return float(arg.get_literal_value())
+ return JSLiteral(float(arguments[0].get_literal_value()), traverser=traverser)
except (ValueError, TypeError):
- return actions.get_NaN(traverser)
+ return utils.get_NaN(traverser)
def boolean_global(wrapper, arguments, traverser):
if not arguments:
- return JSWrapper(False, traverser=traverser)
- arg = traverser.traverse_node(arguments[0])
- return JSWrapper(bool(arg.get_literal_value()), traverser=traverser)
+ return JSLiteral(False, traverser=traverser)
+ return JSLiteral(bool(arguments[0].get_literal_value()),
+ traverser=traverser)
def python_wrap(func, args, nargs=False):
@@ -61,21 +57,19 @@ def python_wrap(func, args, nargs=False):
def _process_literal(type_, literal):
if type_ == "string":
- return actions._get_as_str(literal)
+ return utils.get_as_str(literal)
elif type_ == "num":
- return actions._get_as_num(literal)
+ return utils.get_as_num(literal)
return literal
def wrap(wrapper, arguments, traverser):
- passed_args = map(traverser.traverse_node, arguments)
-
params = []
if not nargs:
# Handle definite argument lists.
for type_, def_value in args:
- if passed_args:
- parg = passed_args[0]
- passed_args = passed_args[1:]
+ if arguments:
+ parg = arguments[0]
+ arguments = arguments[1:]
passed_literal = parg.get_literal_value()
passed_literal = _process_literal(type_, passed_literal)
@@ -84,53 +78,47 @@ def wrap(wrapper, arguments, traverser):
params.append(def_value)
else:
# Handle dynamic argument lists.
- for arg in passed_args:
+ for arg in arguments:
literal = arg.get_literal_value()
params.append(_process_literal(args[0], literal))
- traverser._debug("Calling wrapped Python function with: (%s)" %
- ", ".join(map(str, params)))
+ # traverser._debug("Calling wrapped Python function with: (%s)" %
+ # ", ".join(map(str, params)))
try:
- output = func(*params)
+ return JSLiteral(func(*params), traverser=traverser)
except:
# If we cannot compute output, just return nothing.
- output = None
-
- return JSWrapper(output, traverser=traverser)
+ return JSLiteral(None, traverser=traverser)
return wrap
def math_log(wrapper, arguments, traverser):
"""Return a better value than the standard python log function."""
- args = map(traverser.traverse_node, arguments)
- if not args:
- return JSWrapper(0, traverser=traverser)
+ if not arguments:
+ return JSLiteral(0, traverser=traverser)
- arg = actions._get_as_num(args[0].get_literal_value())
+ arg = utils.get_as_num(arguments[0].get_literal_value())
if arg == 0:
- return JSWrapper(float('-inf'), traverser=traverser)
+ return JSLiteral(float('-inf'), traverser=traverser)
if arg < 0:
- return JSWrapper(traverser=traverser)
+ return JSLiteral(None, traverser=traverser)
- arg = math.log(arg)
- return JSWrapper(arg, traverser=traverser)
+ return JSLiteral(math.log(arg), traverser=traverser)
def math_round(wrapper, arguments, traverser):
"""Return a better value than the standard python round function."""
- args = map(traverser.traverse_node, arguments)
- if not args:
- return JSWrapper(0, traverser=traverser)
+ if not arguments:
+ return JSLiteral(0, traverser=traverser)
- arg = actions._get_as_num(args[0].get_literal_value())
+ arg = utils.get_as_num(arguments[0].get_literal_value())
# Prevent nasty infinity tracebacks.
if abs(arg) == float("inf"):
- return args[0]
+ return arguments[0]
# Python rounds away from zero, JS rounds "up".
if arg < 0 and int(arg) != arg:
arg += 0.0000000000000001
- arg = round(arg)
- return JSWrapper(arg, traverser=traverser)
+ return JSLiteral(round(arg), traverser=traverser)
View
74 appvalidator/testcases/javascript/entity_values.py
@@ -1,6 +1,3 @@
-from appvalidator.constants import BUGZILLA_BUG
-
-
ENTITIES = {}
def register_entity(name):
"""Allow an entity's modifier to be registered for use."""
@@ -11,49 +8,47 @@ def wrap(function):
def entity(name, result=None):
- def return_wrap(t):
- output = ENTITIES[name](traverser=t)
- if result is not None:
- return result
- return output or {"value": {}}
- return {"value": return_wrap}
+ ent = ENTITIES[name]
+ if callable(ent):
+ ent = ent()
+ ENTITIES[name] = ent
+ return ent or {"value": {}}
@register_entity("Function")
@register_entity("eval")
-def csp_warning_function(traverser):
+def csp_warning_function():
def call_wrap(*args, **kwargs):
+ traverser = kwargs.get("traverser") or args[-1]
from appvalidator.csp import warn
warn(err=traverser.err,
filename=traverser.filename,
line=traverser.line,
column=traverser.position,
context=traverser.context,
violation_type="script")
- return False
return {
- "dangerous": call_wrap,
+ "new": call_wrap,
"return": call_wrap,
"value": call_wrap,
}
@register_entity("setTimeout")
@register_entity("setInterval")
-def csp_warning_timeout(traverser):
- def wrap(a, t, e):
- if a and a[0]["type"] != "FunctionExpression":
+def csp_warning_timeout():
+ def wrap(wrapper, arguments, traverser):
+ if arguments and not arguments[0].callable:
from appvalidator.csp import warn
warn(err=traverser.err,
filename=traverser.filename,
line=traverser.line,
column=traverser.position,
context=traverser.context,
violation_type="set*")
- return False
- return {"dangerous": wrap}
+ return {"return": wrap}
GUM_FEATURES = {
@@ -63,18 +58,18 @@ def wrap(a, t, e):
}
@register_entity("getUserMedia")
-def getUserMedia(traverser):
+def getUserMedia():
def method(wrapper, arguments, traverser):
if not arguments:
return False
- options = traverser.traverse_node(arguments[0])
+ options = arguments[0]
for feature in GUM_FEATURES:
- if (options.has_property(feature) and
+ if (options.has_var(feature) and
options.get(traverser, feature).get_literal_value() == True):
traverser.log_feature(GUM_FEATURES[feature])
- if (options.has_property("video") and
- options.get(traverser, "video").has_property("mandatory") and
+ if (options.has_var("video") and
+ options.get(traverser, "video").has_var("mandatory") and
options.get(traverser, "video").get(traverser, "mandatory") and
options.get(traverser, "video").get(traverser, "mandatory"
).get(traverser, "chromeMediaSource"
@@ -85,24 +80,31 @@ def method(wrapper, arguments, traverser):
@register_entity("XMLHttpRequest")
-def XMLHttpRequest(traverser):
- def dangerous(a, t, e):
- print a, e
- if a and len(a) >= 3 and not t(a[2]).get_literal_value():
- return ("Synchronous HTTP requests can cause serious UI "
- "performance problems, especially to users with "
- "slow network connections.")
-
- def new(traverser, node, elem):
+def XMLHttpRequest():
+ def return_(wrapper, arguments, traverser):
+ if (arguments and len(arguments) >= 3 and
+ not arguments[2].get_literal_value()):
+ traverser.err.warning(
+ err_id=("javascript", "xhr", "sync"),
+ warning="Synchronous XHR should not be used",
+ description="Synchronous HTTP requests can cause serious UI "
+ "performance problems, especially to users with "
+ "slow network connections.",
+ filename=traverser.filename,
+ line=traverser.line,
+ column=traverser.position,
+ context=traverser.context)
+ return wrapper
+
+ def new(node, arguments, traverser):
if not node["arguments"]: # Ignore XHR without args
- return elem
+ return
arg = traverser.traverse_node(node["arguments"][0])
- if (arg.has_property("mozSystem") and
+ if (arg.has_var("mozSystem") and
arg.get(traverser, "mozSystem").get_literal_value()):
traverser.log_feature("SYSTEMXHR")
- return elem
return {
- "value": {u"open": {"dangerous": dangerous}},
+ "value": {u"open": {"return": return_}},
"new": new,
- }
+ }
View
23 appvalidator/testcases/javascript/instanceactions.py
@@ -10,10 +10,10 @@
the current node being evaluated
"""
-import actions
+import jstypes
+import utils
from appvalidator.constants import BUGZILLA_BUG
from appvalidator.csp import warn
-from .jstypes import *
from .instanceproperties import _set_HTML_property
@@ -23,12 +23,10 @@ def createElement(args, traverser, node, wrapper):
if not args:
return
- simple_args = map(traverser.traverse_node, args)
-
- first_as_str = actions._get_as_str(simple_args[0].get_literal_value())
+ first_as_str = utils.get_as_str(args[0].get_literal_value())
if first_as_str.lower() == u"script":
_create_script_tag(traverser)
- elif not simple_args[0].is_literal():
+ elif not isinstance(args[0], jstypes.JSLiteral):
_create_variable_element(traverser)
@@ -38,12 +36,10 @@ def createElementNS(args, traverser, node, wrapper):
if not args or len(args) < 2:
return
- simple_args = map(traverser.traverse_node, args)
-
- second_as_str = actions._get_as_str(simple_args[1].get_literal_value())
+ second_as_str = utils.get_as_str(args[1].get_literal_value())
if "script" in second_as_str.lower():
_create_script_tag(traverser)
- elif not simple_args[1].is_literal():
+ elif not isinstance(args[1], jstypes.JSLiteral):
_create_variable_element(traverser)
@@ -78,8 +74,7 @@ def insertAdjacentHTML(args, traverser, node, wrapper):
if not args or len(args) < 2:
return
- content = traverser.traverse_node(args[1])
- _set_HTML_property("insertAdjacentHTML", content, traverser)
+ _set_HTML_property("insertAdjacentHTML", args[1], traverser)
def setAttribute(args, traverser, node, wrapper):
@@ -88,9 +83,7 @@ def setAttribute(args, traverser, node, wrapper):
if not args:
return
- simple_args = [traverser.traverse_node(a) for a in args]
-
- first_as_str = actions._get_as_str(simple_args[0].get_literal_value())
+ first_as_str = utils.get_as_str(args[0].get_literal_value())
if first_as_str.lower().startswith("on"):
warn(traverser.err,
filename=traverser.filename,
View
12 appvalidator/testcases/javascript/instanceproperties.py
@@ -21,10 +21,8 @@ def set_outerHTML(new_value, traverser):
def _set_HTML_property(function, new_value, traverser):
- if not isinstance(new_value, jstypes.JSWrapper):
- new_value = jstypes.JSWrapper(new_value, traverser=traverser)
-
- if new_value.is_literal():
+ if isinstance(new_value, jstypes.JSLiteral):
+ # TODO: This might be optimizable as get_as_str
literal_value = new_value.get_literal_value()
if isinstance(literal_value, types.StringTypes):
# Static string assignments
@@ -56,10 +54,8 @@ def _set_HTML_property(function, new_value, traverser):
def set_on_event(new_value, traverser):
"""Ensure that on* properties are not assigned string values."""
- is_literal = new_value.is_literal()
-
- if is_literal and isinstance(new_value.get_literal_value(),
- types.StringTypes):
+ if (isinstance(new_value, jstypes.JSLiteral) and
+ isinstance(new_value.get_literal_value(), types.StringTypes)):
warn(traverser.err,
filename=traverser.filename,
line=traverser.line,
View
496 appvalidator/testcases/javascript/jstypes.py
@@ -1,4 +1,11 @@
+import types
+
import instanceproperties
+import utils
+
+
+def fake(traverser, **kw):
+ return JSObject(traverser=traverser, **kw)
class JSObject(object):
@@ -7,15 +14,21 @@ class JSObject(object):
context to enable static analysis of `with` statements.
"""
- def __init__(self, data=None):
+ TYPEOF = "object"
+
+ def __init__(self, data=None, traverser=None, callable_=False, const=False):
+ self.const = False
+ self.traverser = traverser
self.type_ = "object" # For use when an object is pushed as a context.
- self.data = {u"prototype": lambda: JSPrototype()}
+ self.data = {}
if data:
self.data.update(data)
+ self.callable = callable_
+ self.const = const
self.recursing = False
- def get(self, name, instantiate=False, traverser=None):
+ def get(self, traverser, name, instantiate=False):
"Returns the value associated with a property name"
name = unicode(name)
output = None
@@ -24,38 +37,49 @@ def get(self, name, instantiate=False, traverser=None):
output = self.data[name]
if callable(output):
output = output()
- elif instantiate:
- output = JSWrapper(JSObject(), dirty=True, traverser=traverser)
+ elif instantiate or name in ('constructor', 'prototype'):
+ output = fake(traverser)
self.set(name, output, traverser=traverser)
if traverser:
modifier = instanceproperties.get_operation("get", name)
if modifier:
modifier(traverser)
- dirty = False
if output is None:
- dirty = True
- output = JSObject()
+ return fake(traverser)
- if dirty or not isinstance(output, JSWrapper):
- output = JSWrapper(output, dirty=dirty, traverser=traverser)
return output
- def get_literal_value(self):
+ def get_literal_value(self, traverser=None):
return u"[object Object]"
def set(self, name, value, traverser=None):
+ traverser = self.traverser or traverser
if traverser:
modifier = instanceproperties.get_operation("set", name)
if modifier:
modified_value = modifier(value, traverser)
if modified_value is not None:
value = modified_value
- self.data[name] = value
+ if (self.has_var(name, traverser) and
+ self.get(traverser, name).const):
+
+ traverser.err.warning(
+ err_id=("js", "JSWrapper_set_value", "const_overwrite"),
+ warning="Overwritten constant value",
+ description="A variable declared as constant has been "
+ "overwritten in some JS code.",
+ filename=traverser.filename,
+ line=traverser.line,
+ column=traverser.position,
+ context=traverser.context)
+ return
+
+ self.data[unicode(name)] = value
- def has_var(self, name):
+ def has_var(self, name, traverser=None):
return unicode(name) in self.data
def output(self):
@@ -69,8 +93,7 @@ def output(self):
for key in self.data.keys():
if callable(self.data[key]):
continue
- elif (isinstance(self.data[key], JSWrapper) and
- self.data[key].value == self):
+ elif self.data[key] == self:
output_dict[key] = u"(self)"
elif self.data[key] is None:
output_dict[key] = u"(None)"
@@ -80,322 +103,189 @@ def output(self):
# Pop from the recursion buster.
self.recursing = False
- return str(output_dict)
-
-
-class JSContext(JSObject):
- """A variable context"""
-
- def __init__(self, context_type):
- super(JSContext, self).__init__()
- self.type_ = context_type
- self.data = {}
-
-
-class JSWrapper(object):
- """Wraps a JS value and handles contextual functions for it."""
-
- def __init__(self, value=None, dirty=False, lazy=False,
- is_global=False, traverser=None, callable_=False,
- setter=None, context="chrome"):
+ return unicode(output_dict)
- if is_global:
- assert not value
-
- if traverser is not None:
- traverser.debug_level += 1
- traverser._debug("-----New JSWrapper-----")
- if isinstance(value, JSWrapper):
- traverser._debug(">>> Rewrap <<<")
- traverser.debug_level -= 1
+ def __str__(self):
+ return self.output()
- self.const = False
- self.traverser = traverser
- self.value = None # Instantiate the placeholder value
- self.is_global = False # Not yet...
- self.dirty = False # Also not yet...
- self.context = context
+ def delete(self, member):
+ if member not in self.data:
+ return
+ del self.data[member]
- # Used for predetermining set operations
- self.setter = setter
- if value is not None:
- self.set_value(value, overwrite_const=True)
+class JSGlobal(JSObject):
- if not self.is_global:
- self.is_global = is_global # Globals are built seperately
+ def __init__(self, global_data, traverser=None, **kw):
+ self.global_data = utils.evaluate_lambdas(traverser, global_data)
+ super(JSGlobal, self).__init__(traverser=traverser, **kw)
- self.dirty = dirty or self.dirty
- self.lazy = lazy
- self.callable = callable_
+ if "typeof" in self.global_data:
+ self.TYPEOF = self.global_data["typeof"]
- def set_value(self, value, traverser=None, overwrite_const=False):
- """Assigns a value to the wrapper"""
+ def _get_contents(self, traverser):
+ if "value" not in self.global_data:
+ return None
+ directory = utils.evaluate_lambdas(
+ traverser, self.global_data["value"])
+ if directory and callable(self.global_data["value"]):
+ self.global_data["value"] = directory
+ return directory
- # Use a global traverser if it's present.
- if traverser is None:
- traverser = self.traverser
+ def get(self, traverser, name, instantiate=False):
+ if name in self.data or instantiate:
+ traverser._debug("Global member found in set data: %s" % name)
+ return super(JSGlobal, self).get(
+ traverser, name, instantiate=instantiate)
+
+ directory = self._get_contents(traverser)
+ if directory and isinstance(directory, dict) and name in directory:
+ traverser._debug("GETTING (%s) FROM GLOBAL" % name)
+ value = utils.evaluate_lambdas(traverser, directory[name])
+ if "literal" in value:
+ lit = utils.evaluate_lambdas(traverser, value["literal"])
+ return JSLiteral(lit, traverser=traverser)
+ return traverser._build_global(name=name, entity=value)
+
+ traverser._debug("JSObject fallback for member %s in %s" %
+ (name, directory))
+ return super(JSGlobal, self).get(traverser, name)
- if self.const and not overwrite_const:
- traverser.err.warning(
- err_id=("testcases_javascript_traverser", "JSWrapper_set_value",
- "const_overwrite"),
- warning="Overwritten constant value",
- description="A variable declared as constant has been "
- "overwritten in some JS code.",
+ def set(self, name, value, traverser=None):
+ directory = self._get_contents(traverser)
+ if directory and isinstance(directory, dict) and name in directory:
+ traverser._debug("Setting global member %s" % name)
+ self.get(traverser, name)._set_to(value, traverser=traverser)
+ return super(JSGlobal, self).set(name, value, traverser=traverser)
+
+ def _set_to(self, value, traverser=None):
+ "This is called when the value of this glboal node is set directly."
+
+ traverser._debug("Assigning direct global value")
+ self._get_contents(traverser) # We don't care about the output.
+ if self.global_data.get("readonly"):
+ traverser.err.notice(
+ err_id=("js", "global", "readonly"),
+ notice="Read-only JS global modified",
+ description=["A read-only JS global was modified by some "
+ "code. This may cause issues and usually "
+ "indicates other problems with the code.",
+ "Modified global: %s" %
+ self.global_data.get("name", "(unknown)")],
filename=traverser.filename,
line=traverser.line,
column=traverser.position,
context=traverser.context)
- # Process any setter/modifier
- if self.setter:
- traverser._debug("Running setter on JSWrapper...");
- value = self.setter(value, traverser) or value or None
+ def has_var(self, name, traverser=None):
+ directory = self._get_contents(traverser)
+ if directory and name in directory:
+ return True
+ return super(JSGlobal, self).has_var(name, traverser=traverser)
- if value == self.value:
- return self
+ def get_literal_value(self, traverser=None):
+ if "literal" in self.global_data:
+ lit = utils.evaluate_lambdas(traverser, self.global_data["literal"])
+ return JSLiteral(lit, traverser=traverser)
- if isinstance(value, (bool, str, int, float, long, unicode)):
- self.inspect_literal(value)
- value = JSLiteral(value)
- # If the value being assigned is a wrapper as well, copy it in
- elif isinstance(value, JSWrapper):
- self.value = value.value
- self.lazy = value.lazy
- self.dirty = value.dirty
- self.is_global = value.is_global
- self.context = value.context
- # const does not carry over on reassignment
- return self
- elif callable(value):
- value = value(t=traverser)
-
- if not isinstance(value, dict):
- self.is_global = False
- elif "context" in value:
- self.context = value["context"]
-
- self.value = value
- return self
-
- def has_property(self, prop):
- """Returns a boolean value representing the presence of a property"""
- return (getattr(self.value, "has_var") and
- self.value.has_var(prop))
+ directory = self._get_contents(traverser)
+ if directory and not isinstance(directory, dict):
+ return directory.get_literal_value(traverser=traverser)
- def get(self, traverser, name, instantiate=False):
- """Retrieve a property from the variable."""
-
- value = self.value
- dirty = value is None
- context = self.context
- if self.is_global:
- if "value" not in value:
- output = JSWrapper(JSObject(), traverser=traverser)
- output.value = {}
-
- def apply_value(name):
- if name in self.value:
- output.value[name] = self.value[name]
-
- map(apply_value, ("dangerous", "readonly", "context", "name"))
- output.is_global = True
- output.context = self.context
- return output
-
- def _evaluate_lambdas(node):
- if callable(node):
- return _evaluate_lambdas(node(t=traverser))
- else:
- return node
-
- value_val = value["value"]
- value_val = _evaluate_lambdas(value_val)
-
- if isinstance(value_val, dict):
- if name in value_val:
- value_val = _evaluate_lambdas(value_val[name])
- output = traverser._build_global(name=name,
- entity=value_val)
- if "context" not in value_val:
- output.context = self.context
- return output
- else:
- value = value_val
-
- # Process any getters that are present for the current property.
- modifier = instanceproperties.get_operation("get", name)
- if modifier:
- modifier(traverser)
-
- if value is not None and issubclass(type(value), JSObject):
- output = value.get(name, instantiate=instantiate,
- traverser=traverser)
- else:
- output = None
-
- if not isinstance(output, JSWrapper):
- output = JSWrapper(
- output, traverser=traverser, dirty=output is None or dirty)
-
- output.context = context
-
- # If we can predetermine the setter for the wrapper, we can save a ton
- # of lookbehinds in the future. This greatly simplifies the
- # MemberExpression support.
- setter = instanceproperties.get_operation("set", name)
- if setter:
- output.setter = setter
- return output
-
- def del_value(self, member):
- """The member `member` will be deleted from the value of the wrapper"""
- if self.is_global:
- self.traverser.err.warning(
- err_id=("testcases_js_jstypes", "del_value",
- "global_member_deletion"),
- warning="Global member deletion",
- description="Members of global objects cannot be deleted.",
- filename=self.traverser.filename,
- line=self.traverser.line,
- column=self.traverser.position,
- context=self.traverser.context)
- elif isinstance(self.value, (JSObject, JSPrototype)):
- if member not in self.value.data:
- return
- del self.value.data[member]
-
- def contains(self, value):
- """Serves 'in' for BinaryOperators for lists and dictionaries"""
-
- # Unwrap the rvalue.
- if isinstance(value, JSWrapper):
- value = value.get_literal_value()
-
- if isinstance(self.value, JSArray):
- from actions import _get_as_num
- index = int(_get_as_num(value))
- if len(self.value.elements) > index >= 0:
- return True
-
- if isinstance(self.value, (JSArray, JSObject, JSPrototype)):
- return self.value.has_var(value)
-
- # Nothing else supports "in"
- return False
-
- def is_literal(self):
- """Returns whether the content is a literal"""
- return isinstance(self.value, JSLiteral)
-
- def get_literal_value(self):
- """Returns the literal value of the wrapper"""
-
- if self.is_global:
- if "literal" in self.value:
- return self.value["literal"](self.traverser)
- else:
- return "[object Object]"
- if self.value is None:
- return None
-
- output = self.value.get_literal_value()
- return output
+ return super(JSGlobal, self).get_literal_value(traverser)
def output(self):
- """Returns a readable version of the object"""
- if self.value is None:
- return "(None)"
- elif self.is_global:
- return "(Global)"
-
- return self.value.output()
-
- def inspect_literal(self, value):
- """
- Inspect the value of a literal to see whether it contains a flagged
- value.
- """
- return # This is currently a noop.
-
- # Don't do any processing if we can't return an error.
- if not self.traverser:
- return
+ return "[global %s]{%s}%s" % (
+ getattr(self, "name", "Unnamed"),
+ ",".join("%s:%s" % (repr(k), repr(v)) for
+ k, v in self.global_data.items()),
+ super(JSGlobal, self).output())
- self.traverser._debug("INSPECTING: %s" % value)
- # This is a no-op for now.
- pass
+class JSContext(JSObject):
+ """A variable context"""
+
+ def __init__(self, context_type, traverser=None, **kw):
+ super(JSContext, self).__init__(traverser=traverser, **kw)
+ self.type_ = context_type
+ self.data = {}
- def __str__(self):
- """Returns a textual version of the object."""
- return unicode(self.get_literal_value())
+LITERAL_TYPEOF = {
+ int: "number",
+ float: "number",
+ long: "number",
+ str: "string",
+ unicode: "string",
+ bool: "boolean",
+}
class JSLiteral(JSObject):
"""Represents a literal JavaScript value."""
- def __init__(self, value=None):
- super(JSLiteral, self).__init__()
- self.value = value
-
- def set_value(self, value):
- self.value = value
+ def __init__(self, value=None, traverser=None, **kw):
+ super(JSLiteral, self).__init__(traverser=traverser, **kw)
+ if isinstance(value, JSLiteral):
+ self.value = value.value
+ else:
+ self.value = value
+ self.TYPEOF = LITERAL_TYPEOF.get(type(value), self.TYPEOF)
def __str__(self):
if isinstance(self.value, bool):
- return str(self.output()).lower()
- return str(self.output())
+ return unicode(self.output()).lower()
+ return unicode(self.output())
+
+ def __repr__(self):
+ return u'<JSLiteral %r>' % self.value
def output(self):
return self.value
- def get_literal_value(self):
+ def get_literal_value(self, traverser=None):
"Returns the literal value of a this literal. Heh."
return self.value
+ def has_var(self, name, traverser=None):
+ return False
-class JSPrototype(JSObject):
- """
- A lazy JavaScript object that is assumed not to contain any default
- methods.
- """
-
- def __init__(self):
- super(JSPrototype, self).__init__()
- self.data = {}
-
- def get(self, name, instantiate=False, traverser=None):
- name = unicode(name)
- output = super(JSPrototype, self).get(name, instantiate, traverser)
- if output is not None:
- return output
- if name == "prototype":
- prototype = JSWrapper(JSPrototype(), traverser=traverser)
- self.data[name] = prototype
-
- return output
+ def delete(self, member):
+ pass
class JSArray(JSObject):
"""A class that represents both a JS Array and a JS list."""
- def __init__(self):
- super(JSArray, self).__init__()
- self.elements = []
+ def __init__(self, elements=None, traverser=None, **kw):
+ super(JSArray, self).__init__(traverser=traverser, **kw)
+ self.elements = elements or []
- def get(self, index, instantiate=False, traverser=None):
+ def get(self, traverser, index, instantiate=False):
if index == "length":
- return len(self.elements)
+ return JSLiteral(len(self.elements), traverser=traverser)
# Courtesy of Ian Bicking: http://bit.ly/hxv6qt
try:
- return self.elements[int(index.strip().split()[0])]
+ el = self.elements[int(index.strip().split()[0])]
+ if el is None:
+ el = JSLiteral(None, traverser=traverser)
+ return el
except (ValueError, IndexError, KeyError):
- return super(JSArray, self).get(index, instantiate, traverser)
+ return super(JSArray, self).get(traverser, index, instantiate)
+
+ def has_var(self, name, traverser=None):
+ index = None
+ if isinstance(name, types.StringTypes) and name.isdigit():
+ index = utils.get_as_num(name, traverser)
+ elif isinstance(name, int):
+ index = name
- def get_literal_value(self):
+ if index is not None and len(self.elements) > index >= 0:
+ return True
+
+ return super(JSArray, self).has_var(name, traverser=traverser)
+
+ def get_literal_value(self, traverser=None):
"""Arrays return a comma-delimited version of themselves."""
if self.recursing:
@@ -408,33 +298,39 @@ def get_literal_value(self):
# y = x * 3 // y = 12 since x equals "4"
output = u",".join(
- [unicode(w.get_literal_value() if w is not None else u"") for w in
- self.elements if
- not (isinstance(w, JSWrapper) and w.value == self)])
+ unicode(w.get_literal_value(traverser=self.traverser) if
+ w is not None and w is not self else u"") for
+ w in self.elements)
self.recursing = False
return output
def set(self, index, value, traverser=None):
- try:
- index = int(index)
- f_index = float(index)
- # Ignore floating point indexes
- if index != float(index) or index < 0:
- return super(JSArray, self).set(value, traverser)
- except ValueError:
+ if index.isdigit():
+ try:
+ i_index = min(int(index), 100000)
+ # Ignore floating point indexes
+ if i_index != float(index) or i_index < 0:
+ return super(JSArray, self).set(index, value, traverser)
+ if len(self.elements) <= i_index:
+ for i in xrange(i_index - len(self.elements) + 1):
+ self.elements.append(None)
+ self.elements[i_index] = value
+ return value
+ except ValueError:
+ return super(JSArray, self).set(index, value, traverser)
+ else:
return super(JSArray, self).set(index, value, traverser)
- if len(self.elements) > index:
- self.elements[index] = JSWrapper(value=value, traverser=traverser)
+ def delete(self, member):
+ if member.isdigit() and self.has_var(member):
+ index = int(member)
+ if index == len(self.elements) - 1:
+ self.elements.pop()
+ else:
+ self.elements[member] = None
else:
- # Max out the array size at 100000
- index = min(index, 100000)
- # Assigning to an index higher than the top of the list pads the
- # list with nulls
- while len(self.elements) < index:
- self.elements.append(None)
- self.elements.append(JSWrapper(value=value, traverser=traverser))
+ super(JSArray, self).delete(member)
def output(self):
return u"[%s]" % self.get_literal_value()
View
643 appvalidator/testcases/javascript/nodedefinitions.py
@@ -1,4 +1,602 @@
-import actions
+import types
+
+from appvalidator.constants import MAX_STR_SIZE
+from appvalidator.python.copy import deepcopy
+
+import instanceactions
+import utils
+from jstypes import JSArray, JSContext, JSGlobal, JSLiteral, JSObject
+
+
+NUMERIC_TYPES = (int, long, float, complex)
+
+# None of these operations (or their augmented assignment counterparts) should
+# be performed on non-numeric data. Any time we get non-numeric data for these
+# guys, we just return window.NaN.
+NUMERIC_OPERATORS = ("-", "*", "/", "%", "<<", ">>", ">>>", "|", "^", "&")
+NUMERIC_OPERATORS += tuple("%s=" % op for op in NUMERIC_OPERATORS)
+
+
+def ExpressionStatement(traverser, node):
+ return traverser.traverse_node(node["expression"])
+
+
+def WithStatement(traverser, node):
+ obj = traverser.traverse_node(node["object"])
+ traverser.contexts[-1] = obj
+ traverser.contexts.append(JSContext("block"))
+
+
+def _function(traverser, node):
+ """
+ A helper function that traverses and instantiates function declarations and
+ function expressions.
+ """
+
+ def wrap(traverser, node):
+ me = JSObject()
+
+ traverser.function_collection.append([])
+
+ # Replace the current context with a prototypeable JS object.
+ traverser._pop_context()
+ me.type_ = "default" # Treat the function as a normal object.
+ traverser._push_context(me)
+ traverser._debug("THIS_PUSH")
+ traverser.this_stack.append(me) # Allow references to "this"
+
+ # 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"])
+
+ local_context = traverser._peek_context(1)
+ for param in params:
+ var = JSObject(traverser=traverser)
+
+ # 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)
+
+ traverser.traverse_node(node["body"])
+
+ # 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.
+ if traverser.function_collection:
+ traverser.function_collection[-1].append(lambda: wrap(traverser, node))
+ else:
+ wrap(traverser, node)
+
+ return JSObject(traverser=traverser, callable_=True)
+
+
+def FunctionDeclaration(traverser, node):
+ me = _function(traverser, node)
+ traverser._peek_context(2).set(node["id"]["name"], me)
+ return me
+
+# It's just an alias.
+FunctionExpression = _function
+
+
+def VariableDeclaration(traverser, node):
+ traverser._debug("VARIABLE_DECLARATION")
+ traverser.debug_level += 1
+
+ for declaration in node["declarations"]:
+
+ # It could be deconstruction of variables :(
+ if declaration["id"]["type"] == "ArrayPattern":
+
+ vars = []
+ for element in declaration["id"]["elements"]:
+ # NOTE : Multi-level array destructuring sucks. Maybe implement
+ # it someday if you're bored, but it's so rarely used and it's
+ # so utterly complex, there's probably no need to ever code it
+ # up.
+ if element is None or element["type"] != "Identifier":
+ vars.append(None)
+ continue
+ vars.append(element["name"])
+
+ # The variables are not initialized
+ if declaration["init"] is None:
+ # Simple instantiation; no initialization
+ for var in filter(None, vars):
+ traverser._declare_variable(
+ var, JSObject(traverser=traverser))
+
+ # The variables are declared inline
+ elif declaration["init"]["type"] == "ArrayPattern":
+ # TODO : Test to make sure len(values) == len(vars)
+ for value in declaration["init"]["elements"]:
+ if vars[0]:
+ traverser._declare_variable(
+ vars[0], traverser.traverse_node(value))
+ vars = vars[1:] # Pop off the first value
+
+ # It's being assigned by a JSArray (presumably)
+ elif declaration["init"]["type"] == "ArrayExpression":
+ assigner = traverser.traverse_node(declaration["init"])
+ for value, var in zip(assigner.elements, vars):
+ traverser._declare_variable(var, value)
+
+ elif declaration[