From 13299158d1e22d1af1cd36434fc403a74743ecb1 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 4 Mar 2018 19:05:26 -0800 Subject: Add sol-cover implementation --- packages/sol-cov/src/ast_visitor.ts | 115 ++++++++++++++++++ packages/sol-cov/src/collect_contract_data.ts | 40 +++++++ packages/sol-cov/src/constants.ts | 3 + packages/sol-cov/src/coverage_manager.ts | 166 ++++++++++++++++++++++++++ packages/sol-cov/src/coverage_subprovider.ts | 124 +++++++++++++++++++ packages/sol-cov/src/globals.d.ts | 6 + packages/sol-cov/src/index.ts | 1 + packages/sol-cov/src/instructions.ts | 24 ++++ packages/sol-cov/src/instrument_solidity.ts | 16 +++ packages/sol-cov/src/source_maps.ts | 77 ++++++++++++ packages/sol-cov/src/types.ts | 89 ++++++++++++++ packages/sol-cov/src/utils.ts | 13 ++ 12 files changed, 674 insertions(+) create mode 100644 packages/sol-cov/src/ast_visitor.ts create mode 100644 packages/sol-cov/src/collect_contract_data.ts create mode 100644 packages/sol-cov/src/constants.ts create mode 100644 packages/sol-cov/src/coverage_manager.ts create mode 100644 packages/sol-cov/src/coverage_subprovider.ts create mode 100644 packages/sol-cov/src/globals.d.ts create mode 100644 packages/sol-cov/src/index.ts create mode 100644 packages/sol-cov/src/instructions.ts create mode 100644 packages/sol-cov/src/instrument_solidity.ts create mode 100644 packages/sol-cov/src/source_maps.ts create mode 100644 packages/sol-cov/src/types.ts create mode 100644 packages/sol-cov/src/utils.ts (limited to 'packages/sol-cov/src') 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 = _.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; + constructor( + artifactsPath: string, + sourcesPath: string, + networkId: number, + getContractCodeAsync: (address: string) => Promise, + ) { + 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 { + 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 { + 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 { + await this._coverageManager.writeCoverageAsync(); + } + private async _onTransactionSentAsync( + txData: Web3.TxData, + err: Error | null, + txHash?: string, + cb?: Callback, + ): Promise { + 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, + blockNumber: Web3.BlockParam, + err: Error | null, + callResult: string, + cb: Callback, + ): Promise { + await this._recordCallTraceAsync(callData, blockNumber); + cb(); + } + private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise { + 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, blockNumber: Web3.BlockParam): Promise { + 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 { + 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 + ); + }, +}; -- cgit v1.2.3 From 62f45f7b41fdd984025ba70008c682eeded2fadb Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 9 Mar 2018 14:36:19 +0100 Subject: Cache code parsing --- packages/sol-cov/src/instrument_solidity.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/instrument_solidity.ts b/packages/sol-cov/src/instrument_solidity.ts index ac58270cb..163e3c7b9 100644 --- a/packages/sol-cov/src/instrument_solidity.ts +++ b/packages/sol-cov/src/instrument_solidity.ts @@ -1,3 +1,4 @@ +import * as ethUtil from 'ethereumjs-util'; import * as fs from 'fs'; import * as _ from 'lodash'; import * as path from 'path'; @@ -6,11 +7,19 @@ import * as SolidityParser from 'solidity-parser-sc'; import { ASTVisitor, CoverageEntriesDescription } from './ast_visitor'; import { getLocationByOffset } from './source_maps'; +// Parsing source code for each transaction/code is slow and therefore we cache it +const coverageEntriesBySourceHash: { [sourceHash: string]: CoverageEntriesDescription } = {}; + 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; + const time = Date.now(); + const sourceHash = ethUtil.sha3(contractSource).toString('hex'); + if (_.isUndefined(coverageEntriesBySourceHash[sourceHash])) { + const ast = SolidityParser.parse(contractSource); + const locationByOffset = getLocationByOffset(contractSource); + const astVisitor = new ASTVisitor(locationByOffset); + astVisitor.walkAST(ast); + coverageEntriesBySourceHash[sourceHash] = astVisitor.getCollectedCoverageEntries(); + } + const coverageEntriesDescription = coverageEntriesBySourceHash[sourceHash]; + return coverageEntriesDescription; }; -- cgit v1.2.3 From 98f32d6f1ff3c94544cc3ad8bdf1df02daca3d74 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 9 Mar 2018 15:11:30 +0100 Subject: Stop making an assumption that contract code is immutable --- packages/sol-cov/src/coverage_manager.ts | 87 +++++++++++++--------------- packages/sol-cov/src/coverage_subprovider.ts | 24 +++++++- packages/sol-cov/src/types.ts | 14 ++++- 3 files changed, 73 insertions(+), 52 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index aced9208f..5d11800f9 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -23,6 +23,8 @@ import { StatementDescription, StatementMap, TraceInfo, + TraceInfoExistingContract, + TraceInfoNewContract, } from './types'; import { utils } from './utils'; @@ -72,9 +74,8 @@ function getSingleFileCoverageForTrace( } export class CoverageManager { - private _traceInfoByAddress: { [address: string]: TraceInfo[] } = {}; + private _traceInfos: TraceInfo[] = []; private _contractsData: ContractData[] = []; - private _txDataByHash: { [txHash: string]: string } = {}; private _getContractCodeAsync: (address: string) => Promise; constructor( artifactsPath: string, @@ -85,14 +86,8 @@ export class CoverageManager { 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 appendTraceInfo(traceInfo: TraceInfo): void { + this._traceInfos.push(traceInfo); } public async writeCoverageAsync(): Promise { const finalCoverage = await this._computeCoverageAsync(); @@ -103,13 +98,13 @@ export class CoverageManager { } private async _computeCoverageAsync(): Promise { const collector = new Collector(); - for (const address of _.keys(this._traceInfoByAddress)) { - if (address !== constants.NEW_CONTRACT) { + for (const traceInfo of this._traceInfos) { + if (traceInfo.address !== constants.NEW_CONTRACT) { // Runtime transaction - const runtimeBytecode = await this._getContractCodeAsync(address); + const runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode; const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData; if (_.isUndefined(contractData)) { - throw new Error(`Transaction to an unknown address: ${address}`); + throw new Error(`Transaction to an unknown address: ${traceInfo.address}`); } const bytecodeHex = contractData.runtimeBytecode.slice(2); const sourceMap = contractData.sourceMapRuntime; @@ -120,44 +115,40 @@ export class CoverageManager { 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); - }); + 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, + const bytecode = (traceInfo as TraceInfoNewContract).bytecode; + 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, ); - for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = getSingleFileCoverageForTrace( - contractData, - traceInfo.coveredPcs, - pcToSourceRange, - fileIndex, - ); - collector.add(singleFileCoverageForTrace); - } - }); + collector.add(singleFileCoverageForTrace); + } } } // TODO: Submit a PR to DT diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index ef425ee81..d3783abb2 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -5,6 +5,7 @@ import * as Web3 from 'web3'; import { constants } from './constants'; import { CoverageManager } from './coverage_manager'; +import { TraceInfoExistingContract, TraceInfoNewContract } from './types'; /* * This class implements the web3-provider-engine subprovider interface and collects traces of all transactions that were sent and all calls that were executed. @@ -84,15 +85,32 @@ export class CoverageSubprovider extends Subprovider { cb(); } private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise { - this._coverageManager.setTxDataByHash(txHash, data || ''); - const payload = { + let 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 }); + if (address === constants.NEW_CONTRACT) { + const traceInfo: TraceInfoNewContract = { + coveredPcs, + txHash, + address, + bytecode: data as string, + }; + this._coverageManager.appendTraceInfo(traceInfo); + } else { + payload = { method: 'eth_getCode', params: [address, 'latest'] }; + const runtimeBytecode = (await this.emitPayloadAsync(payload)).result; + const traceInfo: TraceInfoExistingContract = { + coveredPcs, + txHash, + address, + runtimeBytecode, + }; + this._coverageManager.appendTraceInfo(traceInfo); + } } private async _recordCallTraceAsync(callData: Partial, blockNumber: Web3.BlockParam): Promise { const snapshotId = Number((await this.emitPayloadAsync({ method: 'evm_snapshot' })).result); diff --git a/packages/sol-cov/src/types.ts b/packages/sol-cov/src/types.ts index 5d07cd01b..d6491100b 100644 --- a/packages/sol-cov/src/types.ts +++ b/packages/sol-cov/src/types.ts @@ -83,7 +83,19 @@ export interface ContractData { sources: string[]; } -export interface TraceInfo { +export interface TraceInfoBase { coveredPcs: number[]; txHash: string; } + +export interface TraceInfoNewContract extends TraceInfoBase { + address: 'NEW_CONTRACT'; + bytecode: string; +} + +export interface TraceInfoExistingContract extends TraceInfoBase { + address: string; + runtimeBytecode: string; +} + +export type TraceInfo = TraceInfoNewContract | TraceInfoExistingContract; -- cgit v1.2.3 From 21aac75533620545b9982cd73a2f0753930db021 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 9 Mar 2018 15:58:25 +0100 Subject: Use string enum for branch types --- packages/sol-cov/src/ast_visitor.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts index f450dadbb..588d179d4 100644 --- a/packages/sol-cov/src/ast_visitor.ts +++ b/packages/sol-cov/src/ast_visitor.ts @@ -9,6 +9,12 @@ export interface CoverageEntriesDescription { statementMap: StatementMap; } +enum BranchType { + If = 'if', + ConditionalExpression = 'cond-expr', + BinaryExpression = 'binary-expr', +} + export class ASTVisitor { private _entryId = 0; private _fnMap: FnMap = {}; @@ -47,7 +53,7 @@ export class ASTVisitor { return coverageEntriesDescription; } private _visitConditionalExpression(ast: SolidityParser.AST): void { - this._visitBinaryBranch(ast, ast.consequent, ast.alternate, 'cond-expr'); + this._visitBinaryBranch(ast, ast.consequent, ast.alternate, BranchType.ConditionalExpression); } private _visitFunctionDeclaration(ast: SolidityParser.AST): void { const loc = this._getExpressionRange(ast); @@ -58,11 +64,11 @@ export class ASTVisitor { }; } private _visitBinaryExpression(ast: SolidityParser.AST): void { - this._visitBinaryBranch(ast, ast.left, ast.right, 'binary-expr'); + this._visitBinaryBranch(ast, ast.left, ast.right, BranchType.BinaryExpression); } private _visitIfStatement(ast: SolidityParser.AST): void { this._visitStatement(ast); - this._visitBinaryBranch(ast, ast.consequent, ast.alternate || ast, 'if'); + this._visitBinaryBranch(ast, ast.consequent, ast.alternate || ast, BranchType.If); } private _visitBreakStatement(ast: SolidityParser.AST): void { this._visitStatement(ast); @@ -92,7 +98,7 @@ export class ASTVisitor { ast: SolidityParser.AST, left: SolidityParser.AST, right: SolidityParser.AST, - type: 'if' | 'cond-expr' | 'binary-expr', + type: BranchType, ): void { this._branchMap[this._entryId++] = { line: this._getExpressionRange(ast).start.line, -- cgit v1.2.3 From 6b7f48644c79c59a7137cd84f9e753910e506df9 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 9 Mar 2018 15:59:46 +0100 Subject: Fix a typo --- packages/sol-cov/src/collect_contract_data.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/collect_contract_data.ts b/packages/sol-cov/src/collect_contract_data.ts index dffccce2f..d97a7937f 100644 --- a/packages/sol-cov/src/collect_contract_data.ts +++ b/packages/sol-cov/src/collect_contract_data.ts @@ -12,7 +12,8 @@ export const collectContractsData = (artifactsPath: string, sourcesPath: string, 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 + // 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 useful with it return {}; } const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString()); -- cgit v1.2.3 From f590aa11f79d3dfb8585b0b42103db74dcb6ee13 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 9 Mar 2018 16:01:31 +0100 Subject: Throw an error if artifacts not found --- packages/sol-cov/src/collect_contract_data.ts | 3 +++ 1 file changed, 3 insertions(+) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/collect_contract_data.ts b/packages/sol-cov/src/collect_contract_data.ts index d97a7937f..a0ce2640f 100644 --- a/packages/sol-cov/src/collect_contract_data.ts +++ b/packages/sol-cov/src/collect_contract_data.ts @@ -25,6 +25,9 @@ export const collectContractsData = (artifactsPath: string, sourcesPath: string, const includedSourceCode = fs.readFileSync(source).toString(); return includedSourceCode; }); + if (_.isUndefined(artifact.networks[networkId])) { + throw new Error(`No ${baseName} artifacts found for networkId ${networkId}`); + } const contractData = { baseName, sourceCodes, -- cgit v1.2.3 From ee31d5e24d92fc42c003bd49c98e674498a0dfed Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 9 Mar 2018 16:04:37 +0100 Subject: Introduce redundant vars --- packages/sol-cov/src/coverage_manager.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index 5d11800f9..c1ca7142e 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -41,7 +41,8 @@ function getSingleFileCoverageForTrace( 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 branchIds = _.keys(coverageEntriesDescription.branchMap); + for (const branchId of branchIds) { const branchDescription = coverageEntriesDescription.branchMap[branchId]; const isCovered = _.map(branchDescription.locations, location => _.some(sourceRanges, range => utils.isRangeInside(range.location, location)), @@ -49,13 +50,15 @@ function getSingleFileCoverageForTrace( branchCoverage[branchId] = isCovered; } const statementCoverage: StatementCoverage = {}; - for (const statementId of _.keys(coverageEntriesDescription.statementMap)) { + const statementIds = _.keys(coverageEntriesDescription.statementMap); + for (const statementId of statementIds) { 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 functionIds = _.keys(coverageEntriesDescription.fnMap); + for (const fnId of functionIds) { const functionDescription = coverageEntriesDescription.fnMap[fnId]; const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, functionDescription.loc)); functionCoverage[fnId] = isCovered; -- cgit v1.2.3 From 633039c52893579ad929fbcbca084ea5726ce20e Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 9 Mar 2018 16:07:11 +0100 Subject: Use better variable name --- packages/sol-cov/src/coverage_manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index c1ca7142e..1e4df9d29 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -44,10 +44,10 @@ function getSingleFileCoverageForTrace( const branchIds = _.keys(coverageEntriesDescription.branchMap); for (const branchId of branchIds) { const branchDescription = coverageEntriesDescription.branchMap[branchId]; - const isCovered = _.map(branchDescription.locations, location => + const isCoveredByBranchIndex = _.map(branchDescription.locations, location => _.some(sourceRanges, range => utils.isRangeInside(range.location, location)), ); - branchCoverage[branchId] = isCovered; + branchCoverage[branchId] = isCoveredByBranchIndex; } const statementCoverage: StatementCoverage = {}; const statementIds = _.keys(coverageEntriesDescription.statementMap); -- cgit v1.2.3 From 2298a34c37b373e265d3b4b4665d195cfa1c67fd Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 9 Mar 2018 16:09:03 +0100 Subject: Make _getSingleFileCoverageForTrace a private static method --- packages/sol-cov/src/coverage_manager.ts | 101 ++++++++++++++++--------------- 1 file changed, 51 insertions(+), 50 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index 1e4df9d29..870dc2bdb 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -28,58 +28,59 @@ import { } 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 = {}; - const branchIds = _.keys(coverageEntriesDescription.branchMap); - for (const branchId of branchIds) { - const branchDescription = coverageEntriesDescription.branchMap[branchId]; - const isCoveredByBranchIndex = _.map(branchDescription.locations, location => - _.some(sourceRanges, range => utils.isRangeInside(range.location, location)), - ); - branchCoverage[branchId] = isCoveredByBranchIndex; - } - const statementCoverage: StatementCoverage = {}; - const statementIds = _.keys(coverageEntriesDescription.statementMap); - for (const statementId of statementIds) { - const statementDescription = coverageEntriesDescription.statementMap[statementId]; - const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, statementDescription)); - statementCoverage[statementId] = isCovered; - } - const functionCoverage: FunctionCoverage = {}; - const functionIds = _.keys(coverageEntriesDescription.fnMap); - for (const fnId of functionIds) { - 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 _traceInfos: TraceInfo[] = []; private _contractsData: ContractData[] = []; private _getContractCodeAsync: (address: string) => Promise; + private static _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 = {}; + const branchIds = _.keys(coverageEntriesDescription.branchMap); + for (const branchId of branchIds) { + const branchDescription = coverageEntriesDescription.branchMap[branchId]; + const isCoveredByBranchIndex = _.map(branchDescription.locations, location => + _.some(sourceRanges, range => utils.isRangeInside(range.location, location)), + ); + branchCoverage[branchId] = isCoveredByBranchIndex; + } + const statementCoverage: StatementCoverage = {}; + const statementIds = _.keys(coverageEntriesDescription.statementMap); + for (const statementId of statementIds) { + const statementDescription = coverageEntriesDescription.statementMap[statementId]; + const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, statementDescription)); + statementCoverage[statementId] = isCovered; + } + const functionCoverage: FunctionCoverage = {}; + const functionIds = _.keys(coverageEntriesDescription.fnMap); + for (const fnId of functionIds) { + 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; + } constructor( artifactsPath: string, sourcesPath: string, @@ -118,7 +119,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, @@ -144,7 +145,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, -- cgit v1.2.3 From 3b158cb72649c19a6b569821d23672ea7454b45b Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 9 Mar 2018 16:44:44 +0100 Subject: Address feedback --- packages/sol-cov/src/coverage_subprovider.ts | 16 +++++++++++----- packages/sol-cov/src/source_maps.ts | 13 +++++++++---- 2 files changed, 20 insertions(+), 9 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index d3783abb2..f91b95c79 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -13,8 +13,10 @@ import { TraceInfoExistingContract, TraceInfoNewContract } from './types'; */ export class CoverageSubprovider extends Subprovider { private _coverageManager: CoverageManager; - constructor(artifactsPath: string, sourcesPath: string, networkId: number) { + private _defaultFromAddress: string; + constructor(artifactsPath: string, sourcesPath: string, networkId: number, defaultFromAddress: string) { super(); + this._defaultFromAddress = defaultFromAddress; this._coverageManager = new CoverageManager( artifactsPath, sourcesPath, @@ -96,7 +98,7 @@ export class CoverageSubprovider extends Subprovider { const traceInfo: TraceInfoNewContract = { coveredPcs, txHash, - address, + address: address as 'NEW_CONTRACT', bytecode: data as string, }; this._coverageManager.appendTraceInfo(traceInfo); @@ -116,9 +118,9 @@ export class CoverageSubprovider extends Subprovider { const snapshotId = Number((await this.emitPayloadAsync({ method: 'evm_snapshot' })).result); const txData = callData; if (_.isUndefined(txData.from)) { - txData.from = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; // TODO + txData.from = this._defaultFromAddress; } - const txDataWithFromAddress = txData as Web3.TxData & { from: string }; + const txDataWithFromAddress = txData as Web3.TxData; try { const txHash = (await this.emitPayloadAsync({ method: 'eth_sendTransaction', @@ -128,7 +130,11 @@ export class CoverageSubprovider extends Subprovider { } catch (err) { await this._onTransactionSentAsync(txDataWithFromAddress, err, undefined); } - const didRevert = (await this.emitPayloadAsync({ method: 'evm_revert', params: [snapshotId] })).result; + const jsonRPCResponse = await this.emitPayloadAsync({ method: 'evm_revert', params: [snapshotId] }); + const didRevert = jsonRPCResponse.result; + if (!didRevert) { + throw new Error('Failed to revert the snapshot'); + } } private async _getContractCodeAsync(address: string): Promise { const payload = { diff --git a/packages/sol-cov/src/source_maps.ts b/packages/sol-cov/src/source_maps.ts index 795b15a9b..9b3ea9e24 100644 --- a/packages/sol-cov/src/source_maps.ts +++ b/packages/sol-cov/src/source_maps.ts @@ -11,7 +11,7 @@ export interface SourceLocation { fileIndex: number; } -export const getLocationByOffset = (str: string) => { +export function getLocationByOffset(str: string): LocationByOffset { const locationByOffset: LocationByOffset = {}; let currentOffset = 0; for (const char of str.split('')) { @@ -24,11 +24,16 @@ export const getLocationByOffset = (str: string) => { 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[]) => { +export function parseSourceMap( + sourceCodes: string[], + srcMap: string, + bytecodeHex: string, + sources: string[], +): { [programCounter: number]: SourceRange } { const bytecode = Uint8Array.from(Buffer.from(bytecodeHex, 'hex')); const pcToInstructionIndex: { [programCounter: number]: number } = getPcToInstructionIndexMapping(bytecode); const locationByOffsetByFileIndex = _.map(sourceCodes, getLocationByOffset); @@ -74,4 +79,4 @@ export const parseSourceMap = (sourceCodes: string[], srcMap: string, bytecodeHe pcsToSourceRange[pc] = instructionIndexToSourceRange[instructionIndex]; } return pcsToSourceRange; -}; +} -- cgit v1.2.3 From 10f6a17857d183bea6b2825cd76ed2eb26d453ed Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sat, 10 Mar 2018 04:26:59 +0100 Subject: Add builtin modifier 'constant' --- packages/sol-cov/src/ast_visitor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts index 588d179d4..9193f6a18 100644 --- a/packages/sol-cov/src/ast_visitor.ts +++ b/packages/sol-cov/src/ast_visitor.ts @@ -89,7 +89,7 @@ export class ASTVisitor { this._visitStatement(ast); } private _visitModifierArgument(ast: SolidityParser.AST): void { - const BUILTIN_MODIFIERS = ['public', 'view', 'payable', 'external', 'internal', 'pure']; + const BUILTIN_MODIFIERS = ['public', 'view', 'payable', 'external', 'internal', 'pure', 'constant']; if (!_.includes(BUILTIN_MODIFIERS, ast.name)) { this._visitStatement(ast); } -- cgit v1.2.3 From bd7517cfd489a9789f81c247fb45329881274d15 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sat, 10 Mar 2018 06:07:55 +0100 Subject: Add support for async calls under coverage --- packages/sol-cov/src/coverage_subprovider.ts | 52 ++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 15 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index f91b95c79..c84211d3a 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -1,21 +1,32 @@ import { Callback, NextCallback, Subprovider } from '@0xproject/subproviders'; import { promisify } from '@0xproject/utils'; import * as _ from 'lodash'; +import { Lock } from 'semaphore-async-await'; import * as Web3 from 'web3'; import { constants } from './constants'; import { CoverageManager } from './coverage_manager'; import { TraceInfoExistingContract, TraceInfoNewContract } from './types'; +interface MaybeFakeTxData extends Web3.TxData { + isFakeTransaction?: boolean; +} + /* * This class implements the web3-provider-engine subprovider interface and collects traces of all transactions that were sent and all calls that were executed. + * Because there is no notion of call trace in the rpc - we collect them in rather non-obvious/hacky way. + * On each call - we create a snapshot, execute the call as a transaction, get the trace, revert the snapshot. + * That allows us to not influence the test behaviour. * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js */ export class CoverageSubprovider extends Subprovider { + // Lock is used to not accept normal transactions while doing call/snapshot magic because they'll be reverted later otherwise + private _lock: Lock; private _coverageManager: CoverageManager; private _defaultFromAddress: string; constructor(artifactsPath: string, sourcesPath: string, networkId: number, defaultFromAddress: string) { super(); + this._lock = new Lock(); this._defaultFromAddress = defaultFromAddress; this._coverageManager = new CoverageManager( artifactsPath, @@ -50,11 +61,16 @@ export class CoverageSubprovider extends Subprovider { await this._coverageManager.writeCoverageAsync(); } private async _onTransactionSentAsync( - txData: Web3.TxData, + txData: MaybeFakeTxData, err: Error | null, - txHash?: string, - cb?: Callback, + txHash: string | undefined, + cb: Callback, ): Promise { + if (!txData.isFakeTransaction) { + // This transaction is a usual ttransaction. Not a call executed as one. + // And we don't want it to be executed within a snapshotting period + await this._lock.acquire(); + } if (_.isNull(err)) { await this._recordTxTraceAsync(txData.to || constants.NEW_CONTRACT, txData.data, txHash as string); } else { @@ -72,9 +88,12 @@ export class CoverageSubprovider extends Subprovider { ); } } - if (!_.isUndefined(cb)) { - cb(); + if (!txData.isFakeTransaction) { + // This transaction is a usual ttransaction. Not a call executed as one. + // And we don't want it to be executed within a snapshotting period + await this._lock.release(); } + cb(); } private async _onCallExecutedAsync( callData: Partial, @@ -115,22 +134,25 @@ export class CoverageSubprovider extends Subprovider { } } private async _recordCallTraceAsync(callData: Partial, blockNumber: Web3.BlockParam): Promise { + // We don't want other transactions to be exeucted during snashotting period, that's why we lock the + // transaction execution for all transactions except our fake ones. + await this._lock.acquire(); const snapshotId = Number((await this.emitPayloadAsync({ method: 'evm_snapshot' })).result); - const txData = callData; - if (_.isUndefined(txData.from)) { - txData.from = this._defaultFromAddress; - } - const txDataWithFromAddress = txData as Web3.TxData; + const fakeTxData: MaybeFakeTxData = { + from: this._defaultFromAddress, + isFakeTransaction: true, // This transaction (and only it) is allowed to come through when the lock is locked + ...callData, + }; try { - const txHash = (await this.emitPayloadAsync({ + await this.emitPayloadAsync({ method: 'eth_sendTransaction', - params: [txDataWithFromAddress], - })).result; - await this._onTransactionSentAsync(txDataWithFromAddress, null, txHash); + params: [fakeTxData], + }); } catch (err) { - await this._onTransactionSentAsync(txDataWithFromAddress, err, undefined); + // Even if this transaction failed - we've already recorded it's trace. } const jsonRPCResponse = await this.emitPayloadAsync({ method: 'evm_revert', params: [snapshotId] }); + await this._lock.release(); const didRevert = jsonRPCResponse.result; if (!didRevert) { throw new Error('Failed to revert the snapshot'); -- cgit v1.2.3 From 22f78a2c52d0aa573940cec59b02d1976c172af5 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sat, 10 Mar 2018 06:16:06 +0100 Subject: Don't await lock releases --- packages/sol-cov/src/coverage_subprovider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index c84211d3a..e391b0f5c 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -91,7 +91,7 @@ export class CoverageSubprovider extends Subprovider { if (!txData.isFakeTransaction) { // This transaction is a usual ttransaction. Not a call executed as one. // And we don't want it to be executed within a snapshotting period - await this._lock.release(); + this._lock.release(); } cb(); } @@ -152,7 +152,7 @@ export class CoverageSubprovider extends Subprovider { // Even if this transaction failed - we've already recorded it's trace. } const jsonRPCResponse = await this.emitPayloadAsync({ method: 'evm_revert', params: [snapshotId] }); - await this._lock.release(); + this._lock.release(); const didRevert = jsonRPCResponse.result; if (!didRevert) { throw new Error('Failed to revert the snapshot'); -- cgit v1.2.3 From 870995933a60b67af03b3f42a2aed169d33bd87f Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 12 Mar 2018 03:35:11 +0100 Subject: Remove redundant Date.now() --- packages/sol-cov/src/collect_coverage_entries.ts | 24 +++++++++++++++++++++++ packages/sol-cov/src/coverage_manager.ts | 2 +- packages/sol-cov/src/instrument_solidity.ts | 25 ------------------------ 3 files changed, 25 insertions(+), 26 deletions(-) create mode 100644 packages/sol-cov/src/collect_coverage_entries.ts delete mode 100644 packages/sol-cov/src/instrument_solidity.ts (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/collect_coverage_entries.ts b/packages/sol-cov/src/collect_coverage_entries.ts new file mode 100644 index 000000000..d29fa2c37 --- /dev/null +++ b/packages/sol-cov/src/collect_coverage_entries.ts @@ -0,0 +1,24 @@ +import * as ethUtil from 'ethereumjs-util'; +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'; + +// Parsing source code for each transaction/code is slow and therefore we cache it +const coverageEntriesBySourceHash: { [sourceHash: string]: CoverageEntriesDescription } = {}; + +export const collectCoverageEntries = (contractSource: string, fileName: string) => { + const sourceHash = ethUtil.sha3(contractSource).toString('hex'); + if (_.isUndefined(coverageEntriesBySourceHash[sourceHash])) { + const ast = SolidityParser.parse(contractSource); + const locationByOffset = getLocationByOffset(contractSource); + const astVisitor = new ASTVisitor(locationByOffset); + astVisitor.walkAST(ast); + coverageEntriesBySourceHash[sourceHash] = astVisitor.getCollectedCoverageEntries(); + } + const coverageEntriesDescription = coverageEntriesBySourceHash[sourceHash]; + return coverageEntriesDescription; +}; diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index 870dc2bdb..4ca6b0ec8 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -4,8 +4,8 @@ import * as _ from 'lodash'; import * as path from 'path'; import { collectContractsData } from './collect_contract_data'; +import { collectCoverageEntries } from './collect_coverage_entries'; import { constants } from './constants'; -import { collectCoverageEntries } from './instrument_solidity'; import { parseSourceMap } from './source_maps'; import { BranchCoverage, diff --git a/packages/sol-cov/src/instrument_solidity.ts b/packages/sol-cov/src/instrument_solidity.ts deleted file mode 100644 index 163e3c7b9..000000000 --- a/packages/sol-cov/src/instrument_solidity.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as ethUtil from 'ethereumjs-util'; -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'; - -// Parsing source code for each transaction/code is slow and therefore we cache it -const coverageEntriesBySourceHash: { [sourceHash: string]: CoverageEntriesDescription } = {}; - -export const collectCoverageEntries = (contractSource: string, fileName: string) => { - const time = Date.now(); - const sourceHash = ethUtil.sha3(contractSource).toString('hex'); - if (_.isUndefined(coverageEntriesBySourceHash[sourceHash])) { - const ast = SolidityParser.parse(contractSource); - const locationByOffset = getLocationByOffset(contractSource); - const astVisitor = new ASTVisitor(locationByOffset); - astVisitor.walkAST(ast); - coverageEntriesBySourceHash[sourceHash] = astVisitor.getCollectedCoverageEntries(); - } - const coverageEntriesDescription = coverageEntriesBySourceHash[sourceHash]; - return coverageEntriesDescription; -}; -- cgit v1.2.3 From 2a9913b8fbcc4911593eab0ecdec3b62328e0b31 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 12 Mar 2018 06:07:01 +0100 Subject: Treap transactions to address 0x0 as contract creation --- packages/sol-cov/src/coverage_subprovider.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index e391b0f5c..ba7552fe6 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -72,7 +72,8 @@ export class CoverageSubprovider extends Subprovider { await this._lock.acquire(); } if (_.isNull(err)) { - await this._recordTxTraceAsync(txData.to || constants.NEW_CONTRACT, txData.data, txHash as string); + const toAddress = _.isUndefined(txData.to) || txData.to === '0x0' ? constants.NEW_CONTRACT : txData.to; + await this._recordTxTraceAsync(toAddress, txData.data, txHash as string); } else { const payload = { method: 'eth_getBlockByNumber', @@ -81,11 +82,8 @@ export class CoverageSubprovider extends Subprovider { 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, - ); + const toAddress = _.isUndefined(txData.to) || txData.to === '0x0' ? constants.NEW_CONTRACT : txData.to; + await this._recordTxTraceAsync(toAddress, transaction.data, transaction.hash); } } if (!txData.isFakeTransaction) { -- cgit v1.2.3 From 1cdfbbadaa1363ed8f5ecfca61576db70c0c1fb1 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 12 Mar 2018 12:31:33 +0100 Subject: Add a HACK to detect coverage of the modifiers with no parameters --- packages/sol-cov/src/ast_visitor.ts | 4 ++++ packages/sol-cov/src/coverage_manager.ts | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts index 9193f6a18..701cbbc88 100644 --- a/packages/sol-cov/src/ast_visitor.ts +++ b/packages/sol-cov/src/ast_visitor.ts @@ -7,6 +7,7 @@ export interface CoverageEntriesDescription { fnMap: FnMap; branchMap: BranchMap; statementMap: StatementMap; + modifiersStatementIds: number[]; } enum BranchType { @@ -19,6 +20,7 @@ export class ASTVisitor { private _entryId = 0; private _fnMap: FnMap = {}; private _branchMap: BranchMap = {}; + private _modifiersStatementIds: number[] = []; private _statementMap: StatementMap = {}; private _locationByOffset: LocationByOffset; private static _doesLookLikeAnASTNode(ast: any): boolean { @@ -49,6 +51,7 @@ export class ASTVisitor { fnMap: this._fnMap, branchMap: this._branchMap, statementMap: this._statementMap, + modifiersStatementIds: this._modifiersStatementIds, }; return coverageEntriesDescription; } @@ -91,6 +94,7 @@ export class ASTVisitor { private _visitModifierArgument(ast: SolidityParser.AST): void { const BUILTIN_MODIFIERS = ['public', 'view', 'payable', 'external', 'internal', 'pure', 'constant']; if (!_.includes(BUILTIN_MODIFIERS, ast.name)) { + this._modifiersStatementIds.push(this._entryId); this._visitStatement(ast); } } diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index 4ca6b0ec8..b70ca6f3f 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -69,6 +69,30 @@ export class CoverageManager { ); functionCoverage[fnId] = isCovered; } + // HACK: Solidity doesn't emit any opcodes that map back to modifiers with no args, that's why we map back to the + // function range and check if there is any covered statement within that range. + for (const modifierStatementId of coverageEntriesDescription.modifiersStatementIds) { + if (statementCoverage[modifierStatementId]) { + // Already detected as covered + continue; + } + const modifierDescription = coverageEntriesDescription.statementMap[modifierStatementId]; + const enclosingFunction = _.find(coverageEntriesDescription.fnMap, functionDescription => + utils.isRangeInside(modifierDescription, functionDescription.loc), + ) as FunctionDescription; + const isModifierCovered = _.some( + coverageEntriesDescription.statementMap, + (statementDescription: StatementDescription, statementId: number) => { + const isInsideTheModifierEnclosingFunction = utils.isRangeInside( + statementDescription, + enclosingFunction.loc, + ); + const isCovered = statementCoverage[statementId]; + return isInsideTheModifierEnclosingFunction && isCovered; + }, + ); + statementCoverage[modifierStatementId] = isModifierCovered; + } const partialCoverage = { [contractData.sources[fileIndex]]: { ...coverageEntriesDescription, -- cgit v1.2.3 From 20826e0f08e0e812572aae6b879ea2a9162d93c4 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 12 Mar 2018 12:56:22 +0100 Subject: Add an explanatory comment for making ranges unique --- packages/sol-cov/src/coverage_manager.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index b70ca6f3f..9b812446a 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -42,6 +42,7 @@ export class CoverageManager { 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. + // By default lodash does a shallow object comparasion. We JSON.stringify them and compare as strings. 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 = {}; -- cgit v1.2.3 From 88c6694ffc74d836e2f193d415eec426e11ddbc7 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 12 Mar 2018 13:24:07 +0100 Subject: Submit a TD PR --- packages/sol-cov/src/coverage_manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index 9b812446a..b1ba8b22b 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -180,7 +180,7 @@ export class CoverageManager { } } } - // TODO: Submit a PR to DT + // TODO: Remove any cast as soon as https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24233 gets merged return (collector as any).getFinalCoverage(); } } -- cgit v1.2.3 From efb0ee4c02e3a8048ae2395158f11dced8c5f90e Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Tue, 13 Mar 2018 11:53:45 +0100 Subject: Start using solidity-parser-antlr --- packages/sol-cov/src/ast_visitor.ts | 90 +++---- packages/sol-cov/src/collect_coverage_entries.ts | 10 +- packages/sol-cov/src/coverage_subprovider.ts | 2 +- packages/sol-cov/src/globals.d.ts | 317 ++++++++++++++++++++++- 4 files changed, 356 insertions(+), 63 deletions(-) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts index 701cbbc88..0dddbb16e 100644 --- a/packages/sol-cov/src/ast_visitor.ts +++ b/packages/sol-cov/src/ast_visitor.ts @@ -1,5 +1,5 @@ import * as _ from 'lodash'; -import * as SolidityParser from 'solidity-parser-sc'; +import * as Parser from 'solidity-parser-antlr'; import { BranchMap, FnMap, LocationByOffset, SingleFileSourceRange, StatementMap } from './types'; @@ -23,29 +23,9 @@ export class ASTVisitor { private _modifiersStatementIds: number[] = []; 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, @@ -55,43 +35,38 @@ export class ASTVisitor { }; return coverageEntriesDescription; } - private _visitConditionalExpression(ast: SolidityParser.AST): void { - this._visitBinaryBranch(ast, ast.consequent, ast.alternate, BranchType.ConditionalExpression); - } - private _visitFunctionDeclaration(ast: SolidityParser.AST): void { - const loc = this._getExpressionRange(ast); - this._fnMap[this._entryId++] = { - name: ast.name, - line: loc.start.line, - loc, - }; + public IfStatement(ast: Parser.IfStatement): void { + this._visitStatement(ast); + this._visitBinaryBranch(ast, ast.trueBody, ast.falseBody || ast, BranchType.If); } - private _visitBinaryExpression(ast: SolidityParser.AST): void { - this._visitBinaryBranch(ast, ast.left, ast.right, BranchType.BinaryExpression); + public FunctionDefinition(ast: Parser.FunctionDefinition): void { + this._visitFunctionLikeDefinition(ast); } - private _visitIfStatement(ast: SolidityParser.AST): void { - this._visitStatement(ast); - this._visitBinaryBranch(ast, ast.consequent, ast.alternate || ast, BranchType.If); + public ModifierDefinition(ast: Parser.ModifierDefinition): void { + this._visitFunctionLikeDefinition(ast); } - private _visitBreakStatement(ast: SolidityParser.AST): void { + public ForStatement(ast: Parser.ForStatement): void { this._visitStatement(ast); } - private _visitContractStatement(ast: SolidityParser.AST): void { + public ReturnStatement(ast: Parser.ReturnStatement): void { this._visitStatement(ast); } - private _visitExpressionStatement(ast: SolidityParser.AST): void { + public BreakStatement(ast: Parser.BreakStatement): void { this._visitStatement(ast); } - private _visitForStatement(ast: SolidityParser.AST): void { - this._visitStatement(ast); + public ExpressionStatement(ast: Parser.ExpressionStatement): void { + this._visitStatement(ast.expression); } - private _visitPlaceholderStatement(ast: SolidityParser.AST): void { - this._visitStatement(ast); + public BinaryOperation(ast: Parser.BinaryOperation): void { + const BRANCHING_BIN_OPS = ['&&', '||']; + if (_.includes(BRANCHING_BIN_OPS, ast.operator)) { + this._visitBinaryBranch(ast, ast.left, ast.right, BranchType.BinaryExpression); + } } - private _visitReturnStatement(ast: SolidityParser.AST): void { - this._visitStatement(ast); + public Conditional(ast: Parser.Conditional): void { + this._visitBinaryBranch(ast, ast.trueExpression, ast.falseExpression, BranchType.ConditionalExpression); } - private _visitModifierArgument(ast: SolidityParser.AST): void { + public ModifierInvocation(ast: Parser.ModifierInvocation): void { const BUILTIN_MODIFIERS = ['public', 'view', 'payable', 'external', 'internal', 'pure', 'constant']; if (!_.includes(BUILTIN_MODIFIERS, ast.name)) { this._modifiersStatementIds.push(this._entryId); @@ -99,9 +74,9 @@ export class ASTVisitor { } } private _visitBinaryBranch( - ast: SolidityParser.AST, - left: SolidityParser.AST, - right: SolidityParser.AST, + ast: Parser.ASTNode, + left: Parser.ASTNode, + right: Parser.ASTNode, type: BranchType, ): void { this._branchMap[this._entryId++] = { @@ -110,16 +85,25 @@ export class ASTVisitor { locations: [this._getExpressionRange(left), this._getExpressionRange(right)], }; } - private _visitStatement(ast: SolidityParser.AST): void { + private _visitStatement(ast: Parser.ASTNode): 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]; + private _getExpressionRange(ast: Parser.ASTNode): SingleFileSourceRange { + const start = this._locationByOffset[ast.range[0] - 1]; + const end = this._locationByOffset[ast.range[1]]; const range = { start, end, }; return range; } + private _visitFunctionLikeDefinition(ast: Parser.ModifierDefinition | Parser.FunctionDefinition): void { + const loc = this._getExpressionRange(ast); + this._fnMap[this._entryId++] = { + name: ast.name, + line: loc.start.line, + loc, + }; + this._visitStatement(ast); + } } diff --git a/packages/sol-cov/src/collect_coverage_entries.ts b/packages/sol-cov/src/collect_coverage_entries.ts index d29fa2c37..6da81fbfc 100644 --- a/packages/sol-cov/src/collect_coverage_entries.ts +++ b/packages/sol-cov/src/collect_coverage_entries.ts @@ -2,7 +2,7 @@ import * as ethUtil from 'ethereumjs-util'; import * as fs from 'fs'; import * as _ from 'lodash'; import * as path from 'path'; -import * as SolidityParser from 'solidity-parser-sc'; +import * as parser from 'solidity-parser-antlr'; import { ASTVisitor, CoverageEntriesDescription } from './ast_visitor'; import { getLocationByOffset } from './source_maps'; @@ -13,11 +13,11 @@ const coverageEntriesBySourceHash: { [sourceHash: string]: CoverageEntriesDescri export const collectCoverageEntries = (contractSource: string, fileName: string) => { const sourceHash = ethUtil.sha3(contractSource).toString('hex'); if (_.isUndefined(coverageEntriesBySourceHash[sourceHash])) { - const ast = SolidityParser.parse(contractSource); + const ast = parser.parse(contractSource, { range: true }); const locationByOffset = getLocationByOffset(contractSource); - const astVisitor = new ASTVisitor(locationByOffset); - astVisitor.walkAST(ast); - coverageEntriesBySourceHash[sourceHash] = astVisitor.getCollectedCoverageEntries(); + const visitor = new ASTVisitor(locationByOffset); + parser.visit(ast, visitor); + coverageEntriesBySourceHash[sourceHash] = visitor.getCollectedCoverageEntries(); } const coverageEntriesDescription = coverageEntriesBySourceHash[sourceHash]; return coverageEntriesDescription; diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index ba7552fe6..71d90bba7 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -137,9 +137,9 @@ export class CoverageSubprovider extends Subprovider { await this._lock.acquire(); const snapshotId = Number((await this.emitPayloadAsync({ method: 'evm_snapshot' })).result); const fakeTxData: MaybeFakeTxData = { - from: this._defaultFromAddress, isFakeTransaction: true, // This transaction (and only it) is allowed to come through when the lock is locked ...callData, + from: callData.from || this._defaultFromAddress, }; try { await this.emitPayloadAsync({ diff --git a/packages/sol-cov/src/globals.d.ts b/packages/sol-cov/src/globals.d.ts index f807730cc..54ee64684 100644 --- a/packages/sol-cov/src/globals.d.ts +++ b/packages/sol-cov/src/globals.d.ts @@ -1,6 +1,315 @@ // 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; +declare module 'solidity-parser-antlr' { + export interface BaseASTNode { + range: [number, number]; + } + export interface SourceUnit extends BaseASTNode {} + export interface PragmaDirective extends BaseASTNode {} + export interface PragmaName extends BaseASTNode {} + export interface PragmaValue extends BaseASTNode {} + export interface Version extends BaseASTNode {} + export interface VersionOperator extends BaseASTNode {} + export interface VersionConstraint extends BaseASTNode {} + export interface ImportDeclaration extends BaseASTNode {} + export interface ImportDirective extends BaseASTNode {} + export interface ContractDefinition extends BaseASTNode {} + export interface InheritanceSpecifier extends BaseASTNode {} + export interface ContractPart extends BaseASTNode {} + export interface StateVariableDeclaration extends BaseASTNode { + variables: VariableDeclaration[]; + } + export interface UsingForDeclaration extends BaseASTNode {} + export interface StructDefinition extends BaseASTNode {} + export interface ModifierDefinition extends BaseASTNode { + name: string; + } + export interface ModifierInvocation extends BaseASTNode { + name: string; + } + export interface FunctionDefinition extends BaseASTNode { + name: string; + } + export interface ReturnParameters extends BaseASTNode {} + export interface ModifierList extends BaseASTNode {} + export interface EventDefinition extends BaseASTNode {} + export interface EnumValue extends BaseASTNode {} + export interface EnumDefinition extends BaseASTNode {} + export interface ParameterList extends BaseASTNode {} + export interface Parameter extends BaseASTNode {} + export interface EventParameterList extends BaseASTNode {} + export interface EventParameter extends BaseASTNode {} + export interface FunctionTypeParameterList extends BaseASTNode {} + export interface FunctionTypeParameter extends BaseASTNode {} + export interface VariableDeclaration extends BaseASTNode { + visibility: 'public' | 'private'; + isStateVar: boolean; + } + export interface TypeName extends BaseASTNode {} + export interface UserDefinedTypeName extends BaseASTNode {} + export interface Mapping extends BaseASTNode {} + export interface FunctionTypeName extends BaseASTNode {} + export interface StorageLocation extends BaseASTNode {} + export interface StateMutability extends BaseASTNode {} + export interface Block extends BaseASTNode {} + export interface Statement extends BaseASTNode {} + export interface ExpressionStatement extends BaseASTNode { + expression: ASTNode; + } + export interface IfStatement extends BaseASTNode { + trueBody: ASTNode; + falseBody: ASTNode; + } + export interface WhileStatement extends BaseASTNode {} + export interface SimpleStatement extends BaseASTNode {} + export interface ForStatement extends BaseASTNode {} + export interface InlineAssemblyStatement extends BaseASTNode {} + export interface DoWhileStatement extends BaseASTNode {} + export interface ContinueStatement extends BaseASTNode {} + export interface BreakStatement extends BaseASTNode {} + export interface ReturnStatement extends BaseASTNode {} + export interface ThrowStatement extends BaseASTNode {} + export interface VariableDeclarationStatement extends BaseASTNode {} + export interface IdentifierList extends BaseASTNode {} + export interface ElementaryTypeName extends BaseASTNode {} + export interface Expression extends BaseASTNode {} + export interface PrimaryExpression extends BaseASTNode {} + export interface ExpressionList extends BaseASTNode {} + export interface NameValueList extends BaseASTNode {} + export interface NameValue extends BaseASTNode {} + export interface FunctionCallArguments extends BaseASTNode {} + export interface AssemblyBlock extends BaseASTNode {} + export interface AssemblyItem extends BaseASTNode {} + export interface AssemblyExpression extends BaseASTNode {} + export interface AssemblyCall extends BaseASTNode {} + export interface AssemblyLocalDefinition extends BaseASTNode {} + export interface AssemblyAssignment extends BaseASTNode {} + export interface AssemblyIdentifierOrList extends BaseASTNode {} + export interface AssemblyIdentifierList extends BaseASTNode {} + export interface AssemblyStackAssignment extends BaseASTNode {} + export interface LabelDefinition extends BaseASTNode {} + export interface AssemblySwitch extends BaseASTNode {} + export interface AssemblyCase extends BaseASTNode {} + export interface AssemblyFunctionDefinition extends BaseASTNode {} + export interface AssemblyFunctionReturns extends BaseASTNode {} + export interface AssemblyFor extends BaseASTNode {} + export interface AssemblyIf extends BaseASTNode {} + export interface AssemblyLiteral extends BaseASTNode {} + export interface SubAssembly extends BaseASTNode {} + export interface TupleExpression extends BaseASTNode {} + export interface ElementaryTypeNameExpression extends BaseASTNode {} + export interface NumberLiteral extends BaseASTNode {} + export interface Identifier extends BaseASTNode {} + export type BinOp = + | '+' + | '-' + | '*' + | '/' + | '**' + | '%' + | '<<' + | '>>' + | '&&' + | '||' + | '&' + | '|' + | '^' + | '<' + | '>' + | '<=' + | '>=' + | '==' + | '!=' + | '=' + | '|=' + | '^=' + | '&=' + | '<<=' + | '>>=' + | '+=' + | '-=' + | '*=' + | '/=' + | '%='; + export interface BinaryOperation extends BaseASTNode { + left: ASTNode; + right: ASTNode; + operator: BinOp; + } + export interface Conditional extends BaseASTNode { + trueExpression: ASTNode; + falseExpression: ASTNode; + } + + export type ASTNode = + | SourceUnit + | PragmaDirective + | PragmaName + | PragmaValue + | Version + | VersionOperator + | VersionConstraint + | ImportDeclaration + | ImportDirective + | ContractDefinition + | InheritanceSpecifier + | ContractPart + | StateVariableDeclaration + | UsingForDeclaration + | StructDefinition + | ModifierDefinition + | ModifierInvocation + | FunctionDefinition + | ReturnParameters + | ModifierList + | EventDefinition + | EnumValue + | EnumDefinition + | ParameterList + | Parameter + | EventParameterList + | EventParameter + | FunctionTypeParameterList + | FunctionTypeParameter + | VariableDeclaration + | TypeName + | UserDefinedTypeName + | Mapping + | FunctionTypeName + | StorageLocation + | StateMutability + | Block + | Statement + | ExpressionStatement + | IfStatement + | WhileStatement + | SimpleStatement + | ForStatement + | InlineAssemblyStatement + | DoWhileStatement + | ContinueStatement + | BreakStatement + | ReturnStatement + | ThrowStatement + | VariableDeclarationStatement + | IdentifierList + | ElementaryTypeName + | Expression + | PrimaryExpression + | ExpressionList + | NameValueList + | NameValue + | FunctionCallArguments + | AssemblyBlock + | AssemblyItem + | AssemblyExpression + | AssemblyCall + | AssemblyLocalDefinition + | AssemblyAssignment + | AssemblyIdentifierOrList + | AssemblyIdentifierList + | AssemblyStackAssignment + | LabelDefinition + | AssemblySwitch + | AssemblyCase + | AssemblyFunctionDefinition + | AssemblyFunctionReturns + | AssemblyFor + | AssemblyIf + | AssemblyLiteral + | SubAssembly + | TupleExpression + | ElementaryTypeNameExpression + | NumberLiteral + | Identifier + | BinaryOperation + | Conditional; + export interface Visitor { + SourceUnit?: (node: SourceUnit) => void; + PragmaDirective?: (node: PragmaDirective) => void; + PragmaName?: (node: PragmaName) => void; + PragmaValue?: (node: PragmaValue) => void; + Version?: (node: Version) => void; + VersionOperator?: (node: VersionOperator) => void; + VersionConstraint?: (node: VersionConstraint) => void; + ImportDeclaration?: (node: ImportDeclaration) => void; + ImportDirective?: (node: ImportDirective) => void; + ContractDefinition?: (node: ContractDefinition) => void; + InheritanceSpecifier?: (node: InheritanceSpecifier) => void; + ContractPart?: (node: ContractPart) => void; + StateVariableDeclaration?: (node: StateVariableDeclaration) => void; + UsingForDeclaration?: (node: UsingForDeclaration) => void; + StructDefinition?: (node: StructDefinition) => void; + ModifierDefinition?: (node: ModifierDefinition) => void; + ModifierInvocation?: (node: ModifierInvocation) => void; + FunctionDefinition?: (node: FunctionDefinition) => void; + ReturnParameters?: (node: ReturnParameters) => void; + ModifierList?: (node: ModifierList) => void; + EventDefinition?: (node: EventDefinition) => void; + EnumValue?: (node: EnumValue) => void; + EnumDefinition?: (node: EnumDefinition) => void; + ParameterList?: (node: ParameterList) => void; + Parameter?: (node: Parameter) => void; + EventParameterList?: (node: EventParameterList) => void; + EventParameter?: (node: EventParameter) => void; + FunctionTypeParameterList?: (node: FunctionTypeParameterList) => void; + FunctionTypeParameter?: (node: FunctionTypeParameter) => void; + VariableDeclaration?: (node: VariableDeclaration) => void; + TypeName?: (node: TypeName) => void; + UserDefinedTypeName?: (node: UserDefinedTypeName) => void; + Mapping?: (node: Mapping) => void; + FunctionTypeName?: (node: FunctionTypeName) => void; + StorageLocation?: (node: StorageLocation) => void; + StateMutability?: (node: StateMutability) => void; + Block?: (node: Block) => void; + Statement?: (node: Statement) => void; + ExpressionStatement?: (node: ExpressionStatement) => void; + IfStatement?: (node: IfStatement) => void; + WhileStatement?: (node: WhileStatement) => void; + SimpleStatement?: (node: SimpleStatement) => void; + ForStatement?: (node: ForStatement) => void; + InlineAssemblyStatement?: (node: InlineAssemblyStatement) => void; + DoWhileStatement?: (node: DoWhileStatement) => void; + ContinueStatement?: (node: ContinueStatement) => void; + BreakStatement?: (node: BreakStatement) => void; + ReturnStatement?: (node: ReturnStatement) => void; + ThrowStatement?: (node: ThrowStatement) => void; + VariableDeclarationStatement?: (node: VariableDeclarationStatement) => void; + IdentifierList?: (node: IdentifierList) => void; + ElementaryTypeName?: (node: ElementaryTypeName) => void; + Expression?: (node: Expression) => void; + PrimaryExpression?: (node: PrimaryExpression) => void; + ExpressionList?: (node: ExpressionList) => void; + NameValueList?: (node: NameValueList) => void; + NameValue?: (node: NameValue) => void; + FunctionCallArguments?: (node: FunctionCallArguments) => void; + AssemblyBlock?: (node: AssemblyBlock) => void; + AssemblyItem?: (node: AssemblyItem) => void; + AssemblyExpression?: (node: AssemblyExpression) => void; + AssemblyCall?: (node: AssemblyCall) => void; + AssemblyLocalDefinition?: (node: AssemblyLocalDefinition) => void; + AssemblyAssignment?: (node: AssemblyAssignment) => void; + AssemblyIdentifierOrList?: (node: AssemblyIdentifierOrList) => void; + AssemblyIdentifierList?: (node: AssemblyIdentifierList) => void; + AssemblyStackAssignment?: (node: AssemblyStackAssignment) => void; + LabelDefinition?: (node: LabelDefinition) => void; + AssemblySwitch?: (node: AssemblySwitch) => void; + AssemblyCase?: (node: AssemblyCase) => void; + AssemblyFunctionDefinition?: (node: AssemblyFunctionDefinition) => void; + AssemblyFunctionReturns?: (node: AssemblyFunctionReturns) => void; + AssemblyFor?: (node: AssemblyFor) => void; + AssemblyIf?: (node: AssemblyIf) => void; + AssemblyLiteral?: (node: AssemblyLiteral) => void; + SubAssembly?: (node: SubAssembly) => void; + TupleExpression?: (node: TupleExpression) => void; + ElementaryTypeNameExpression?: (node: ElementaryTypeNameExpression) => void; + NumberLiteral?: (node: NumberLiteral) => void; + Identifier?: (node: Identifier) => void; + BinaryOperation?: (node: BinaryOperation) => void; + Conditional?: (node: Conditional) => void; + } + export interface ParserOpts { + range?: boolean; + } + export function parse(sourceCode: string, parserOpts: ParserOpts): ASTNode; + export function visit(ast: ASTNode, visitor: Visitor): void; } -- cgit v1.2.3 From 5827170815904175d83908bd565d6028cd631737 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Tue, 13 Mar 2018 15:32:36 +0100 Subject: Add other statement types --- packages/sol-cov/src/ast_visitor.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'packages/sol-cov/src') diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts index 0dddbb16e..66190afec 100644 --- a/packages/sol-cov/src/ast_visitor.ts +++ b/packages/sol-cov/src/ast_visitor.ts @@ -54,9 +54,33 @@ export class ASTVisitor { public BreakStatement(ast: Parser.BreakStatement): void { this._visitStatement(ast); } + public ContinueStatement(ast: Parser.ContinueStatement): void { + this._visitStatement(ast); + } + public VariableDeclarationStatement(ast: Parser.VariableDeclarationStatement): void { + this._visitStatement(ast); + } + public Statement(ast: Parser.Statement): void { + this._visitStatement(ast); + } + public WhileStatement(ast: Parser.WhileStatement): void { + this._visitStatement(ast); + } + public SimpleStatement(ast: Parser.SimpleStatement): void { + this._visitStatement(ast); + } + public ThrowStatement(ast: Parser.ThrowStatement): void { + this._visitStatement(ast); + } + public DoWhileStatement(ast: Parser.DoWhileStatement): void { + this._visitStatement(ast); + } public ExpressionStatement(ast: Parser.ExpressionStatement): void { this._visitStatement(ast.expression); } + public InlineAssemblyStatement(ast: Parser.InlineAssemblyStatement): void { + this._visitStatement(ast); + } public BinaryOperation(ast: Parser.BinaryOperation): void { const BRANCHING_BIN_OPS = ['&&', '||']; if (_.includes(BRANCHING_BIN_OPS, ast.operator)) { -- cgit v1.2.3