manage generated sources

pull/374/head
yann300 4 years ago
parent 8f4f9118b4
commit 948332300a
  1. 31
      apps/remix-ide-e2e/src/tests/debugger.test.ts
  2. 59
      apps/remix-ide/src/app/tabs/debugger/debuggerUI.js
  3. 2
      apps/remix-ide/src/remixAppManager.js
  4. 2
      libs/remix-astwalker/src/astWalker.ts
  5. 15
      libs/remix-debug/src/Ethdebugger.js
  6. 1
      libs/remix-debug/src/cmdline/index.js
  7. 16
      libs/remix-debug/src/debugger/debugger.js
  8. 130
      libs/remix-debug/src/solidity-decoder/internalCallTree.js
  9. 30
      libs/remix-debug/src/solidity-decoder/solidityProxy.js
  10. 57
      libs/remix-debug/src/source/astWalker.js
  11. 25
      libs/remix-debug/src/source/sourceLocationTracker.js
  12. 4
      libs/remix-debug/test/debugger.js

@ -148,6 +148,26 @@ module.exports = {
.click('*[data-id="treeViewLoadMore"]') .click('*[data-id="treeViewLoadMore"]')
.assert.containsText('*[data-id="solidityLocals"]', '149: 0 uint256') .assert.containsText('*[data-id="solidityLocals"]', '149: 0 uint256')
.notContainsText('*[data-id="solidityLocals"]', '150: 0 uint256') .notContainsText('*[data-id="solidityLocals"]', '150: 0 uint256')
},
'Should debug using generated sources': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('solidity')
.setSolidityCompilerVersion('soljson-v0.7.2+commit.51b20bc0.js')
.clickLaunchIcon('udapp')
.testContracts('withGeneratedSources.sol', sources[4]['browser/withGeneratedSources.sol'], ['A'])
.createContract('')
.clickInstance(4)
.clickFunction('f - transact (not payable)', {types: 'uint256[] ', values: '[]'})
.debugTransaction(8)
.pause(2000)
.click('*[data-id="debuggerTransactionStartButton"]') // stop debugging
.click('*[data-id="debugGeneratedSourcesLabel"]') // select debug with generated sources
.click('*[data-id="debuggerTransactionStartButton"]') // start debugging
.pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.indexOf('if slt(sub(dataEnd, headStart), 32) { revert(0, 0) }') != -1, 'current displayed content is not a generated source')
})
.end() .end()
}, },
@ -227,6 +247,17 @@ const sources = [
} }
` `
} }
},
{
'browser/withGeneratedSources.sol': {
content: `
// SPDX-License-Identifier: GPL-3.0
pragma experimental ABIEncoderV2;
contract A {
function f(uint[] memory) public returns (uint256) { }
}
`
}
} }
] ]

