Code coverage for Solidity smart-contracts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
solidity-coverage/lib/registrar.js

510 lines
15 KiB

/**
* Registers injections points (e.g source location, type) and their associated data with
* a contract / instrumentation target. Run during the `parse` step. This data is
* consumed by the Injector as it modifies the source code in instrumentation's final step.
*/
class Registrar {
constructor(){
// When, at *certain nodes* we don't want to inject statements
// because they're an unnecessary expense. ex: `receive`, this
// can be toggled on/off in the parser
this.trackStatements = true;
// These are set by user option and enable/disable the measurement completely
this.enabled = {
statements: true,
functions: true,
modifiers: true,
branches: true,
lines: true
}
this.modifierWhitelist = [];
}
_seekSemiColon(contract, pos) {
const end = pos + 5;
for(pos; pos <= end; pos++) {
if (contract[pos] === ';') break;
}
return pos;
}
/**
* Adds injection point to injection points map
* @param {Object} contract instrumentation target
* @param {String} key injection point `type`
* @param {Number} value injection point `id`
*/
_createInjectionPoint(contract, key, value) {
value.contractName = contract.contractName;
(contract.injectionPoints[key])
? contract.injectionPoints[key].push(value)
: contract.injectionPoints[key] = [value];
}
/**
* Registers injections for statement measurements
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
statement(contract, expression) {
if (!this.trackStatements || !this.enabled.statements) return;
const startContract = contract.instrumented.slice(0, expression.range[0]);
const startline = ( startContract.match(/\n/g) || [] ).length + 1;
const startcol = expression.range[0] - startContract.lastIndexOf('\n') - 1;
const expressionContent = contract.instrumented.slice(
expression.range[0],
expression.range[1] + 1
);
const endline = startline + (contract, expressionContent.match('/\n/g') || []).length;
let endcol;
if (expressionContent.lastIndexOf('\n') >= 0) {
endcol = contract.instrumented.slice(
expressionContent.lastIndexOf('\n'),
expression.range[1]
).length;
} else endcol = startcol + (contract, expressionContent.length - 1);
contract.statementId += 1;
contract.statementMap[contract.statementId] = {
start: { line: startline, column: startcol },
end: { line: endline, column: endcol },
};
this._createInjectionPoint(contract, expression.range[0], {
type: 'injectStatement', statementId: contract.statementId,
});
};
/**
* Registers injections for line measurements
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
line(contract, expression) {
if (!this.enabled.lines) return;
const startchar = expression.range[0];
const endchar = expression.range[1] + 1;
const lastNewLine = contract.instrumented.slice(0, startchar).lastIndexOf('\n');
const nextNewLine = startchar + contract.instrumented.slice(startchar).indexOf('\n');
const contractSnipped = contract.instrumented.slice(lastNewLine, nextNewLine);
const restOfLine = contract.instrumented.slice(endchar, nextNewLine);
if (
contract.instrumented.slice(lastNewLine, startchar).trim().length === 0 &&
(
restOfLine.replace(';', '').trim().length === 0 ||
restOfLine.replace(';', '').trim().substring(0, 2) === '//'
)
)
{
this._createInjectionPoint(contract, lastNewLine + 1, { type: 'injectLine' });
} else if (
contract.instrumented.slice(lastNewLine, startchar).replace('{', '').trim().length === 0 &&
contract.instrumented.slice(endchar, nextNewLine).replace(/[;}]/g, '').trim().length === 0)
{
this._createInjectionPoint(contract, expression.range[0], { type: 'injectLine' });
}
};
/**
* Registers injections for function measurements
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
functionDeclaration(contract, expression) {
if (!this.enabled.functions) return;
let start = 0;
contract.fnId += 1;
if (expression.modifiers && expression.modifiers.length){
for (let modifier of expression.modifiers ){
// It's possible functions will have modifiers that take string args
// which contains an open curly brace. Skip ahead...
if (modifier.range[1] > start){
start = modifier.range[1];
}
// Add modifier branch coverage
if (
!this.enabled.modifiers ||
expression.isConstructor ||
this.modifierWhitelist.includes(modifier.name)
) {
continue;
}
this.addNewBranch(contract, modifier);
this._createInjectionPoint(
contract,
modifier.range[0],
{
type: 'injectModifier',
branchId: contract.branchId,
modifierName: modifier.name,
fnId: contract.fnId,
condition: 'pre'
}
);
this._createInjectionPoint(
contract,
modifier.range[1] + 1,
{
type: 'injectModifier',
branchId: contract.branchId,
modifierName: modifier.name,
fnId: contract.fnId,
condition: 'post'
}
);
}
} else {
start = expression.range[0];
}
const startContract = contract.instrumented.slice(0, start);
const startline = ( startContract.match(/\n/g) || [] ).length + 1;
const startcol = start - startContract.lastIndexOf('\n') - 1;
const endlineDelta = contract.instrumented.slice(start).indexOf('{');
const functionDefinition = contract.instrumented.slice(
start,
start + endlineDelta
);
contract.fnMap[contract.fnId] = {
name: expression.isConstructor ? 'constructor' : expression.name,
line: startline,
loc: expression.loc
};
this._createInjectionPoint(
contract,
start + endlineDelta + 1,
{
type: 'injectFunction',
fnId: contract.fnId,
}
);
};
/**
* Registers injections for branch measurements. This generic is consumed by
* the `require` and `if` registration methods.
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
addNewBranch(contract, expression) {
const startContract = contract.instrumented.slice(0, expression.range[0]);
const startline = ( startContract.match(/\n/g) || [] ).length + 1;
const startcol = expression.range[0] - startContract.lastIndexOf('\n') - 1;
contract.branchId += 1;
// NB locations for if branches in istanbul are zero
// length and associated with the start of the if.
contract.branchMap[contract.branchId] = {
line: startline,
type: 'if',
locations: [{
start: {
line: startline, column: startcol,
},
end: {
line: startline, column: startcol,
},
}, {
start: {
line: startline, column: startcol,
},
end: {
line: startline, column: startcol,
},
}],
};
};
addNewConditionalBranch(contract, expression){
let start;
// Instabul HTML highlighting location data...
const trueZero = expression.trueExpression.range[0];
const trueOne = expression.trueExpression.range[1];
const falseZero = expression.falseExpression.range[0];
const falseOne = expression.falseExpression.range[1];
start = contract.instrumented.slice(0, trueZero);
const trueStartLine = ( start.match(/\n/g) || [] ).length + 1;
const trueStartCol = trueZero - start.lastIndexOf('\n') - 1;
start = contract.instrumented.slice(0, trueOne);
const trueEndLine = ( start.match(/\n/g) || [] ).length + 1;
const trueEndCol = trueOne - start.lastIndexOf('\n') - 1;
start = contract.instrumented.slice(0, falseZero);
const falseStartLine = ( start.match(/\n/g) || [] ).length + 1;
const falseStartCol = falseZero - start.lastIndexOf('\n') - 1;
start = contract.instrumented.slice(0, falseOne);
const falseEndLine = ( start.match(/\n/g) || [] ).length + 1;
const falseEndCol = falseOne - start.lastIndexOf('\n') - 1;
contract.branchId += 1;
contract.branchMap[contract.branchId] = {
line: trueStartLine,
type: 'if',
locations: [{
start: {
line: trueStartLine, column: trueStartCol,
},
end: {
line: trueEndLine, column: trueEndCol,
},
}, {
start: {
line: falseStartLine, column: falseStartCol,
},
end: {
line: falseEndLine, column: falseEndCol,
},
}],
};
}
/**
* Registers injections for branch measurements. Used by logicalOR registration method.
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
addNewLogicalORBranch(contract, expression) {
let start;
// Instabul HTML highlighting location data...
const leftZero = expression.left.range[0];
const leftOne = expression.left.range[1];
const rightZero = expression.right.range[0];
const rightOne = expression.right.range[1];
start = contract.instrumented.slice(0, leftZero);
const leftStartLine = ( start.match(/\n/g) || [] ).length + 1;
const leftStartCol = leftZero - start.lastIndexOf('\n') - 1;
start = contract.instrumented.slice(0, leftOne);
const leftEndLine = ( start.match(/\n/g) || [] ).length + 1;
const leftEndCol = leftOne - start.lastIndexOf('\n') - 1;
start = contract.instrumented.slice(0, rightZero);
const rightStartLine = ( start.match(/\n/g) || [] ).length + 1;
const rightStartCol = rightZero - start.lastIndexOf('\n') - 1;
start = contract.instrumented.slice(0, rightOne);
const rightEndLine = ( start.match(/\n/g) || [] ).length + 1;
const rightEndCol = rightOne - start.lastIndexOf('\n') - 1;
contract.branchId += 1;
// NB locations for if branches in istanbul are zero
// length and associated with the start of the if.
contract.branchMap[contract.branchId] = {
line: leftStartLine,
type: 'cond-expr',
locations: [{
start: {
line: leftStartLine, column: leftStartCol,
},
end: {
line: leftEndLine, column: leftEndCol,
},
}, {
start: {
line: rightStartLine, column: rightStartCol,
},
end: {
line: rightEndLine, column: rightEndCol,
},
}],
};
};
conditional(contract, expression){
if (!this.enabled.branches) return;
this.addNewConditionalBranch(contract, expression);
// Double open parens
this._createInjectionPoint(
contract,
expression.condition.range[0],
{
type: 'injectOpenParen',
}
);
this._createInjectionPoint(
contract,
expression.condition.range[0],
{
type: 'injectOpenParen',
}
);
// False condition: (these get sorted in reverse order, so create in reversed order)
this._createInjectionPoint(
contract,
expression.condition.range[1] + 1,
{
type: 'injectOrFalse',
branchId: contract.branchId,
locationIdx: 1
}
);
// True condition
this._createInjectionPoint(
contract,
expression.condition.range[1] + 1,
{
type: 'injectAndTrue',
branchId: contract.branchId,
locationIdx: 0
}
);
}
/**
* Registers injections for logicalOR clause measurements (branches)
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
* @param {Number} injectionIdx pre/post branch index (left=0, right=1)
*/
logicalOR(contract, expression) {
if (!this.enabled.branches) return;
this.addNewLogicalORBranch(contract, expression);
// Left
this._createInjectionPoint(
contract,
expression.left.range[0],
{
type: 'injectOpenParen',
}
);
this._createInjectionPoint(
contract,
expression.left.range[1] + 1,
{
type: 'injectAndTrue',
branchId: contract.branchId,
locationIdx: 0
}
);
// Right
this._createInjectionPoint(
contract,
expression.right.range[0],
{
type: 'injectOpenParen',
}
);
this._createInjectionPoint(
contract,
expression.right.range[1] + 1,
{
type: 'injectAndTrue',
branchId: contract.branchId,
locationIdx: 1
}
);
}
/**
* Registers injections for require statement measurements (branches)
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
requireBranch(contract, expression) {
if (!this.enabled.branches) return;
this.addNewBranch(contract, expression);
this._createInjectionPoint(
contract,
expression.range[0],
{
type: 'injectRequirePre',
branchId: contract.branchId,
}
);
this._createInjectionPoint(
contract,
this._seekSemiColon(contract.instrumented, expression.range[1] + 1) + 1,
{
type: 'injectRequirePost',
branchId: contract.branchId,
}
);
};
/**
* Registers injections for if statement measurements (branches)
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
ifStatement(contract, expression) {
if (!this.enabled.branches) return;
this.addNewBranch(contract, expression);
if (expression.trueBody.type === 'Block') {
this._createInjectionPoint(
contract,
expression.trueBody.range[0] + 1,
{
type: 'injectBranch',
branchId: contract.branchId,
locationIdx: 0,
}
);
}
if (expression.falseBody && expression.falseBody.type === 'IfStatement') {
// Do nothing - we must be pre-preprocessing
} else if (expression.falseBody && expression.falseBody.type === 'Block') {
this._createInjectionPoint(
contract,
expression.falseBody.range[0] + 1,
{
type: 'injectBranch',
branchId: contract.branchId,
locationIdx: 1,
}
);
} else {
this._createInjectionPoint(
contract,
expression.trueBody.range[1] + 1,
{
type: 'injectEmptyBranch',
branchId: contract.branchId,
locationIdx: 1,
}
);
}
}
}
module.exports = Registrar;