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 ExpressionStatement(ast: Parser.ExpressionStatement): void { this._visitStatement(ast.expression); } 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); } }