diff --git a/apps/remix-ide-e2e/src/tests/debugger.test.ts b/apps/remix-ide-e2e/src/tests/debugger.test.ts index 69bc648801..f95ae93049 100644 --- a/apps/remix-ide-e2e/src/tests/debugger.test.ts +++ b/apps/remix-ide-e2e/src/tests/debugger.test.ts @@ -147,7 +147,27 @@ module.exports = { .waitForElementPresent('*[data-id="treeViewLoadMore"]') .click('*[data-id="treeViewLoadMore"]') .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() }, @@ -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) { } + } + ` + } } ] @@ -293,4 +324,4 @@ const localVariable_step717_ABIEncoder = { "error": "", "type": "bytes" } -} \ No newline at end of file +} diff --git a/apps/remix-ide/src/app/tabs/debugger/debuggerUI.js b/apps/remix-ide/src/app/tabs/debugger/debuggerUI.js index f37bda0d0c..927efd55b4 100644 --- a/apps/remix-ide/src/app/tabs/debugger/debuggerUI.js +++ b/apps/remix-ide/src/app/tabs/debugger/debuggerUI.js @@ -22,6 +22,26 @@ var css = csjs` .statusMessage { 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 { @@ -32,7 +52,9 @@ class DebuggerUI { this.event = new EventManager() this.isActive = false - + this.opt = { + debugWithGeneratedSources: false + } this.sourceHighlighter = new SourceHighlighter() this.startTxBrowser() @@ -72,12 +94,29 @@ class DebuggerUI { 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( this.currentReceipt.contractAddress || this.currentReceipt.to, this.currentReceipt) 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) { await this.debuggerModule.call('editor', 'discardHighlight') await this.debuggerModule.call('editor', 'highlight', lineColumnPos, path) @@ -137,7 +176,8 @@ class DebuggerUI { console.error(e) } return null - } + }, + debugWithGeneratedSources: this.opt.debugWithGeneratedSources }) this.listenToEvents() @@ -167,7 +207,8 @@ class DebuggerUI { console.error(e) } return null - } + }, + debugWithGeneratedSources: false }) debug.debugger.traceManager.traceRetriever.getTrace(hash, (error, trace) => { if (error) return reject(error) @@ -186,12 +227,18 @@ class DebuggerUI { this.stepManagerView = yo`
` var view = yo` -
+
+
+

Debugger Configuration

+
+ { this.opt.debugWithGeneratedSources = event.target.checked }} type="checkbox" title="Debug with generated sources"> + +
+
${this.txBrowser.render()} ${this.stepManagerView} - ${this.debuggerHeadPanelsView} -
+ ${this.debuggerHeadPanelsView}
${this.statusMessage}
${this.debuggerPanelsView}
diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 6dbd09aac8..5062df1c99 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -10,7 +10,7 @@ const requiredModules = [ // services + layout views + system views 'terminal', 'settings', 'pluginManager'] export function isNative (name) { - const nativePlugins = ['vyper', 'workshops'] + const nativePlugins = ['vyper', 'workshops', 'debugger'] return nativePlugins.includes(name) || requiredModules.includes(name) } diff --git a/libs/remix-astwalker/src/astWalker.ts b/libs/remix-astwalker/src/astWalker.ts index 2eea5b75d9..a65085931b 100644 --- a/libs/remix-astwalker/src/astWalker.ts +++ b/libs/remix-astwalker/src/astWalker.ts @@ -12,7 +12,7 @@ const isObject = function(obj: any): boolean { export function isAstNode(node: Record): boolean { return ( isObject(node) && - 'id' in node && + // 'id' in node && 'nodeType' in node && 'src' in node ) diff --git a/libs/remix-debug/src/Ethdebugger.js b/libs/remix-debug/src/Ethdebugger.js index d15d55458f..85d6060ae9 100644 --- a/libs/remix-debug/src/Ethdebugger.js +++ b/libs/remix-debug/src/Ethdebugger.js @@ -26,6 +26,7 @@ const {SolidityProxy, stateDecoder, localDecoder, InternalCallTree} = require('. function Ethdebugger (opts) { this.compilationResult = opts.compilationResult || function (contractAddress) { return null } this.web3 = opts.web3 + this.opts = opts 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.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 () { @@ -44,8 +50,13 @@ Ethdebugger.prototype.setManagers = function () { 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.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') } diff --git a/libs/remix-debug/src/cmdline/index.js b/libs/remix-debug/src/cmdline/index.js index 8e928f7ad0..1a0d5f4e6a 100644 --- a/libs/remix-debug/src/cmdline/index.js +++ b/libs/remix-debug/src/cmdline/index.js @@ -79,6 +79,7 @@ class CmdLine { this.txHash = txNumber this.debugger.debug(null, txNumber, null, () => { this.debugger.event.register('newSourceLocation', (lineColumnPos, rawLocation) => { + if (!lineColumnPos) return this.lineColumnPos = lineColumnPos this.rawLocation = rawLocation this.events.emit('source', [lineColumnPos, rawLocation]) diff --git a/libs/remix-debug/src/debugger/debugger.js b/libs/remix-debug/src/debugger/debugger.js index ec94fb5b68..609cafbdc4 100644 --- a/libs/remix-debug/src/debugger/debugger.js +++ b/libs/remix-debug/src/debugger/debugger.js @@ -18,7 +18,8 @@ function Debugger (options) { this.debugger = new Ethdebugger({ web3: options.web3, - compilationResult: this.compilationResult + debugWithGeneratedSources: options.debugWithGeneratedSources, + compilationResult: this.compilationResult, }) 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) => { if (compilationResultForAddress && compilationResultForAddress.data) { - var lineColumnPos = this.offsetToLineColumnConverter.offsetToLineColumn(rawLocation, rawLocation.file, compilationResultForAddress.source.sources, compilationResultForAddress.data.sources) - this.event.trigger('newSourceLocation', [lineColumnPos, rawLocation]) + const generatedSources = this.debugger.callTree.sourceLocationTracker.getGeneratedSourcesFromAddress(address) + 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 { this.event.trigger('newSourceLocation', [null]) } diff --git a/libs/remix-debug/src/solidity-decoder/internalCallTree.js b/libs/remix-debug/src/solidity-decoder/internalCallTree.js index cd94aa80bc..dc37cf5f43 100644 --- a/libs/remix-debug/src/solidity-decoder/internalCallTree.js +++ b/libs/remix-debug/src/solidity-decoder/internalCallTree.js @@ -22,21 +22,24 @@ class InternalCallTree { * @param {Object} traceManager - trace manager * @param {Object} solidityProxy - solidity proxy * @param {Object} codeManager - code manager - * @param {Object} opts - { includeLocalVariables } + * @param {Object} opts - { includeLocalVariables, debugWithGeneratedSources } */ constructor (debuggerEvent, traceManager, solidityProxy, codeManager, opts) { this.includeLocalVariables = opts.includeLocalVariables + this.debugWithGeneratedSources = opts.debugWithGeneratedSources this.event = new EventManager() this.solidityProxy = solidityProxy this.traceManager = traceManager - this.sourceLocationTracker = new SourceLocationTracker(codeManager) + this.sourceLocationTracker = new SourceLocationTracker(codeManager, { debugWithGeneratedSources: opts.debugWithGeneratedSources }) debuggerEvent.register('newTraceLoaded', (trace) => { this.reset() if (!this.solidityProxy.loaded()) { this.event.trigger('callTreeBuildFailed', ['compilation result not loaded. Cannot build internal call tree']) } else { // 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) { this.event.trigger('callTreeBuildFailed', [result.error]) } 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 tree.scopeStarts[step] = scopeId - tree.scopes[scopeId] = { firstStep: step, locals: {} } + tree.scopes[scopeId] = { firstStep: step, locals: {}, isCreation } function callDepthChange (step, trace) { if (step + 1 < trace.length) { @@ -167,7 +170,7 @@ async function buildTree (tree, step, scopeId, isExternalCall) { included.file === source.file) } - let currentSourceLocation = {start: -1, length: -1, file: -1} + let currentSourceLocation = { start: -1, length: -1, file: -1 } let previousSourceLocation = currentSourceLocation while (step < tree.traceManager.trace.length) { let sourceLocation @@ -186,10 +189,11 @@ async function buildTree (tree, step, scopeId, isExternalCall) { return { outStep: step, error: 'InternalCallTree - No source Location. ' + 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 if (isCallInstruction || sourceLocation.jump === 'i') { 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) { return { outStep: step, error: 'InternalCallTree - ' + externalCallResult.error } } else { @@ -221,8 +225,19 @@ function createReducedTrace (tree, index) { tree.reducedTrace.push(index) } -function includeVariableDeclaration (tree, step, sourceLocation, scopeId, newLocation, previousSourceLocation) { - const variableDeclaration = resolveVariableDeclaration(tree, step, sourceLocation) +function getGeneratedSources (tree, scopeId, contractObj) { + 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, // 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. @@ -231,60 +246,61 @@ function includeVariableDeclaration (tree, step, sourceLocation, scopeId, newLoc const stack = tree.traceManager.getStackAt(step) // 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. - tree.solidityProxy.contractNameAt(step).then((contractName) => { - if (variableDeclaration.name !== '') { - var states = tree.solidityProxy.extractStatesDefinitions() - var location = typesUtil.extractLocationFromAstVariable(variableDeclaration) - location = location === 'default' ? 'storage' : location - // we push the new local variable in our tree - tree.scopes[scopeId].locals[variableDeclaration.name] = { - name: variableDeclaration.name, - type: decodeInfo.parseType(variableDeclaration.typeDescriptions.typeString, states, contractName, location), - stackDepth: stack.length, - sourceLocation: sourceLocation - } + if (variableDeclaration.name !== '') { + states = tree.solidityProxy.extractStatesDefinitions() + var location = typesUtil.extractLocationFromAstVariable(variableDeclaration) + location = location === 'default' ? 'storage' : location + // we push the new local variable in our tree + tree.scopes[scopeId].locals[variableDeclaration.name] = { + name: variableDeclaration.name, + type: decodeInfo.parseType(variableDeclaration.typeDescriptions.typeString, states, contractObj.name, location), + stackDepth: stack.length, + sourceLocation: sourceLocation } - }) + } } catch (error) { console.log(error) } } // 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 - const functionDefinition = resolveFunctionDefinition(tree, step, previousSourceLocation) - if (functionDefinition && (newLocation && traceHelper.isJumpDestInstruction(tree.traceManager.trace[step - 1]) || functionDefinition.kind === 'constructor')) { + const functionDefinition = resolveFunctionDefinition(tree, previousSourceLocation, generatedSources) + 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) - const functionDefinitionAndInputs = {functionDefinition, inputs: []} + const functionDefinitionAndInputs = { functionDefinition, inputs: [] } // means: the previous location was a function definition && JUMPDEST // => we are at the beginning of the function and input/output are setup - tree.solidityProxy.contractNameAt(step).then((contractName) => { // cached - try { - const stack = tree.traceManager.getStackAt(step) - var states = tree.solidityProxy.extractStatesDefinitions() - if (functionDefinition.parameters) { - let inputs = functionDefinition.parameters - let outputs = functionDefinition.returnParameters - // for (const element of functionDefinition.parameters) { - // if (element.nodeType === 'ParameterList') { - // if (!inputs) inputs = element - // else { - // outputs = element - // break - // } - // } - // } - // input params - if (inputs) { - 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) + try { + const stack = tree.traceManager.getStackAt(step) + states = tree.solidityProxy.extractStatesDefinitions() + if (functionDefinition.parameters) { + let inputs = functionDefinition.parameters + let outputs = functionDefinition.returnParameters + // for (const element of functionDefinition.parameters) { + // if (element.nodeType === 'ParameterList') { + // if (!inputs) inputs = element + // else { + // outputs = element + // break + // } + // } + // } + // input params + if (inputs) { + functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj.name, previousSourceLocation, stack.length, inputs.parameters.length, -1) } - } catch (error) { - console.log(error) + // output params + if (outputs) addParams(outputs, tree, scopeId, states, contractObj.name, previousSourceLocation, stack.length, 0, 1) } - }) + } catch (error) { + console.log(error) + } 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 // and keep this in a cache -function resolveVariableDeclaration (tree, step, sourceLocation) { +function resolveVariableDeclaration (tree, sourceLocation, generatedSources) { if (!tree.variableDeclarationByFile[sourceLocation.file]) { - const ast = tree.solidityProxy.ast(sourceLocation) + const ast = tree.solidityProxy.ast(sourceLocation, generatedSources) if (ast) { tree.variableDeclarationByFile[sourceLocation.file] = extractVariableDeclarations(ast, tree.astWalker) } else { - // console.log('Ast not found for step ' + step + '. file ' + sourceLocation.file) return null } } @@ -307,13 +322,12 @@ function resolveVariableDeclaration (tree, step, sourceLocation) { // this extract all the function definition for a given ast and file // and keep this in a cache -function resolveFunctionDefinition (tree, step, sourceLocation) { +function resolveFunctionDefinition (tree, sourceLocation, generatedSources) { if (!tree.functionDefinitionByFile[sourceLocation.file]) { - const ast = tree.solidityProxy.ast(sourceLocation) + const ast = tree.solidityProxy.ast(sourceLocation, generatedSources) if (ast) { tree.functionDefinitionByFile[sourceLocation.file] = extractFunctionDefinitions(ast, tree.astWalker) } else { - // console.log('Ast not found for step ' + step + '. file ' + sourceLocation.file) return null } } @@ -323,7 +337,7 @@ function resolveFunctionDefinition (tree, step, sourceLocation) { function extractVariableDeclarations (ast, astWalker) { const ret = {} astWalker.walkFull(ast, (node) => { - if (node.nodeType === 'VariableDeclaration') { + if (node.nodeType === 'VariableDeclaration' || node.nodeType === 'YulVariableDeclaration') { ret[node.src] = node } }) @@ -333,7 +347,7 @@ function extractVariableDeclarations (ast, astWalker) { function extractFunctionDefinitions (ast, astWalker) { const ret = {} astWalker.walkFull(ast, (node) => { - if (node.nodeType === 'FunctionDefinition') { + if (node.nodeType === 'FunctionDefinition' || node.nodeType === 'YulFunctionDefinition') { ret[node.src] = node } }) diff --git a/libs/remix-debug/src/solidity-decoder/solidityProxy.js b/libs/remix-debug/src/solidity-decoder/solidityProxy.js index ac3d7052cb..d9fbc74ce6 100644 --- a/libs/remix-debug/src/solidity-decoder/solidityProxy.js +++ b/libs/remix-debug/src/solidity-decoder/solidityProxy.js @@ -39,15 +39,15 @@ class SolidityProxy { * @param {Int} vmTraceIndex - index in the vm trave where to resolve the executed contract name * @param {Function} cb - callback returns (error, contractName) */ - async contractNameAt (vmTraceIndex) { + async contractObjectAt (vmTraceIndex) { const address = this.getCurrentCalledAddressAt(vmTraceIndex) - if (this.cache.contractNameByAddress[address]) { - return this.cache.contractNameByAddress[address] + if (this.cache.contractObjectByAddress[address]) { + return this.cache.contractObjectByAddress[address] } const code = await this.getCode(address) - const contractName = contractNameFromCode(this.contracts, code.bytecode, address) - this.cache.contractNameByAddress[address] = contractName - return contractName + const contract = contractObjectFromCode(this.contracts, code.bytecode, address) + this.cache.contractObjectByAddress[address] = contract + return contract } /** @@ -86,8 +86,8 @@ class SolidityProxy { * @return {Object} - returns state variables of @args vmTraceIndex */ async extractStateVariablesAt (vmtraceIndex) { - const contractName = await this.contractNameAt(vmtraceIndex) - return this.extractStateVariables(contractName) + const contract = await this.contractObjectAt(vmtraceIndex) + return this.extractStateVariables(contract.name) } /** @@ -96,9 +96,13 @@ class SolidityProxy { * @param {Object} sourceLocation - source location containing the 'file' to retrieve the AST from * @return {Object} - AST of the current file */ - ast (sourceLocation) { + ast (sourceLocation, generatedSources) { 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 null @@ -115,13 +119,13 @@ class SolidityProxy { } } -function contractNameFromCode (contracts, code, address) { +function contractObjectFromCode (contracts, code, address) { const isCreation = traceHelper.isContractCreation(address) for (let file in contracts) { for (let contract in contracts[file]) { const bytecode = isCreation ? contracts[file][contract].evm.bytecode.object : contracts[file][contract].evm.deployedBytecode.object if (util.compareByteCode(code, '0x' + bytecode)) { - return contract + return { name: contract, contract: contracts[file][contract] } } } } @@ -133,7 +137,7 @@ class Cache { this.reset() } reset () { - this.contractNameByAddress = {} + this.contractObjectByAddress = {} this.stateVariablesByContractName = {} this.contractDeclarations = null this.statesDefinitions = null diff --git a/libs/remix-debug/src/source/astWalker.js b/libs/remix-debug/src/source/astWalker.js new file mode 100644 index 0000000000..30ca6d3b1c --- /dev/null +++ b/libs/remix-debug/src/source/astWalker.js @@ -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[] will be called for + * every node of 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 diff --git a/libs/remix-debug/src/source/sourceLocationTracker.js b/libs/remix-debug/src/source/sourceLocationTracker.js index 5612633b0c..9d8362b03e 100644 --- a/libs/remix-debug/src/source/sourceLocationTracker.js +++ b/libs/remix-debug/src/source/sourceLocationTracker.js @@ -9,7 +9,10 @@ const util = remixLib.util /** * 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.event = new EventManager() this.sourceMappingDecoder = new SourceMappingDecoder() @@ -25,7 +28,7 @@ function SourceLocationTracker (_codeManager) { */ SourceLocationTracker.prototype.getSourceLocationFromInstructionIndex = async function (address, index, 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) { const sourceMap = await extractSourceMap(this, this.codeManager, address, contracts) 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 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 } } } } diff --git a/libs/remix-debug/test/debugger.js b/libs/remix-debug/test/debugger.js index 43d8a9e5f8..aba3a821db 100644 --- a/libs/remix-debug/test/debugger.js +++ b/libs/remix-debug/test/debugger.js @@ -1,4 +1,5 @@ var tape = require('tape') +var deepequal = require('deep-equal') var remixLib = require('@remix-project/remix-lib') var compilerInput = require('./helpers/compilerHelper').compilerInput var SourceMappingDecoder = require('../src/source/sourceMappingDecoder') @@ -263,7 +264,8 @@ function testDebugging (debugManager) { const location = await debugManager.sourceLocationFromVMTraceIndex(address, 330) debugManager.decodeLocalsAt(330, location, (error, decodedlocals) => { 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) { return t.end(error)