|
|
|
// var SolidityParser = require("solidity-parser");
|
|
|
|
var solparse = require("solparse");
|
|
|
|
var fs = require('fs');
|
|
|
|
|
|
|
|
var path = require("path");
|
|
|
|
module.exports = function(pathToFile, instrumentingActive){
|
|
|
|
|
|
|
|
// var result = SolidityParser.parseFile("./" + pathToFile);
|
|
|
|
var contract = fs.readFileSync("./" + pathToFile).toString();
|
|
|
|
var result = solparse.parse(contract);
|
|
|
|
var instrumented = "";
|
|
|
|
const __INDENTATION__ = " ";
|
|
|
|
var parse = {};
|
|
|
|
var runnableLines=[];
|
|
|
|
var fnMap = {};
|
|
|
|
var fnId = 0;
|
|
|
|
var branchMap = {};
|
|
|
|
var branchId = 0;
|
|
|
|
var statementMap = {};
|
|
|
|
var statementId = 0;
|
|
|
|
var linecount = 1;
|
|
|
|
var fileName = path.basename(pathToFile);
|
|
|
|
var injectionPoints = {};
|
|
|
|
|
|
|
|
function createOrAppendInjectionPoint(key, value){
|
|
|
|
if (injectionPoints[key]){
|
|
|
|
injectionPoints[key].push(value);
|
|
|
|
}else{
|
|
|
|
injectionPoints[key] = [value];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function instrumentAssignmentExpression(expression){
|
|
|
|
//The only time we instrument an assignment expression is if there's a conditional expression on
|
|
|
|
//the right
|
|
|
|
if (expression.right.type==='ConditionalExpression'){
|
|
|
|
if (expression.left.type==='DeclarativeExpression'){
|
|
|
|
//Then we need to go from bytes32 varname = (conditional expression)
|
|
|
|
//to bytes32 varname; (,varname) = (conditional expression)
|
|
|
|
createOrAppendInjectionPoint(expression.left.end, {type:"literal", string:"; (," + expression.left.name + ")"});
|
|
|
|
}else {
|
|
|
|
console.log(expression.left)
|
|
|
|
process.exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function instrumentConditionalExpression(expression){
|
|
|
|
branchId +=1;
|
|
|
|
|
|
|
|
startline = (contract.slice(0,expression.start).match(/\n/g)||[]).length + 1;
|
|
|
|
var startcol = expression.start - contract.slice(0,expression.start).lastIndexOf('\n') -1;
|
|
|
|
consequentStartCol = startcol + expression.consequent.start - expression.start;
|
|
|
|
consequentEndCol = consequentStartCol + expression.consequent.end - expression.consequent.start;
|
|
|
|
alternateStartCol = startcol + expression.alternate.start - expression.start;
|
|
|
|
alternateEndCol = alternateStartCol + expression.alternate.end - expression.alternate.start;
|
|
|
|
//NB locations for conditional branches in istanbul are length 1 and associated with the : and ?.
|
|
|
|
branchMap[branchId] = {line:startline, type:'cond-expr', locations:[{start:{line:startline, column:consequentStartCol},end:{line:startline,column:consequentEndCol}},{start:{line:startline, column:alternateStartCol},end:{line:startline,column:alternateEndCol}}]}
|
|
|
|
|
|
|
|
//Right, this could be being used just by itself or as an assignment. In the case of the latter, because
|
|
|
|
//the comma operator doesn't exist, we're going to have to get funky.
|
|
|
|
//if we're on a line by ourselves, this is easier
|
|
|
|
//
|
|
|
|
//Now if we've got to wrap the expression it's being set equal to, do that...
|
|
|
|
|
|
|
|
|
|
|
|
//Wrap the consequent
|
|
|
|
createOrAppendInjectionPoint(expression.consequent.start, {type:"openParen"});
|
|
|
|
createOrAppendInjectionPoint(expression.consequent.start, {type:"callBranchEvent",comma:true, branchId: branchId, locationIdx: 0});
|
|
|
|
createOrAppendInjectionPoint(expression.consequent.end, {type:"closeParen"});
|
|
|
|
|
|
|
|
//Wrap the alternate
|
|
|
|
createOrAppendInjectionPoint(expression.alternate.start, {type:"openParen"});
|
|
|
|
createOrAppendInjectionPoint(expression.alternate.start, {type:"callBranchEvent",comma:true, branchId: branchId, locationIdx: 1});
|
|
|
|
createOrAppendInjectionPoint(expression.alternate.end, {type:"closeParen"});
|
|
|
|
}
|
|
|
|
|
|
|
|
function instrumentStatement(expression){
|
|
|
|
canCover = false;
|
|
|
|
//Can only instrument here if this is a self-contained statement
|
|
|
|
//So if this is the start of the line, we're good
|
|
|
|
if ( contract.slice(contract.slice(0,expression.start).lastIndexOf('\n'), expression.start).trim().length===0 ){
|
|
|
|
canCover=true;
|
|
|
|
}
|
|
|
|
//If it's preceeded by a '{', we're good
|
|
|
|
if ( contract.slice(contract.slice(0,expression.start).lastIndexOf('{')+1, expression.start).trim().length===0 ){
|
|
|
|
canCover=true;
|
|
|
|
}
|
|
|
|
|
|
|
|
//If it's preceeded by a ';', we're good
|
|
|
|
if ( contract.slice(contract.slice(0,expression.start).lastIndexOf(';')+1, expression.start).trim().length===0 ){
|
|
|
|
canCover=true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!canCover){return;}
|
|
|
|
//We need to work out the lines and columns the expression starts and ends
|
|
|
|
statementId +=1;
|
|
|
|
linecount = (contract.slice(0,expression.start).match(/\n/g)||[]).length + 1;
|
|
|
|
var startline = linecount;
|
|
|
|
var startcol = expression.start - contract.slice(0,expression.start).lastIndexOf('\n') -1;
|
|
|
|
var expressionContent = contract.slice(expression.start, expression.end);
|
|
|
|
|
|
|
|
var endline = startline + (expressionContent.match('/\n/g') || []).length ;
|
|
|
|
var endcol;
|
|
|
|
if (expressionContent.lastIndexOf('\n')>=0){
|
|
|
|
endcol = contract.slice(expressionContent.lastIndexOf('\n'), expression.end).length -1;
|
|
|
|
}else{
|
|
|
|
endcol = startcol + expressionContent.length -1;
|
|
|
|
|
|
|
|
}
|
|
|
|
statementMap[statementId] = {start:{line: startline, column:startcol},end:{line:endline, column:endcol}}
|
|
|
|
|
|
|
|
createOrAppendInjectionPoint(expression.start, {type:"statement", statementId: statementId});
|
|
|
|
}
|
|
|
|
|
|
|
|
function instrumentLine(expression){
|
|
|
|
//what's the position of the most recent newline?
|
|
|
|
var startchar = expression.start
|
|
|
|
var endchar = expression.end
|
|
|
|
|
|
|
|
lastNewLine = contract.slice(0, startchar).lastIndexOf('\n');
|
|
|
|
nextNewLine = startchar + contract.slice(startchar).indexOf('\n');
|
|
|
|
var contractSnipped = contract.slice(lastNewLine, nextNewLine);
|
|
|
|
//Remove comments
|
|
|
|
while (contractSnipped.trim().slice(0,2)==='//'){
|
|
|
|
contractSnipped.replace(/\/\/.*?/g,"");
|
|
|
|
}
|
|
|
|
// Is everything before us and after us on this line whitespace?
|
|
|
|
if (contract.slice(lastNewLine, startchar).trim().length===0 && contract.slice(endchar,nextNewLine).replace(';','').trim().length===0){
|
|
|
|
createOrAppendInjectionPoint(lastNewLine+1,{type:"callEvent"});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function instrumentFunctionDeclaration(expression){
|
|
|
|
fnId+=1;
|
|
|
|
linecount = (contract.slice(0,expression.start).match(/\n/g)||[]).length + 1;
|
|
|
|
//We need to work out the lines and columns the function declaration starts and ends
|
|
|
|
var startline = linecount;
|
|
|
|
var startcol = expression.start - contract.slice(0,expression.start).lastIndexOf('\n') -1;
|
|
|
|
var endlineDelta = contract.slice(expression.start).indexOf('{')+1;
|
|
|
|
var functionDefinition = contract.slice(expression.start, expression.start + endlineDelta);
|
|
|
|
var endline = startline + (functionDefinition.match(/\n/g)||[]).length;
|
|
|
|
var endcol = functionDefinition.length - functionDefinition.lastIndexOf('\n')
|
|
|
|
fnMap[fnId] = {name: expression.name, line: linecount, loc:{start:{line: startline, column:startcol},end:{line:endline, column:endcol}}}
|
|
|
|
createOrAppendInjectionPoint(expression.start + endlineDelta+1,{type: "callFunctionEvent", fnId: fnId} );
|
|
|
|
}
|
|
|
|
|
|
|
|
function instrumentIfStatement(expression){
|
|
|
|
branchId +=1;
|
|
|
|
startline = (contract.slice(0,expression.start).match(/\n/g)||[]).length + 1;
|
|
|
|
var startcol = expression.start - contract.slice(0,expression.start).lastIndexOf('\n') -1;
|
|
|
|
//NB locations for if branches in istanbul are zero length and associated with the start of the if.
|
|
|
|
branchMap[branchId] = {line:linecount, type:'if', locations:[{start:{line:startline, column:startcol},end:{line:startline,column:startcol}},{start:{line:startline, column:startcol},end:{line:startline,column:startcol}}]}
|
|
|
|
createOrAppendInjectionPoint(expression.consequent.start+1,{type: "callBranchEvent", branchId: branchId, locationIdx: 0} )
|
|
|
|
if (expression.alternate && expression.alternate.type==='IfStatement'){
|
|
|
|
createOrAppendInjectionPoint(expression.alternate.start, {type: "callBranchEvent", branchId: branchId, locationIdx:1, openBracket: true})
|
|
|
|
createOrAppendInjectionPoint(expression.alternate.end, {type:"closeBracket"});
|
|
|
|
//It should get instrumented when we parse it
|
|
|
|
} else if (expression.alternate){
|
|
|
|
createOrAppendInjectionPoint(expression.alternate.start+1, {type: "callBranchEvent", branchId: branchId, locationIdx: 1})
|
|
|
|
} else {
|
|
|
|
createOrAppendInjectionPoint(expression.consequent.end, {type: "callEmptyBranchEvent", branchId: branchId, locationIdx: 1});
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["AssignmentExpression"] = function (expression, instrument){
|
|
|
|
if (instrument){instrumentStatement(expression)}
|
|
|
|
if (instrument){instrumentAssignmentExpression(expression)}
|
|
|
|
parse[expression.left.type](expression.left, instrument);
|
|
|
|
parse[expression.right.type](expression.right, instrument);
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["ConditionalExpression"] = function(expression, instrument){
|
|
|
|
if (instrument){ instrumentConditionalExpression(expression); }
|
|
|
|
parse[expression.test.left.type](expression.test.left, instrument);
|
|
|
|
parse[expression.test.right.type](expression.test.right,instrument);
|
|
|
|
parse[expression.consequent.type](expression.consequent, instrument)
|
|
|
|
parse[expression.alternate.type](expression.alternate,instrument);
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["Identifier"] = function(expression, instrument){
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["InformalParameter"] = function(expression, instrument){
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["Literal"] = function(expression, instrument){
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["ModifierName"] = function(expression, instrument){
|
|
|
|
};
|
|
|
|
|
|
|
|
parse["Modifiers"] = function(modifiers, instrument){
|
|
|
|
for (x in modifiers){
|
|
|
|
parse[modifiers[x].type](modifiers[x], instrument);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["ThisExpression"] = function(expression, instrument){
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["ReturnStatement"] = function(expression, instrument){
|
|
|
|
if (instrument){instrumentStatement(expression)}
|
|
|
|
parse[expression.argument.type](expression.argument, instrument);
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["NewExpression"] = function(expression, instrument){
|
|
|
|
parse[expression.callee.type](expression.callee, instrument);
|
|
|
|
for (x in expression.arguments){
|
|
|
|
parse[expression.arguments[x].type](expression.arguments[x], instrument)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["MemberExpression"] = function (expression, instrument){
|
|
|
|
parse[expression.object.type](expression.object, instrument);
|
|
|
|
parse[expression.property.type](expression.property, instrument);
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["CallExpression"] = function (expression,instrument){
|
|
|
|
if (instrument){instrumentStatement(expression)}
|
|
|
|
parse[expression.callee.type](expression.callee, instrument);
|
|
|
|
for (x in expression.arguments){
|
|
|
|
parse[expression.arguments[x].type](expression.arguments[x], instrument)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["UnaryExpression"] = function(expression, instrument){
|
|
|
|
parse[expression.argument.type](expression.argument, instrument);
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["ThrowStatement"] = function(expression, instrument){
|
|
|
|
if (instrument){instrumentStatement(expression)}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["BinaryExpression"] = function(expression, instrument){
|
|
|
|
parse[expression.left.type](expression.left, instrument)
|
|
|
|
parse[expression.right.type](expression.right, instrument)
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["IfStatement"] = function(expression, instrument){
|
|
|
|
if (instrument){instrumentStatement(expression)}
|
|
|
|
if (instrument) {instrumentIfStatement(expression)}
|
|
|
|
parse[expression.test.type](expression.test, instrument)
|
|
|
|
parse[expression.consequent.type](expression.consequent, instrument)
|
|
|
|
if (expression.alternate){
|
|
|
|
parse[expression.alternate.type](expression.alternate, instrument)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["SequenceExpression"] = function(expression, instrument){
|
|
|
|
parse[expression.expressions[x].type](expression.expressions[x], instrument) + ', ';
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["ImportStatement"] = function(expression, instrument){
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["DeclarativeExpression"] = function(expression, instrument){
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["ExpressionStatement"] = function(content, instrument){
|
|
|
|
// if (instrument){instrumentStatement(content.expression)}
|
|
|
|
parse[content.expression.type](content.expression, instrument);
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["EnumDeclaration"] = function(expression, instrument){
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["EventDeclaration"]=function(expression, instrument){
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["VariableDeclarationTuple"] = function(expression, instrument){
|
|
|
|
parse[expression.init.type](expression.init, instrument)
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["BlockStatement"] = function(expression, instrument){
|
|
|
|
for (var x=0; x < expression.body.length; x++){
|
|
|
|
if (instrument){ instrumentLine(expression.body[x]); }
|
|
|
|
parse[expression.body[x].type](expression.body[x], instrument);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["VariableDeclaration"] = function(expression, instrument){
|
|
|
|
if (instrument){instrumentStatement(expression)}
|
|
|
|
if (expression.declarations.length>1){
|
|
|
|
console.log('more than one declaration')
|
|
|
|
}
|
|
|
|
parse[expression.declarations[0].id.type](expression.declarations[0].id, instrument);
|
|
|
|
parse[expression.declarations[0].init.type](expression.declarations[0].init, instrument);
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["Type"] = function(expression, instrument){
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["UsingStatement"] = function(expression, instrument){
|
|
|
|
parse[expression.for.type](expression.for, instrument)
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["FunctionDeclaration"] = function(expression, instrument){
|
|
|
|
parse["Modifiers"](expression.modifiers, instrument);
|
|
|
|
if (expression.body){
|
|
|
|
instrumentFunctionDeclaration(expression);
|
|
|
|
parse[expression.body.type](expression.body, instrumentingActive);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["ContractStatement"] = function(expression, instrument){
|
|
|
|
//Inject our coverage event if we're covering
|
|
|
|
if (instrumentingActive){
|
|
|
|
//This is harder because of where .start and .end represent, and how documented comments are validated
|
|
|
|
//by solc upon compilation. From the start of this contract statement, find the first '{', and inject
|
|
|
|
//there.
|
|
|
|
var injectionPoint = expression.start + contract.slice(expression.start).indexOf('{')+2;
|
|
|
|
if (injectionPoints[injectionPoint]){
|
|
|
|
injectionPoints[expression.start + contract.slice(expression.start).indexOf('{')+2].push({type:"eventDefinition"});
|
|
|
|
}else{
|
|
|
|
injectionPoints[expression.start + contract.slice(expression.start).indexOf('{')+2] = [{type:"eventDefinition"}];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (x in expression.body){
|
|
|
|
parse[expression.body[x].type](expression.body[x], instrument);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["LibraryStatement"] = function(expression, instrument){
|
|
|
|
//Inject our coverage event;
|
|
|
|
if(instrumentingActive){
|
|
|
|
|
|
|
|
//This is harder because of where .start and .end represent, and how documented comments are validated
|
|
|
|
//by solc upon compilation. From the start of this contract statement, find the first '{', and inject
|
|
|
|
//there.
|
|
|
|
var injectionPoint = expression.start + contract.slice(expression.start).indexOf('{')+2;
|
|
|
|
if (injectionPoints[injectionPoint]){
|
|
|
|
injectionPoints[expression.start + contract.slice(expression.start).indexOf('{')+2].push({type:"eventDefinition"});
|
|
|
|
}else{
|
|
|
|
injectionPoints[expression.start + contract.slice(expression.start).indexOf('{')+2] = [{type:"eventDefinition"}];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (x in expression.body){
|
|
|
|
parse[expression.body[x].type](expression.body[x], instrument);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["ModifierDeclaration"] = function(expression, instrument){
|
|
|
|
instrumentFunctionDeclaration(expression);
|
|
|
|
parse[expression.body.type](expression.body, instrumentingActive);
|
|
|
|
}
|
|
|
|
|
|
|
|
parse["Program"] = function(expression, instrument){
|
|
|
|
for (x in expression.body){
|
|
|
|
parse[expression.body[x].type](expression.body[x], instrument);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var instrumented = parse[result.type](result);
|
|
|
|
//We have to iterate through these injection points in descending order to not mess up
|
|
|
|
//the injection process.
|
|
|
|
var sortedPoints = Object.keys(injectionPoints).sort(function(a,b){return a-b});
|
|
|
|
for (x = sortedPoints.length-1; x>=0; x--){
|
|
|
|
injectionPoint = sortedPoints[x];
|
|
|
|
//Line instrumentation has to happen first
|
|
|
|
injectionPoints[injectionPoint].sort(function(a,b){
|
|
|
|
var eventTypes = ["openParen", "closeBracket","callBranchEvent","callEvent"];
|
|
|
|
return eventTypes.indexOf(b.type) - eventTypes.indexOf(a.type);
|
|
|
|
});
|
|
|
|
for (y in injectionPoints[injectionPoint]){
|
|
|
|
injection = injectionPoints[injectionPoint][y];
|
|
|
|
if (injection.type==='callEvent'){
|
|
|
|
linecount = (contract.slice(0, injectionPoint).match(/\n/g)||[]).length+1;
|
|
|
|
runnableLines.push(linecount);
|
|
|
|
contract = contract.slice(0, injectionPoint) + "Coverage('" + fileName + "'," + linecount + ");\n" + contract.slice(injectionPoint);
|
|
|
|
}else if (injection.type==='callFunctionEvent'){
|
|
|
|
contract = contract.slice(0, injectionPoint) + "FunctionCoverage('" + fileName + "'," + injection.fnId + ");\n" + contract.slice(injectionPoint);
|
|
|
|
}else if (injection.type==='callBranchEvent'){
|
|
|
|
contract = contract.slice(0, injectionPoint) + (injection.openBracket? '{' : '') + "BranchCoverage('" + fileName + "'," + injection.branchId + "," + injection.locationIdx + ")" + (injection.comma ? ',' : ";") + "\n" + contract.slice(injectionPoint);
|
|
|
|
}else if (injection.type==='callEmptyBranchEvent'){
|
|
|
|
contract = contract.slice(0, injectionPoint) + "else { BranchCoverage('" + fileName + "'," + injection.branchId + "," + injection.locationIdx + ");}\n" + contract.slice(injectionPoint);
|
|
|
|
}else if (injection.type==='openParen'){
|
|
|
|
contract = contract.slice(0, injectionPoint) + "(" + contract.slice(injectionPoint);
|
|
|
|
}else if (injection.type==='closeParen'){
|
|
|
|
contract = contract.slice(0, injectionPoint) + ")" + contract.slice(injectionPoint);
|
|
|
|
}else if (injection.type==='closeBracket'){
|
|
|
|
contract = contract.slice(0, injectionPoint) + "}" + contract.slice(injectionPoint);
|
|
|
|
}else if (injection.type==='literal'){
|
|
|
|
contract = contract.slice(0, injectionPoint) + injection.string + contract.slice(injectionPoint);
|
|
|
|
}else if (injection.type==='statement'){
|
|
|
|
contract = contract.slice(0, injectionPoint) + " StatementCoverage('" + fileName + "'," + injection.statementId + ");\n" + contract.slice(injectionPoint);
|
|
|
|
}else if (injection.type==='eventDefinition'){
|
|
|
|
contract = contract.slice(0, injectionPoint) + "event Coverage(string fileName, uint256 lineNumber);\nevent FunctionCoverage(string fileName, uint256 fnId);\nevent StatementCoverage(string fileName, uint256 statementId);\nevent BranchCoverage(string fileName, uint256 branchId, uint256 locationIdx);\n" + contract.slice(injectionPoint);
|
|
|
|
}else{
|
|
|
|
console.log('Unknown injection.type');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {contract: contract, runnableLines: runnableLines, fnMap: fnMap, branchMap: branchMap, statementMap: statementMap};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|