Skip to content

Commit

Permalink
Added support for inserting trace markers at end of functions.
Browse files Browse the repository at this point in the history
Created two new methods on the esmorph.Tracer object:

  `.traceFunctionExit`
  Insert marker at end of function bodies.

  `.traceFunctionEntranceAndExit`
  Insert marker at beginning and end of function bodies.

Temporarily disabled trace on non-function declaration nodes.

Also added unit tests, written using Mocha.
  • Loading branch information
sethmcl committed Nov 23, 2012
1 parent 874d2e5 commit d459808
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 69 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
*.swo
*.swp
node_modules/
203 changes: 134 additions & 69 deletions lib/esmorph.js
Expand Up @@ -25,9 +25,13 @@
/*jslint node:true browser:true */
/*global esmorph:true,esprima:true */

(function (exports) {
(function (exports, esprimaRef) {
'use strict';

if(module && module.exports) {
var esprima = require('esprima');
}

var Syntax = {
AssignmentExpression: 'AssignmentExpression',
ArrayExpression: 'ArrayExpression',
Expand Down Expand Up @@ -93,7 +97,7 @@
}
}

// Insert a prolog in the body of every function.
// Insert trace function call(s) in function bodies
// It will be in the form of a function call:
//
// traceName(object);
Expand All @@ -112,106 +116,165 @@
// the result will be used as the entire prolog. The arguments for the
// invocation are the function name, range, and location info.

function traceFunctionEntrance(traceName) {
function traceFunctionBody(traceName, options) {
options = options || {};

return function (code) {
var tree,
functionList,
param,
signature,
pos,
i;

i,
j,
lastChild,
posPre,
posPost,
flItem;

tree = esprima.parse(code, { range: true, loc: true });

functionList = [];
traverse(tree, function (node, path) {
var parent;
var parent,
pos = [],
posEntrance,
posExit;

if (node.type === Syntax.FunctionDeclaration) {
functionList.push({
name: node.id.name,
range: node.range,
loc: node.loc,
blockStart: node.body.range[0]
});
} else if (node.type === Syntax.FunctionExpression) {
parent = path[0];
if (parent.type === Syntax.AssignmentExpression) {
if (typeof parent.left.range !== 'undefined') {
functionList.push({
name: code.slice(parent.left.range[0],
parent.left.range[1] + 1),
range: node.range,
loc: node.loc,
blockStart: node.body.range[0]
});
}
} else if (parent.type === Syntax.VariableDeclarator) {
functionList.push({
name: parent.id.name,
range: node.range,
loc: node.loc,
blockStart: node.body.range[0]
});
} else if (parent.type === Syntax.CallExpression) {
functionList.push({
name: parent.id ? parent.id.name : '[Anonymous]',
range: node.range,
loc: node.loc,
blockStart: node.body.range[0]
});
} else if (typeof parent.length === 'number') {
functionList.push({
name: parent.id ? parent.id.name : '[Anonymous]',
range: node.range,
loc: node.loc,
blockStart: node.body.range[0]
});
} else if (typeof parent.key !== 'undefined') {
if (parent.key.type === 'Identifier') {
if (parent.value === node && parent.key.name) {
functionList.push({
name: parent.key.name,
range: node.range,
loc: node.loc,
blockStart: node.body.range[0]
});
}
}
}
// calculate entrance position
posEntrance = node.body.range[0];

// calculate exit position
lastChild = node.body.body[node.body.body.length - 1];

if(!lastChild) {
posExit = node.body.range[0];
} else if (lastChild.type === Syntax.ReturnStatement) {
posExit = lastChild.range[0] - 1;
} else {
posExit = lastChild.range[1] - 1;
}

if(options.entrance) {
pos.push(posEntrance);
}

if(options.exit) {
pos.push(posExit);
}

if(!options.exit && !options.entrance) {
pos.push(posEntrance, posExit);
}

functionList.push({
name: node.id.name,
range: node.range,
loc: node.loc,
blockStarts: pos
});
}
//else if (node.type === Syntax.FunctionExpression) {
//parent = path[0];
//if (parent.type === Syntax.AssignmentExpression) {
//if (typeof parent.left.range !== 'undefined') {
//functionList.push({
//name: code.slice(parent.left.range[0],
//parent.left.range[1] + 1),
//range: node.range,
//loc: node.loc,
//blockStart: node.body.range[0]
//});
//}
//} else if (parent.type === Syntax.VariableDeclarator) {
//functionList.push({
//name: parent.id.name,
//range: node.range,
//loc: node.loc,
//blockStart: node.body.range[0]
//});
//} else if (parent.type === Syntax.CallExpression) {
//functionList.push({
//name: parent.id ? parent.id.name : '[Anonymous]',
//range: node.range,
//loc: node.loc,
//blockStart: node.body.range[0]
//});
//} else if (typeof parent.length === 'number') {
//functionList.push({
//name: parent.id ? parent.id.name : '[Anonymous]',
//range: node.range,
//loc: node.loc,
//blockStart: node.body.range[0]
//});
//} else if (typeof parent.key !== 'undefined') {
//if (parent.key.type === 'Identifier') {
//if (parent.value === node && parent.key.name) {
//functionList.push({
//name: parent.key.name,
//range: node.range,
//loc: node.loc,
//blockStart: node.body.range[0]
//});
//}
//}
//}
//}
});

// Insert the instrumentation code from the last entry.
// This is to ensure that the range for each entry remains valid)
// This is to ensure that the range for each entry remains valid
// (it won't shift due to some new inserting string before the range).
for (i = functionList.length - 1; i >= 0; i -= 1) {
flItem = functionList[i];
param = {
name: functionList[i].name,
range: functionList[i].range,
loc: functionList[i].loc
name: flItem.name,
range: flItem.range,
loc: flItem.loc
};
if (typeof traceName === 'function') {
signature = traceName.call(null, param);
} else {
signature = traceName + '({ ';
signature += 'name: \'' + functionList[i].name + '\', ';
if (typeof functionList[i].loc !== 'undefined') {
signature += 'lineNumber: ' + functionList[i].loc.start.line + ', ';
signature += 'name: \'' + flItem.name + '\', ';
if (typeof flItem.loc !== 'undefined') {
signature += 'lineNumber: ' + flItem.loc.start.line + ', ';
}
signature += 'range: [' + functionList[i].range[0] + ', ' +
functionList[i].range[1] + '] ';
signature += 'range: [' + flItem.range[0] + ', ' +
flItem.range[1] + '] ';
signature += '});';
}
pos = functionList[i].blockStart + 1;
code = code.slice(0, pos) + '\n' + signature + code.slice(pos, code.length);

for (j = flItem.blockStarts.length - 1; j >= 0; j -= 1) {
pos = flItem.blockStarts[j] + 1;
//code = code.slice(0, pos) + '\n' + signature + code.slice(pos, code.length);
code = code.slice(0, pos) + signature + code.slice(pos, code.length);
}
}

return code;
};
}

// Insert trace at beginning of function bodies

function traceFunctionEntrance(traceName) {
return traceFunctionBody(traceName, { entrance: true } );
}

// Same as traceFunctionEntrance, but inserts trace at end of function bodies

function traceFunctionExit(traceName) {
return traceFunctionBody(traceName, { exit: true } );
}

// Combination of traceFunctionEntrance and traceFunctionExit

function traceFunctionEntranceAndExit(traceName) {
return traceFunctionBody(traceName, { entrance: true, exit: true } );
}

function modify(code, modifiers) {
var i;

Expand All @@ -234,7 +297,9 @@
exports.modify = modify;

exports.Tracer = {
FunctionEntrance: traceFunctionEntrance
FunctionEntrance: traceFunctionEntrance,
FunctionExit: traceFunctionExit,
FunctionEntranceAndExit: traceFunctionEntranceAndExit
};

}(typeof exports === 'undefined' ? (esmorph = {}) : exports));
7 changes: 7 additions & 0 deletions package.json
Expand Up @@ -4,6 +4,9 @@
"homepage": "http://github.com/ariya/esmorph",
"main": "lib/esmorph.js",
"version": "0.0.0-dev",
"scripts": {
"test": "mocha test/specs"
},
"engines": {
"node": ">=0.4.0"
},
Expand All @@ -22,5 +25,9 @@
}],
"dependencies": {
"esprima": "*"
},
"devDependencies": {
"mocha": "1.7.0",
"chai": "1.0.4"
}
}
7 changes: 7 additions & 0 deletions test/data/sample_source_code/helloWorld.js
@@ -0,0 +1,7 @@
function helloWorld() {
var a = 3 * 5;

console.log('hi');

return 'hello world!';
}
4 changes: 4 additions & 0 deletions test/data/sample_source_code/helloWorld.tracedEntrance.js
@@ -0,0 +1,4 @@
function helloWorld() {
myTrace({ name: 'helloWorld', lineNumber: 1, range: [0, 50] });
return 'hello world!';
}
4 changes: 4 additions & 0 deletions test/data/sample_source_code/helloWorld.tracedExit.js
@@ -0,0 +1,4 @@
function helloWorld() {
myTrace({ name: 'helloWorld', lineNumber: 1, range: [0, 50] });
return 'hello world!';
}
20 changes: 20 additions & 0 deletions test/data/sample_source_code/threeFunctions.js
@@ -0,0 +1,20 @@
function helloWorld() {
var a = 3 * 5;

console.log('hi');

return 'hello world!';
}

function goodbye() {
var a = 3 * 5;

console.log('hi');

return 'hello world!';
}

function setValue( name ) {
window.foo.name = name;
return 'hello world!';
}
46 changes: 46 additions & 0 deletions test/lib/testHelpers.js
@@ -0,0 +1,46 @@
/**
* Some simple test helpers used in unit tests for esmorph
* @author Seth McLaughlin <s@sethmcl.com>
*/
'use strict';

// dependencies
var fs = require('fs'),
path = require('path');

// locals
var sourceCode = {};

/**
* Load sample source code
* @param {String} codeFile the file name. Will be resolved relative
* to the /test/data/sample_source_code/ directory
* @return {String} contents of codeFile
*/
module.exports.loadSampleCode = function( codeFile ) {
var code,
codeRoot = path.resolve( __dirname, '..', 'data', 'sample_source_code' );

// check if we have already cached the file contents
if(sourceCode[codeFile]) {
code = sourceCode[codeFile];
} else {
try {
code = sourceCode[codeFile] = fs.readFileSync( path.resolve( codeRoot, codeFile + '.js' ) ).toString();
} catch(e) {
throw Error('Could not load codeFile "' + codeFile + '": ' + e.message);
}
}

return code;
};

/**
* Build string of source code from array
* @param {Array} lines code
* @return {String} source code
*/
module.exports.buildSrc = function( lines ) {
return lines.join('');
};

0 comments on commit d459808

Please sign in to comment.