@ -22,6 +22,26 @@ var css = csjs`
.statusMessage { .statusMessage {
margin-left: 15px; margin-left: 15px;
} }
.debuggerConfig {
display: flex;
align-items: center;
}
.debuggerConfig label {
margin: 0;
}
.debuggerSection {
padding: 12px 24px 16px;
}
.debuggerLabel {
margin-bottom: 2px;
font-size: 11px;
line-height: 12px;
text-transform: uppercase;
}
` `
class DebuggerUI { class DebuggerUI {
@ -32,7 +52,9 @@ class DebuggerUI {
this.event = new EventManager() this.event = new EventManager()
this.isActive = false this.isActive = false
this.opt = {
debugWithGeneratedSources: false
}
this.sourceHighlighter = new SourceHighlighter() this.sourceHighlighter = new SourceHighlighter()
this.startTxBrowser() this.startTxBrowser()
@ -72,12 +94,29 @@ class DebuggerUI {
this.isActive = isActive this.isActive = isActive
}) })
this.debugger.event.register('newSourceLocation', async (lineColumnPos, rawLocation) => { this.debugger.event.register('newSourceLocation', async (lineColumnPos, rawLocation, generatedSources) => {
if (!lineColumnPos) return
const contracts = await this.fetchContractAndCompile( const contracts = await this.fetchContractAndCompile(
this.currentReceipt.contractAddress || this.currentReceipt.to, this.currentReceipt.contractAddress || this.currentReceipt.to,
this.currentReceipt) this.currentReceipt)
if (contracts) { if (contracts) {
const path = contracts.getSourceName(rawLocation.file) let path = contracts.getSourceName(rawLocation.file)
if (!path) {
// check in generated sources
for (const source of generatedSources) {
if (source.id === rawLocation.file) {
path = `.debugger/generated-sources/${source.name}`
let content
try {
content = await this.debuggerModule.call('fileManager', 'getFile', path, source.contents)
} catch (e) {}
if (content !== source.contents) {
await this.debuggerModule.call('fileManager', 'setFile', path, source.contents)
}
break
}
}
}
if (path) { if (path) {
await this.debuggerModule.call('editor', 'discardHighlight') await this.debuggerModule.call('editor', 'discardHighlight')
await this.debuggerModule.call('editor', 'highlight', lineColumnPos, path) await this.debuggerModule.call('editor', 'highlight', lineColumnPos, path)
@ -137,7 +176,8 @@ class DebuggerUI {
console.error(e) console.error(e)
} }
return null return null
} },
debugWithGeneratedSources: this.opt.debugWithGeneratedSources
}) })
this.listenToEvents() this.listenToEvents()
@ -167,7 +207,8 @@ class DebuggerUI {
console.error(e) console.error(e)
} }
return null return null
} },
debugWithGeneratedSources: false
}) })
debug.debugger.traceManager.traceRetriever.getTrace(hash, (error, trace) => { debug.debugger.traceManager.traceRetriever.getTrace(hash, (error, trace) => {
if (error) return reject(error) if (error) return reject(error)
@ -188,10 +229,16 @@ class DebuggerUI {
var view = yo` var view = yo`
<div> <div>
<div class="px-2"> <div class="px-2">
<div class="mt-3">
<p class="mt-2 ${css.debuggerLabel}">Debugger Configuration</p>
<div class="mt-2 ${css.debuggerConfig} custom-control custom-checkbox">
<input class="custom-control-input" id="debugGeneratedSourcesInput" onchange=${(event) => { this.opt.debugWithGeneratedSources = event.target.checked }} type="checkbox" title="Debug with generated sources">
<label data-id="debugGeneratedSourcesLabel" class="form-check-label custom-control-label" for="debugGeneratedSourcesInput">Debug Generated sources if available (>= Solidity 0.7.2)</label>
</div>
</div>
${this.txBrowser.render()} ${this.txBrowser.render()}
${this.stepManagerView} ${this.stepManagerView}
${this.debuggerHeadPanelsView} ${this.debuggerHeadPanelsView}
</div>
<div class="${css.statusMessage}">${this.statusMessage}</div> <div class="${css.statusMessage}">${this.statusMessage}</div>
${this.debuggerPanelsView} ${this.debuggerPanelsView}
</div> </div>

@ -10,7 +10,7 @@ const requiredModules = [ // services + layout views + system views
'terminal', 'settings', 'pluginManager'] 'terminal', 'settings', 'pluginManager']
export function isNative (name) { export function isNative (name) {
const nativePlugins = ['vyper', 'workshops'] const nativePlugins = ['vyper', 'workshops', 'debugger']
return nativePlugins.includes(name) || requiredModules.includes(name) return nativePlugins.includes(name) || requiredModules.includes(name)
} }

@ -12,7 +12,7 @@ const isObject = function(obj: any): boolean {
export function isAstNode(node: Record<string, unknown>): boolean { export function isAstNode(node: Record<string, unknown>): boolean {
return ( return (
isObject(node) && isObject(node) &&
'id' in node && // 'id' in node &&
'nodeType' in node && 'nodeType' in node &&
'src' in node 'src' in node
) )

@ -26,6 +26,7 @@ const {SolidityProxy, stateDecoder, localDecoder, InternalCallTree} = require('.
function Ethdebugger (opts) { function Ethdebugger (opts) {
this.compilationResult = opts.compilationResult || function (contractAddress) { return null } this.compilationResult = opts.compilationResult || function (contractAddress) { return null }
this.web3 = opts.web3 this.web3 = opts.web3
this.opts = opts
this.event = new EventManager() this.event = new EventManager()
@ -36,7 +37,12 @@ function Ethdebugger (opts) {
this.solidityProxy = new SolidityProxy({getCurrentCalledAddressAt: this.traceManager.getCurrentCalledAddressAt.bind(this.traceManager), getCode: this.codeManager.getCode.bind(this.codeManager)}) this.solidityProxy = new SolidityProxy({getCurrentCalledAddressAt: this.traceManager.getCurrentCalledAddressAt.bind(this.traceManager), getCode: this.codeManager.getCode.bind(this.codeManager)})
this.storageResolver = null this.storageResolver = null
this.callTree = new InternalCallTree(this.event, this.traceManager, this.solidityProxy, this.codeManager, { includeLocalVariables: true }) const includeLocalVariables = true
this.callTree = new InternalCallTree(this.event,
this.traceManager,
this.solidityProxy,
this.codeManager,
{ ...opts, includeLocalVariables})
} }
Ethdebugger.prototype.setManagers = function () { Ethdebugger.prototype.setManagers = function () {
@ -44,8 +50,13 @@ Ethdebugger.prototype.setManagers = function () {
this.codeManager = new CodeManager(this.traceManager) this.codeManager = new CodeManager(this.traceManager)
this.solidityProxy = new SolidityProxy({getCurrentCalledAddressAt: this.traceManager.getCurrentCalledAddressAt.bind(this.traceManager), getCode: this.codeManager.getCode.bind(this.codeManager)}) this.solidityProxy = new SolidityProxy({getCurrentCalledAddressAt: this.traceManager.getCurrentCalledAddressAt.bind(this.traceManager), getCode: this.codeManager.getCode.bind(this.codeManager)})
this.storageResolver = null this.storageResolver = null
const includeLocalVariables = true
this.callTree = new InternalCallTree(this.event, this.traceManager, this.solidityProxy, this.codeManager, { includeLocalVariables: true }) this.callTree = new InternalCallTree(this.event,
this.traceManager,
this.solidityProxy,
this.codeManager,
{ ...this.opts, includeLocalVariables})
this.event.trigger('managersChanged') this.event.trigger('managersChanged')
} }

@ -79,6 +79,7 @@ class CmdLine {
this.txHash = txNumber this.txHash = txNumber
this.debugger.debug(null, txNumber, null, () => { this.debugger.debug(null, txNumber, null, () => {
this.debugger.event.register('newSourceLocation', (lineColumnPos, rawLocation) => { this.debugger.event.register('newSourceLocation', (lineColumnPos, rawLocation) => {
if (!lineColumnPos) return
this.lineColumnPos = lineColumnPos this.lineColumnPos = lineColumnPos
this.rawLocation = rawLocation this.rawLocation = rawLocation
this.events.emit('source', [lineColumnPos, rawLocation]) this.events.emit('source', [lineColumnPos, rawLocation])

@ -18,7 +18,8 @@ function Debugger (options) {
this.debugger = new Ethdebugger({ this.debugger = new Ethdebugger({
web3: options.web3, web3: options.web3,
compilationResult: this.compilationResult debugWithGeneratedSources: options.debugWithGeneratedSources,
compilationResult: this.compilationResult,
}) })
const {traceManager, callTree, solidityProxy} = this.debugger const {traceManager, callTree, solidityProxy} = this.debugger
@ -59,8 +60,17 @@ Debugger.prototype.registerAndHighlightCodeItem = async function (index) {
this.debugger.callTree.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, index, compilationResultForAddress.data.contracts).then((rawLocation) => { this.debugger.callTree.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, index, compilationResultForAddress.data.contracts).then((rawLocation) => {
if (compilationResultForAddress && compilationResultForAddress.data) { if (compilationResultForAddress && compilationResultForAddress.data) {
var lineColumnPos = this.offsetToLineColumnConverter.offsetToLineColumn(rawLocation, rawLocation.file, compilationResultForAddress.source.sources, compilationResultForAddress.data.sources) const generatedSources = this.debugger.callTree.sourceLocationTracker.getGeneratedSourcesFromAddress(address)
this.event.trigger('newSourceLocation', [lineColumnPos, rawLocation]) const astSources = Object.assign({}, compilationResultForAddress.data.sources)
const sources = Object.assign({}, compilationResultForAddress.source.sources)
if (generatedSources) {
for (const genSource of generatedSources) {
astSources[genSource.name] = { id: genSource.id, ast: genSource.ast }
sources[genSource.name] = { content: genSource.contents }
}
}
var lineColumnPos = this.offsetToLineColumnConverter.offsetToLineColumn(rawLocation, rawLocation.file, sources, astSources)
this.event.trigger('newSourceLocation', [lineColumnPos, rawLocation, generatedSources])
} else { } else {
this.event.trigger('newSourceLocation', [null]) this.event.trigger('newSourceLocation', [null])
} }

@ -22,21 +22,24 @@ class InternalCallTree {
* @param {Object} traceManager - trace manager * @param {Object} traceManager - trace manager
* @param {Object} solidityProxy - solidity proxy * @param {Object} solidityProxy - solidity proxy
* @param {Object} codeManager - code manager * @param {Object} codeManager - code manager
* @param {Object} opts - { includeLocalVariables } * @param {Object} opts - { includeLocalVariables, debugWithGeneratedSources }
*/ */
constructor (debuggerEvent, traceManager, solidityProxy, codeManager, opts) { constructor (debuggerEvent, traceManager, solidityProxy, codeManager, opts) {
this.includeLocalVariables = opts.includeLocalVariables this.includeLocalVariables = opts.includeLocalVariables
this.debugWithGeneratedSources = opts.debugWithGeneratedSources
this.event = new EventManager() this.event = new EventManager()
this.solidityProxy = solidityProxy this.solidityProxy = solidityProxy
this.traceManager = traceManager this.traceManager = traceManager
this.sourceLocationTracker = new SourceLocationTracker(codeManager) this.sourceLocationTracker = new SourceLocationTracker(codeManager, { debugWithGeneratedSources: opts.debugWithGeneratedSources })
debuggerEvent.register('newTraceLoaded', (trace) => { debuggerEvent.register('newTraceLoaded', (trace) => {
this.reset() this.reset()
if (!this.solidityProxy.loaded()) { if (!this.solidityProxy.loaded()) {
this.event.trigger('callTreeBuildFailed', ['compilation result not loaded. Cannot build internal call tree']) this.event.trigger('callTreeBuildFailed', ['compilation result not loaded. Cannot build internal call tree'])
} else { } else {
// each recursive call to buildTree represent a new context (either call, delegatecall, internal function) // each recursive call to buildTree represent a new context (either call, delegatecall, internal function)
buildTree(this, 0, '', true).then((result) => { const calledAddress = traceManager.getCurrentCalledAddressAt(0)
const isCreation = traceHelper.isContractCreation(calledAddress)
buildTree(this, 0, '', true, isCreation).then((result) => {
if (result.error) { if (result.error) {
this.event.trigger('callTreeBuildFailed', [result.error]) this.event.trigger('callTreeBuildFailed', [result.error])
} else { } else {
@ -146,10 +149,10 @@ class InternalCallTree {
} }
} }
async function buildTree (tree, step, scopeId, isExternalCall) { async function buildTree (tree, step, scopeId, isExternalCall, isCreation) {
let subScope = 1 let subScope = 1
tree.scopeStarts[step] = scopeId tree.scopeStarts[step] = scopeId
tree.scopes[scopeId] = { firstStep: step, locals: {} } tree.scopes[scopeId] = { firstStep: step, locals: {}, isCreation }
function callDepthChange (step, trace) { function callDepthChange (step, trace) {
if (step + 1 < trace.length) { if (step + 1 < trace.length) {
@ -167,7 +170,7 @@ async function buildTree (tree, step, scopeId, isExternalCall) {
included.file === source.file) included.file === source.file)
} }
let currentSourceLocation = {start: -1, length: -1, file: -1} let currentSourceLocation = { start: -1, length: -1, file: -1 }
let previousSourceLocation = currentSourceLocation let previousSourceLocation = currentSourceLocation
while (step < tree.traceManager.trace.length) { while (step < tree.traceManager.trace.length) {
let sourceLocation let sourceLocation
@ -186,10 +189,11 @@ async function buildTree (tree, step, scopeId, isExternalCall) {
return { outStep: step, error: 'InternalCallTree - No source Location. ' + step } return { outStep: step, error: 'InternalCallTree - No source Location. ' + step }
} }
const isCallInstruction = traceHelper.isCallInstruction(tree.traceManager.trace[step]) const isCallInstruction = traceHelper.isCallInstruction(tree.traceManager.trace[step])
const isCreateInstruction = traceHelper.isCreateInstruction(tree.traceManager.trace[step])
// we are checking if we are jumping in a new CALL or in an internal function // we are checking if we are jumping in a new CALL or in an internal function
if (isCallInstruction || sourceLocation.jump === 'i') { if (isCallInstruction || sourceLocation.jump === 'i') {
try { try {
const externalCallResult = await buildTree(tree, step + 1, scopeId === '' ? subScope.toString() : scopeId + '.' + subScope, isCallInstruction) const externalCallResult = await buildTree(tree, step + 1, scopeId === '' ? subScope.toString() : scopeId + '.' + subScope, isCallInstruction, isCreateInstruction)
if (externalCallResult.error) { if (externalCallResult.error) {
return { outStep: step, error: 'InternalCallTree - ' + externalCallResult.error } return { outStep: step, error: 'InternalCallTree - ' + externalCallResult.error }
} else { } else {
@ -221,8 +225,19 @@ function createReducedTrace (tree, index) {
tree.reducedTrace.push(index) tree.reducedTrace.push(index)
} }
function includeVariableDeclaration (tree, step, sourceLocation, scopeId, newLocation, previousSourceLocation) { function getGeneratedSources (tree, scopeId, contractObj) {
const variableDeclaration = resolveVariableDeclaration(tree, step, sourceLocation) if (tree.debugWithGeneratedSources && contractObj && tree.scopes[scopeId]) {
return tree.scopes[scopeId].isCreation ? contractObj.contract.evm.bytecode.generatedSources : contractObj.contract.evm.deployedBytecode.generatedSources
}
return null
}
async function includeVariableDeclaration (tree, step, sourceLocation, scopeId, newLocation, previousSourceLocation) {
const contractObj = await tree.solidityProxy.contractObjectAt(step)
let states = null
const generatedSources = getGeneratedSources(tree, scopeId, contractObj)
const variableDeclaration = resolveVariableDeclaration(tree, sourceLocation, generatedSources)
// using the vm trace step, the current source location and the ast, // using the vm trace step, the current source location and the ast,
// we check if the current vm trace step target a new ast node of type VariableDeclaration // we check if the current vm trace step target a new ast node of type VariableDeclaration
// that way we know that there is a new local variable from here. // that way we know that there is a new local variable from here.
@ -231,60 +246,61 @@ function includeVariableDeclaration (tree, step, sourceLocation, scopeId, newLoc
const stack = tree.traceManager.getStackAt(step) const stack = tree.traceManager.getStackAt(step)
// the stack length at this point is where the value of the new local variable will be stored. // the stack length at this point is where the value of the new local variable will be stored.
// so, either this is the direct value, or the offset in memory. That depends on the type. // so, either this is the direct value, or the offset in memory. That depends on the type.
tree.solidityProxy.contractNameAt(step).then((contractName) => { if (variableDeclaration.name !== '') {
if (variableDeclaration.name !== '') { states = tree.solidityProxy.extractStatesDefinitions()
var states = tree.solidityProxy.extractStatesDefinitions() var location = typesUtil.extractLocationFromAstVariable(variableDeclaration)
var location = typesUtil.extractLocationFromAstVariable(variableDeclaration) location = location === 'default' ? 'storage' : location
location = location === 'default' ? 'storage' : location // we push the new local variable in our tree
// we push the new local variable in our tree tree.scopes[scopeId].locals[variableDeclaration.name] = {
tree.scopes[scopeId].locals[variableDeclaration.name] = { name: variableDeclaration.name,
name: variableDeclaration.name, type: decodeInfo.parseType(variableDeclaration.typeDescriptions.typeString, states, contractObj.name, location),
type: decodeInfo.parseType(variableDeclaration.typeDescriptions.typeString, states, contractName, location), stackDepth: stack.length,
stackDepth: stack.length, sourceLocation: sourceLocation
sourceLocation: sourceLocation
}
} }
}) }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
} }
// we check here if we are at the beginning inside a new function. // we check here if we are at the beginning inside a new function.
// if that is the case, we have to add to locals tree the inputs and output params // if that is the case, we have to add to locals tree the inputs and output params
const functionDefinition = resolveFunctionDefinition(tree, step, previousSourceLocation) const functionDefinition = resolveFunctionDefinition(tree, previousSourceLocation, generatedSources)
if (functionDefinition && (newLocation && traceHelper.isJumpDestInstruction(tree.traceManager.trace[step - 1]) || functionDefinition.kind === 'constructor')) { if (!functionDefinition) return
const previousIsJumpDest2 = traceHelper.isJumpDestInstruction(tree.traceManager.trace[step - 2])
const previousIsJumpDest1 = traceHelper.isJumpDestInstruction(tree.traceManager.trace[step - 1])
const isConstructor = functionDefinition.kind === 'constructor'
if (newLocation && (previousIsJumpDest1 || previousIsJumpDest2 || isConstructor)) {
tree.functionCallStack.push(step) tree.functionCallStack.push(step)
const functionDefinitionAndInputs = {functionDefinition, inputs: []} const functionDefinitionAndInputs = { functionDefinition, inputs: [] }
// means: the previous location was a function definition && JUMPDEST // means: the previous location was a function definition && JUMPDEST
// => we are at the beginning of the function and input/output are setup // => we are at the beginning of the function and input/output are setup
tree.solidityProxy.contractNameAt(step).then((contractName) => { // cached try {
try { const stack = tree.traceManager.getStackAt(step)
const stack = tree.traceManager.getStackAt(step) states = tree.solidityProxy.extractStatesDefinitions()
var states = tree.solidityProxy.extractStatesDefinitions() if (functionDefinition.parameters) {
if (functionDefinition.parameters) { let inputs = functionDefinition.parameters
let inputs = functionDefinition.parameters let outputs = functionDefinition.returnParameters
let outputs = functionDefinition.returnParameters // for (const element of functionDefinition.parameters) {
// for (const element of functionDefinition.parameters) { // if (element.nodeType === 'ParameterList') {
// if (element.nodeType === 'ParameterList') { // if (!inputs) inputs = element
// if (!inputs) inputs = element // else {
// else { // outputs = element
// outputs = element // break
// break // }
// } // }
// } // }
// } // input params
// input params if (inputs) {
if (inputs) { functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj.name, previousSourceLocation, stack.length, inputs.parameters.length, -1)
functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractName, previousSourceLocation, stack.length, inputs.parameters.length, -1)
}
// output params
if (outputs) addParams(outputs, tree, scopeId, states, contractName, previousSourceLocation, stack.length, 0, 1)
} }
} catch (error) { // output params
console.log(error) if (outputs) addParams(outputs, tree, scopeId, states, contractObj.name, previousSourceLocation, stack.length, 0, 1)
} }
}) } catch (error) {
console.log(error)
}
tree.functionDefinitionsByScope[scopeId] = functionDefinitionAndInputs tree.functionDefinitionsByScope[scopeId] = functionDefinitionAndInputs
} }
@ -292,13 +308,12 @@ function includeVariableDeclaration (tree, step, sourceLocation, scopeId, newLoc
// this extract all the variable declaration for a given ast and file // this extract all the variable declaration for a given ast and file
// and keep this in a cache // and keep this in a cache
function resolveVariableDeclaration (tree, step, sourceLocation) { function resolveVariableDeclaration (tree, sourceLocation, generatedSources) {
if (!tree.variableDeclarationByFile[sourceLocation.file]) { if (!tree.variableDeclarationByFile[sourceLocation.file]) {
const ast = tree.solidityProxy.ast(sourceLocation) const ast = tree.solidityProxy.ast(sourceLocation, generatedSources)
if (ast) { if (ast) {
tree.variableDeclarationByFile[sourceLocation.file] = extractVariableDeclarations(ast, tree.astWalker) tree.variableDeclarationByFile[sourceLocation.file] = extractVariableDeclarations(ast, tree.astWalker)
} else { } else {
// console.log('Ast not found for step ' + step + '. file ' + sourceLocation.file)
return null return null
} }
} }
@ -307,13 +322,12 @@ function resolveVariableDeclaration (tree, step, sourceLocation) {
// this extract all the function definition for a given ast and file // this extract all the function definition for a given ast and file
// and keep this in a cache // and keep this in a cache
function resolveFunctionDefinition (tree, step, sourceLocation) { function resolveFunctionDefinition (tree, sourceLocation, generatedSources) {
if (!tree.functionDefinitionByFile[sourceLocation.file]) { if (!tree.functionDefinitionByFile[sourceLocation.file]) {
const ast = tree.solidityProxy.ast(sourceLocation) const ast = tree.solidityProxy.ast(sourceLocation, generatedSources)
if (ast) { if (ast) {
tree.functionDefinitionByFile[sourceLocation.file] = extractFunctionDefinitions(ast, tree.astWalker) tree.functionDefinitionByFile[sourceLocation.file] = extractFunctionDefinitions(ast, tree.astWalker)
} else { } else {
// console.log('Ast not found for step ' + step + '. file ' + sourceLocation.file)
return null return null
} }
} }
@ -323,7 +337,7 @@ function resolveFunctionDefinition (tree, step, sourceLocation) {
function extractVariableDeclarations (ast, astWalker) { function extractVariableDeclarations (ast, astWalker) {
const ret = {} const ret = {}
astWalker.walkFull(ast, (node) => { astWalker.walkFull(ast, (node) => {
if (node.nodeType === 'VariableDeclaration') { if (node.nodeType === 'VariableDeclaration' || node.nodeType === 'YulVariableDeclaration') {
ret[node.src] = node ret[node.src] = node
} }
}) })
@ -333,7 +347,7 @@ function extractVariableDeclarations (ast, astWalker) {
function extractFunctionDefinitions (ast, astWalker) { function extractFunctionDefinitions (ast, astWalker) {
const ret = {} const ret = {}
astWalker.walkFull(ast, (node) => { astWalker.walkFull(ast, (node) => {
if (node.nodeType === 'FunctionDefinition') { if (node.nodeType === 'FunctionDefinition' || node.nodeType === 'YulFunctionDefinition') {
ret[node.src] = node ret[node.src] = node
} }
}) })

@ -39,15 +39,15 @@ class SolidityProxy {
* @param {Int} vmTraceIndex - index in the vm trave where to resolve the executed contract name * @param {Int} vmTraceIndex - index in the vm trave where to resolve the executed contract name
* @param {Function} cb - callback returns (error, contractName) * @param {Function} cb - callback returns (error, contractName)
*/ */
async contractNameAt (vmTraceIndex) { async contractObjectAt (vmTraceIndex) {
const address = this.getCurrentCalledAddressAt(vmTraceIndex) const address = this.getCurrentCalledAddressAt(vmTraceIndex)
if (this.cache.contractNameByAddress[address]) { if (this.cache.contractObjectByAddress[address]) {
return this.cache.contractNameByAddress[address] return this.cache.contractObjectByAddress[address]
} }
const code = await this.getCode(address) const code = await this.getCode(address)
const contractName = contractNameFromCode(this.contracts, code.bytecode, address) const contract = contractObjectFromCode(this.contracts, code.bytecode, address)
this.cache.contractNameByAddress[address] = contractName this.cache.contractObjectByAddress[address] = contract
return contractName return contract
} }
/** /**
@ -86,8 +86,8 @@ class SolidityProxy {
* @return {Object} - returns state variables of @args vmTraceIndex * @return {Object} - returns state variables of @args vmTraceIndex
*/ */
async extractStateVariablesAt (vmtraceIndex) { async extractStateVariablesAt (vmtraceIndex) {
const contractName = await this.contractNameAt(vmtraceIndex) const contract = await this.contractObjectAt(vmtraceIndex)
return this.extractStateVariables(contractName) return this.extractStateVariables(contract.name)
} }
/** /**
@ -96,9 +96,13 @@ class SolidityProxy {
* @param {Object} sourceLocation - source location containing the 'file' to retrieve the AST from * @param {Object} sourceLocation - source location containing the 'file' to retrieve the AST from
* @return {Object} - AST of the current file * @return {Object} - AST of the current file
*/ */
ast (sourceLocation) { ast (sourceLocation, generatedSources) {
const file = this.fileNameFromIndex(sourceLocation.file) const file = this.fileNameFromIndex(sourceLocation.file)
if (this.sources[file]) { if (!file && generatedSources && generatedSources.length) {
for (const source of generatedSources) {
if (source.id === sourceLocation.file) return source.ast
}
} else if (this.sources[file]) {
return this.sources[file].ast return this.sources[file].ast
} }
return null return null
@ -115,13 +119,13 @@ class SolidityProxy {
} }
} }
function contractNameFromCode (contracts, code, address) { function contractObjectFromCode (contracts, code, address) {
const isCreation = traceHelper.isContractCreation(address) const isCreation = traceHelper.isContractCreation(address)
for (let file in contracts) { for (let file in contracts) {
for (let contract in contracts[file]) { for (let contract in contracts[file]) {
const bytecode = isCreation ? contracts[file][contract].evm.bytecode.object : contracts[file][contract].evm.deployedBytecode.object const bytecode = isCreation ? contracts[file][contract].evm.bytecode.object : contracts[file][contract].evm.deployedBytecode.object
if (util.compareByteCode(code, '0x' + bytecode)) { if (util.compareByteCode(code, '0x' + bytecode)) {
return contract return { name: contract, contract: contracts[file][contract] }
} }
} }
} }
@ -133,7 +137,7 @@ class Cache {
this.reset() this.reset()
} }
reset () { reset () {
this.contractNameByAddress = {} this.contractObjectByAddress = {}
this.stateVariablesByContractName = {} this.stateVariablesByContractName = {}
this.contractDeclarations = null this.contractDeclarations = null
this.statesDefinitions = null this.statesDefinitions = null

@ -0,0 +1,57 @@
'use strict'
/**
* Crawl the given AST through the function walk(ast, callback)
*/
function AstWalker () {} // eslint-disable-line
/**
* visit all the AST nodes
*
* @param {Object} ast - AST node
* @param {Object or Function} callback - if (Function) the function will be called for every node.
* - if (Object) callback[<Node Type>] will be called for
* every node of type <Node Type>. callback["*"] will be called for all other nodes.
* in each case, if the callback returns false it does not descend into children.
* If no callback for the current type, children are visited.
*/
AstWalker.prototype.walk = function (ast, callback) {
if (callback instanceof Function) {
callback = { '*': callback }
}
if (!('*' in callback)) {
callback['*'] = function () { return true }
}
const nodes = ast.nodes || (ast.body && ast.body.statements) || ast.declarations || ast.statements
if (nodes && ast.initializationExpression) { // 'for' loop handling
nodes.push(ast.initializationExpression)
}
if (manageCallBack(ast, callback) && nodes && nodes.length > 0) {
for (let k in nodes) {
const child = nodes[k]
this.walk(child, callback)
}
}
}
/**
* walk the given @astList
*
* @param {Object} sourcesList - sources list (containing root AST node)
* @param {Function} - callback used by AstWalker to compute response
*/
AstWalker.prototype.walkAstList = function (sourcesList, callback) {
const walker = new AstWalker()
for (let k in sourcesList) {
walker.walk(sourcesList[k].ast, callback)
}
}
function manageCallBack (node, callback) {
if (node.nodeType in callback) {
return callback[node.nodeType](node)
} else {
return callback['*'](node)
}
}
module.exports = AstWalker

@ -9,7 +9,10 @@ const util = remixLib.util
/** /**
* Process the source code location for the current executing bytecode * Process the source code location for the current executing bytecode
*/ */
function SourceLocationTracker (_codeManager) { function SourceLocationTracker (_codeManager, { debugWithGeneratedSources }) {
this.opts = {
debugWithGeneratedSources: debugWithGeneratedSources || false
}
this.codeManager = _codeManager this.codeManager = _codeManager
this.event = new EventManager() this.event = new EventManager()
this.sourceMappingDecoder = new SourceMappingDecoder() this.sourceMappingDecoder = new SourceMappingDecoder()
@ -25,7 +28,7 @@ function SourceLocationTracker (_codeManager) {
*/ */
SourceLocationTracker.prototype.getSourceLocationFromInstructionIndex = async function (address, index, contracts) { SourceLocationTracker.prototype.getSourceLocationFromInstructionIndex = async function (address, index, contracts) {
const sourceMap = await extractSourceMap(this, this.codeManager, address, contracts) const sourceMap = await extractSourceMap(this, this.codeManager, address, contracts)
return this.sourceMappingDecoder.atIndex(index, sourceMap) return this.sourceMappingDecoder.atIndex(index, sourceMap.map)
} }
/** /**
@ -38,7 +41,19 @@ SourceLocationTracker.prototype.getSourceLocationFromInstructionIndex = async fu
SourceLocationTracker.prototype.getSourceLocationFromVMTraceIndex = async function (address, vmtraceStepIndex, contracts) { SourceLocationTracker.prototype.getSourceLocationFromVMTraceIndex = async function (address, vmtraceStepIndex, contracts) {
const sourceMap = await extractSourceMap(this, this.codeManager, address, contracts) const sourceMap = await extractSourceMap(this, this.codeManager, address, contracts)
const index = this.codeManager.getInstructionIndex(address, vmtraceStepIndex) const index = this.codeManager.getInstructionIndex(address, vmtraceStepIndex)
return this.sourceMappingDecoder.atIndex(index, sourceMap) return this.sourceMappingDecoder.atIndex(index, sourceMap.map)
}
/**
* Returns the generated sources from a specific @arg address
*
* @param {String} address - contract address from which has generated sources
* @param {Object} generatedSources - Object containing the sourceid, ast and the source code.
*/
SourceLocationTracker.prototype.getGeneratedSourcesFromAddress = function (address) {
if (!this.opts.debugWithGeneratedSources) return null
if (this.sourceMapByAddress[address]) return this.sourceMapByAddress[address].generatedSources
return null
} }
/** /**
@ -73,7 +88,9 @@ function getSourceMap (address, code, contracts) {
bytes = isCreation ? bytecode.object : deployedBytecode.object bytes = isCreation ? bytecode.object : deployedBytecode.object
if (util.compareByteCode(code, '0x' + bytes)) { if (util.compareByteCode(code, '0x' + bytes)) {
return isCreation ? bytecode.sourceMap : deployedBytecode.sourceMap const generatedSources = isCreation ? bytecode.generatedSources : deployedBytecode.generatedSources
const map = isCreation ? bytecode.sourceMap : deployedBytecode.sourceMap
return { generatedSources, map }
} }
} }
} }

@ -1,4 +1,5 @@
var tape = require('tape') var tape = require('tape')
var deepequal = require('deep-equal')
var remixLib = require('@remix-project/remix-lib') var remixLib = require('@remix-project/remix-lib')
var compilerInput = require('./helpers/compilerHelper').compilerInput var compilerInput = require('./helpers/compilerHelper').compilerInput
var SourceMappingDecoder = require('../src/source/sourceMappingDecoder') var SourceMappingDecoder = require('../src/source/sourceMappingDecoder')
@ -263,7 +264,8 @@ function testDebugging (debugManager) {
const location = await debugManager.sourceLocationFromVMTraceIndex(address, 330) const location = await debugManager.sourceLocationFromVMTraceIndex(address, 330)
debugManager.decodeLocalsAt(330, location, (error, decodedlocals) => { debugManager.decodeLocalsAt(330, location, (error, decodedlocals) => {
if (error) return t.end(error) if (error) return t.end(error)
t.equal(JSON.stringify(decodedlocals), JSON.stringify(tested))
t.ok(deepequal(decodedlocals, tested), `locals does not match. expected: ${JSON.stringify(tested)} - current: ${decodedlocals}`)
}) })
} catch (error) { } catch (error) {
return t.end(error) return t.end(error)

Loading…
Cancel
Save