strong-trace
Version:
StrongTrace Node.js Tracer
158 lines (136 loc) • 6.03 kB
JavaScript
"use strict";
module.exports = FunctionTrace
var scopetracer = require("./scopetracer")
var est = require("estraverse")
var shouldInstrument = require("./should_instrument")
var tracer = require("./core_tracer").getTracer()
var helpers = require("./format_helpers")
var getLeadingIndent = helpers.getLeadingIndent
function FunctionTrace(config) {
function mutator(content, filename) {
var insertions = []
tracer.addFile(filename)
var complexity = shouldInstrument.complexityStats(this)
var id = tracer.addFunction(filename, this.fnName, this.parent.loc.start, complexity)
if (!shouldInstrument(this, config, complexity)) {
return insertions
}
var enterInsert = "var __concurix_traceme = __concurix_tracer.enter('" + id + "');"
var exitInsert = ";if (__concurix_traceme) {__concurix_tracer.exit('" + id + "')}"
// Instrument function entry
var nextNode = (this.isStrict) ? this.body[1] : this.body[0]
var enterInsertionPoint
if (nextNode != null) {
enterInsertionPoint = nextNode.range[0]
}
else {
// Empty body (noop)
// + 1 for curly brace
enterInsertionPoint = this.range[0] + 1
}
var preserveIndent = getLeadingIndent(content, enterInsertionPoint)
insertions.push({pos: enterInsertionPoint, insertion: enterInsert + preserveIndent, pri: 0})
// Instrument exit points
// Traverse this function scope and see if it contains any early exits
est.traverse(this, {
enter: function enter(node, parent) {
// Synchronous exit points are 'return' or 'throw'
if (node.type == "ReturnStatement" || node.type == "ThrowStatement") {
// Beware the trailing comments...
var myEnd = (node.argument && node.range[1] - node.argument.range[1] > 1) ? node.argument.range[1] + 1 : node.range[1]
var actualEnd = helpers.findActualEnd(content, myEnd)
if (actualEnd !== node.range[1] && /\S/.test(content.slice(actualEnd, node.range[1]))) {
actualEnd = node.range[1]
}
var preserveIndent = getLeadingIndent(content, node.range[0])
if (config.rewrite_returns && needsRewrite(node)) {
var proxy = "var __concurix_rproxy = "
var toRemove = "return"
if (node.type == "ThrowStatement") {
toRemove = "throw"
}
var reply = toRemove + " __concurix_rproxy;"
// Return blocks with comma operators are evil.
// If we see them attempt to wrap the SequenceExpression with parens
if (node.argument && node.argument.type == "SequenceExpression") {
proxy = proxy + "("
var endParen = actualEnd
// Semicolon check.
if (content.slice(actualEnd - 1, actualEnd) === ";") {
endParen--
}
insertions.push({pos: endParen, insertion: ")", pri: 2})
actualEnd++
}
if (parent.type != "BlockStatement") {
// If not in a BlockStatement, need to put it in one.
proxy = "{ " + proxy
reply = reply + " }"
}
insertions.push({pos: node.range[0], remove: toRemove.length, insertion: proxy, pri: 1})
insertions.push({pos: actualEnd, insertion: preserveIndent + exitInsert + preserveIndent + reply, pri: 3})
}
else {
// TBD: some sort of special-case handling of throw?
if (parent.type != "BlockStatement") {
// If this is not in a BlockStatement, we need to put it in one.
insertions.push({pos: node.range[0], insertion: "{ " + exitInsert + preserveIndent, pri: 1})
// Esprima will emit an overlapping range when no semicolon on inline if consequents
// Thus we do some sniffing...
insertions.push({pos: actualEnd, insertion: " }"})
}
else {
insertions.push({pos: node.range[0], insertion: exitInsert + preserveIndent, pri: 2})
}
}
}
// Don't decend into nested functions
else if (node.type == "FunctionExpression" || node.type == "FunctionDeclaration") {
return est.VisitorOption.Skip
}
}
})
// Insert at end of block if previous statment wasn't return or throw
// e.g. exit at run off end of block
var lastMember = this.body[this.body.length - 1]
if (lastMember == null) {
// Empty body (noop function?)
insertions.push({pos: this.range[1] - 1, insertion: exitInsert, pri: 1})
}
else if (lastMember.type != "ReturnStatement" && lastMember.type != "ThrowStatement") {
preserveIndent = getLeadingIndent(content, lastMember.range[0])
if (!/\n/.test(preserveIndent) && content.slice(lastMember.range[1] - 1, lastMember.range[1]) !== ";") {
// no newline or semicolon on previous statement -- add a semicolon
preserveIndent = ";" + preserveIndent
}
// - 1 because of closing curly
var postIndent = getLeadingIndent(content, this.range[1] - 1)
insertions.push({pos: this.range[1] - 1, insertion: preserveIndent + exitInsert + postIndent, pri: 1})
}
return insertions
}
function nodeTest(content, filename) {
return (
this.parent != null && (
this.parent.type == "FunctionExpression" ||
this.parent.type == "FunctionDeclaration"
)
)
}
return scopetracer(mutator, nodeTest)
}
function needsRewrite(exitNode) {
var doRewrite = false
// crawl exitNode arguments to see if they contain a CallExpression or NewExpression
est.traverse(exitNode, {
enter: function enter(node, parent) {
if (node.type == "FunctionExpression" || node.type == "FunctionDeclaration") {
return est.VisitorOption.Skip
}
if (node.type == "CallExpression" || node.type == "NewExpression") {
doRewrite = true
}
}
})
return doRewrite
}