#
# Jasy - Web Tooling Framework
# Copyright 2010-2012 Zynga Inc.
# Copyright 2013-2014 Sebastian Werner
#
from jasy.script.output.Compressor import Compressor
# Shared instance
compressor = Compressor()
pseudoTypes = set(["any", "var", "undefined", "null", "true", "false", "this", "arguments"])
builtinTypes = set(["Object", "String", "Number", "Boolean", "Array", "Function", "RegExp", "Date"])
# Basic user friendly node type to human type
nodeTypeToDocType = {
# Primitives
"string": "String",
"number": "Number",
"not": "Boolean",
"true": "Boolean",
"false": "Boolean",
# Literals
"function": "Function",
"regexp": "RegExp",
"object_init": "Map",
"array_init": "Array",
# We could figure out the real class automatically - at least that's the case quite often
"new": "Object",
"new_with_args": "Object",
# Comparisons
"eq" : "Boolean",
"ne" : "Boolean",
"strict_eq" : "Boolean",
"strict_ne" : "Boolean",
"lt" : "Boolean",
"le" : "Boolean",
"gt" : "Boolean",
"ge" : "Boolean",
"in" : "Boolean",
"instanceof" : "Boolean",
# Numbers
"lsh": "Number",
"rsh": "Number",
"ursh": "Number",
"minus": "Number",
"mul": "Number",
"div": "Number",
"mod": "Number",
"bitwise_and": "Number",
"bitwise_xor": "Number",
"bitwise_or": "Number",
"bitwise_not": "Number",
"increment": "Number",
"decrement": "Number",
"unary_minus": "Number",
"unary_plus": "Number",
# This is not 100% correct, but I don't like to introduce a BooleanLike type.
# If the author likes something different he is still able to override it via API docs
"and": "Boolean",
"or": "Boolean",
# Operators/Built-ins
"void": "undefined",
"null": "null",
"typeof": "String",
"delete": "Boolean",
"this": "This",
# These are not real types, we try to figure out the real value behind automatically
"call": "Call",
"hook": "Hook",
"assign": "Assign",
"plus": "Plus",
"identifier" : "Identifier",
"dot": "Object",
"index": "var"
}
[docs]def getVisibility(name):
"""Returns the visibility of the given name by convention."""
if name.startswith("__"):
return "private"
elif name.startswith("_"):
return "internal"
else:
return "public"
[docs]def requiresDocumentation(name):
"""Whether the given name suggests that documentation is required."""
return not name.startswith("_")
[docs]def getKeyValue(dict, key):
"""Returns the value node of the given key inside the given object initializer."""
for propertyInit in dict:
if propertyInit[0].value == key:
return propertyInit[1]
[docs]def findAssignments(name, node):
"""Returns a list of assignments which might have impact on the value used in the given node."""
# Looking for all script blocks
scripts = []
parent = node
while parent:
if parent.type == "script":
scope = getattr(parent, "scope", None)
if scope and name in scope.modified:
scripts.append(parent)
parent = getattr(parent, "parent", None)
def assignMatcher(node):
if node.type == "assign" and node[0].type == "identifier" and node[0].value == name:
return True
if node.type == "declaration" and node.name == name and getattr(node, "initializer", None):
return True
if node.type == "function" and node.functionForm == "declared_form" and node.name == name:
return True
return False
# Query all relevant script nodes
assignments = []
for script in scripts:
queryResult = queryAll(script, assignMatcher, False)
assignments.extend(queryResult)
# Collect assigned values
values = []
for assignment in assignments:
if assignment.type == "function":
values.append(assignment)
elif assignment.type == "assign":
values.append(assignment[1])
else:
values.append(assignment.initializer)
return assignments, values
[docs]def findFunction(node):
"""Returns the first function inside the given node."""
return query(node, lambda node: node.type == "function")
[docs]def findReturn(node):
"""Finds the first return inside the given node."""
return query(node, lambda node: node.type == "return", True)
[docs]def valueToString(node):
"""Converts the value of the given node into something human friendly."""
if node.type in ("number", "string", "false", "true", "regexp", "null"):
return compressor.compress(node)
elif node.type in nodeTypeToDocType:
if node.type == "plus":
return detectPlusType(node)
elif node.type in ("new", "new_with_args", "dot"):
return detectObjectType(node)
else:
return nodeTypeToDocType[node.type]
else:
return "Other"
[docs]def queryAll(node, matcher, deep=True, inner=False, result=None):
"""
Recurses the tree starting with the given node and returns a list of nodes matched by the given matcher method.
- node: any node
- matcher: function which should return a truish value when node matches
- deep: whether inner scopes should be scanned, too
- inner: used internally to differentiate between current and inner nodes
- result: can be used to extend an existing list, otherwise a new list is created and returned
"""
if result is None:
result = []
# Don't do in closure functions
if inner and node.type == "script" and not deep:
return None
if matcher(node):
result.append(node)
for child in node:
queryAll(child, matcher, deep, True, result)
return result
[docs]def query(node, matcher, deep=True, inner=False):
"""
Recurses the tree starting with the given node and returns the first node which is matched by the given matcher
method.
- node: any node
- matcher: function which should return a truish value when node matches
- deep: whether inner scopes should be scanned, too
- inner: used internally to differentiate between current and inner nodes
"""
# Don't do in closure functions
if inner and node.type == "script" and not deep:
return None
if matcher(node):
return node
for child in node:
result = query(child, matcher, deep, True)
if result is not None:
return result
return None
[docs]def findCall(node, methodName):
"""Recurses the tree starting with the given node and returns the first node which calls the given method name
(supports namespaces, too)"""
if isinstance(methodName, str):
methodName = set([methodName])
def matcher(node):
call = getCallName(node)
if call and call in methodName:
return call
return query(node, matcher)
[docs]def getCallName(node):
if node.type == "call":
if node[0].type == "dot":
return assembleDot(node[0])
elif node[0].type == "identifier":
return node[0].value
return None
[docs]def getParameterFromCall(call, index=0):
"""Returns a parameter node by index on the call node."""
try:
return call[1][index]
except:
return None
[docs]def getParamNamesFromFunction(func):
"""Returns a human readable list of parameter names (sorted by their order in the given function)"""
params = getattr(func, "params", None)
if params:
return [identifier.value for identifier in params]
else:
return None
[docs]def detectPlusType(plusNode):
"""Analyses the given "plus" node and tries to figure out if a "string" or "number" result is produced."""
if plusNode[0].type == "string" or plusNode[1].type == "string":
return "String"
elif plusNode[0].type == "number" and plusNode[1].type == "number":
return "Number"
elif plusNode[0].type == "plus" and detectPlusType(plusNode[0]) == "String":
return "String"
else:
return "var"
[docs]def detectObjectType(objectNode):
"""Returns a human readable type information of the given node."""
if objectNode.type in ("new", "new_with_args"):
construct = objectNode[0]
else:
construct = objectNode
# Only support built-in top level constructs
if construct.type == "identifier" and construct.value in ("Array", "Boolean", "Date", "Function", "Number", "Object", "String", "RegExp"):
return construct.value
# And namespaced custom classes
elif construct.type == "dot":
assembled = assembleDot(construct)
if assembled:
return assembled
return "Object"
[docs]def resolveIdentifierNode(identifierNode):
assignNodes, assignValues = findAssignments(identifierNode.value, identifierNode)
if assignNodes:
assignCommentNode = None
# Find first relevant assignment with comment! Otherwise just first one.
for assign in assignNodes:
# The parent is the relevant doc comment container
# It's either a "var" (declaration) or "semicolon" (assignment)
if getDocComment(assign):
assignCommentNode = assign
break
elif getDocComment(assign.parent):
assignCommentNode = assign.parent
break
return assignValues[0], assignCommentNode or assignValues[0]
return None, None
[docs]def assembleDot(node, result=None):
"""Joins a dot node (cascaded supported, too) into a single string like "foo.bar.Baz"."""
if result is None:
result = []
for child in node:
if child.type == "identifier":
result.append(child.value)
elif child.type == "dot":
assembleDot(child, result)
else:
return None
return ".".join(result)