350 lines
11 KiB
JavaScript
350 lines
11 KiB
JavaScript
const SourceMap = require('./source_map');
|
|
|
|
class ContractSource {
|
|
constructor(file, path, body) {
|
|
let self = this;
|
|
|
|
this.file = file;
|
|
this.path = path;
|
|
this.body = body;
|
|
|
|
this.lineLengths = body.split("\n").map((line) => { return line.length; });
|
|
this.lineCount = this.lineLengths.length;
|
|
|
|
this.lineOffsets = this.lineLengths.reduce((sum, _elt, i) => {
|
|
sum[i] = (i === 0) ? 0 : self.lineLengths[i-1] + sum[i-1] + 1;
|
|
return sum;
|
|
}, []);
|
|
|
|
this.contracts = {};
|
|
}
|
|
|
|
sourceMapToLocations(sourceMap) {
|
|
var [offset, length, ..._] = sourceMap.split(":").map((val) => {
|
|
return parseInt(val, 10);
|
|
});
|
|
|
|
var locations = {};
|
|
|
|
for(let i = 0; i < this.lineCount; i++) {
|
|
if(this.lineOffsets[i+1] <= offset) continue;
|
|
|
|
locations.start = {line: i, column: offset - this.lineOffsets[i]};
|
|
break;
|
|
}
|
|
|
|
for(var i = locations.start.line; i < this.lineCount; i++) {
|
|
if(this.lineOffsets[i+1] <= offset + length) continue;
|
|
|
|
locations.end = {line: i, column: ((offset + length) - this.lineOffsets[i])};
|
|
break;
|
|
}
|
|
|
|
// Ensure we return an "end" as a safeguard if the marker ends up to be
|
|
// or surpass the offset for last character.
|
|
if(!locations.end) {
|
|
var lastLine = this.lineCount - 1;
|
|
locations.end = {line: lastLine, column: this.lineLengths[lastLine]};
|
|
}
|
|
|
|
// Istanbul likes lines to be 1-indexed, so we'll increment here before returning.
|
|
locations.start.line++;
|
|
locations.end.line++;
|
|
return locations;
|
|
}
|
|
|
|
parseSolcOutput(source, contracts) {
|
|
this.id = source.id;
|
|
this.ast = source.ast;
|
|
this.contractBytecode = {};
|
|
|
|
for(var contractName in contracts) {
|
|
this.contractBytecode[contractName] = {};
|
|
|
|
var contract = contracts[contractName];
|
|
var bytecodeMapping = this.contractBytecode[contractName];
|
|
var opcodes = contract.evm.deployedBytecode.opcodes.trim().split(' ');
|
|
var sourceMaps = contract.evm.deployedBytecode.sourceMap.split(';');
|
|
|
|
var bytecodeIdx = 0;
|
|
var pc = 0;
|
|
var instructions = 0;
|
|
var previousSourceMap = null;
|
|
|
|
do {
|
|
let sourceMap;
|
|
|
|
if(previousSourceMap === null) {
|
|
sourceMap = new SourceMap(sourceMaps[instructions]);
|
|
} else {
|
|
sourceMap = previousSourceMap.createRelativeTo(sourceMaps[instructions]);
|
|
}
|
|
|
|
var instruction = opcodes[bytecodeIdx];
|
|
var length = this._instructionLength(instruction);
|
|
bytecodeMapping[pc] = {
|
|
instruction: instruction,
|
|
sourceMap: sourceMap,
|
|
jump: sourceMap.jump,
|
|
seen: false
|
|
};
|
|
|
|
pc += length;
|
|
instructions++;
|
|
bytecodeIdx += (length > 1) ? 2 : 1;
|
|
previousSourceMap = sourceMap;
|
|
} while(bytecodeIdx < opcodes.length);
|
|
}
|
|
}
|
|
|
|
isInterface() {
|
|
return this.contractBytecode !== undefined &&
|
|
Object.values(this.contractBytecode).every((contractBytecode) => { return (Object.values(contractBytecode).length <= 1); });
|
|
}
|
|
|
|
/*eslint complexity: ["error", 40]*/
|
|
generateCodeCoverage(trace) {
|
|
if(!this.ast || !this.contractBytecode) throw new Error('Error generating coverage: solc output was not assigned');
|
|
|
|
let coverage = {
|
|
code: this.body.trim().split("\n"),
|
|
l: {},
|
|
path: this.path,
|
|
s: {},
|
|
b: {},
|
|
f: {},
|
|
fnMap: {},
|
|
statementMap: {},
|
|
branchMap: {}
|
|
};
|
|
|
|
var nodesRequiringVisiting = [this.ast];
|
|
var sourceMapToNodeType = {};
|
|
|
|
do {
|
|
let node = nodesRequiringVisiting.pop();
|
|
if(!node) continue;
|
|
|
|
let children = [];
|
|
let markLocations = [];
|
|
let location;
|
|
|
|
switch(node.nodeType) {
|
|
case 'Assignment':
|
|
case 'EventDefinition':
|
|
case 'ImportDirective':
|
|
case 'Literal':
|
|
case 'PlaceholderStatement':
|
|
case 'PragmaDirective':
|
|
case 'StructDefinition':
|
|
case 'VariableDeclaration':
|
|
// We don't need to do anything with these. Just carry on.
|
|
break;
|
|
|
|
case 'IfStatement': {
|
|
location = this.sourceMapToLocations(node.src);
|
|
let trueBranchLocation = this.sourceMapToLocations(node.trueBody.src);
|
|
|
|
let declarationSourceMap = new SourceMap(node.src).subtract(new SourceMap(node.trueBody.src));
|
|
let declarationLocation = this.sourceMapToLocations(declarationSourceMap.toString());
|
|
|
|
var falseBranchLocation;
|
|
if(node.falseBody) {
|
|
falseBranchLocation = this.sourceMapToLocations(node.falseBody.src);
|
|
} else {
|
|
falseBranchLocation = trueBranchLocation;
|
|
}
|
|
|
|
coverage.b[node.id] = [0,0];
|
|
coverage.branchMap[node.id] = {
|
|
type: 'if',
|
|
locations: [trueBranchLocation, falseBranchLocation],
|
|
line: location.start.line
|
|
};
|
|
|
|
markLocations = [declarationLocation];
|
|
children = [node.condition];
|
|
|
|
let trueExpression = (node.trueBody && node.trueBody.statements && node.trueBody.statements[0]) || node.trueBody;
|
|
if(trueExpression) {
|
|
children = children.concat(trueExpression);
|
|
trueExpression._parent = {type: 'b', id: node.id, idx: 0};
|
|
}
|
|
|
|
let falseExpression = (node.falseBody && node.falseBody.statements && node.falseBody.statements[0]) || node.falseBody;
|
|
if(falseExpression) {
|
|
children = children.concat(falseExpression);
|
|
falseExpression._parent = {type: 'b', id: node.id, idx: 1};
|
|
}
|
|
|
|
sourceMapToNodeType[node.src] = [{type: 'b', id: node.id, body: {loc: location}}];
|
|
break;
|
|
}
|
|
|
|
case 'EmitStatement': {
|
|
children = [node.eventCall];
|
|
break;
|
|
}
|
|
|
|
case 'BinaryOperation':
|
|
case 'ExpressionStatement':
|
|
case 'FunctionCall':
|
|
case 'Identifier':
|
|
case 'Return':
|
|
case 'UnaryOperation':
|
|
coverage.s[node.id] = 0;
|
|
|
|
location = this.sourceMapToLocations(node.src);
|
|
coverage.statementMap[node.id] = location;
|
|
|
|
if(!sourceMapToNodeType[node.src]) sourceMapToNodeType[node.src] = [];
|
|
sourceMapToNodeType[node.src].push({
|
|
type: 's',
|
|
id: node.id,
|
|
body: {loc: coverage.statementMap[node.id]},
|
|
parent: node._parent
|
|
});
|
|
|
|
markLocations = [location];
|
|
break;
|
|
|
|
case 'ContractDefinition':
|
|
case 'SourceUnit':
|
|
children = node.nodes;
|
|
break;
|
|
|
|
case 'ModifierDefinition':
|
|
case 'FunctionDefinition':
|
|
// Istanbul only wants the function definition, not the body, so we're
|
|
// going to do some fun math here.
|
|
var functionSourceMap = new SourceMap(node.src);
|
|
var functionParametersSourceMap = new SourceMap(node.parameters.src);
|
|
|
|
var functionDefinitionSourceMap = new SourceMap(
|
|
functionSourceMap.offset,
|
|
(functionParametersSourceMap.offset + functionParametersSourceMap.length) - functionSourceMap.offset
|
|
).toString();
|
|
|
|
var fnName = node.isConstructor ? "(constructor)" : node.name;
|
|
location = this.sourceMapToLocations(functionDefinitionSourceMap);
|
|
|
|
coverage.f[node.id] = 0;
|
|
coverage.fnMap[node.id] = {
|
|
name: fnName,
|
|
line: location.start.line,
|
|
loc: location
|
|
};
|
|
|
|
// Record function positions.
|
|
sourceMapToNodeType[node.src] = [{type: 'f', id: node.id, body: coverage.fnMap[node.id]}];
|
|
|
|
if(node.body) children = node.body.statements;
|
|
markLocations = [location];
|
|
break;
|
|
|
|
case 'ForStatement': {
|
|
// For statements will be a bit of a special case. We want to count the body
|
|
// iterations but we only want to count the for loop being hit once. Because
|
|
// of this, we cover the initialization on the node.
|
|
let sourceMap = new SourceMap(node.src);
|
|
let bodySourceMap = new SourceMap(node.body.src);
|
|
let forLoopDeclaration = sourceMap.subtract(bodySourceMap).toString();
|
|
|
|
location = this.sourceMapToLocations(forLoopDeclaration);
|
|
|
|
let markExpression = node.initializationExpression || node.loopExpression;
|
|
let expressionLocation = this.sourceMapToLocations(markExpression.src);
|
|
|
|
if(!sourceMapToNodeType[markExpression.src]) sourceMapToNodeType[markExpression.src] = [];
|
|
sourceMapToNodeType[markExpression.src].push({type: 's', id: node.id, body: {loc: location}});
|
|
markLocations = [expressionLocation];
|
|
|
|
coverage.s[node.id] = 0;
|
|
coverage.statementMap[node.id] = location;
|
|
|
|
children = node.body.statements;
|
|
break;
|
|
}
|
|
|
|
case 'VariableDeclarationStatement': {
|
|
location = this.sourceMapToLocations(node.src);
|
|
|
|
coverage.s[node.id] = 0;
|
|
coverage.statementMap[node.id] = location;
|
|
markLocations = [location];
|
|
|
|
if(!sourceMapToNodeType[node.src]) sourceMapToNodeType[node.src] = [];
|
|
sourceMapToNodeType[node.src].push({type: 's', id: node.id, body: {loc: location}, foo: 'bar'});
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
//console.log(`Don't know how to handle node type ${node.nodeType}`);
|
|
break;
|
|
}
|
|
|
|
nodesRequiringVisiting = nodesRequiringVisiting.concat(children);
|
|
|
|
markLocations.forEach((location) => {
|
|
for(var i = location.start.line; i <= location.end.line; i++) {
|
|
coverage.l[i] = 0;
|
|
}
|
|
});
|
|
|
|
} while(nodesRequiringVisiting.length > 0);
|
|
|
|
var contractMatches = true;
|
|
for(var contractName in this.contractBytecode) {
|
|
var bytecode = this.contractBytecode[contractName];
|
|
|
|
// Try to match the contract to the bytecode. If it doesn't,
|
|
// then we bail.
|
|
contractMatches = trace.structLogs.every((step) => { return bytecode[step.pc]; });
|
|
if(!contractMatches) continue;
|
|
|
|
trace.structLogs.forEach((step) => {
|
|
step = bytecode[step.pc];
|
|
if(!step.sourceMap || step.sourceMap === '' || step.sourceMap === SourceMap.empty()) return;
|
|
let sourceMapString = step.sourceMap.toString(this.id);
|
|
var nodes = sourceMapToNodeType[sourceMapString];
|
|
|
|
if(!nodes) return;
|
|
|
|
nodes.forEach((node) => {
|
|
// Skip duplicate function reports by only reporting when there is a jump.
|
|
if(node.type == 'f' && step.jump) return;
|
|
|
|
if(node.type != 'b' && node.body && node.body.loc) {
|
|
for(var line = node.body.loc.start.line; line <= node.body.loc.end.line; line++) {
|
|
coverage.l[line]++;
|
|
}
|
|
}
|
|
|
|
if(node.type !== 'b') coverage[node.type][node.id]++;
|
|
|
|
if(!node.parent) return;
|
|
|
|
switch(node.parent.type) {
|
|
case 'b':
|
|
coverage.b[node.parent.id][node.parent.idx]++;
|
|
break;
|
|
|
|
default:
|
|
// do nothing
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
return coverage;
|
|
}
|
|
|
|
_instructionLength(instruction) {
|
|
if(instruction.indexOf('PUSH') === -1) return 1;
|
|
return parseInt(instruction.match(/PUSH(\d+)/m)[1], 10) + 1;
|
|
}
|
|
}
|
|
|
|
module.exports = ContractSource;
|