diff --git a/package.json b/package.json index 332a9de..bb83fb9 100644 --- a/package.json +++ b/package.json @@ -243,6 +243,16 @@ "type": "boolean", "default": true, "description": "Enable/Disable all active components of this extension (emergency)." + }, + "solidity-va.uml.options": { + "type": "text", + "default": "", + "description": "Add custom uml options" + }, + "solidity-va.uml.actors.enable": { + "type": "boolean", + "default": false, + "description": "Enable/Disable actors in uml" } } } diff --git a/src/extension.js b/src/extension.js index 753f318..c661251 100644 --- a/src/extension.js +++ b/src/extension.js @@ -718,6 +718,15 @@ function onActivate(context) { ) ) + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.tools.function.signatureForAstItem', + function (item) { + commands.listFunctionSignatureForAstItem(item) + } + ) + ) + context.subscriptions.push( vscode.commands.registerCommand( 'solidity-va.tools.remix.openExternal', @@ -727,6 +736,15 @@ function onActivate(context) { ) ) + context.subscriptions.push( + vscode.commands.registerCommand( + 'solidity-va.uml.contract.outline', + function (doc, contractObjects) { + commands.umlContractsOutline(contractObjects) + } + ) + ) + /** event setup */ /***** DidChange */ vscode.window.onDidChangeActiveTextEditor(editor => { @@ -746,6 +764,12 @@ function onActivate(context) { onDidSave(document); }, null, context.subscriptions); + /****** OnOpen */ + vscode.workspace.onDidOpenTextDocument(document => { + onDidSave(document); + }, null, context.subscriptions); + + context.subscriptions.push( vscode.languages.registerHoverProvider(type, { provideHover(document, position, token) { diff --git a/src/features/codelens.js b/src/features/codelens.js index 8be0c9a..a9c590d 100644 --- a/src/features/codelens.js +++ b/src/features/codelens.js @@ -111,6 +111,13 @@ class SolidityCodeLensProvider { ) ) + codeLens.push(new vscode.CodeLens(firstLine, { + command: 'solidity-va.uml.contract.outline', + title: 'uml', + arguments: [document, Object.values(parser.contracts)] + }) + ) + let annotateContractTypes = ["contract","library"] /** all contract decls */ for(let name in parser.contracts){ @@ -147,6 +154,13 @@ class SolidityCodeLensProvider { arguments: [document, item.name, []] }) ) + + lenses.push(new vscode.CodeLens(range, { + command: 'solidity-va.uml.contract.outline', + title: 'uml', + arguments: [document, [item]] + }) + ) return lenses } @@ -161,6 +175,14 @@ class SolidityCodeLensProvider { arguments: [document, contractName+"::"+item._node.name, "all", document.uri.path] }) ) + + lenses.push(new vscode.CodeLens(range, { + command: 'solidity-va.tools.function.signatureForAstItem', + title: 'funcSig', + arguments: [item] + }) + ) + return lenses } } diff --git a/src/features/commands.js b/src/features/commands.js index 47a025d..49b12a6 100644 --- a/src/features/commands.js +++ b/src/features/commands.js @@ -15,6 +15,7 @@ const settings = require('../settings') const mod_templates = require('./templates'); const mod_utils = require('./utils.js') +const mod_symbols = require('./symbols.js') const surya = require('surya') @@ -410,7 +411,13 @@ ${topLevelContractsText}` } async listFunctionSignatures(document, asJson){ - let sighashes = mod_utils.functionSignatureExtractor(document.getText()) + let sighash_colls = mod_utils.functionSignatureExtractor(document.getText()) + let sighashes = sighash_colls.sighashes; + + if(sighash_colls.collisions){ + vscode.window.showErrorMessage('🔥 FuncSig collisions detected! ' + sighash_colls.collisions.join(",")) + } + let content if(asJson){ content = JSON.stringify(sighashes) @@ -419,6 +426,11 @@ ${topLevelContractsText}` for(let hash in sighashes){ content += hash + " => " + sighashes[hash] + "\n" } + if(sighash_colls.collisions){ + 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)) @@ -427,12 +439,16 @@ ${topLevelContractsText}` async listFunctionSignaturesForWorkspace(asJson){ let sighashes = {} + let collisions = [] await vscode.workspace.findFiles("**/*.sol",'**/node_modules', 500) .then(uris => { uris.forEach(uri => { try { - let currSigs = mod_utils.functionSignatureExtractor(fs.readFileSync(uri.path).toString('utf-8')); + let sig_colls = mod_utils.functionSignatureExtractor(fs.readFileSync(uri.path).toString('utf-8')); + collisions = collisions.concat(sig_colls.collisions) //we're not yet checking sighash collisions across contracts + + let currSigs = sig_colls.sighashes; for(let k in currSigs){ sighashes[k]=currSigs[k] } @@ -440,6 +456,10 @@ ${topLevelContractsText}` }) }) + if(collisions){ + vscode.window.showErrorMessage('🔥 FuncSig collisions detected! ' + collisions.join(",")) + } + let content if(asJson){ content = JSON.stringify(sighashes) @@ -448,10 +468,163 @@ ${topLevelContractsText}` for(let hash in sighashes){ content += hash + " => " + sighashes[hash] + " \n" } + if(collisions){ + content += "\n\n"; + content += "collisions 🔥🔥🔥 \n========================\n" + content += 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); + + 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" + } + } + vscode.workspace.openTextDocument({content: content, language: "markdown"}) + .then(doc => vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside)) + } + + async umlContractsOutline(contractObjects){ + const ENABLE_ACTORS = false; + + const stateMutabilityToIcon = { + view:"🔍", + pure:"🔍", + constant:"🔍", + payable:"💰" + } + + const functionVisibility = { + "public": '+', + "external": '+', //~ + "internal": '#', //mapped to protected; ot + "private": '-' , + "default": '+' // public + } + const variableVisibility = { + "public": '+', + "external": '+', //~ + "internal": '#', //mapped to protected; ot + "private": '-' , + "default": "#" // internal + } + const contractNameMapping = { + "contract":"class", + "interface":"interface", + "library":"abstract" + } + + function _mapAstFunctionName(name) { + switch(name) { + case null: + return "**__constructor__**"; + case "": + return "**__fallback__**"; + default: + return name; + } + } + + let content = `@startuml +' -- for auto-render install: https://marketplace.visualstudio.com/items?itemName=jebbs.plantuml +' -- options -- +${solidityVAConfig.uml.options} +${solidityVAConfig.uml.actors.enable ? "allowmixing": ""} + +' -- classes -- +` + + content += contractObjects.reduce((umlTxt, contractObj) => { + + return umlTxt + `\n +${contractNameMapping[contractObj._node.kind] || "class"} ${contractObj.name} { + ' -- inheritance -- +${Object.values(contractObj.dependencies).reduce((txt, name) => { + return txt + `\t{abstract}${name}\n` + },"") +} + ' -- usingFor -- +${Object.values(contractObj.usingFor).reduce((txt, astNode) => { + return txt + `\t{abstract}📚${astNode.libraryName} for [[${mod_symbols.getVariableDeclarationType(astNode)}]]\n` + },"") +} + ' -- vars -- +${Object.values(contractObj.stateVars).reduce((umlSvarTxt, astNode) => { + return umlSvarTxt + `\t${variableVisibility[astNode.visibility] || ""}${astNode.isDeclaredConst?"{static}":""}[[${mod_symbols.getVariableDeclarationType(astNode).replace(/\(/g,"").replace(/\)/g,"")}]] ${astNode.name}\n` + }, "") +} + ' -- methods -- +${Object.values(contractObj.functions).reduce((umlFuncTxt, funcObj) => { + return umlFuncTxt + `\t${functionVisibility[funcObj._node.visibility] || ""}${stateMutabilityToIcon[funcObj._node.stateMutability]||""}${_mapAstFunctionName(funcObj._node.name)}()\n` + }, "") +} +} +` +}, "") + + content += "' -- inheritance / usingFor --\n" + contractObjects.reduce((umlTxt, contractObj) => { + return umlTxt + + Object.values(contractObj.dependencies).reduce((txt, name) => { + return txt + `${contractObj.name} <|--[#DarkGoldenRod] ${name}\n` + }, "") + + Object.values(contractObj.usingFor).reduce((txt, astNode) => { + return txt + `${contractObj.name} <|..[#DarkOliveGreen] ${astNode.libraryName} : //for ${mod_symbols.getVariableDeclarationType(astNode)}//\n` + }, "") + }, "") + + + if(solidityVAConfig.uml.actors.enable){ + //lets see if we can get actors as well :) + + let addresses = [] + + for (let contractObj of contractObjects) { + addresses = addresses.concat(Object.values(contractObj.stateVars).filter(astNode => !astNode.isDeclaredConst && astNode.typeName.name =="address").map(astNode => astNode.name)) + for (let fidx in contractObj.functions){ + let functionObj = contractObj.functions[fidx] + addresses = addresses.concat(Object.values(functionObj.arguments).filter(astNode => astNode.typeName.name =="address").map(astNode => astNode.name)) + } + } + + let actors = [...new Set(addresses)] + actors = actors.filter( item => { + if (item === null) return false // no nulls + if (item.startsWith("_") && actors.indexOf(item.slice(1))) return false // no _ dupes + return true + }) + console.error(actors) + + content += ` +' -- actors -- +together { +${actors.reduce((txt, name) => txt + `\tactor ${name}\n`, "")} +} +` + } + + content += "\n@enduml" + + vscode.workspace.openTextDocument({content: content, language: "plantuml"}) + .then(doc => vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) + .then(editor => { + vscode.commands.executeCommand("plantuml.preview") + .catch(error => { + //command does not exist + }) + }) + ) + } } diff --git a/src/features/symbols.js b/src/features/symbols.js index cbaf071..c1b80ef 100644 --- a/src/features/symbols.js +++ b/src/features/symbols.js @@ -580,5 +580,6 @@ class SolidityDocumentSymbolProvider{ module.exports = { - SolidityDocumentSymbolProvider:SolidityDocumentSymbolProvider + SolidityDocumentSymbolProvider:SolidityDocumentSymbolProvider, + getVariableDeclarationType:getVariableDeclarationType } \ No newline at end of file diff --git a/src/features/utils.js b/src/features/utils.js index dbee927..2e595f0 100644 --- a/src/features/utils.js +++ b/src/features/utils.js @@ -66,16 +66,22 @@ function functionSignatureExtractor(content) { const funcSigRegex = /function\s+(?[^\(\s]+)\s?\((?[^\)]*)\)/g let match; let sighashes = {} + let collisions = []; while (match = funcSigRegex.exec(content)) { let args = [] match.groups.args.split(",").forEach(item => { args.push(canonicalizeEvmType(item.trim().split(" ")[0])) }) - let fnsig = `${match.groups.name.trim()}(${args.join(',')})` - sighashes[createKeccakHash('keccak256').update(fnsig).digest('hex').toString('hex').slice(0, 8)] = fnsig + let fnsig = `${match.groups.name.trim()}(${args.join(',')})`; + let sighash = createKeccakHash('keccak256').update(fnsig).digest('hex').toString('hex').slice(0, 8); + + if(sighash in sighashes && sighashes[sighash]!==fnsig){ + collisions.push(sighash) + } + sighashes[sighash] = fnsig } - return sighashes + return {sighashes:sighashes, collisions:collisions} } function getCanonicalizedArgumentFromAstNode(node){