|
|
|
/**
|
|
|
|
* Methods in this file walk the AST and call the instrumenter
|
|
|
|
* functions where appropriate, which determine where to inject events.
|
|
|
|
* (Listed in alphabetical order)
|
|
|
|
*/
|
|
|
|
const semver = require('semver');
|
|
|
|
const Registrar = require('./registrar');
|
|
|
|
const register = new Registrar();
|
|
|
|
|
|
|
|
const FILE_SCOPED_ID = "fileScopedId";
|
|
|
|
const parse = {};
|
|
|
|
|
|
|
|
// Utilities
|
|
|
|
parse.configure = function(_enabled){
|
|
|
|
register.enabled = Object.assign(register.enabled, _enabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Nodes
|
|
|
|
parse.AssignmentExpression = function(contract, expression) {
|
|
|
|
register.statement(contract, expression);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.Block = function(contract, expression) {
|
|
|
|
for (let x = 0; x < expression.statements.length; x++) {
|
|
|
|
register.line(contract, expression.statements[x]);
|
|
|
|
parse[expression.statements[x].type] &&
|
|
|
|
parse[expression.statements[x].type](contract, expression.statements[x]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.BinaryOperation = function(contract, expression, skipStatementRegistry) {
|
|
|
|
// Free-floating ternary conditional
|
|
|
|
if (expression.left && expression.left.type === 'Conditional'){
|
|
|
|
parse[expression.left.type](contract, expression.left, true);
|
|
|
|
register.statement(contract, expression);
|
|
|
|
|
|
|
|
// Ternary conditional assignment
|
|
|
|
} else if (expression.right && expression.right.type === 'Conditional'){
|
|
|
|
parse[expression.right.type](contract, expression.right, true);
|
|
|
|
register.statement(contract, expression);
|
|
|
|
|
|
|
|
// Regular binary operation
|
|
|
|
} else if(!skipStatementRegistry){
|
|
|
|
register.statement(contract, expression);
|
|
|
|
|
|
|
|
// LogicalOR condition search...
|
|
|
|
} else {
|
|
|
|
parse[expression.left.type] &&
|
|
|
|
parse[expression.left.type](contract, expression.left, true);
|
|
|
|
|
|
|
|
parse[expression.right.type] &&
|
|
|
|
parse[expression.right.type](contract, expression.right, true);
|
|
|
|
|
|
|
|
if (expression.operator === '||'){
|
|
|
|
register.logicalOR(contract, expression);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse.TupleExpression = function(contract, expression, skipStatementRegistry) {
|
|
|
|
expression.components.forEach(component => {
|
|
|
|
parse[component.type] &&
|
|
|
|
parse[component.type](contract, component, skipStatementRegistry);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
parse.FunctionCall = function(contract, expression, skipStatementRegistry) {
|
|
|
|
// In any given chain of call expressions, only the last one will fail this check.
|
|
|
|
// This makes sure we don't instrument a chain of expressions multiple times.
|
|
|
|
if (expression.expression.type !== 'FunctionCall') {
|
|
|
|
|
|
|
|
// Don't register sub-expressions (like intermediate method calls)
|
|
|
|
if (!skipStatementRegistry){
|
|
|
|
register.statement(contract, expression);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (expression.expression.name === 'require') {
|
|
|
|
register.requireBranch(contract, expression);
|
|
|
|
expression.arguments.forEach(arg => {
|
|
|
|
parse[arg.type] && parse[arg.type](contract, arg, true);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
parse[expression.expression.type] &&
|
|
|
|
parse[expression.expression.type](contract, expression.expression);
|
|
|
|
} else {
|
|
|
|
parse[expression.expression.type] &&
|
|
|
|
parse[expression.expression.type](contract, expression.expression);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.Conditional = function(contract, expression, skipStatementRegistry) {
|
|
|
|
parse[expression.condition.type] &&
|
|
|
|
parse[expression.condition.type](contract, expression.condition, skipStatementRegistry);
|
|
|
|
|
|
|
|
register.conditional(contract, expression);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.ContractDefinition = function(contract, expression) {
|
|
|
|
parse.ContractOrLibraryStatement(contract, expression);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.ContractOrLibraryStatement = function(contract, expression) {
|
|
|
|
|
|
|
|
// We need to define a method to pass coverage hashes into at top of each contract.
|
|
|
|
// This lets us get a fresh stack for the hash and avoid stack-too-deep errors.
|
|
|
|
if (expression.kind !== 'interface'){
|
|
|
|
let start = 0;
|
|
|
|
|
|
|
|
// It's possible a base contract will have constructor string arg
|
|
|
|
// which contains an open curly brace. Skip ahead pass the bases...
|
|
|
|
if (expression.baseContracts && expression.baseContracts.length){
|
|
|
|
for (let base of expression.baseContracts ){
|
|
|
|
if (base.range[1] > start){
|
|
|
|
start = base.range[1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
start = expression.range[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
const end = contract.instrumented.slice(start).indexOf('{') + 1;
|
|
|
|
const loc = start + end;;
|
|
|
|
|
|
|
|
contract.contractName = expression.name;
|
|
|
|
|
|
|
|
(contract.injectionPoints[loc])
|
|
|
|
? contract.injectionPoints[loc].push({ type: 'injectHashMethod', contractName: expression.name})
|
|
|
|
: contract.injectionPoints[loc] = [{ type: 'injectHashMethod', contractName: expression.name}];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (expression.subNodes) {
|
|
|
|
// Set flag to allow alternate cov id generation of file-level fn defs
|
|
|
|
contract.isContractScoped = true;
|
|
|
|
expression.subNodes.forEach(construct => {
|
|
|
|
parse[construct.type] &&
|
|
|
|
parse[construct.type](contract, construct);
|
|
|
|
});
|
|
|
|
// Unset flag...
|
|
|
|
contract.isContractScoped = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.EmitStatement = function(contract, expression){
|
|
|
|
register.statement(contract, expression);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.ExpressionStatement = function(contract, content) {
|
|
|
|
parse[content.expression.type] &&
|
|
|
|
parse[content.expression.type](contract, content.expression);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.ForStatement = function(contract, expression) {
|
|
|
|
register.statement(contract, expression);
|
|
|
|
parse[expression.body.type] &&
|
|
|
|
parse[expression.body.type](contract, expression.body);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.FunctionDefinition = function(contract, expression) {
|
|
|
|
// Use generic name component to generate the hash for cov fn ids
|
|
|
|
// if we're not inside a contract
|
|
|
|
let tempContractName = contract.contractName;
|
|
|
|
if (!contract.isContractScoped){
|
|
|
|
contract.contractName = FILE_SCOPED_ID;
|
|
|
|
}
|
|
|
|
|
|
|
|
parse.Modifiers(contract, expression.modifiers);
|
|
|
|
if (expression.body) {
|
|
|
|
// Skip fn & statement instrumentation for `receive` methods to
|
|
|
|
// minimize gas distortion
|
|
|
|
(expression.name === null && expression.isReceiveEther)
|
|
|
|
? register.trackStatements = false
|
|
|
|
: register.functionDeclaration(contract, expression);
|
|
|
|
|
|
|
|
parse[expression.body.type] &&
|
|
|
|
parse[expression.body.type](contract, expression.body);
|
|
|
|
register.trackStatements = true;
|
|
|
|
}
|
|
|
|
// Reset contractName in case it was changed for file scoped methods
|
|
|
|
contract.contractName = tempContractName;
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.IfStatement = function(contract, expression) {
|
|
|
|
register.statement(contract, expression);
|
|
|
|
register.ifStatement(contract, expression);
|
|
|
|
|
|
|
|
parse[expression.condition.type] &&
|
|
|
|
parse[expression.condition.type](contract, expression.condition, true);
|
|
|
|
|
|
|
|
parse[expression.trueBody.type] &&
|
|
|
|
parse[expression.trueBody.type](contract, expression.trueBody);
|
|
|
|
|
|
|
|
if (expression.falseBody) {
|
|
|
|
parse[expression.falseBody.type] &&
|
|
|
|
parse[expression.falseBody.type](contract, expression.falseBody);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// TODO: Investigate Node structure
|
|
|
|
/*parse.MemberAccess = function(contract, expression) {
|
|
|
|
parse[expression.object.type] &&
|
|
|
|
parse[expression.object.type](contract, expression.object);
|
|
|
|
};*/
|
|
|
|
|
|
|
|
parse.Modifiers = function(contract, modifiers) {
|
|
|
|
if (modifiers) {
|
|
|
|
modifiers.forEach(modifier => {
|
|
|
|
parse[modifier.type] && parse[modifier.type](contract, modifier);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.ModifierDefinition = function(contract, expression) {
|
|
|
|
register.functionDeclaration(contract, expression);
|
|
|
|
parse[expression.body.type] &&
|
|
|
|
parse[expression.body.type](contract, expression.body);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.NewExpression = function(contract, expression) {
|
|
|
|
parse[expression.typeName.type] &&
|
|
|
|
parse[expression.typeName.type](contract, expression.typeName);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.PragmaDirective = function(contract, expression){
|
|
|
|
let minVersion;
|
|
|
|
|
|
|
|
// Some solidity pragmas crash semver (ex: ABIEncoderV2)
|
|
|
|
try {
|
|
|
|
minVersion = semver.minVersion(expression.value);
|
|
|
|
} catch(e){
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// pragma abicoder v2 passes the semver test above but needs to be ignored
|
|
|
|
if (expression.name === 'abicoder'){
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// From solc >=0.7.4, every file should have instrumentation methods
|
|
|
|
// defined at the file level which file scoped fns can use...
|
|
|
|
if (semver.lt("0.7.3", minVersion)){
|
|
|
|
const start = expression.range[0];
|
|
|
|
const end = contract.instrumented.slice(start).indexOf(';') + 1;
|
|
|
|
const loc = start + end;
|
|
|
|
|
|
|
|
const injectionObject = {
|
|
|
|
type: 'injectHashMethod',
|
|
|
|
contractName: FILE_SCOPED_ID,
|
|
|
|
isFileScoped: true
|
|
|
|
};
|
|
|
|
|
|
|
|
contract.injectionPoints[loc] = [injectionObject];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parse.SourceUnit = function(contract, expression) {
|
|
|
|
expression.children.forEach(construct => {
|
|
|
|
parse[construct.type] &&
|
|
|
|
parse[construct.type](contract, construct);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.ReturnStatement = function(contract, expression) {
|
|
|
|
register.statement(contract, expression);
|
|
|
|
|
|
|
|
expression.expression &&
|
|
|
|
parse[expression.expression.type] &&
|
|
|
|
parse[expression.expression.type](contract, expression.expression, true);
|
|
|
|
};
|
|
|
|
|
|
|
|
// TODO:Investigate node structure
|
|
|
|
/*parse.UnaryOperation = function(contract, expression) {
|
|
|
|
parse[subExpression.argument.type] &&
|
|
|
|
parse[subExpression.argument.type](contract, expression.argument);
|
|
|
|
};*/
|
|
|
|
|
|
|
|
parse.TryStatement = function(contract, expression) {
|
|
|
|
register.statement(contract, expression);
|
|
|
|
parse[expression.body.type] &&
|
|
|
|
parse[expression.body.type](contract, expression.body);
|
|
|
|
|
|
|
|
for (let x = 0; x < expression.catchClauses.length; x++) {
|
|
|
|
parse[expression.catchClauses[x].body.type] &&
|
|
|
|
parse[expression.catchClauses[x].body.type](contract, expression.catchClauses[x].body);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.UncheckedStatement = function(contract, expression) {
|
|
|
|
parse[expression.block.type] &&
|
|
|
|
parse[expression.block.type](contract, expression.block);
|
|
|
|
}
|
|
|
|
|
|
|
|
parse.UsingStatement = function (contract, expression) {
|
|
|
|
parse[expression.for.type] &&
|
|
|
|
parse[expression.for.type](contract, expression.for);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.VariableDeclarationStatement = function (contract, expression) {
|
|
|
|
if (expression.initialValue && expression.initialValue.type === 'Conditional'){
|
|
|
|
parse[expression.initialValue.type](contract, expression.initialValue, true)
|
|
|
|
}
|
|
|
|
register.statement(contract, expression);
|
|
|
|
};
|
|
|
|
|
|
|
|
parse.WhileStatement = function (contract, expression) {
|
|
|
|
register.statement(contract, expression);
|
|
|
|
|
|
|
|
parse[expression.condition.type] &&
|
|
|
|
parse[expression.condition.type](contract, expression.condition, true);
|
|
|
|
|
|
|
|
parse[expression.body.type] &&
|
|
|
|
parse[expression.body.type](contract, expression.body);
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = parse;
|