diff options
Diffstat (limited to 'packages/sol-cov/src')
-rw-r--r-- | packages/sol-cov/src/ast_visitor.ts | 133 | ||||
-rw-r--r-- | packages/sol-cov/src/collect_contract_data.ts | 44 | ||||
-rw-r--r-- | packages/sol-cov/src/collect_coverage_entries.ts | 24 | ||||
-rw-r--r-- | packages/sol-cov/src/constants.ts | 3 | ||||
-rw-r--r-- | packages/sol-cov/src/coverage_manager.ts | 186 | ||||
-rw-r--r-- | packages/sol-cov/src/coverage_subprovider.ts | 168 | ||||
-rw-r--r-- | packages/sol-cov/src/globals.d.ts | 315 | ||||
-rw-r--r-- | packages/sol-cov/src/index.ts | 1 | ||||
-rw-r--r-- | packages/sol-cov/src/instructions.ts | 24 | ||||
-rw-r--r-- | packages/sol-cov/src/source_maps.ts | 82 | ||||
-rw-r--r-- | packages/sol-cov/src/types.ts | 101 | ||||
-rw-r--r-- | packages/sol-cov/src/utils.ts | 13 |
12 files changed, 1094 insertions, 0 deletions
diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts new file mode 100644 index 000000000..66190afec --- /dev/null +++ b/packages/sol-cov/src/ast_visitor.ts @@ -0,0 +1,133 @@ +import * as _ from 'lodash'; +import * as Parser from 'solidity-parser-antlr'; + +import { BranchMap, FnMap, LocationByOffset, SingleFileSourceRange, StatementMap } from './types'; + +export interface CoverageEntriesDescription { + fnMap: FnMap; + branchMap: BranchMap; + statementMap: StatementMap; + modifiersStatementIds: number[]; +} + +enum BranchType { + If = 'if', + ConditionalExpression = 'cond-expr', + BinaryExpression = 'binary-expr', +} + +export class ASTVisitor { + private _entryId = 0; + private _fnMap: FnMap = {}; + private _branchMap: BranchMap = {}; + private _modifiersStatementIds: number[] = []; + private _statementMap: StatementMap = {}; + private _locationByOffset: LocationByOffset; + constructor(locationByOffset: LocationByOffset) { + this._locationByOffset = locationByOffset; + } + public getCollectedCoverageEntries(): CoverageEntriesDescription { + const coverageEntriesDescription = { + fnMap: this._fnMap, + branchMap: this._branchMap, + statementMap: this._statementMap, + modifiersStatementIds: this._modifiersStatementIds, + }; + return coverageEntriesDescription; + } + public IfStatement(ast: Parser.IfStatement): void { + this._visitStatement(ast); + this._visitBinaryBranch(ast, ast.trueBody, ast.falseBody || ast, BranchType.If); + } + public FunctionDefinition(ast: Parser.FunctionDefinition): void { + this._visitFunctionLikeDefinition(ast); + } + public ModifierDefinition(ast: Parser.ModifierDefinition): void { + this._visitFunctionLikeDefinition(ast); + } + public ForStatement(ast: Parser.ForStatement): void { + this._visitStatement(ast); + } + public ReturnStatement(ast: Parser.ReturnStatement): void { + this._visitStatement(ast); + } + 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)) { + this._visitBinaryBranch(ast, ast.left, ast.right, BranchType.BinaryExpression); + } + } + public Conditional(ast: Parser.Conditional): void { + this._visitBinaryBranch(ast, ast.trueExpression, ast.falseExpression, BranchType.ConditionalExpression); + } + 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); + this._visitStatement(ast); + } + } + private _visitBinaryBranch( + ast: Parser.ASTNode, + left: Parser.ASTNode, + right: Parser.ASTNode, + type: BranchType, + ): void { + this._branchMap[this._entryId++] = { + line: this._getExpressionRange(ast).start.line, + type, + locations: [this._getExpressionRange(left), this._getExpressionRange(right)], + }; + } + private _visitStatement(ast: Parser.ASTNode): void { + this._statementMap[this._entryId++] = this._getExpressionRange(ast); + } + 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_contract_data.ts b/packages/sol-cov/src/collect_contract_data.ts new file mode 100644 index 000000000..a0ce2640f --- /dev/null +++ b/packages/sol-cov/src/collect_contract_data.ts @@ -0,0 +1,44 @@ +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as _ from 'lodash'; +import * as path from 'path'; + +import { ContractData } from './types'; + +export const collectContractsData = (artifactsPath: string, sourcesPath: string, networkId: number) => { + const sourcesGlob = `${sourcesPath}/**/*.sol`; + const sourceFileNames = glob.sync(sourcesGlob, { absolute: true }); + const contractsDataIfExists: Array<ContractData | {}> = _.map(sourceFileNames, sourceFileName => { + const baseName = path.basename(sourceFileName, '.sol'); + const artifactFileName = path.join(artifactsPath, `${baseName}.json`); + if (!fs.existsSync(artifactFileName)) { + // If the contract isn't directly compiled, but is imported as the part of the other contract - we don't + // have an artifact for it and therefore can't do anything useful 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; + }); + if (_.isUndefined(artifact.networks[networkId])) { + throw new Error(`No ${baseName} artifacts found for networkId ${networkId}`); + } + 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/collect_coverage_entries.ts b/packages/sol-cov/src/collect_coverage_entries.ts new file mode 100644 index 000000000..6da81fbfc --- /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 parser from 'solidity-parser-antlr'; + +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 = parser.parse(contractSource, { range: true }); + const locationByOffset = getLocationByOffset(contractSource); + 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/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..b1ba8b22b --- /dev/null +++ b/packages/sol-cov/src/coverage_manager.ts @@ -0,0 +1,186 @@ +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 { collectCoverageEntries } from './collect_coverage_entries'; +import { constants } from './constants'; +import { parseSourceMap } from './source_maps'; +import { + BranchCoverage, + BranchDescription, + BranchMap, + ContractData, + Coverage, + FnMap, + FunctionCoverage, + FunctionDescription, + LineColumn, + SingleFileSourceRange, + SourceRange, + StatementCoverage, + StatementDescription, + StatementMap, + TraceInfo, + TraceInfoExistingContract, + TraceInfoNewContract, +} from './types'; +import { utils } from './utils'; + +export class CoverageManager { + private _traceInfos: TraceInfo[] = []; + private _contractsData: ContractData[] = []; + private _getContractCodeAsync: (address: string) => Promise<string>; + 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. + // 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 = {}; + 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; + } + // 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, + 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, + networkId: number, + getContractCodeAsync: (address: string) => Promise<string>, + ) { + this._getContractCodeAsync = getContractCodeAsync; + this._contractsData = collectContractsData(artifactsPath, sourcesPath, networkId); + } + public appendTraceInfo(traceInfo: TraceInfo): void { + this._traceInfos.push(traceInfo); + } + public async writeCoverageAsync(): Promise<void> { + const finalCoverage = await this._computeCoverageAsync(); + const jsonReplacer: null = null; + const numberOfJsonSpaces = 4; + const stringifiedCoverage = JSON.stringify(finalCoverage, jsonReplacer, numberOfJsonSpaces); + fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); + } + private async _computeCoverageAsync(): Promise<Coverage> { + const collector = new Collector(); + for (const traceInfo of this._traceInfos) { + if (traceInfo.address !== constants.NEW_CONTRACT) { + // Runtime transaction + 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: ${traceInfo.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++) { + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( + contractData, + traceInfo.coveredPcs, + pcToSourceRange, + fileIndex, + ); + collector.add(singleFileCoverageForTrace); + } + } else { + // Contract creation transaction + 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 = CoverageManager._getSingleFileCoverageForTrace( + contractData, + traceInfo.coveredPcs, + pcToSourceRange, + fileIndex, + ); + collector.add(singleFileCoverageForTrace); + } + } + } + // TODO: Remove any cast as soon as https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24233 gets merged + 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..71d90bba7 --- /dev/null +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -0,0 +1,168 @@ +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, + sourcesPath, + networkId, + this._getContractCodeAsync.bind(this), + ); + } + public handleRequest( + payload: Web3.JSONRPCRequestPayload, + next: NextCallback, + end: (err: Error | null, result: any) => void, + ) { + switch (payload.method) { + case 'eth_sendTransaction': + const txData = payload.params[0]; + next(this._onTransactionSentAsync.bind(this, txData)); + return; + + case 'eth_call': + const callData = payload.params[0]; + const blockNumber = payload.params[1]; + next(this._onCallExecutedAsync.bind(this, callData, blockNumber)); + return; + + default: + next(); + return; + } + } + public async writeCoverageAsync(): Promise<void> { + await this._coverageManager.writeCoverageAsync(); + } + private async _onTransactionSentAsync( + txData: MaybeFakeTxData, + err: Error | null, + txHash: string | undefined, + cb: Callback, + ): Promise<void> { + 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)) { + 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', + params: ['latest', true], + }; + const jsonRPCResponsePayload = await this.emitPayloadAsync(payload); + const transactions = jsonRPCResponsePayload.result.transactions; + for (const transaction of transactions) { + const toAddress = _.isUndefined(txData.to) || txData.to === '0x0' ? constants.NEW_CONTRACT : txData.to; + await this._recordTxTraceAsync(toAddress, transaction.data, transaction.hash); + } + } + 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 + this._lock.release(); + } + cb(); + } + private async _onCallExecutedAsync( + callData: Partial<Web3.CallData>, + blockNumber: Web3.BlockParam, + err: Error | null, + callResult: string, + cb: Callback, + ): Promise<void> { + await this._recordCallTraceAsync(callData, blockNumber); + cb(); + } + private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> { + 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); + if (address === constants.NEW_CONTRACT) { + const traceInfo: TraceInfoNewContract = { + coveredPcs, + txHash, + address: address as 'NEW_CONTRACT', + 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<Web3.CallData>, blockNumber: Web3.BlockParam): Promise<void> { + // 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 fakeTxData: MaybeFakeTxData = { + 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({ + method: 'eth_sendTransaction', + params: [fakeTxData], + }); + } catch (err) { + // Even if this transaction failed - we've already recorded it's trace. + } + const jsonRPCResponse = await this.emitPayloadAsync({ method: 'evm_revert', params: [snapshotId] }); + this._lock.release(); + const didRevert = jsonRPCResponse.result; + if (!didRevert) { + throw new Error('Failed to revert the snapshot'); + } + } + private async _getContractCodeAsync(address: string): Promise<string> { + const payload = { + method: 'eth_getCode', + params: [address, 'latest'], + }; + const jsonRPCResponsePayload = await this.emitPayloadAsync(payload); + const contractCode: string = jsonRPCResponsePayload.result; + return contractCode; + } +} diff --git a/packages/sol-cov/src/globals.d.ts b/packages/sol-cov/src/globals.d.ts new file mode 100644 index 000000000..54ee64684 --- /dev/null +++ b/packages/sol-cov/src/globals.d.ts @@ -0,0 +1,315 @@ +// tslint:disable:completed-docs +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; +} 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/source_maps.ts b/packages/sol-cov/src/source_maps.ts new file mode 100644 index 000000000..9b3ea9e24 --- /dev/null +++ b/packages/sol-cov/src/source_maps.ts @@ -0,0 +1,82 @@ +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 function getLocationByOffset(str: string): LocationByOffset { + 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 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); + 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..d6491100b --- /dev/null +++ b/packages/sol-cov/src/types.ts @@ -0,0 +1,101 @@ +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 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; 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 + ); + }, +}; |