Fix/simplify handling of revert trace snippets

This commit is contained in:
Leonid Logvinov 2019-01-17 14:37:15 +01:00
parent 4c5bde1b54
commit fcdd0de9ee
No known key found for this signature in database
GPG Key ID: 0DD294BFDE8C95D4
5 changed files with 22 additions and 193 deletions

View File

@ -109,9 +109,7 @@ export class RevertTraceSubprovider extends TraceCollectionSubprovider {
const fileNameToFileIndex = _.invert(contractData.sources); const fileNameToFileIndex = _.invert(contractData.sources);
const fileIndex = _.parseInt(fileNameToFileIndex[sourceRange.fileName]); const fileIndex = _.parseInt(fileNameToFileIndex[sourceRange.fileName]);
const sourceSnippet = getSourceRangeSnippet(sourceRange, contractData.sourceCodes[fileIndex]); const sourceSnippet = getSourceRangeSnippet(sourceRange, contractData.sourceCodes[fileIndex]);
if (sourceSnippet !== null) { sourceSnippets.push(sourceSnippet);
sourceSnippets.push(sourceSnippet);
}
} }
const filteredSnippets = filterSnippets(sourceSnippets); const filteredSnippets = filterSnippets(sourceSnippets);
if (filteredSnippets.length > 0) { if (filteredSnippets.length > 0) {
@ -135,9 +133,7 @@ function filterSnippets(sourceSnippets: SourceSnippet[]): SourceSnippet[] {
const results: SourceSnippet[] = [sourceSnippets[0]]; const results: SourceSnippet[] = [sourceSnippets[0]];
let prev = sourceSnippets[0]; let prev = sourceSnippets[0];
for (const sourceSnippet of sourceSnippets) { for (const sourceSnippet of sourceSnippets) {
if (sourceSnippet.type === 'IfStatement') { if (sourceSnippet.source === prev.source) {
continue;
} else if (sourceSnippet.source === prev.source) {
prev = sourceSnippet; prev = sourceSnippet;
continue; continue;
} }
@ -157,12 +153,5 @@ function getStackTraceString(sourceSnippet: SourceSnippet): string {
} }
function getSourceSnippetString(sourceSnippet: SourceSnippet): string { function getSourceSnippetString(sourceSnippet: SourceSnippet): string {
switch (sourceSnippet.type) { return `${sourceSnippet.source}`;
case 'ContractDefinition':
return `contract ${sourceSnippet.name}`;
case 'FunctionDefinition':
return `function ${sourceSnippet.name}`;
default:
return `${sourceSnippet.source}`;
}
} }

View File

@ -9,6 +9,10 @@
{ {
"note": "Fix a bug when `TraceCollectionSubprovider` was hanging on the fake `Geth` snapshot transaction", "note": "Fix a bug when `TraceCollectionSubprovider` was hanging on the fake `Geth` snapshot transaction",
"pr": "TODO" "pr": "TODO"
},
{
"note": "Fix/simplify handling of revert trace snippets",
"pr": "TODO"
} }
] ]
}, },

View File

