/ lib / modules / coverage / contract_source.js
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;