#
# Jasy - Web Tooling Framework
# Copyright 2010-2012 Zynga Inc.
# Copyright 2013-2014 Sebastian Werner
#
import os
import copy
import fnmatch
import re
import jasy.core.MetaData as MetaData
import jasy.core.Console as Console
import jasy.item.Abstract
import jasy.script.parse.Parser as Parser
import jasy.script.parse.ScopeScanner as ScopeScanner
import jasy.script.clean.DeadCode
import jasy.script.clean.Unused
import jasy.script.clean.Permutate
import jasy.script.optimize.Translation
import jasy.script.output.Optimization
import jasy.script.api.Data
import jasy.script.output.Compressor as Compressor
import jasy.script.util as Util
from jasy import UserError
try:
from pygments import highlight
from pygments.lexers import JavascriptLexer
from pygments.formatters import HtmlFormatter
except:
highlight = None
aliases = {}
[docs]def collectFields(node, keys=None):
if keys is None:
keys = set()
# Always the first parameter
# Supported calls: jasy.Env.isSet(key, expected?), jasy.Env.getValue(key), jasy.Env.select(key, map)
calls = ("jasy.Env.isSet", "jasy.Env.getValue", "jasy.Env.select")
if node.type == "dot" and node.parent.type == "call" and Util.assembleDot(node) in calls:
stringNode = node.parent[1][0]
if stringNode.type == "string":
keys.add(stringNode.value)
elif stringNode.type == "identifier":
# Tolerate identifiers for supporting dynamic requests e.g. for asset placeholders
pass
else:
raise Exception("Could not handle non string type in jasy.Env call at line: %s" % node.line)
# Process children
for child in reversed(node):
if child is not None:
collectFields(child, keys)
return keys
[docs]class ScriptError(Exception):
def __init__(self, inst, msg):
self.__msg = msg
self.__inst = inst
def __str__(self):
return "Error processing class %s: %s" % (self.__inst, self.__msg)
[docs]class ScriptItem(jasy.item.Abstract.AbstractItem):
kind = "jasy.Script"
[docs] def generateId(self, relpath, package):
"""Generates the fileId of this item as being used by other modules."""
if package:
fileId = "%s/" % package
else:
fileId = ""
return (fileId + os.path.splitext(relpath)[0]).replace("/", ".")
def __getTree(self):
"""Returns the abstract syntax tree."""
field = "script:tree[%s]" % self.id
tree = self.project.getCache().read(field, self.mtime)
if not tree:
Console.info("Processing class %s...", Console.colorize(self.id, "bold"))
Console.indent()
tree = Parser.parse(self.getText(), self.id)
ScopeScanner.scan(tree)
Console.outdent()
self.project.getCache().store(field, tree, self.mtime, True)
return tree
def __getOptimizedTree(self, permutation=None):
"""Returns an optimized tree with permutations applied."""
field = "script:opt-tree[%s]-%s" % (self.id, permutation)
tree = self.project.getCache().read(field, self.mtime)
if not tree:
tree = copy.deepcopy(self.__getTree())
# Logging
msg = "Optimizing class %s" % Console.colorize(self.id, "bold")
if permutation:
msg += Console.colorize(" (%s)" % permutation, "grey")
Console.info("%s..." % msg)
Console.indent()
# Apply permutation
if permutation:
Console.debug("Patching tree with permutation: %s", permutation)
Console.indent()
jasy.script.clean.Permutate.patch(tree, permutation)
Console.outdent()
# Cleanups
jasy.script.clean.DeadCode.cleanup(tree)
ScopeScanner.scan(tree)
jasy.script.clean.Unused.cleanup(tree)
self.project.getCache().store(field, tree, self.mtime, True)
Console.outdent()
return tree
[docs] def getBreaks(self, permutation=None, items=None):
"""
Returns all down-priorized dependencies. This are dependencies which are required to
make the module work, but are not required being available before the current item.
"""
meta = self.getMetaData(permutation)
result = set()
for name in meta.breaks:
if name != self.id and name in items and items[name].kind == "jasy.Script":
result.add(items[name])
elif "*" in name:
reobj = re.compile(fnmatch.translate(name))
for className in items:
if className != self.id:
if reobj.match(className):
result.add(items[className])
return result
[docs] def getDependencies(self, permutation=None, items=None, fields=None, warnings=True):
"""
Returns a set of dependencies seen through the given list of known classes (ignoring all unknown items in
original set) and configured fields with their individual detection classes.
This method also makes use of the meta data and the variable data.
"""
permutation = self.filterPermutation(permutation)
meta = self.getMetaData(permutation)
scope = self.getScopeData(permutation)
result = set()
# Match fields with current permutation and give detection classes
# Add detection classes of fields which are accessed but not permutated
# to the list of dependencies for this class.
if fields:
accessedFields = self.getFields()
if accessedFields:
for fieldName in accessedFields:
if permutation is None or not permutation.has(fieldName):
if fieldName in fields:
result.add(fields[fieldName])
# Manually defined names/classes
for name in meta.requires:
if name != self.id and name in items and items[name].kind == "jasy.Script":
result.add(items[name])
elif "*" in name:
reobj = re.compile(fnmatch.translate(name))
for className in items:
if className != self.id:
if reobj.match(className):
result.add(items[className])
elif warnings:
Console.warn("Missing class (required): %s in %s", name, self.id)
# Globally modified names (mostly relevant when working without namespaces)
for name in scope.shared:
if name != self.id and name in items and items[name].kind == "jasy.Script":
result.add(items[name])
# Add classes from detected package access
for package in scope.packages:
if package in aliases:
className = aliases[package]
if className in items:
result.add(items[className])
continue
orig = package
while True:
if package == self.id:
break
elif package in items and items[package].kind == "jasy.Script":
aliases[orig] = package
result.add(items[package])
break
else:
pos = package.rfind(".")
if pos == -1:
break
package = package[0:pos]
# Manually excluded names/classes
for name in meta.optionals:
if name != self.id and name in items and items[name].kind == "jasy.Script":
result.remove(items[name])
elif warnings:
Console.warn("Missing class (optional): %s in %s", name, self.id)
return result
[docs] def getScopeData(self, permutation=None):
"""Returns the top level scope object which contains information about the global variable and package
usage/influence."""
permutation = self.filterPermutation(permutation)
field = "script:scope[%s]-%s" % (self.id, permutation)
scope = self.project.getCache().read(field, self.mtime)
if scope is None:
scope = self.__getOptimizedTree(permutation).scope
self.project.getCache().store(field, scope, self.mtime)
return scope
[docs] def getApi(self, highlight=True):
field = "script:api[%s]-%s" % (self.id, highlight)
apidata = self.project.getCache().read(field, self.mtime, inMemory=False)
if apidata is None:
apidata = jasy.script.api.Data.ApiData(self.id, highlight)
tree = self.__getTree()
Console.indent()
apidata.scanTree(tree)
Console.outdent()
metaData = self.getMetaData()
apidata.addAssets(metaData.assets)
for require in metaData.requires:
apidata.addUses(require)
for optional in metaData.optionals:
apidata.removeUses(optional)
apidata.addFields(self.getFields())
self.project.getCache().store(field, apidata, self.mtime, inMemory=False)
return apidata
[docs] def getHighlightedCode(self):
field = "script:highlighted[%s]" % self.id
source = self.project.getCache().read(field, self.mtime)
if source is None:
if highlight is None:
raise UserError("Could not highlight JavaScript code! Please install Pygments.")
lexer = JavascriptLexer(tabsize=2)
formatter = HtmlFormatter(full=True, style="autumn", linenos="table", lineanchors="line")
source = highlight(self.getText(), lexer, formatter)
self.project.getCache().store(field, source, self.mtime)
return source
[docs] def getFields(self):
field = "script:fields[%s]" % (self.id)
fields = self.project.getCache().read(field, self.mtime)
if fields is None:
try:
fields = collectFields(self.__getTree())
except Exception as ex:
raise Exception("Unable to collect fields in file %s: %s" % (self.id, ex))
self.project.getCache().store(field, fields, self.mtime)
return fields
[docs] def getTranslations(self):
field = "script:translations[%s]" % (self.id)
result = self.project.getCache().read(field, self.mtime)
if result is None:
result = jasy.script.optimize.Translation.collectTranslations(self.__getTree())
self.project.getCache().store(field, result, self.mtime)
return result
[docs] def filterPermutation(self, permutation):
if permutation:
fields = self.getFields()
if fields:
return permutation.filter(fields)
return None
[docs] def getCompressed(self, profile):
field = "script:compressed[%s]-%s" % (self.id, profile.getId())
compressed = self.project.getCache().read(field, self.mtime)
if compressed is None:
permutation = self.filterPermutation(profile.getCurrentPermutation())
tree = self.__getOptimizedTree(permutation)
translation = profile.getCurrentTranslation()
optimization = profile.getCurrentOptimization()
formatting = profile.getCurrentFormatting()
if translation or optimization:
tree = copy.deepcopy(tree)
if translation:
jasy.script.optimize.Translation.optimize(tree, translation)
if optimization:
try:
optimization.apply(tree)
except jasy.script.output.Optimization.Error as error:
raise ScriptError(self, "Could not compress class! %s" % error)
compressed = Compressor.Compressor(formatting).compress(tree)
self.project.getCache().store(field, compressed, self.mtime)
return compressed