strong-trace
Version:
StrongTrace Node.js Tracer
193 lines (171 loc) • 5.15 kB
JavaScript
;
module.exports = scopenodes
var esprima = require("esprima")
var est = require("estraverse")
var esprimaOptions = {
loc: true, // to allow references to location in orig file
range: true, // needed for replacement
raw: false,
tokens: false,
comment: false,
tolerant: true
}
var shebangExpr = /^\s*#\!.*?\n/m
function scopenodes(content) {
var scopes = []
// If the script has a shebang it will mess with esprima. hide it.
var hasSheBang = shebangExpr.test(content)
if (hasSheBang) {
content = content.replace(/#\!/, "//")
}
var ast = esprima.parse(content, esprimaOptions)
//console.log(JSON.stringify(ast, null, 2))
var path = []
// Track name assignment to avoid duplicate names
var names = {}
// Scope entry points:
// parent: null, node: Program
// parent: FunctionExpression, node: BlockStatement
// parent: FunctionDeclaration, node: BlockStatement
// TBD catch blocks are pseudo-scopes...
function isScopeEntry(node, parent) {
if (parent == null && node.type == "Program") {
return true
}
if (node.type != "BlockStatement") {
return false
}
if (parent.type == "FunctionExpression" || parent.type == "FunctionDeclaration") {
return true
}
return false
}
est.traverse(ast, {
enter: function testNode(node, parent) {
// Look for a possible name candidate in case this ends up being anonymous
if (node.type == "FunctionExpression") {
if (parent.type == "AssignmentExpression") {
if (parent.left.type == "MemberExpression") {
node.maybeName = content.slice(parent.left.range[0], parent.left.range[1])
}
// TBD other expected stuff here?
}
if (parent.type == "VariableDeclarator") {
node.maybeName = parent.id.name
}
if (parent.type == "CallExpression" || parent.type == "NewExpression") {
node.maybeName = nameFnArgument(content, parent)
}
if (parent.type == "Property") {
node.maybeName = parent.key.name
}
}
if (!isScopeEntry(node, parent)) {
// This does not define a scope entry point
//console.log("Skipping -- not a scope entry")
return
}
//console.log("entering... (depth: %s)", nestDepth)
//console.log(node)
node.path = path.slice(0)
node.parent = parent
node.isStrict = isStrict(node)
var name = getName(node)
// Avoid name dupes
if (names[name] == null) {
names[name] = 1
}
else {
name = name + "{" + (++names[name]) + "}"
}
node.fnName = name
scopes.push(node)
path.push(node)
},
leave: function leave(node, parent) {
if (!isScopeEntry(node, parent)) {
// This was not a scope block
return
}
path.pop()
//console.log("leaving... (depth: %s)", nestDepth)
}
})
return scopes
}
function isStrict(node) {
if (node == null || node.body == null || node.body.length === 0) {
return false
}
if (node.body[0].expression == null) {
return false
}
return (node.body[0].expression.value == "use strict")
}
var complexRe = /.+[)].*(\.\w+)$/m
function nameFnArgument(content, parent) {
var caller = (parent.type == "NewExpression") ? "new " : ""
var isListener = false
if (parent.callee.type == "MemberExpression") {
caller += content.slice(parent.callee.range[0], parent.callee.range[1])
if (/\.on$/.test(caller) && parent.arguments[0] && parent.arguments[0].type == "Literal") {
// making the guess that this is a listener
isListener = true
caller += " '" + parent.arguments[0].value + "' listener"
}
}
else if (parent.callee.type == "Identifier") {
caller += parent.callee.name
}
else {
// TBD any other cases we want to attempt?
return ""
}
// Simplify overly-complex call expressiong, e.g.
// foo.forEach(...).filter(...).sort(...)
// TBD this doesn't catch foo.forEach(...)[0]() ...
var tooComplex = caller.replace(/\r?\n/g, "").match(complexRe)
if (tooComplex) {
caller = tooComplex[1]
}
if (/\n|;/.test(caller)) {
// Something ugly happened.
return ""
}
if (isListener) {
return caller
}
return caller + "() fn argument"
}
var proxyRe = /^__concurix/
function getName(node) {
var names = []
if (node.path.length === 0) {
return ""
}
for (var i = 0; i < node.path.length; i++) {
var ancestor = node.path[i]
if (ancestor.parent == null) {
continue
}
else if (ancestor.parent.id && ancestor.parent.id.name) {
names.push(ancestor.parent.id.name)
}
else if (ancestor.parent.maybeName && !proxyRe.test(ancestor.parent.maybeName)) {
names.push(ancestor.parent.maybeName)
}
else {
names.push("anonymous")
}
}
if (node.parent.id && node.parent.id.name) {
names.push(node.parent.id.name)
}
else if (node.parent.maybeName && !proxyRe.test(node.parent.maybeName)) {
names.push(node.parent.maybeName)
}
else {
names.push("anonymous")
}
return names.join(">")
}