@ -1,185 +1,16 @@
import * as ethUtil from 'ethereumjs-util'; import { SourceRange, SourceSnippet } from './types';
import * as _ from 'lodash';
import * as Parser from 'solidity-parser-antlr';
import { SingleFileSourceRange, SourceRange, SourceSnippet } from './types';
import { utils } from './utils'; import { utils } from './utils';
interface ASTInfo {
type: string;
node: Parser.ASTNode;
name: string | null;
range?: SingleFileSourceRange;
}
// Parsing source code for each transaction/code is slow and therefore we cache it
const hashToParsedSource: { [sourceHash: string]: Parser.ASTNode } = {};
/** /**
* Gets the source range snippet by source range to be used by revert trace. * Gets the source range snippet by source range to be used by revert trace.
* @param sourceRange source range * @param sourceRange source range
* @param sourceCode source code * @param sourceCode source code
*/ */
export function getSourceRangeSnippet(sourceRange: SourceRange, sourceCode: string): SourceSnippet | null { export function getSourceRangeSnippet(sourceRange: SourceRange, sourceCode: string): SourceSnippet {
const sourceHash = ethUtil.sha3(sourceCode).toString('hex');
if (_.isUndefined(hashToParsedSource[sourceHash])) {
hashToParsedSource[sourceHash] = Parser.parse(sourceCode, { loc: true });
}
const astNode = hashToParsedSource[sourceHash];
const visitor = new ASTInfoVisitor();
Parser.visit(astNode, visitor);
const astInfo = visitor.getASTInfoForRange(sourceRange);
if (astInfo === null) {
return null;
}
const sourceCodeInRange = utils.getRange(sourceCode, sourceRange.location); const sourceCodeInRange = utils.getRange(sourceCode, sourceRange.location);
return { return {
...astInfo, range: sourceRange.location,
range: astInfo.range as SingleFileSourceRange,
source: sourceCodeInRange, source: sourceCodeInRange,
fileName: sourceRange.fileName, fileName: sourceRange.fileName,
}; };
} }
// A visitor which collects ASTInfo for most nodes in the AST.
class ASTInfoVisitor {
private readonly _astInfos: ASTInfo[] = [];
public getASTInfoForRange(sourceRange: SourceRange): ASTInfo | null {
// HACK(albrow): Sometimes the source range doesn't exactly match that
// of astInfo. To work around that we try with a +/-1 offset on
// end.column. If nothing matches even with the offset, we return null.
const offset = {
start: {
line: 0,
column: 0,
},
end: {
line: 0,
column: 0,
},
};
let astInfo = this._getASTInfoForRange(sourceRange, offset);
if (astInfo !== null) {
return astInfo;
}
offset.end.column += 1;
astInfo = this._getASTInfoForRange(sourceRange, offset);
if (astInfo !== null) {
return astInfo;
}
offset.end.column -= 2;
astInfo = this._getASTInfoForRange(sourceRange, offset);
if (astInfo !== null) {
return astInfo;
}
return null;
}
public ContractDefinition(ast: Parser.ContractDefinition): void {
this._visitContractDefinition(ast);
}
public IfStatement(ast: Parser.IfStatement): void {
this._visitStatement(ast);
}
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 EmitStatement(ast: any /* TODO: Parser.EmitStatement */): 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 ModifierInvocation(ast: Parser.ModifierInvocation): void {
const BUILTIN_MODIFIERS = ['public', 'view', 'payable', 'external', 'internal', 'pure', 'constant'];
if (!_.includes(BUILTIN_MODIFIERS, ast.name)) {
this._visitStatement(ast);
}
}
private _visitStatement(ast: Parser.ASTNode): void {
this._astInfos.push({
type: ast.type,
node: ast,
name: null,
range: ast.loc,
});
}
private _visitFunctionLikeDefinition(ast: Parser.ModifierDefinition | Parser.FunctionDefinition): void {
this._astInfos.push({
type: ast.type,
node: ast,
name: ast.name,
range: ast.loc,
});
}
private _visitContractDefinition(ast: Parser.ContractDefinition): void {
this._astInfos.push({
type: ast.type,
node: ast,
name: ast.name,
range: ast.loc,
});
}
private _getASTInfoForRange(sourceRange: SourceRange, offset: SingleFileSourceRange): ASTInfo | null {
const offsetSourceRange = {
...sourceRange,
location: {
start: {
line: sourceRange.location.start.line + offset.start.line,
column: sourceRange.location.start.column + offset.start.column,
},
end: {
line: sourceRange.location.end.line + offset.end.line,
column: sourceRange.location.end.column + offset.end.column,
},
},
};
for (const astInfo of this._astInfos) {
const astInfoRange = astInfo.range as SingleFileSourceRange;
if (
astInfoRange.start.column === offsetSourceRange.location.start.column &&
astInfoRange.start.line === offsetSourceRange.location.start.line &&
astInfoRange.end.column === offsetSourceRange.location.end.column &&
astInfoRange.end.line === offsetSourceRange.location.end.line
) {
return astInfo;
}
}
return null;
}
}

View File

@ -126,8 +126,5 @@ export type EvmCallStack = EvmCallStackEntry[];
export interface SourceSnippet { export interface SourceSnippet {
source: string; source: string;
fileName: string; fileName: string;
type: string;
node: Parser.ASTNode;
name: string | null;
range: SingleFileSourceRange; range: SingleFileSourceRange;
} }

View File

@ -8,6 +8,7 @@ import { ContractData, LineColumn, SingleFileSourceRange } from './types';
// This is the minimum length of valid contract bytecode. The Solidity compiler // This is the minimum length of valid contract bytecode. The Solidity compiler
// metadata is 86 bytes. If you add the '0x' prefix, we get 88. // metadata is 86 bytes. If you add the '0x' prefix, we get 88.
const MIN_CONTRACT_BYTECODE_LENGTH = 88; const MIN_CONTRACT_BYTECODE_LENGTH = 88;
const STATICCALL_GAS_COST = 40;
export const utils = { export const utils = {
compareLineColumn(lhs: LineColumn, rhs: LineColumn): number { compareLineColumn(lhs: LineColumn, rhs: LineColumn): number {
@ -76,10 +77,17 @@ export const utils = {
normalizeStructLogs(structLogs: StructLog[]): StructLog[] { normalizeStructLogs(structLogs: StructLog[]): StructLog[] {
if (structLogs[0].depth === 1) { if (structLogs[0].depth === 1) {
// Geth uses 1-indexed depth counter whilst ganache starts from 0 // Geth uses 1-indexed depth counter whilst ganache starts from 0
const newStructLogs = _.map(structLogs, structLog => ({ const newStructLogs = _.map(structLogs, structLog => {
...structLog, const newStructLog = {
depth: structLog.depth - 1, ...structLog,
})); depth: structLog.depth - 1,
};
if (newStructLog.op === 'STATICCALL') {
// HACK(leo): Geth traces sometimes returns those gas costs incorrectly as very big numbers so we manually fix them.
newStructLog.gasCost = STATICCALL_GAS_COST;
}
return newStructLog;
});
return newStructLogs; return newStructLogs;
} }
return structLogs; return structLogs;