diff --git a/README.md b/README.md index fe07092..e069c3b 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,8 @@ And there is more to come 🙌 stay tuned! ![vscode-auditor-flaterra](https://user-images.githubusercontent.com/2865694/55907553-5db8d000-5bd7-11e9-8a11-8cef3964e284.gif) * list all function signatures (human readable or json format) ![vscode-auditor-funcsigs](https://user-images.githubusercontent.com/2865694/55907153-3f9ea000-5bd6-11e9-8a47-e69a762963e9.gif) +* list all custom error signatures (human readable or json format) +* list all event signatures (human readable or json format) * open remix in external browser Please refer to the extension's contribution section to show an up-to-date list of commands. diff --git a/package.json b/package.json index 02e92a7..3871597 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,16 @@ "onCommand:solidity-va.tools.flattenCandidates", "onCommand:solidity-va.tools.function.signatures", "onCommand:solidity-va.tools.function.signatures.json", + "onCommand:solidity-va.tools.function.signatures.forWorkspace", "onCommand:solidity-va.tools.function.signatures.forWorkspace.json", + "onCommand:solidity-va.tools.error.signatures", + "onCommand:solidity-va.tools.error.signatures.json", + "onCommand:solidity-va.tools.error.signatures.forWorkspace", + "onCommand:solidity-va.tools.error.signatures.forWorkspace.json", + "onCommand:solidity-va.tools.event.signatures", + "onCommand:solidity-va.tools.event.signatures.json", + "onCommand:solidity-va.tools.event.signatures.forWorkspace", + "onCommand:solidity-va.tools.event.signatures.forWorkspace.json", "onCommand:solidity-va.tools.remix.openExternal", "onCommand:solidity-va.cockpit.explorer.refresh", "onCommand:solidity-va.cockpit.topLevelContracts.refresh", @@ -232,9 +241,54 @@ "title": "Tools - list function signatures (json)", "category": "Solidity Visual Developer" }, + { + "command": "solidity-va.tools.function.signatures.forWorkspace", + "title": "Tools - list function signatures for workspace", + "category": "Solidity Visual Developer" + }, { "command": "solidity-va.tools.function.signatures.forWorkspace.json", - "title": "Tools - list function signatures for all solidity files in workspace (json)", + "title": "Tools - list function signatures for workspace (json)", + "category": "Solidity Visual Developer" + }, + { + "command": "solidity-va.tools.error.signatures", + "title": "Tools - list custom error signatures", + "category": "Solidity Visual Developer" + }, + { + "command": "solidity-va.tools.error.signatures.json", + "title": "Tools - list custom error signatures (json)", + "category": "Solidity Visual Developer" + }, + { + "command": "solidity-va.tools.error.signatures.forWorkspace", + "title": "Tools - list custom error signatures for workspace", + "category": "Solidity Visual Developer" + }, + { + "command": "solidity-va.tools.error.signatures.forWorkspace.json", + "title": "Tools - list custom error signatures for workspace (json)", + "category": "Solidity Visual Developer" + }, + { + "command": "solidity-va.tools.event.signatures", + "title": "Tools - list event signatures", + "category": "Solidity Visual Developer" + }, + { + "command": "solidity-va.tools.event.signatures.json", + "title": "Tools - list event signatures (json)", + "category": "Solidity Visual Developer" + }, + { + "command": "solidity-va.tools.event.signatures.forWorkspace", + "title": "Tools - list event signatures for workspace", + "category": "Solidity Visual Developer" + }, + { + "command": "solidity-va.tools.event.signatures.forWorkspace.json", + "title": "Tools - list event signatures for workspace (json)", "category": "Solidity Visual Developer" }, { @@ -443,6 +497,16 @@ "default": true, "description": "Enable/Disable codelens 'funcSigs'" }, + "solidity-va.codelens.errorSigs.enable": { + "type": "boolean", + "default": true, + "description": "Enable/Disable codelens 'errorSigs'" + }, + "solidity-va.codelens.eventSigs.enable": { + "type": "boolean", + "default": true, + "description": "Enable/Disable codelens 'eventSigs'" + }, "solidity-va.codelens.uml.enable": { "type": "boolean", "default": true, diff --git a/src/extension.js b/src/extension.js index 51ae9fe..0c337f5 100644 --- a/src/extension.js +++ b/src/extension.js @@ -631,6 +631,15 @@ function onActivate(context) { ) ); + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.function.signatures.forWorkspace', + function (doc) { + commands.listFunctionSignaturesForWorkspace(false); + } + ) + ); + context.subscriptions.push( vscode.commands.registerCommand( 'solidity-va.tools.function.signatures.forWorkspace.json', @@ -649,6 +658,87 @@ function onActivate(context) { ) ); + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.error.signatures', + function (doc, asJson) { + commands.listErrorSignatures(doc || vscode.window.activeTextEditor.document, asJson); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.error.signatures.json', + function (doc) { + commands.listErrorSignatures(doc || vscode.window.activeTextEditor.document, true); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.error.signatures.forWorkspace', + function (doc) { + commands.listErrorSignaturesForWorkspace(false); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.error.signatures.forWorkspace.json', + function (doc) { + commands.listErrorSignaturesForWorkspace(true); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.error.signatureForAstItem', + function (item) { + commands.listErrorSignatureForAstItem(item); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.event.signatures', + function (doc, asJson) { + commands.listEventSignatures(doc || vscode.window.activeTextEditor.document, asJson); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.event.signatures.json', + function (doc) { + commands.listEventSignatures(doc || vscode.window.activeTextEditor.document, true); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.event.signatures.forWorkspace', + function (doc) { + commands.listEventSignaturesForWorkspace(false); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.event.signatures.forWorkspace.json', + function (doc) { + commands.listEventSignaturesForWorkspace(true); + } + ) + ); + context.subscriptions.push( vscode.commands.registerCommand( 'solidity-va.tools.remix.openExternal', diff --git a/src/features/codelens.js b/src/features/codelens.js index d59c0e5..27a7fa2 100644 --- a/src/features/codelens.js +++ b/src/features/codelens.js @@ -112,6 +112,26 @@ class SolidityCodeLensProvider { ) ); + config.errorSigs.enable && codeLens.push( + new vscode.CodeLens( + firstLine, { + command: 'solidity-va.tools.error.signatures', + title: 'errorSigs', + arguments: [document] + } + ) + ); + + config.eventSigs.enable && codeLens.push( + new vscode.CodeLens( + firstLine, { + command: 'solidity-va.tools.event.signatures', + title: 'eventSigs', + arguments: [document] + } + ) + ); + let parser = this.g_workspace.sourceUnits[document.uri.fsPath]; if(!parser) { console.warn("[ERR] parser was not ready while adding codelenses. omitting contract specific lenses."); @@ -134,7 +154,7 @@ class SolidityCodeLensProvider { ); - let annotateContractTypes = ["contract","library", "abstract"]; + let annotateContractTypes = ["contract", "library", "abstract"]; /** all contract decls */ for(let contractObj of Object.values(parser.contracts)){ if(token.isCancellationRequested){ @@ -148,6 +168,12 @@ class SolidityCodeLensProvider { for(let funcObj of contractObj.functions){ codeLens = codeLens.concat(this.onFunctionDecl(document, contractObj.name, funcObj)); } + + for(let node of contractObj._node.subNodes){ + if (node.type == 'CustomErrorDefinition') { + codeLens = codeLens.concat(this.onCustomErrorDecl(node)); + } + } } else if (contractObj._node.kind == "interface"){ // add uml to interface let item = contractObj; @@ -213,7 +239,7 @@ class SolidityCodeLensProvider { }) ); //exclude constructor (item._node.name == null) - config.funcSigs.enable && item._node.name && lenses.push(new vscode.CodeLens(range, { + config.funcSigs.enable && item._node.name && ['public', 'external'].includes(item._node.visibility) && lenses.push(new vscode.CodeLens(range, { command: 'solidity-va.tools.function.signatureForAstItem', title: 'funcSig', arguments: [item] @@ -222,6 +248,22 @@ class SolidityCodeLensProvider { return lenses; } + + onCustomErrorDecl(item) { + let lenses = []; + let range = elemLocToRange(item); + + let config = settings.extensionConfig().codelens; + + config.errorSigs.enable && lenses.push(new vscode.CodeLens(range, { + command: 'solidity-va.tools.error.signatureForAstItem', + title: 'errorSig', + arguments: [item] + }) + ); + + return lenses; + } } module.exports = { diff --git a/src/features/commands.js b/src/features/commands.js index 5dcfc83..ae8b971 100644 --- a/src/features/commands.js +++ b/src/features/commands.js @@ -526,87 +526,119 @@ ${topLevelContractsText}`; } async listFunctionSignatures(document, asJson) { - let sighash_colls = mod_utils.functionSignatureExtractor(document.getText()); - let sighashes = sighash_colls.sighashes; + const filename = this._getCurrentFilename(); + const data = mod_utils.functionSignatureExtractor(document.getText()); + this._showSignatures({ [filename]: data }, 'Function', asJson); + } - if(sighash_colls.collisions.length){ - vscode.window.showErrorMessage('🔥 FuncSig collisions detected! ' + sighash_colls.collisions.join(",")); - } + async listFunctionSignaturesForWorkspace(asJson) { + this._listSignaturesForWorkspace('functionSignatureExtractor', 'Function', asJson); + } - let content; - if(asJson){ - content = JSON.stringify(sighashes); - } else { - content = "Sighash | Function Signature\n========================\n"; - for(let hash in sighashes){ - content += hash + " => " + sighashes[hash] + "\n"; - } - if(sighash_colls.collisions.length){ - content += "\n\n"; - content += "collisions 🔥🔥🔥 \n========================\n"; - content += sighash_colls.collisions.join("\n"); - } - } - vscode.workspace.openTextDocument({content: content, language: "markdown"}) - .then(doc => vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside)); + async listFunctionSignatureForAstItem(item, asJson) { + let sighashes = mod_utils.functionSignatureFromAstNode(item); + this._showSignatures({ '': {sighashes, collisions: [] } }, 'Function', asJson); } - async listFunctionSignaturesForWorkspace(asJson) { + async listErrorSignatures(document, asJson) { + const filename = this._getCurrentFilename(); + let data = mod_utils.errorSignatureExtractor(document.getText()); + this._showSignatures({ [filename]: data }, 'Custom Error', asJson); + } - let sighashes = {}; - let collisions = []; + async listErrorSignaturesForWorkspace(asJson) { + this._listSignaturesForWorkspace('errorSignatureExtractor', 'Custom Error', asJson); + } - await vscode.workspace.findFiles("**/*.sol", settings.DEFAULT_FINDFILES_EXCLUDES, 500) - .then(uris => { - uris.forEach(uri => { - try { - let sig_colls = mod_utils.functionSignatureExtractor(fs.readFileSync(uri.fsPath).toString('utf-8')); - collisions = collisions.concat(sig_colls.collisions); //we're not yet checking sighash collisions across contracts + async listErrorSignatureForAstItem(item, asJson) { + let sighashes = mod_utils.errorSignatureFromAstNode(item); + this._showSignatures({ '': {sighashes, collisions: [] } }, 'Custom Error', asJson); + } - let currSigs = sig_colls.sighashes; - for(let k in currSigs){ - sighashes[k]=currSigs[k]; - } - } catch (e) {} - }); + async listEventSignatures(document, asJson) { + let sighash_colls = mod_utils.eventSignatureExtractor(document.getText()); + this._showSignatures({ [this._getCurrentFilename()]: sighash_colls }, 'Event', asJson); + } + + async listEventSignaturesForWorkspace(asJson) { + this._listSignaturesForWorkspace('eventSignatureExtractor', 'Event', asJson); + } + + async _listSignaturesForWorkspace(functionName, sigType, asJson) { + const filenames = {}; + + await vscode.workspace.findFiles("**/*.sol", settings.DEFAULT_FINDFILES_EXCLUDES, 500) + .then(uris => { + uris.forEach(uri => { + try { + let sig_colls = mod_utils[functionName](fs.readFileSync(uri.fsPath).toString('utf-8')); + + filenames[this._getCleanFilename(uri.fsPath)] = sig_colls; + } catch (e) {} }); + }); - if(collisions.length){ - vscode.window.showErrorMessage('🔥 FuncSig collisions detected! ' + collisions.join(",")); - } + this._showSignatures(filenames, sigType, asJson); + } - let content; - if(asJson){ - content = JSON.stringify(sighashes); - } else { - content = "Sighash | Function Signature\n======================== \n"; - for(let hash in sighashes){ - content += hash + " => " + sighashes[hash] + " \n"; + _getCurrentFilename() { + return this._getCleanFilename(vscode.window.activeTextEditor.document.fileName); + } + + _getCleanFilename(fileName) { + return fileName.replace(vscode.workspace.workspaceFolders[0].uri.path, '.'); + } + + async _showSignatures(filenames, sigType, asJson, header) { + let content = ''; + let language = 'plaintext'; + + // just one file + if (Object.keys(filenames).length == 1) { + const filename = Object.keys(filenames)[0]; + const {sighashes, collisions = []} = filenames[filename]; + + if (Object.keys(sighashes).length == 0) { + return vscode.window.showErrorMessage('❌ No ' + sigType + ' signatures detected in ' + filename + '!'); } + if(collisions.length){ - content += "\n\n"; - content += "collisions 🔥🔥🔥 \n========================\n"; - content += collisions.join("\n"); + vscode.window.showErrorMessage('🔥 ' + sigType + ' signature collisions detected for ' + filename); } } - vscode.workspace.openTextDocument({content: content, language: "markdown"}) - .then(doc => vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside)); - } - - async listFunctionSignatureForAstItem(item, asJson) { - - let sighashes = mod_utils.functionSignatureFromAstNode(item); - let content; if(asJson){ - content = JSON.stringify(sighashes); + const sighashes = Object.keys(filenames).reduce((acc, filename) => { + // add each sighash to the accumulator + Object.keys(filenames[filename].sighashes).forEach(sighash => { + acc[sighash] = filenames[filename].sighashes[sighash]; + }); + return acc; + }, {}); + + content = JSON.stringify(sighashes, null, 2); + language = 'json' } else { - content = "Sighash | Function Signature\n======================== \n"; - for(let hash in sighashes){ - content += hash + " => " + sighashes[hash] + " \n"; + for(let filename in filenames){ + const {sighashes, collisions = []} = filenames[filename]; + + content += `SigHash ${sigType} ${filename}\n\n`; + content += "Hashes | " + sigType + " Signatures\n===================================\n"; + for(let hash in sighashes){ + content += hash + " => " + sighashes[hash] + "\n"; + } + if(collisions.length){ + content += "\n\n"; + content += "Collisions 🔥🔥🔥 \n===================================\n"; + content += collisions.join("\n"); + } + content += "\n\n"; } + + content = content.trim(); } - vscode.workspace.openTextDocument({content: content, language: "markdown"}) + + vscode.workspace.openTextDocument({content: content, language: language}) .then(doc => vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside)); } diff --git a/src/features/utils.js b/src/features/utils.js index 0cec7d1..d163e19 100644 --- a/src/features/utils.js +++ b/src/features/utils.js @@ -65,16 +65,30 @@ function canonicalizeEvmType(evmArg) { const foundings = groups.pop(); return `${TYPE_ALIASES[foundings.type]}${foundings.tail}`; } - return evmArg.replace(evmTypeRegex, replacer); + return evmArg && evmArg.replace(evmTypeRegex, replacer); } function functionSignatureExtractor(content) { - const funcSigRegex = /function\s+(?[^\(\s]+)\s?\((?[^\)]*)\)/g; + return _signatureExtractor(/function\s+(?[^\(\s]+)\s?\((?[^\)]*)\)(?[^{]*)/g, content); +} + +function errorSignatureExtractor(content) { + return _signatureExtractor(/error\s+(?[^\(\s]+)\s?\((?[^\)]*)\)/g, content); +} + +function eventSignatureExtractor(content) { + return _signatureExtractor(/event\s+(?[^\(\s]+)\s?\((?[^\)]*)\)/g, content); +} + +function _signatureExtractor(regex, content) { let match; let sighashes = {}; let collisions = []; // cleanup newlines, cleanup comment blocks - while (match = funcSigRegex.exec(content)) { + while (match = regex.exec(content)) { + if (/private|internal/.test(match.groups.extra)) { + continue; + } let args = match.groups.args.replace(commentRegex(), "").split(",").map(item => canonicalizeEvmType(item.trim().split(" ")[0])); let fnsig = `${match.groups.name.trim()}(${args.join(',')})`; let sighash = createKeccakHash('keccak256').update(fnsig).digest('hex').toString('hex').slice(0, 8); @@ -107,25 +121,28 @@ function getCanonicalizedArgumentFromAstNode(node){ } else { return null; } -} - -function functionSignatureFromAstNode(item){ +} - let funcname = item._node.name; +function signatureFromAstNode(item){ + const node = item._node || item; + const name = node.name; - let argsItem = item._node.parameters.type === "ParameterList" ? item._node.parameters.parameters : item._node.parameters; - let args = argsItem.map(o => canonicalizeEvmType(getCanonicalizedArgumentFromAstNode(o))); + const argsItem = node.parameters.type === "ParameterList" ? node.parameters.parameters : node.parameters; + const args = argsItem.map(o => canonicalizeEvmType(getCanonicalizedArgumentFromAstNode(o))); - let fnsig = `${funcname}(${args.join(',')})`; - let sighash = createKeccakHash('keccak256').update(fnsig).digest('hex').toString('hex').slice(0, 8); + const sig = `${name}(${args.join(',')})`; + const sighash = createKeccakHash('keccak256').update(sig).digest('hex').toString('hex').slice(0, 8); - let result = {}; - result[sighash] = fnsig; + const result = {}; + result[sighash] = sig; return result; } module.exports = { CommentMapperRex : CommentMapperRex, functionSignatureExtractor : functionSignatureExtractor, - functionSignatureFromAstNode : functionSignatureFromAstNode + errorSignatureExtractor : errorSignatureExtractor, + eventSignatureExtractor : eventSignatureExtractor, + functionSignatureFromAstNode : signatureFromAstNode, + errorSignatureFromAstNode : signatureFromAstNode, }; \ No newline at end of file