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/injector.js

474 lines
14 KiB

const web3Utils = require("web3-utils");
class Injector {
constructor(viaIR){
this.viaIR = viaIR;
this.hashCounter = 0;
this.modifierCounter = 0;
this.modifiers = {};
}
_split(contract, injectionPoint){
return {
start: contract.instrumented.slice(0, injectionPoint),
end: contract.instrumented.slice(injectionPoint)
}
}
_getInjectable(id, hash, type){
switch(type){
case 'and-true':
return ` && ${this._getTrueMethodIdentifier(id)}(${hash}))`;
case 'or-false':
return ` || ${this._getFalseMethodIdentifier(id)}(${hash}))`;
case 'modifier':
return ` ${this._getModifierIdentifier(id)} `;
default:
return (this.viaIR)
? `${this._getAbiEncodeStatementHash(hash)} /* ${type} */ \n`
: `${this._getDefaultMethodIdentifier(id)}(${hash}); /* ${type} */ \n`;
}
}
_getHash(id) {
this.hashCounter++;
return web3Utils.keccak256(`${id}:${this.hashCounter}`).slice(0,18);
}
// Method returns void
_getDefaultMethodIdentifier(id){
return `c_${web3Utils.keccak256(id).slice(2,10)}`
}
// Method returns boolean: true
_getTrueMethodIdentifier(id){
return `c_true${web3Utils.keccak256(id).slice(2,10)}`
}
// Method returns boolean: false
_getFalseMethodIdentifier(id){
return `c_false${web3Utils.keccak256(id).slice(2,10)}`
}
_getModifierIdentifier(id){
return `c_mod${web3Utils.keccak256(id).slice(2,10)}`
}
// Way to get hash on the stack with viaIR (which seems to ignore abi.encode builtin)
// Tested with v0.8.17, v0.8.24
_getAbiEncodeStatementHash(hash){
return `abi.encode(${hash}); `
}
_getAbiEncodeStatementVar(hash){
return `abi.encode(c__${hash}); `
}
_getInjectionComponents(contract, injectionPoint, id, type){
const { start, end } = this._split(contract, injectionPoint);
const hash = this._getHash(id)
const injectable = this._getInjectable(id, hash, type);
return {
start: start,
end: end,
hash: hash,
injectable: injectable
}
}
/**
* Generates an instrumentation fn definition for contract scoped methods.
* Declared once per contract.
* @param {String} id
* @return {String}
*/
_getDefaultMethodDefinition(id){
const hash = web3Utils.keccak256(id).slice(2,10);
const method = this._getDefaultMethodIdentifier(id);
return (this.viaIR)
? ``
: `\nfunction ${method}(bytes8 c__${hash}) internal pure {}\n`;
}
/**
* Generates an instrumentation fn definition for file scoped methods.
* Declared once per file. (Has no visibility modifier)
* @param {String} id
* @return {String}
*/
_getFileScopedHashMethodDefinition(id, contract){
const hash = web3Utils.keccak256(id).slice(2,10);
const method = this._getDefaultMethodIdentifier(id);
const abi = this._getAbiEncodeStatementVar(hash);
return (this.viaIR)
? `\nfunction ${method}(bytes8 c__${hash}) pure { ${abi} }\n`
: `\nfunction ${method}(bytes8 c__${hash}) pure {}\n`;
}
/**
* Generates a solidity statement injection defining a method
* *which returns boolean true* to pass instrumentation hash to.
* @param {String} fileName
* @return {String} ex: bytes32[1] memory _sc_82e0891
*/
_getTrueMethodDefinition(id){
const hash = web3Utils.keccak256(id).slice(2,10);
const method = this._getTrueMethodIdentifier(id);
const abi = this._getAbiEncodeStatementVar(hash);
return (this.viaIR)
? `function ${method}(bytes8 c__${hash}) internal pure returns (bool){ ${abi} return true; }\n`
: `function ${method}(bytes8 c__${hash}) internal pure returns (bool){ return true; }\n`;
}
/**
* Generates a solidity statement injection defining a method
* *which returns boolean true* to pass instrumentation hash to.
* Declared once per file. (Has no visibility modifier)
* @param {String} fileName
* @return {String} ex: bytes32[1] memory _sc_82e0891
*/
_getFileScopeTrueMethodDefinition(id){
const hash = web3Utils.keccak256(id).slice(2,10);
const method = this._getTrueMethodIdentifier(id);
const abi = this._getAbiEncodeStatementVar(hash);
return (this.viaIR)
? `function ${method}(bytes8 c__${hash}) pure returns (bool){ ${abi} return true; }\n`
: `function ${method}(bytes8 c__${hash}) pure returns (bool){ return true; }\n`;
}
/**
* Generates a solidity statement injection defining a method
* *which returns boolean false* to pass instrumentation hash to.
* @param {String} fileName
* @return {String} ex: bytes32[1] memory _sc_82e0891
*/
_getFalseMethodDefinition(id){
const hash = web3Utils.keccak256(id).slice(2,10);
const method = this._getFalseMethodIdentifier(id);
const abi = this._getAbiEncodeStatementVar(hash);
return (this.viaIR)
? `function ${method}(bytes8 c__${hash}) internal pure returns (bool){ ${abi} return false; }\n`
: `function ${method}(bytes8 c__${hash}) internal pure returns (bool){ return false; }\n`;
}
/**
* Generates a solidity statement injection defining a method
* *which returns boolean false* to pass instrumentation hash to.
* Declared once per file. (Has no visibility modifier)
* @param {String} fileName
* @return {String} ex: bytes32[1] memory _sc_82e0891
*/
_getFileScopedFalseMethodDefinition(id){
const hash = web3Utils.keccak256(id).slice(2,10);
const method = this._getFalseMethodIdentifier(id);
const abi = this._getAbiEncodeStatementVar(hash);
return (this.viaIR)
? `function ${method}(bytes8 c__${hash}) pure returns (bool){ ${abi} return false; }\n`
: `function ${method}(bytes8 c__${hash}) pure returns (bool){ return false; }\n`;
}
_getModifierDefinitions(contractId, instrumentation){
let injection = '';
if (this.modifiers[contractId]){
for (const item of this.modifiers[contractId]){
injection += `modifier ${this._getModifierIdentifier(item.modifierId)}{ `;
let hash = this._getHash(item.modifierId);
let comment = `modifier-${item.condition}`;
let injectable = this._getInjectable(item.contractId, hash, comment);
// Process modifiers in the same step as `require` stmts in coverage.js
let type = (item.condition === 'pre') ? 'requirePre' : 'requirePost';
instrumentation[hash] = {
id: item.branchId,
type: type,
contractPath: item.fileName,
hits: 0
}
injection += `${injectable} _; }\n`;
}
}
return injection;
}
_cacheModifier(injection){
if (!this.modifiers[injection.contractId]) {
this.modifiers[injection.contractId] = [];
}
this.modifiers[injection.contractId].push(injection);
}
resetModifierMapping(){
this.modifiers = {};
}
injectLine(contract, fileName, injectionPoint, injection, instrumentation){
const type = 'line';
const { start, end } = this._split(contract, injectionPoint);
const id = `${fileName}:${injection.contractName}`;
const newLines = start.match(/\n/g);
const linecount = ( newLines || []).length + 1;
contract.runnableLines.push(linecount);
const hash = this._getHash(id)
const injectable = this._getInjectable(id, hash, type);
instrumentation[hash] = {
id: linecount,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectStatement(contract, fileName, injectionPoint, injection, instrumentation) {
const type = 'statement';
const id = `${fileName}:${injection.contractName}`;
const {
start,
end,
hash,
injectable
} = this._getInjectionComponents(contract, injectionPoint, id, type);
instrumentation[hash] = {
id: injection.statementId,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
};
injectFunction(contract, fileName, injectionPoint, injection, instrumentation){
const type = 'function';
const id = `${fileName}:${injection.contractName}`;
const {
start,
end,
hash,
injectable
} = this._getInjectionComponents(contract, injectionPoint, id, type);
instrumentation[hash] = {
id: injection.fnId,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectBranch(contract, fileName, injectionPoint, injection, instrumentation){
const type = 'branch';
const id = `${fileName}:${injection.contractName}`;
const {
start,
end,
hash,
injectable
} = this._getInjectionComponents(contract, injectionPoint, id, type);
instrumentation[hash] = {
id: injection.branchId,
locationIdx: injection.locationIdx,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectEmptyBranch(contract, fileName, injectionPoint, injection, instrumentation) {
const type = 'branch';
const id = `${fileName}:${injection.contractName}`;
const {
start,
end,
hash,
injectable
} = this._getInjectionComponents(contract, injectionPoint, id, type);
instrumentation[hash] = {
id: injection.branchId,
locationIdx: injection.locationIdx,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}else { ${injectable}}${end}`;
}
injectRequirePre(contract, fileName, injectionPoint, injection, instrumentation) {
const type = 'requirePre';
const id = `${fileName}:${injection.contractName}`;
const {
start,
end,
hash,
injectable
} = this._getInjectionComponents(contract, injectionPoint, id, type);
instrumentation[hash] = {
id: injection.branchId,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectRequirePost(contract, fileName, injectionPoint, injection, instrumentation) {
const type = 'requirePost';
const id = `${fileName}:${injection.contractName}`;
const {
start,
end,
hash,
injectable
} = this._getInjectionComponents(contract, injectionPoint, id, type);
instrumentation[hash] = {
id: injection.branchId,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectHashMethod(contract, fileName, injectionPoint, injection, instrumentation){
const start = contract.instrumented.slice(0, injectionPoint);
const end = contract.instrumented.slice(injectionPoint);
const id = `${fileName}:${injection.contractName}`;
const methodDefinition = (injection.isFileScoped)
? this._getFileScopedHashMethodDefinition(id)
: this._getDefaultMethodDefinition(id);
const trueMethodDefinition = (injection.isFileScoped)
? this._getFileScopeTrueMethodDefinition(id)
: this._getTrueMethodDefinition(id);
const falseMethodDefinition = (injection.isFileScoped)
? this._getFileScopedFalseMethodDefinition(id)
: this._getFalseMethodDefinition(id);
const modifierDefinition = (injection.isFileScoped)
? ""
: this._getModifierDefinitions(id, instrumentation);
contract.instrumented = `${start}` +
`${methodDefinition}` +
`${trueMethodDefinition}` +
`${falseMethodDefinition}` +
`${modifierDefinition}` +
`${end}`;
}
injectOpenParen(contract, fileName, injectionPoint, injection, instrumentation){
const start = contract.instrumented.slice(0, injectionPoint);
const end = contract.instrumented.slice(injectionPoint);
contract.instrumented = `${start}(${end}`;
}
injectAndTrue(contract, fileName, injectionPoint, injection, instrumentation){
const type = 'and-true';
const id = `${fileName}:${injection.contractName}`;
const {
start,
end,
hash,
injectable
} = this._getInjectionComponents(contract, injectionPoint, id, type);
instrumentation[hash] = {
id: injection.branchId,
locationIdx: injection.locationIdx,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectOrFalse(contract, fileName, injectionPoint, injection, instrumentation){
const type = 'or-false';
const id = `${fileName}:${injection.contractName}`;
const {
start,
end,
hash,
injectable
} = this._getInjectionComponents(contract, injectionPoint, id, type);
instrumentation[hash] = {
id: injection.branchId,
locationIdx: injection.locationIdx,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectModifier(contract, fileName, injectionPoint, injection, instrumentation){
this.modifierCounter++;
const type = 'modifier';
const contractId = `${fileName}:${injection.contractName}`;
const modifierId = `${fileName}:${injection.contractName}:` +
`${injection.modifierName}:${injection.fnId}:` +
`${injection.condition}:${this.modifierCounter}`;
const {
start,
end,
hash,
injectable
} = this._getInjectionComponents(contract, injectionPoint, modifierId, type);
this._cacheModifier({
contractId,
modifierId,
fileName,
...injection
});
contract.instrumented = `${start}${injectable}${end}`;
}
};
module.exports = Injector;