contract_source.js
1 const SourceMap = require('./source_map'); 2 3 class ContractSource { 4 constructor(file, path, body) { 5 let self = this; 6 7 this.file = file; 8 this.path = path; 9 this.body = body; 10 11 this.lineLengths = body.split("\n").map((line) => { return line.length; }); 12 this.lineCount = this.lineLengths.length; 13 14 this.lineOffsets = this.lineLengths.reduce((sum, _elt, i) => { 15 sum[i] = (i === 0) ? 0 : self.lineLengths[i-1] + sum[i-1] + 1; 16 return sum; 17 }, []); 18 19 this.contracts = {}; 20 } 21 22 sourceMapToLocations(sourceMap) { 23 var [offset, length, ..._] = sourceMap.split(":").map((val) => { 24 return parseInt(val, 10); 25 }); 26 27 var locations = {}; 28 29 for(let i = 0; i < this.lineCount; i++) { 30 if(this.lineOffsets[i+1] <= offset) continue; 31 32 locations.start = {line: i, column: offset - this.lineOffsets[i]}; 33 break; 34 } 35 36 for(var i = locations.start.line; i < this.lineCount; i++) { 37 if(this.lineOffsets[i+1] <= offset + length) continue; 38 39 locations.end = {line: i, column: ((offset + length) - this.lineOffsets[i])}; 40 break; 41 } 42 43 // Ensure we return an "end" as a safeguard if the marker ends up to be 44 // or surpass the offset for last character. 45 if(!locations.end) { 46 var lastLine = this.lineCount - 1; 47 locations.end = {line: lastLine, column: this.lineLengths[lastLine]}; 48 } 49 50 // Istanbul likes lines to be 1-indexed, so we'll increment here before returning. 51 locations.start.line++; 52 locations.end.line++; 53 return locations; 54 } 55 56 parseSolcOutput(source, contracts) { 57 this.id = source.id; 58 this.ast = source.ast; 59 this.contractBytecode = {}; 60 61 for(var contractName in contracts) { 62 this.contractBytecode[contractName] = {}; 63 64 var contract = contracts[contractName]; 65 var bytecodeMapping = this.contractBytecode[contractName]; 66 var opcodes = contract.evm.deployedBytecode.opcodes.trim().split(' '); 67 var sourceMaps = contract.evm.deployedBytecode.sourceMap.split(';'); 68 69 var bytecodeIdx = 0; 70 var pc = 0; 71 var instructions = 0; 72 var previousSourceMap = null; 73 74 do { 75 let sourceMap; 76 77 if(previousSourceMap === null) { 78 sourceMap = new SourceMap(sourceMaps[instructions]); 79 } else { 80 sourceMap = previousSourceMap.createRelativeTo(sourceMaps[instructions]); 81 } 82 83 var instruction = opcodes[bytecodeIdx]; 84 var length = this._instructionLength(instruction); 85 bytecodeMapping[pc] = { 86 instruction: instruction, 87 sourceMap: sourceMap, 88 jump: sourceMap.jump, 89 seen: false 90 }; 91 92 pc += length; 93 instructions++; 94 bytecodeIdx += (length > 1) ? 2 : 1; 95 previousSourceMap = sourceMap; 96 } while(bytecodeIdx < opcodes.length); 97 } 98 } 99 100 isInterface() { 101 return this.contractBytecode !== undefined && 102 Object.values(this.contractBytecode).every((contractBytecode) => { return (Object.values(contractBytecode).length <= 1); }); 103 } 104 105 /*eslint complexity: ["error", 40]*/ 106 generateCodeCoverage(trace) { 107 if(!this.ast || !this.contractBytecode) throw new Error('Error generating coverage: solc output was not assigned'); 108 109 let coverage = { 110 code: this.body.trim().split("\n"), 111 l: {}, 112 path: this.path, 113 s: {}, 114 b: {}, 115 f: {}, 116 fnMap: {}, 117 statementMap: {}, 118 branchMap: {} 119 }; 120 121 var nodesRequiringVisiting = [this.ast]; 122 var sourceMapToNodeType = {}; 123 124 do { 125 let node = nodesRequiringVisiting.pop(); 126 if(!node) continue; 127 128 let children = []; 129 let markLocations = []; 130 let location; 131 132 switch(node.nodeType) { 133 case 'Assignment': 134 case 'EventDefinition': 135 case 'ImportDirective': 136 case 'Literal': 137 case 'PlaceholderStatement': 138 case 'PragmaDirective': 139 case 'StructDefinition': 140 case 'VariableDeclaration': 141 // We don't need to do anything with these. Just carry on. 142 break; 143 144 case 'IfStatement': { 145 location = this.sourceMapToLocations(node.src); 146 let trueBranchLocation = this.sourceMapToLocations(node.trueBody.src); 147 148 let declarationSourceMap = new SourceMap(node.src).subtract(new SourceMap(node.trueBody.src)); 149 let declarationLocation = this.sourceMapToLocations(declarationSourceMap.toString()); 150 151 var falseBranchLocation; 152 if(node.falseBody) { 153 falseBranchLocation = this.sourceMapToLocations(node.falseBody.src); 154 } else { 155 falseBranchLocation = trueBranchLocation; 156 } 157 158 coverage.b[node.id] = [0,0]; 159 coverage.branchMap[node.id] = { 160 type: 'if', 161 locations: [trueBranchLocation, falseBranchLocation], 162 line: location.start.line 163 }; 164 165 markLocations = [declarationLocation]; 166 children = [node.condition]; 167 168 let trueExpression = (node.trueBody && node.trueBody.statements && node.trueBody.statements[0]) || node.trueBody; 169 if(trueExpression) { 170 children = children.concat(trueExpression); 171 trueExpression._parent = {type: 'b', id: node.id, idx: 0}; 172 } 173 174 let falseExpression = (node.falseBody && node.falseBody.statements && node.falseBody.statements[0]) || node.falseBody; 175 if(falseExpression) { 176 children = children.concat(falseExpression); 177 falseExpression._parent = {type: 'b', id: node.id, idx: 1}; 178 } 179 180 sourceMapToNodeType[node.src] = [{type: 'b', id: node.id, body: {loc: location}}]; 181 break; 182 } 183 184 case 'EmitStatement': { 185 children = [node.eventCall]; 186 break; 187 } 188 189 case 'BinaryOperation': 190 case 'ExpressionStatement': 191 case 'FunctionCall': 192 case 'Identifier': 193 case 'Return': 194 case 'UnaryOperation': 195 coverage.s[node.id] = 0; 196 197 location = this.sourceMapToLocations(node.src); 198 coverage.statementMap[node.id] = location; 199 200 if(!sourceMapToNodeType[node.src]) sourceMapToNodeType[node.src] = []; 201 sourceMapToNodeType[node.src].push({ 202 type: 's', 203 id: node.id, 204 body: {loc: coverage.statementMap[node.id]}, 205 parent: node._parent 206 }); 207 208 markLocations = [location]; 209 break; 210 211 case 'ContractDefinition': 212 case 'SourceUnit': 213 children = node.nodes; 214 break; 215 216 case 'ModifierDefinition': 217 case 'FunctionDefinition': 218 // Istanbul only wants the function definition, not the body, so we're 219 // going to do some fun math here. 220 var functionSourceMap = new SourceMap(node.src); 221 var functionParametersSourceMap = new SourceMap(node.parameters.src); 222 223 var functionDefinitionSourceMap = new SourceMap( 224 functionSourceMap.offset, 225 (functionParametersSourceMap.offset + functionParametersSourceMap.length) - functionSourceMap.offset 226 ).toString(); 227 228 var fnName = node.isConstructor ? "(constructor)" : node.name; 229 location = this.sourceMapToLocations(functionDefinitionSourceMap); 230 231 coverage.f[node.id] = 0; 232 coverage.fnMap[node.id] = { 233 name: fnName, 234 line: location.start.line, 235 loc: location 236 }; 237 238 // Record function positions. 239 sourceMapToNodeType[node.src] = [{type: 'f', id: node.id, body: coverage.fnMap[node.id]}]; 240 241 if(node.body) children = node.body.statements; 242 markLocations = [location]; 243 break; 244 245 case 'ForStatement': { 246 // For statements will be a bit of a special case. We want to count the body 247 // iterations but we only want to count the for loop being hit once. Because 248 // of this, we cover the initialization on the node. 249 let sourceMap = new SourceMap(node.src); 250 let bodySourceMap = new SourceMap(node.body.src); 251 let forLoopDeclaration = sourceMap.subtract(bodySourceMap).toString(); 252 253 location = this.sourceMapToLocations(forLoopDeclaration); 254 255 let markExpression = node.initializationExpression || node.loopExpression; 256 let expressionLocation = this.sourceMapToLocations(markExpression.src); 257 258 if(!sourceMapToNodeType[markExpression.src]) sourceMapToNodeType[markExpression.src] = []; 259 sourceMapToNodeType[markExpression.src].push({type: 's', id: node.id, body: {loc: location}}); 260 markLocations = [expressionLocation]; 261 262 coverage.s[node.id] = 0; 263 coverage.statementMap[node.id] = location; 264 265 children = node.body.statements; 266 break; 267 } 268 269 case 'VariableDeclarationStatement': { 270 location = this.sourceMapToLocations(node.src); 271 272 coverage.s[node.id] = 0; 273 coverage.statementMap[node.id] = location; 274 markLocations = [location]; 275 276 if(!sourceMapToNodeType[node.src]) sourceMapToNodeType[node.src] = []; 277 sourceMapToNodeType[node.src].push({type: 's', id: node.id, body: {loc: location}, foo: 'bar'}); 278 279 break; 280 } 281 282 default: 283 //console.log(`Don't know how to handle node type ${node.nodeType}`); 284 break; 285 } 286 287 nodesRequiringVisiting = nodesRequiringVisiting.concat(children); 288 289 markLocations.forEach((location) => { 290 for(var i = location.start.line; i <= location.end.line; i++) { 291 coverage.l[i] = 0; 292 } 293 }); 294 295 } while(nodesRequiringVisiting.length > 0); 296 297 var contractMatches = true; 298 for(var contractName in this.contractBytecode) { 299 var bytecode = this.contractBytecode[contractName]; 300 301 // Try to match the contract to the bytecode. If it doesn't, 302 // then we bail. 303 contractMatches = trace.structLogs.every((step) => { return bytecode[step.pc]; }); 304 if(!contractMatches) continue; 305 306 trace.structLogs.forEach((step) => { 307 step = bytecode[step.pc]; 308 if(!step.sourceMap || step.sourceMap === '' || step.sourceMap === SourceMap.empty()) return; 309 let sourceMapString = step.sourceMap.toString(this.id); 310 var nodes = sourceMapToNodeType[sourceMapString]; 311 312 if(!nodes) return; 313 314 nodes.forEach((node) => { 315 // Skip duplicate function reports by only reporting when there is a jump. 316 if(node.type == 'f' && step.jump) return; 317 318 if(node.type != 'b' && node.body && node.body.loc) { 319 for(var line = node.body.loc.start.line; line <= node.body.loc.end.line; line++) { 320 coverage.l[line]++; 321 } 322 } 323 324 if(node.type !== 'b') coverage[node.type][node.id]++; 325 326 if(!node.parent) return; 327 328 switch(node.parent.type) { 329 case 'b': 330 coverage.b[node.parent.id][node.parent.idx]++; 331 break; 332 333 default: 334 // do nothing 335 } 336 }); 337 }); 338 } 339 340 return coverage; 341 } 342 343 _instructionLength(instruction) { 344 if(instruction.indexOf('PUSH') === -1) return 1; 345 return parseInt(instruction.match(/PUSH(\d+)/m)[1], 10) + 1; 346 } 347 } 348 349 module.exports = ContractSource;