aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-cov/src
diff options
context:
space:
mode:
authorLeonid Logvinov <logvinov.leon@gmail.com>2018-03-05 11:05:26 +0800
committerLeonid Logvinov <logvinov.leon@gmail.com>2018-03-12 10:37:27 +0800
commit13299158d1e22d1af1cd36434fc403a74743ecb1 (patch)
tree9b35435c6f8641d2dc3d7bfd530c7c4f040a1f51 /packages/sol-cov/src
parenta6571b09d2087ffb9a4860c0db3d7344321fe2c3 (diff)
downloaddexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.gz
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.bz2
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.lz
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.xz
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.zst
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.zip
Add sol-cover implementation
Diffstat (limited to 'packages/sol-cov/src')
-rw-r--r--packages/sol-cov/src/ast_visitor.ts115
-rw-r--r--packages/sol-cov/src/collect_contract_data.ts40
-rw-r--r--packages/sol-cov/src/constants.ts3
-rw-r--r--packages/sol-cov/src/coverage_manager.ts166
-rw-r--r--packages/sol-cov/src/coverage_subprovider.ts124
-rw-r--r--packages/sol-cov/src/globals.d.ts6
-rw-r--r--packages/sol-cov/src/index.ts1
-rw-r--r--packages/sol-cov/src/instructions.ts24
-rw-r--r--packages/sol-cov/src/instrument_solidity.ts16
-rw-r--r--packages/sol-cov/src/source_maps.ts77
-rw-r--r--packages/sol-cov/src/types.ts89
-rw-r--r--packages/sol-cov/src/utils.ts13
12 files changed, 674 insertions, 0 deletions
diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts
new file mode 100644
index 000000000..f450dadbb
--- /dev/null
+++ b/packages/sol-cov/src/ast_visitor.ts
@@ -0,0 +1,115 @@
+import * as _ from 'lodash';
+import * as SolidityParser from 'solidity-parser-sc';
+
+import { BranchMap, FnMap, LocationByOffset, SingleFileSourceRange, StatementMap } from './types';
+
+export interface CoverageEntriesDescription {
+ fnMap: FnMap;
+ branchMap: BranchMap;
+ statementMap: StatementMap;
+}
+
+export class ASTVisitor {
+ private _entryId = 0;
+ private _fnMap: FnMap = {};
+ private _branchMap: BranchMap = {};
+ private _statementMap: StatementMap = {};
+ private _locationByOffset: LocationByOffset;
+ private static _doesLookLikeAnASTNode(ast: any): boolean {
+ const isAST = _.isObject(ast) && _.isString(ast.type) && _.isNumber(ast.start) && _.isNumber(ast.end);
+ return isAST;
+ }
+ constructor(locationByOffset: LocationByOffset) {
+ this._locationByOffset = locationByOffset;
+ }
+ public walkAST(astNode: SolidityParser.AST): void {
+ if (_.isArray(astNode) || _.isObject(astNode)) {
+ if (ASTVisitor._doesLookLikeAnASTNode(astNode)) {
+ const nodeType = astNode.type;
+ const visitorFunctionName = `_visit${nodeType}`;
+ // tslint:disable-next-line:no-this-assignment
+ const self: { [visitorFunctionName: string]: (ast: SolidityParser.AST) => void } = this as any;
+ if (_.isFunction(self[visitorFunctionName])) {
+ self[visitorFunctionName](astNode);
+ }
+ }
+ _.forEach(astNode, subtree => {
+ this.walkAST(subtree);
+ });
+ }
+ }
+ public getCollectedCoverageEntries(): CoverageEntriesDescription {
+ const coverageEntriesDescription = {
+ fnMap: this._fnMap,
+ branchMap: this._branchMap,
+ statementMap: this._statementMap,
+ };
+ return coverageEntriesDescription;
+ }
+ private _visitConditionalExpression(ast: SolidityParser.AST): void {
+ this._visitBinaryBranch(ast, ast.consequent, ast.alternate, 'cond-expr');
+ }
+ private _visitFunctionDeclaration(ast: SolidityParser.AST): void {
+ const loc = this._getExpressionRange(ast);
+ this._fnMap[this._entryId++] = {
+ name: ast.name,
+ line: loc.start.line,
+ loc,
+ };
+ }
+ private _visitBinaryExpression(ast: SolidityParser.AST): void {
+ this._visitBinaryBranch(ast, ast.left, ast.right, 'binary-expr');
+ }
+ private _visitIfStatement(ast: SolidityParser.AST): void {
+ this._visitStatement(ast);
+ this._visitBinaryBranch(ast, ast.consequent, ast.alternate || ast, 'if');
+ }
+ private _visitBreakStatement(ast: SolidityParser.AST): void {
+ this._visitStatement(ast);
+ }
+ private _visitContractStatement(ast: SolidityParser.AST): void {
+ this._visitStatement(ast);
+ }
+ private _visitExpressionStatement(ast: SolidityParser.AST): void {
+ this._visitStatement(ast);
+ }
+ private _visitForStatement(ast: SolidityParser.AST): void {
+ this._visitStatement(ast);
+ }
+ private _visitPlaceholderStatement(ast: SolidityParser.AST): void {
+ this._visitStatement(ast);
+ }
+ private _visitReturnStatement(ast: SolidityParser.AST): void {
+ this._visitStatement(ast);
+ }
+ private _visitModifierArgument(ast: SolidityParser.AST): void {
+ const BUILTIN_MODIFIERS = ['public', 'view', 'payable', 'external', 'internal', 'pure'];
+ if (!_.includes(BUILTIN_MODIFIERS, ast.name)) {
+ this._visitStatement(ast);
+ }
+ }
+ private _visitBinaryBranch(
+ ast: SolidityParser.AST,
+ left: SolidityParser.AST,
+ right: SolidityParser.AST,
+ type: 'if' | 'cond-expr' | 'binary-expr',
+ ): void {
+ this._branchMap[this._entryId++] = {
+ line: this._getExpressionRange(ast).start.line,
+ type,
+ locations: [this._getExpressionRange(left), this._getExpressionRange(right)],
+ };
+ }
+ private _visitStatement(ast: SolidityParser.AST): void {
+ this._statementMap[this._entryId++] = this._getExpressionRange(ast);
+ }
+ private _getExpressionRange(ast: SolidityParser.AST): SingleFileSourceRange {
+ const start = this._locationByOffset[ast.start - 1];
+ const end = this._locationByOffset[ast.end - 1];
+ const range = {
+ start,
+ end,
+ };
+ return range;
+ }
+}
diff --git a/packages/sol-cov/src/collect_contract_data.ts b/packages/sol-cov/src/collect_contract_data.ts
new file mode 100644
index 000000000..dffccce2f
--- /dev/null
+++ b/packages/sol-cov/src/collect_contract_data.ts
@@ -0,0 +1,40 @@
+import * as fs from 'fs';
+import * as glob from 'glob';
+import * as _ from 'lodash';
+import * as path from 'path';
+
+import { ContractData } from './types';
+
+export const collectContractsData = (artifactsPath: string, sourcesPath: string, networkId: number) => {
+ const sourcesGlob = `${sourcesPath}/**/*.sol`;
+ const sourceFileNames = glob.sync(sourcesGlob, { absolute: true });
+ const contractsDataIfExists: Array<ContractData | {}> = _.map(sourceFileNames, sourceFileName => {
+ const baseName = path.basename(sourceFileName, '.sol');
+ const artifactFileName = path.join(artifactsPath, `${baseName}.json`);
+ if (!fs.existsSync(artifactFileName)) {
+ // If the contract isn't directly compiled, but is imported as the part of the other contract - we don't have an artifact for it and therefore can't do anything usefull with it
+ return {};
+ }
+ const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString());
+ const sources = _.map(artifact.networks[networkId].sources, source => {
+ const includedFileName = glob.sync(`${sourcesPath}/**/${source}`, { absolute: true })[0];
+ return includedFileName;
+ });
+ const sourceCodes = _.map(sources, source => {
+ const includedSourceCode = fs.readFileSync(source).toString();
+ return includedSourceCode;
+ });
+ const contractData = {
+ baseName,
+ sourceCodes,
+ sources,
+ sourceMap: artifact.networks[networkId].source_map,
+ sourceMapRuntime: artifact.networks[networkId].source_map_runtime,
+ runtimeBytecode: artifact.networks[networkId].runtime_bytecode,
+ bytecode: artifact.networks[networkId].bytecode,
+ };
+ return contractData;
+ });
+ const contractsData = _.filter(contractsDataIfExists, contractData => !_.isEmpty(contractData)) as ContractData[];
+ return contractsData;
+};
diff --git a/packages/sol-cov/src/constants.ts b/packages/sol-cov/src/constants.ts
new file mode 100644
index 000000000..970734f2d
--- /dev/null
+++ b/packages/sol-cov/src/constants.ts
@@ -0,0 +1,3 @@
+export const constants = {
+ NEW_CONTRACT: 'NEW_CONTRACT',
+};
diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts
new file mode 100644
index 000000000..aced9208f
--- /dev/null
+++ b/packages/sol-cov/src/coverage_manager.ts
@@ -0,0 +1,166 @@
+import * as fs from 'fs';
+import { Collector } from 'istanbul';
+import * as _ from 'lodash';
+import * as path from 'path';
+
+import { collectContractsData } from './collect_contract_data';
+import { constants } from './constants';
+import { collectCoverageEntries } from './instrument_solidity';
+import { parseSourceMap } from './source_maps';
+import {
+ BranchCoverage,
+ BranchDescription,
+ BranchMap,
+ ContractData,
+ Coverage,
+ FnMap,
+ FunctionCoverage,
+ FunctionDescription,
+ LineColumn,
+ SingleFileSourceRange,
+ SourceRange,
+ StatementCoverage,
+ StatementDescription,
+ StatementMap,
+ TraceInfo,
+} from './types';
+import { utils } from './utils';
+
+function getSingleFileCoverageForTrace(
+ contractData: ContractData,
+ coveredPcs: number[],
+ pcToSourceRange: { [programCounter: number]: SourceRange },
+ fileIndex: number,
+): Coverage {
+ const fileName = contractData.sources[fileIndex];
+ const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex], fileName);
+ let sourceRanges = _.map(coveredPcs, coveredPc => pcToSourceRange[coveredPc]);
+ sourceRanges = _.compact(sourceRanges); // Some PC's don't map to a source range and we just ignore them.
+ sourceRanges = _.uniqBy(sourceRanges, s => JSON.stringify(s)); // We don't care if one PC was covered multiple times within a single transaction
+ sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === fileName);
+ const branchCoverage: BranchCoverage = {};
+ for (const branchId of _.keys(coverageEntriesDescription.branchMap)) {
+ const branchDescription = coverageEntriesDescription.branchMap[branchId];
+ const isCovered = _.map(branchDescription.locations, location =>
+ _.some(sourceRanges, range => utils.isRangeInside(range.location, location)),
+ );
+ branchCoverage[branchId] = isCovered;
+ }
+ const statementCoverage: StatementCoverage = {};
+ for (const statementId of _.keys(coverageEntriesDescription.statementMap)) {
+ const statementDescription = coverageEntriesDescription.statementMap[statementId];
+ const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, statementDescription));
+ statementCoverage[statementId] = isCovered;
+ }
+ const functionCoverage: FunctionCoverage = {};
+ for (const fnId of _.keys(coverageEntriesDescription.fnMap)) {
+ const functionDescription = coverageEntriesDescription.fnMap[fnId];
+ const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, functionDescription.loc));
+ functionCoverage[fnId] = isCovered;
+ }
+ const partialCoverage = {
+ [contractData.sources[fileIndex]]: {
+ ...coverageEntriesDescription,
+ l: {}, // It's able to derive it from statement coverage
+ path: fileName,
+ f: functionCoverage,
+ s: statementCoverage,
+ b: branchCoverage,
+ },
+ };
+ return partialCoverage;
+}
+
+export class CoverageManager {
+ private _traceInfoByAddress: { [address: string]: TraceInfo[] } = {};
+ private _contractsData: ContractData[] = [];
+ private _txDataByHash: { [txHash: string]: string } = {};
+ private _getContractCodeAsync: (address: string) => Promise<string>;
+ constructor(
+ artifactsPath: string,
+ sourcesPath: string,
+ networkId: number,
+ getContractCodeAsync: (address: string) => Promise<string>,
+ ) {
+ this._getContractCodeAsync = getContractCodeAsync;
+ this._contractsData = collectContractsData(artifactsPath, sourcesPath, networkId);
+ }
+ public setTxDataByHash(txHash: string, data: string): void {
+ this._txDataByHash[txHash] = data;
+ }
+ public appendTraceInfo(address: string, traceInfo: TraceInfo): void {
+ if (_.isUndefined(this._traceInfoByAddress[address])) {
+ this._traceInfoByAddress[address] = [];
+ }
+ this._traceInfoByAddress[address].push(traceInfo);
+ }
+ public async writeCoverageAsync(): Promise<void> {
+ const finalCoverage = await this._computeCoverageAsync();
+ const jsonReplacer: null = null;
+ const numberOfJsonSpaces = 4;
+ const stringifiedCoverage = JSON.stringify(finalCoverage, jsonReplacer, numberOfJsonSpaces);
+ fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
+ }
+ private async _computeCoverageAsync(): Promise<Coverage> {
+ const collector = new Collector();
+ for (const address of _.keys(this._traceInfoByAddress)) {
+ if (address !== constants.NEW_CONTRACT) {
+ // Runtime transaction
+ const runtimeBytecode = await this._getContractCodeAsync(address);
+ const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData;
+ if (_.isUndefined(contractData)) {
+ throw new Error(`Transaction to an unknown address: ${address}`);
+ }
+ const bytecodeHex = contractData.runtimeBytecode.slice(2);
+ const sourceMap = contractData.sourceMapRuntime;
+ const pcToSourceRange = parseSourceMap(
+ contractData.sourceCodes,
+ sourceMap,
+ bytecodeHex,
+ contractData.sources,
+ );
+ for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
+ _.forEach(this._traceInfoByAddress[address], (traceInfo: TraceInfo) => {
+ const singleFileCoverageForTrace = getSingleFileCoverageForTrace(
+ contractData,
+ traceInfo.coveredPcs,
+ pcToSourceRange,
+ fileIndex,
+ );
+ collector.add(singleFileCoverageForTrace);
+ });
+ }
+ } else {
+ // Contract creation transaction
+ _.forEach(this._traceInfoByAddress[address], (traceInfo: TraceInfo) => {
+ const bytecode = this._txDataByHash[traceInfo.txHash];
+ const contractData = _.find(this._contractsData, contractDataCandidate =>
+ bytecode.startsWith(contractDataCandidate.bytecode),
+ ) as ContractData;
+ if (_.isUndefined(contractData)) {
+ throw new Error(`Unknown contract creation transaction`);
+ }
+ const bytecodeHex = contractData.bytecode.slice(2);
+ const sourceMap = contractData.sourceMap;
+ const pcToSourceRange = parseSourceMap(
+ contractData.sourceCodes,
+ sourceMap,
+ bytecodeHex,
+ contractData.sources,
+ );
+ for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
+ const singleFileCoverageForTrace = getSingleFileCoverageForTrace(
+ contractData,
+ traceInfo.coveredPcs,
+ pcToSourceRange,
+ fileIndex,
+ );
+ collector.add(singleFileCoverageForTrace);
+ }
+ });
+ }
+ }
+ // TODO: Submit a PR to DT
+ return (collector as any).getFinalCoverage();
+ }
+}
diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts
new file mode 100644
index 000000000..ef425ee81
--- /dev/null
+++ b/packages/sol-cov/src/coverage_subprovider.ts
@@ -0,0 +1,124 @@
+import { Callback, NextCallback, Subprovider } from '@0xproject/subproviders';
+import { promisify } from '@0xproject/utils';
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+
+import { constants } from './constants';
+import { CoverageManager } from './coverage_manager';
+
+/*
+ * This class implements the web3-provider-engine subprovider interface and collects traces of all transactions that were sent and all calls that were executed.
+ * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
+ */
+export class CoverageSubprovider extends Subprovider {
+ private _coverageManager: CoverageManager;
+ constructor(artifactsPath: string, sourcesPath: string, networkId: number) {
+ super();
+ this._coverageManager = new CoverageManager(
+ artifactsPath,
+ sourcesPath,
+ networkId,
+ this._getContractCodeAsync.bind(this),
+ );
+ }
+ public handleRequest(
+ payload: Web3.JSONRPCRequestPayload,
+ next: NextCallback,
+ end: (err: Error | null, result: any) => void,
+ ) {
+ switch (payload.method) {
+ case 'eth_sendTransaction':
+ const txData = payload.params[0];
+ next(this._onTransactionSentAsync.bind(this, txData));
+ return;
+
+ case 'eth_call':
+ const callData = payload.params[0];
+ const blockNumber = payload.params[1];
+ next(this._onCallExecutedAsync.bind(this, callData, blockNumber));
+ return;
+
+ default:
+ next();
+ return;
+ }
+ }
+ public async writeCoverageAsync(): Promise<void> {
+ await this._coverageManager.writeCoverageAsync();
+ }
+ private async _onTransactionSentAsync(
+ txData: Web3.TxData,
+ err: Error | null,
+ txHash?: string,
+ cb?: Callback,
+ ): Promise<void> {
+ if (_.isNull(err)) {
+ await this._recordTxTraceAsync(txData.to || constants.NEW_CONTRACT, txData.data, txHash as string);
+ } else {
+ const payload = {
+ method: 'eth_getBlockByNumber',
+ params: ['latest', true],
+ };
+ const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
+ const transactions = jsonRPCResponsePayload.result.transactions;
+ for (const transaction of transactions) {
+ await this._recordTxTraceAsync(
+ transaction.to || constants.NEW_CONTRACT,
+ transaction.data,
+ transaction.hash,
+ );
+ }
+ }
+ if (!_.isUndefined(cb)) {
+ cb();
+ }
+ }
+ private async _onCallExecutedAsync(
+ callData: Partial<Web3.CallData>,
+ blockNumber: Web3.BlockParam,
+ err: Error | null,
+ callResult: string,
+ cb: Callback,
+ ): Promise<void> {
+ await this._recordCallTraceAsync(callData, blockNumber);
+ cb();
+ }
+ private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> {
+ this._coverageManager.setTxDataByHash(txHash, data || '');
+ const payload = {
+ method: 'debug_traceTransaction',
+ params: [txHash, { disableMemory: true, disableStack: true, disableStorage: true }], // TODO For now testrpc just ignores those parameters https://github.com/trufflesuite/ganache-cli/issues/489
+ };
+ const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
+ const trace: Web3.TransactionTrace = jsonRPCResponsePayload.result;
+ const coveredPcs = _.map(trace.structLogs, log => log.pc);
+ this._coverageManager.appendTraceInfo(address, { coveredPcs, txHash });
+ }
+ private async _recordCallTraceAsync(callData: Partial<Web3.CallData>, blockNumber: Web3.BlockParam): Promise<void> {
+ const snapshotId = Number((await this.emitPayloadAsync({ method: 'evm_snapshot' })).result);
+ const txData = callData;
+ if (_.isUndefined(txData.from)) {
+ txData.from = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; // TODO
+ }
+ const txDataWithFromAddress = txData as Web3.TxData & { from: string };
+ try {
+ const txHash = (await this.emitPayloadAsync({
+ method: 'eth_sendTransaction',
+ params: [txDataWithFromAddress],
+ })).result;
+ await this._onTransactionSentAsync(txDataWithFromAddress, null, txHash);
+ } catch (err) {
+ await this._onTransactionSentAsync(txDataWithFromAddress, err, undefined);
+ }
+ const didRevert = (await this.emitPayloadAsync({ method: 'evm_revert', params: [snapshotId] })).result;
+ }
+ private async _getContractCodeAsync(address: string): Promise<string> {
+ const payload = {
+ method: 'eth_getCode',
+ params: [address, 'latest'],
+ };
+ const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
+ const contractCode: string = jsonRPCResponsePayload.result;
+ return contractCode;
+ }
+}
diff --git a/packages/sol-cov/src/globals.d.ts b/packages/sol-cov/src/globals.d.ts
new file mode 100644
index 000000000..f807730cc
--- /dev/null
+++ b/packages/sol-cov/src/globals.d.ts
@@ -0,0 +1,6 @@
+// tslint:disable:completed-docs
+declare module 'solidity-parser-sc' {
+ // This is too time-consuming to define and we don't rely on it anyway
+ export type AST = any;
+ export function parse(sourceCode: string): AST;
+}
diff --git a/packages/sol-cov/src/index.ts b/packages/sol-cov/src/index.ts
new file mode 100644
index 000000000..e5c5e5be3
--- /dev/null
+++ b/packages/sol-cov/src/index.ts
@@ -0,0 +1 @@
+export { CoverageSubprovider } from './coverage_subprovider';
diff --git a/packages/sol-cov/src/instructions.ts b/packages/sol-cov/src/instructions.ts
new file mode 100644
index 000000000..c6506e58d
--- /dev/null
+++ b/packages/sol-cov/src/instructions.ts
@@ -0,0 +1,24 @@
+// tslint:disable:number-literal-format
+const PUSH1 = 0x60;
+const PUSH32 = 0x7f;
+const isPush = (inst: number) => inst >= PUSH1 && inst <= PUSH32;
+
+const pushDataLength = (inst: number) => inst - PUSH1 + 1;
+
+const instructionLength = (inst: number) => (isPush(inst) ? pushDataLength(inst) + 1 : 1);
+
+export const getPcToInstructionIndexMapping = (bytecode: Uint8Array) => {
+ const result: {
+ [programCounter: number]: number;
+ } = {};
+ let byteIndex = 0;
+ let instructionIndex = 0;
+ while (byteIndex < bytecode.length) {
+ const instruction = bytecode[byteIndex];
+ const length = instructionLength(instruction);
+ result[byteIndex] = instructionIndex;
+ byteIndex += length;
+ instructionIndex += 1;
+ }
+ return result;
+};
diff --git a/packages/sol-cov/src/instrument_solidity.ts b/packages/sol-cov/src/instrument_solidity.ts
new file mode 100644
index 000000000..ac58270cb
--- /dev/null
+++ b/packages/sol-cov/src/instrument_solidity.ts
@@ -0,0 +1,16 @@
+import * as fs from 'fs';
+import * as _ from 'lodash';
+import * as path from 'path';
+import * as SolidityParser from 'solidity-parser-sc';
+
+import { ASTVisitor, CoverageEntriesDescription } from './ast_visitor';
+import { getLocationByOffset } from './source_maps';
+
+export const collectCoverageEntries = (contractSource: string, fileName: string) => {
+ const ast = SolidityParser.parse(contractSource);
+ const locationByOffset = getLocationByOffset(contractSource);
+ const astVisitor = new ASTVisitor(locationByOffset);
+ astVisitor.walkAST(ast);
+ const coverageEntries = astVisitor.getCollectedCoverageEntries();
+ return coverageEntries;
+};
diff --git a/packages/sol-cov/src/source_maps.ts b/packages/sol-cov/src/source_maps.ts
new file mode 100644
index 000000000..795b15a9b
--- /dev/null
+++ b/packages/sol-cov/src/source_maps.ts
@@ -0,0 +1,77 @@
+import * as _ from 'lodash';
+
+import { getPcToInstructionIndexMapping } from './instructions';
+import { LineColumn, LocationByOffset, SourceRange } from './types';
+
+const RADIX = 10;
+
+export interface SourceLocation {
+ offset: number;
+ length: number;
+ fileIndex: number;
+}
+
+export const getLocationByOffset = (str: string) => {
+ const locationByOffset: LocationByOffset = {};
+ let currentOffset = 0;
+ for (const char of str.split('')) {
+ const location = locationByOffset[currentOffset - 1] || { line: 1, column: 0 };
+ const isNewline = char === '\n';
+ locationByOffset[currentOffset] = {
+ line: location.line + (isNewline ? 1 : 0),
+ column: isNewline ? 0 : location.column + 1,
+ };
+ currentOffset++;
+ }
+ return locationByOffset;
+};
+
+// Parses a sourcemap string
+// The solidity sourcemap format is documented here: https://github.com/ethereum/solidity/blob/develop/docs/miscellaneous.rst#source-mappings
+export const parseSourceMap = (sourceCodes: string[], srcMap: string, bytecodeHex: string, sources: string[]) => {
+ const bytecode = Uint8Array.from(Buffer.from(bytecodeHex, 'hex'));
+ const pcToInstructionIndex: { [programCounter: number]: number } = getPcToInstructionIndexMapping(bytecode);
+ const locationByOffsetByFileIndex = _.map(sourceCodes, getLocationByOffset);
+ const entries = srcMap.split(';');
+ const parsedEntries: SourceLocation[] = [];
+ let lastParsedEntry: SourceLocation = {} as any;
+ const instructionIndexToSourceRange: { [instructionIndex: number]: SourceRange } = {};
+ _.each(entries, (entry: string, i: number) => {
+ const [instructionIndexStrIfExists, lengthStrIfExists, fileIndexStrIfExists, jumpTypeStrIfExists] = entry.split(
+ ':',
+ );
+ const instructionIndexIfExists = parseInt(instructionIndexStrIfExists, RADIX);
+ const lengthIfExists = parseInt(lengthStrIfExists, RADIX);
+ const fileIndexIfExists = parseInt(fileIndexStrIfExists, RADIX);
+ const offset = _.isNaN(instructionIndexIfExists) ? lastParsedEntry.offset : instructionIndexIfExists;
+ const length = _.isNaN(lengthIfExists) ? lastParsedEntry.length : lengthIfExists;
+ const fileIndex = _.isNaN(fileIndexIfExists) ? lastParsedEntry.fileIndex : fileIndexIfExists;
+ const parsedEntry = {
+ offset,
+ length,
+ fileIndex,
+ };
+ if (parsedEntry.fileIndex !== -1) {
+ const sourceRange = {
+ location: {
+ start: locationByOffsetByFileIndex[parsedEntry.fileIndex][parsedEntry.offset - 1],
+ end:
+ locationByOffsetByFileIndex[parsedEntry.fileIndex][parsedEntry.offset + parsedEntry.length - 1],
+ },
+ fileName: sources[parsedEntry.fileIndex],
+ };
+ instructionIndexToSourceRange[i] = sourceRange;
+ } else {
+ // Some assembly code generated by Solidity can't be mapped back to a line of source code.
+ // Source: https://github.com/ethereum/solidity/issues/3629
+ }
+ lastParsedEntry = parsedEntry;
+ });
+ const pcsToSourceRange: { [programCounter: number]: SourceRange } = {};
+ for (const programCounterKey of _.keys(pcToInstructionIndex)) {
+ const pc = parseInt(programCounterKey, RADIX);
+ const instructionIndex: number = pcToInstructionIndex[pc];
+ pcsToSourceRange[pc] = instructionIndexToSourceRange[instructionIndex];
+ }
+ return pcsToSourceRange;
+};
diff --git a/packages/sol-cov/src/types.ts b/packages/sol-cov/src/types.ts
new file mode 100644
index 000000000..5d07cd01b
--- /dev/null
+++ b/packages/sol-cov/src/types.ts
@@ -0,0 +1,89 @@
+export interface LineColumn {
+ line: number;
+ column: number;
+}
+
+export interface SourceRange {
+ location: SingleFileSourceRange;
+ fileName: string;
+}
+
+export interface SingleFileSourceRange {
+ start: LineColumn;
+ end: LineColumn;
+}
+
+export interface LocationByOffset {
+ [offset: number]: LineColumn;
+}
+
+export interface FunctionDescription {
+ name: string;
+ line: number;
+ loc: SingleFileSourceRange;
+ skip?: boolean;
+}
+
+export type StatementDescription = SingleFileSourceRange;
+
+export interface BranchDescription {
+ line: number;
+ type: 'if' | 'switch' | 'cond-expr' | 'binary-expr';
+ locations: SingleFileSourceRange[];
+}
+
+export interface FnMap {
+ [functionId: string]: FunctionDescription;
+}
+
+export interface BranchMap {
+ [branchId: string]: BranchDescription;
+}
+
+export interface StatementMap {
+ [statementId: string]: StatementDescription;
+}
+
+export interface LineCoverage {
+ [lineNo: number]: boolean;
+}
+
+export interface FunctionCoverage {
+ [functionId: string]: boolean;
+}
+
+export interface StatementCoverage {
+ [statementId: string]: boolean;
+}
+
+export interface BranchCoverage {
+ [branchId: string]: boolean[];
+}
+
+export interface Coverage {
+ [fineName: string]: {
+ l: LineCoverage;
+ f: FunctionCoverage;
+ s: StatementCoverage;
+ b: BranchCoverage;
+ fnMap: FnMap;
+ branchMap: BranchMap;
+ statementMap: StatementMap;
+ path: string;
+ };
+}
+
+export interface ContractData {
+ bytecode: string;
+ sourceMap: string;
+ runtimeBytecode: string;
+ sourceMapRuntime: string;
+ sourceCodes: string[];
+ baseName: string;
+ sources: string[];
+}
+
+export interface TraceInfo {
+ coveredPcs: number[];
+ txHash: string;
+}
diff --git a/packages/sol-cov/src/utils.ts b/packages/sol-cov/src/utils.ts
new file mode 100644
index 000000000..f155043a1
--- /dev/null
+++ b/packages/sol-cov/src/utils.ts
@@ -0,0 +1,13 @@
+import { LineColumn, SingleFileSourceRange } from './types';
+
+export const utils = {
+ compareLineColumn(lhs: LineColumn, rhs: LineColumn): number {
+ return lhs.line !== rhs.line ? lhs.line - rhs.line : lhs.column - rhs.column;
+ },
+ isRangeInside(childRange: SingleFileSourceRange, parentRange: SingleFileSourceRange): boolean {
+ return (
+ utils.compareLineColumn(parentRange.start, childRange.start) <= 0 &&
+ utils.compareLineColumn(childRange.end, parentRange.end) <= 0
+ );
+ },
+};