diff --git a/package.json b/package.json index 8a8a436..50f1a8e 100644 --- a/package.json +++ b/package.json @@ -512,10 +512,11 @@ "type": "string", "enum": [ "hardhat", - "truffle" + "truffle", + "forge" ], - "default": "truffle", - "description": "Select which default template to use when creating a unitTest stub (e.g. truffle, hardhat)" + "default": "forge", + "description": "Select which default template to use when creating a unitTest stub (e.g. truffle, hardhat, forge)" }, "solidity-va.debug.parser.showExceptions": { "type": "boolean", diff --git a/src/extension.js b/src/extension.js index 992247d..0ad9e2b 100644 --- a/src/extension.js +++ b/src/extension.js @@ -254,10 +254,10 @@ function onActivate(context) { context.subscriptions.push( vscode.commands.registerCommand( "solidity-va.test.createTemplate", - function (doc, contractName) { + function (doc, contract) { commands.generateUnittestStubForContract( doc || vscode.window.activeTextEditor.document, - contractName, + contract, ); }, ), diff --git a/src/features/codelens.js b/src/features/codelens.js index 312aea1..b738e89 100644 --- a/src/features/codelens.js +++ b/src/features/codelens.js @@ -198,7 +198,7 @@ class ParserLensProvider { new vscode.CodeLens(range, { command: "solidity-va.test.createTemplate", title: "UnitTest stub", - arguments: [document, item.name], + arguments: [document, item], }), ); diff --git a/src/features/commands.js b/src/features/commands.js index a78dee1..f4005ed 100644 --- a/src/features/commands.js +++ b/src/features/commands.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; /** * @author github.com/tintinweb * @license GPLv3 @@ -6,66 +6,66 @@ * * */ -const vscode = require("vscode"); -const fs = require("fs"); -const child_process = require("child_process"); -const path = require("path"); +const vscode = require('vscode'); +const fs = require('fs'); +const child_process = require('child_process'); +const path = require('path'); -const settings = require("../settings"); +const settings = require('../settings'); -const mod_templates = require("./templates"); -const mod_utils = require("./utils"); +const mod_templates = require('./templates'); +const mod_utils = require('./utils'); -const { DrawIoCsvWriter } = require("./writer/drawio"); -const { PlantumlWriter } = require("./writer/plantuml"); +const { DrawIoCsvWriter } = require('./writer/drawio'); +const { PlantumlWriter } = require('./writer/plantuml'); -const surya = require("surya"); +const surya = require('surya'); const suryaDefaultColorSchemeDark = { digraph: { - bgcolor: "#2e3e56", + bgcolor: '#2e3e56', nodeAttribs: { - style: "filled", - fillcolor: "#edad56", - color: "#edad56", - penwidth: "3", + style: 'filled', + fillcolor: '#edad56', + color: '#edad56', + penwidth: '3', }, edgeAttribs: { - color: "#fcfcfc", - penwidth: "2", - fontname: "helvetica Neue Ultra Light", + color: '#fcfcfc', + penwidth: '2', + fontname: 'helvetica Neue Ultra Light', }, }, visibility: { isFilled: true, - public: "#FF9797", - external: "#ffbdb9", - private: "#edad56", - internal: "#f2c383", + public: '#FF9797', + external: '#ffbdb9', + private: '#edad56', + internal: '#f2c383', }, nodeType: { isFilled: false, - shape: "doubleoctagon", - modifier: "#1bc6a6", - payable: "brown", + shape: 'doubleoctagon', + modifier: '#1bc6a6', + payable: 'brown', }, call: { - default: "white", - regular: "#1bc6a6", - this: "#80e097", + default: 'white', + regular: '#1bc6a6', + this: '#80e097', }, contract: { defined: { - bgcolor: "#445773", - color: "#445773", - fontcolor: "#f0f0f0", - style: "rounded", + bgcolor: '#445773', + color: '#445773', + fontcolor: '#f0f0f0', + style: 'rounded', }, undefined: { - bgcolor: "#3b4b63", - color: "#e8726d", - fontcolor: "#f0f0f0", - style: "rounded,dashed", + bgcolor: '#3b4b63', + color: '#e8726d', + fontcolor: '#f0f0f0', + style: 'rounded,dashed', }, }, }; @@ -74,26 +74,26 @@ function runCommand(cmd, args, env, cwd, stdin) { cwd = cwd || vscode.workspace.rootPath; return new Promise((resolve, reject) => { - console.log(`running command: ${cmd} ${args.join(" ")}`); + console.log(`running command: ${cmd} ${args.join(' ')}`); let p = child_process.execFile( cmd, args, { env: env, cwd: cwd }, (err, stdout, stderr) => { - p.stdout.on("data", function (data) { + p.stdout.on('data', function (data) { if (stdin) { - p.stdin.setEncoding("utf-8"); + p.stdin.setEncoding('utf-8'); p.stdin.write(stdin); p.stdin.end(); } }); if (err === null || err.code === 0) { - console.log("success"); + console.log('success'); return resolve(err); } err.stderr = stderr; return reject(err); - }, + } ); }); } @@ -106,54 +106,83 @@ class Commands { _checkIsSolidity(document) { if (!document || document.languageId != settings.languageId) { vscode.window.showErrorMessage( - `[Solidity VA] not a solidity source file ${vscode.window.activeTextEditor.document.uri.fsPath}`, + `[Solidity VA] not a solidity source file ${vscode.window.activeTextEditor.document.uri.fsPath}` ); - throw new Error("not a solidity file"); + throw new Error('not a solidity file'); } } - async generateUnittestStubForContract(document, contractName) { + async generateUnittestStubForContract(document, contract) { this._checkIsSolidity(document); let content; - if (settings.extensionConfig().test.defaultUnittestTemplate === "hardhat") { - content = mod_templates.generateHardhatUnittestStubForContract( - document, - this.g_parser, - contractName, - ); - } else { - content = mod_templates.generateUnittestStubForContract( - document, - this.g_parser, - contractName, - ); + let language; + const framework = settings.extensionConfig().test.defaultUnittestTemplate; + switch (framework) { + case 'hardhat': + content = mod_templates.generateHardhatUnittestStubForContract( + document, + this.g_workspace, + contract + ); + language = 'javascript'; + break; + case 'truffle': + content = mod_templates.generateTruffleUnittestStubForContract( + document, + this.g_workspace, + contract + ); + language = 'javascript'; + break; + case 'forge': + let generateForkStub = false; + await vscode.window + .showInformationMessage( + 'Do you want to add fork testing stub?', + 'Yes', + 'No' + ) + .then((answer) => { + if (answer === 'Yes') { + generateForkStub = true; + } + }); + content = mod_templates.generateForgeUnittestStubForContract( + document, + this.g_workspace, + contract, + generateForkStub + ); + language = 'solidity'; + break; + default: + throw new Error('Unsupported testing framework'); } - vscode.workspace - .openTextDocument({ content: content, language: "javascript" }) + .openTextDocument({ content: content, language: language }) .then((doc) => - vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside), + vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) ); } async surya(documentOrListItems, command, args) { //check if input was document or listItem if (!documentOrListItems) { - throw new Error("not a file or list item"); + throw new Error('not a file or list item'); } let ret; let files = []; - if (documentOrListItems.hasOwnProperty("children")) { + if (documentOrListItems.hasOwnProperty('children')) { //hack ;) documentOrListItems = [documentOrListItems]; //allow non array calls } if (Array.isArray(documentOrListItems)) { for (let documentOrListItem of documentOrListItems) { - if (documentOrListItem.hasOwnProperty("children")) { + if (documentOrListItem.hasOwnProperty('children')) { // is a list item -> item.resource.fsPath if (!!path.extname(documentOrListItem.resource.fsPath)) { //file @@ -164,13 +193,13 @@ class Commands { .findFiles( `${documentOrListItem.path}/**/*.sol`, settings.DEFAULT_FINDFILES_EXCLUDES, - 500, + 500 ) .then((uris) => { files = files.concat( uris.map(function (uri) { return uri.fsPath; - }), + }) ); }); } @@ -181,10 +210,10 @@ class Commands { this._checkIsSolidity(documentOrListItems); // throws if ( - settings.extensionConfig().tools.surya.input.contracts == "workspace" + settings.extensionConfig().tools.surya.input.contracts == 'workspace' ) { await vscode.workspace - .findFiles("**/*.sol", settings.DEFAULT_FINDFILES_EXCLUDES, 500) + .findFiles('**/*.sol', settings.DEFAULT_FINDFILES_EXCLUDES, 500) .then((uris) => { files = uris.map(function (uri) { return uri.fsPath; @@ -200,17 +229,17 @@ class Commands { } switch (command) { - case "describe": + case 'describe': ret = surya.describe(files, {}, true); vscode.workspace - .openTextDocument({ content: ret, language: "markdown" }) + .openTextDocument({ content: ret, language: 'markdown' }) .then((doc) => - vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside), + vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) ); break; - case "graphSimple": - case "graph": - if (command == "graphSimple") { + case 'graphSimple': + case 'graph': + if (command == 'graphSimple') { ret = surya.graphSimple(args || files, { colorScheme: suryaDefaultColorSchemeDark, }); @@ -221,11 +250,11 @@ class Commands { } //solidity-va.preview.render.markdown vscode.workspace - .openTextDocument({ content: ret, language: "dot" }) + .openTextDocument({ content: ret, language: 'dot' }) .then((doc) => { if (settings.extensionConfig().preview.dot) { vscode.commands - .executeCommand("graphviz-interactive-preview.preview.beside", { + .executeCommand('graphviz-interactive-preview.preview.beside', { document: doc, content: ret, callback: null, @@ -233,7 +262,7 @@ class Commands { }) .catch((error) => { vscode.commands - .executeCommand("interactive-graphviz.preview.beside", { + .executeCommand('interactive-graphviz.preview.beside', { document: doc, content: ret, callback: null, @@ -241,14 +270,14 @@ class Commands { }) //TODO: remove this in future version. only for transition to new command .catch((error) => { vscode.commands - .executeCommand("graphviz.previewToSide", doc.uri) + .executeCommand('graphviz.previewToSide', doc.uri) .catch((error) => { //command not available. fallback open as text and try graphviz.showPreview vscode.window .showTextDocument(doc, vscode.ViewColumn.Beside) .then((editor) => { vscode.commands - .executeCommand("graphviz.showPreview", editor) // creates new pane + .executeCommand('graphviz.showPreview', editor) // creates new pane .catch((error) => { //command not available - do nothing }); @@ -265,14 +294,14 @@ class Commands { .then(doc => vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside)) */ break; - case "inheritance": + case 'inheritance': ret = surya.inheritance(files, { draggable: false }); vscode.workspace - .openTextDocument({ content: ret, language: "dot" }) + .openTextDocument({ content: ret, language: 'dot' }) .then((doc) => { if (settings.extensionConfig().preview.dot) { vscode.commands - .executeCommand("graphviz-interactive-preview.preview.beside", { + .executeCommand('graphviz-interactive-preview.preview.beside', { document: doc, content: ret, callback: null, @@ -280,7 +309,7 @@ class Commands { }) .catch((error) => { vscode.commands - .executeCommand("interactive-graphviz.preview.beside", { + .executeCommand('interactive-graphviz.preview.beside', { document: doc, content: ret, callback: null, @@ -288,14 +317,14 @@ class Commands { }) //TODO: remove this in future version. only for transition to new command .catch((error) => { vscode.commands - .executeCommand("graphviz.previewToSide", doc.uri) + .executeCommand('graphviz.previewToSide', doc.uri) .catch((error) => { //command not available. fallback open as text and try graphviz.showPreview vscode.window .showTextDocument(doc, vscode.ViewColumn.Beside) .then((editor) => { vscode.commands - .executeCommand("graphviz.showPreview", editor) // creates new pane + .executeCommand('graphviz.showPreview', editor) // creates new pane .catch((error) => { //command not available - do nothing }); @@ -313,15 +342,15 @@ class Commands { createWebViewBesides('imgPreview','imgPreview',draggable) */ break; - case "parse": + case 'parse': ret = surya.parse(documentOrListItems.uri.fsPath); vscode.workspace - .openTextDocument({ content: ret, language: "markdown" }) + .openTextDocument({ content: ret, language: 'markdown' }) .then((doc) => - vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside), + vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) ); break; - case "dependencies": + case 'dependencies': ret = surya.dependencies(files, args[0]); let outTxt = []; @@ -330,7 +359,7 @@ class Commands { outTxt.push(ret[0]); if (ret.length < 2) { - outTxt = ["No Dependencies Found"]; + outTxt = ['No Dependencies Found']; } else { ret.shift(); const reducer = (accumulator, currentValue) => @@ -340,40 +369,40 @@ class Commands { vscode.workspace .openTextDocument({ - content: outTxt.join("\n"), - language: "markdown", + content: outTxt.join('\n'), + language: 'markdown', }) .then((doc) => - vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside), + vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) ); } break; - case "ftrace": + case 'ftrace': // contract::func, all, files if (args[1] === null) { - args[1] = ""; - } else if (args[1] === "") { - args[1] = ""; + args[1] = ''; + } else if (args[1] === '') { + args[1] = ''; } try { ret = surya.ftrace( - args[0] + "::" + args[1], - args[2] || "all", + args[0] + '::' + args[1], + args[2] || 'all', files, {}, - true, + true ); vscode.workspace - .openTextDocument({ content: ret, language: "markdown" }) + .openTextDocument({ content: ret, language: 'markdown' }) .then((doc) => - vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside), + vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) ); } catch (e) { console.error(e); } break; - case "mdreport": + case 'mdreport': ret = surya.mdreport(files, { negModifiers: settings.extensionConfig().tools.surya.option.negModifiers, @@ -382,13 +411,13 @@ class Commands { return; } vscode.workspace - .openTextDocument({ content: ret, language: "markdown" }) + .openTextDocument({ content: ret, language: 'markdown' }) .then((doc) => { if (settings.extensionConfig().preview.markdown) { vscode.commands .executeCommand( - "markdown-preview-enhanced.openPreview", - doc.uri, + 'markdown-preview-enhanced.openPreview', + doc.uri ) .catch((error) => { //command does not exist @@ -396,7 +425,7 @@ class Commands { .showTextDocument(doc, vscode.ViewColumn.Beside) .then((editor) => { vscode.commands - .executeCommand("markdown.extension.togglePreview") + .executeCommand('markdown.extension.togglePreview') .catch((error) => { //command does not exist }); @@ -423,13 +452,13 @@ class Commands { : [workspaceRelativeBaseDirs]; let searchFileString = - "{" + + '{' + workspaceRelativeBaseDirs .map((d) => - d === undefined ? "**/*.sol" : d + path.sep + "**/*.sol", + d === undefined ? '**/*.sol' : d + path.sep + '**/*.sol' ) - .join(",") + - "}"; + .join(',') + + '}'; await vscode.workspace .findFiles(searchFileString, settings.DEFAULT_FINDFILES_EXCLUDES, 500) @@ -443,7 +472,7 @@ class Commands { for (let contractName in sourceUnit.contracts) { if ( - sourceUnit.contracts[contractName]._node.kind == "interface" + sourceUnit.contracts[contractName]._node.kind == 'interface' ) { //ignore interface contracts continue; @@ -460,7 +489,7 @@ class Commands { //files set: only take these sourceUnits await this.g_workspace .getAllContracts() - .filter((c) => c._node.kind != "interface" && c._node.kind != "library") + .filter((c) => c._node.kind != 'interface' && c._node.kind != 'library') .forEach((c) => { dependencies[c.name] = c.dependencies; }); @@ -482,7 +511,7 @@ class Commands { async findTopLevelContracts(files, scanfiles) { let topLevelContracts = await this._findTopLevelContracts(files, scanfiles); - let topLevelContractsText = Object.keys(topLevelContracts).join("\n"); + let topLevelContractsText = Object.keys(topLevelContracts).join('\n'); /* for (var name in topLevelContracts) { topLevelContractsText += name + ' (' + topLevelContracts[name]+')\n'; @@ -495,22 +524,22 @@ Top Level Contracts ${topLevelContractsText}`; vscode.workspace - .openTextDocument({ content: content, language: "markdown" }) + .openTextDocument({ content: content, language: 'markdown' }) .then((doc) => - vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside), + vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) ); } async solidityFlattener(files, callback, showErrors) { switch (settings.extensionConfig().flatten.mode) { - case "truffle": + case 'truffle': vscode.extensions - .getExtension("tintinweb.vscode-solidity-flattener") + .getExtension('tintinweb.vscode-solidity-flattener') .activate() .then( (active) => { vscode.commands - .executeCommand("vscode-solidity-flattener.flatten", { + .executeCommand('vscode-solidity-flattener.flatten', { files: files, callback: callback, showErrors: showErrors, @@ -518,16 +547,16 @@ ${topLevelContractsText}`; .catch((error) => { // command not available vscode.window.showWarningMessage( - "Error running `tintinweb.vscode-solidity-flattener`. Please make sure the extension is installed.\n" + - error, + 'Error running `tintinweb.vscode-solidity-flattener`. Please make sure the extension is installed.\n' + + error ); }); }, (err) => { throw new Error( - `Solidity Auditor: Failed to activate "tintinweb.vscode-solidity-flattener". Make sure the extension is installed from the marketplace. Details: ${err}`, + `Solidity Auditor: Failed to activate "tintinweb.vscode-solidity-flattener". Make sure the extension is installed from the marketplace. Details: ${err}` ); - }, + } ); break; default: @@ -549,9 +578,9 @@ ${topLevelContractsText}`; callback(uri.fsPath, undefined, flat); } else { vscode.workspace - .openTextDocument({ content: flat, language: "solidity" }) + .openTextDocument({ content: flat, language: 'solidity' }) .then((doc) => - vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside), + vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) ); } } catch (e) { @@ -563,16 +592,16 @@ ${topLevelContractsText}`; async flaterra(documentOrUri, noTryInstall) { let docUri = documentOrUri; - if (documentOrUri.hasOwnProperty("uri")) { + if (documentOrUri.hasOwnProperty('uri')) { this._checkIsSolidity(documentOrUri); docUri = documentOrUri.uri; } - let cmd = "python3"; + let cmd = 'python3'; let args = [ - "-m", - "flaterra", - "--contract", + '-m', + 'flaterra', + '--contract', vscode.workspace.asRelativePath(docUri), ]; @@ -582,47 +611,47 @@ ${topLevelContractsText}`; vscode.window.showInformationMessage( `Contract flattened: ${path.basename( docUri.fsPath, - ".sol", - )}_flat.sol`, + '.sol' + )}_flat.sol` ); }, (err) => { - if (err.code === "ENOENT") { + if (err.code === 'ENOENT') { vscode.window.showErrorMessage( - "'`flaterra` failed with error: unable to execute python3", + "'`flaterra` failed with error: unable to execute python3" ); - } else if (err.stderr.indexOf(": No module named flaterra") >= 0) { + } else if (err.stderr.indexOf(': No module named flaterra') >= 0) { if (!noTryInstall) { vscode.window .showWarningMessage( - "Contract Flattener `flaterra` is not installed.\n run `pip3 install flaterra --user` to install? ", - "Install", + 'Contract Flattener `flaterra` is not installed.\n run `pip3 install flaterra --user` to install? ', + 'Install' ) .then((selection) => { - if (selection == "Install") { + if (selection == 'Install') { runCommand( - "pip3", - ["install", "flaterra", "--user"], + 'pip3', + ['install', 'flaterra', '--user'], undefined, undefined, - "y\n", + 'y\n' ) .then( (success) => { vscode.window.showInformationMessage( - "Successfully installed flaterra.", + 'Successfully installed flaterra.' ); this.flaterra(documentOrUri, true); }, (error) => { vscode.window.showErrorMessage( - "Failed to install flaterra.", + 'Failed to install flaterra.' ); - }, + } ) .catch((err) => { vscode.window.showErrorMessage( - "Failed to install flaterra. " + err, + 'Failed to install flaterra. ' + err ); }); } else { @@ -632,22 +661,22 @@ ${topLevelContractsText}`; } } else { vscode.window - .showErrorMessage("`flaterra` failed with: " + err) + .showErrorMessage('`flaterra` failed with: ' + err) .then((selection) => { console.log(selection); }); } - }, + } ) .catch((err) => { - console.log("runcommand threw exception: " + err); + console.log('runcommand threw exception: ' + err); }); } async flattenCandidates(candidates) { // takes object key=contractName value=fsPath let topLevelContracts = candidates || (await this._findTopLevelContracts()); - let content = ""; + let content = ''; this.solidityFlattener( Object.values(topLevelContracts), @@ -655,40 +684,40 @@ ${topLevelContractsText}`; let outpath = path.parse(filepath); fs.writeFile( - path.join(outpath.dir, "flat_" + outpath.base), + path.join(outpath.dir, 'flat_' + outpath.base), content, function (err) { if (err) { return console.log(err); } - }, + } ); - }, + } ); for (let name in topLevelContracts) { //this.flaterra(new vscode.Uri(topLevelContracts[name])) let outpath = path.parse(topLevelContracts[name].fsPath); let outpath_flat = vscode.Uri.file( - path.join(outpath.dir, "flat_" + outpath.base), + path.join(outpath.dir, 'flat_' + outpath.base) ); content += `${ - !fs.existsSync(outpath_flat.fsPath) ? "[ERR] " : "" + !fs.existsSync(outpath_flat.fsPath) ? '[ERR] ' : '' }${name} => ${outpath_flat} \n`; } vscode.workspace - .openTextDocument({ content: content, language: "markdown" }) + .openTextDocument({ content: content, language: 'markdown' }) .then((doc) => - vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside), + vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) ); } async listFunctionSignatures(document, asJson) { this.g_workspace.add(document.fileName).then(async (sourceUnit) => { const signatures = await this._signatureForAstItem( - Object.values(sourceUnit.contracts), + Object.values(sourceUnit.contracts) ); - await this.revealSignatures(signatures, asJson ? "json" : undefined); + await this.revealSignatures(signatures, asJson ? 'json' : undefined); }); } @@ -699,18 +728,18 @@ ${topLevelContractsText}`; // 4) get all function signatures // -- this is kinda resource intensive 🤷‍♂️ await vscode.workspace - .findFiles("**/*.sol", settings.DEFAULT_FINDFILES_EXCLUDES, 500) + .findFiles('**/*.sol', settings.DEFAULT_FINDFILES_EXCLUDES, 500) .then((uris) => { uris.forEach((uri) => { this.g_workspace.add(uri.fsPath); }); }); await this.g_workspace.withParserReady(undefined, true); - console.log("done"); + console.log('done'); const signatures = await this._signatureForAstItem( - this.g_workspace.getAllContracts(), + this.g_workspace.getAllContracts() ); - await this.revealSignatures(signatures, asJson ? "json" : undefined); + await this.revealSignatures(signatures, asJson ? 'json' : undefined); } async signatureForAstItem(items) { @@ -735,13 +764,13 @@ ${topLevelContractsText}`; } async revealSignatures(signatures, format) { - format = format || "markdown"; + format = format || 'markdown'; let errs = []; const res = {}; for (const sig of signatures) { - if (sig.hasOwnProperty("err")) { + if (sig.hasOwnProperty('err')) { errs.push(sig.err); continue; //skip errors } @@ -755,14 +784,14 @@ ${topLevelContractsText}`; let content; switch (format) { - case "json": + case 'json': content = JSON.stringify( { signatures: res, errors: [...new Set(errs)], collisions: Object.values(res).filter((v) => v.size > 1), }, - (_key, value) => (value instanceof Set ? [...value] : value), + (_key, value) => (value instanceof Set ? [...value] : value) ); break; @@ -770,25 +799,25 @@ ${topLevelContractsText}`; // markdown const header = - "| Function Name | Sighash | Function Signature | \n| ------------- | ---------- | ------------------ | \n"; + '| Function Name | Sighash | Function Signature | \n| ------------- | ---------- | ------------------ | \n'; content = header + signatures .map((r) => `| ${r.name} | ${r.sighash} | ${r.signature} |`) - .join("\n"); + .join('\n'); const collisions = Object.values(res).filter((v) => v.size > 1); if (collisions.length) { - content += "\n\n"; + content += '\n\n'; content += - "🔥 Collisions \n========================\n"; - content += collisions.map((s) => [...s]).join("\n"); + '🔥 Collisions \n========================\n'; + content += collisions.map((s) => [...s]).join('\n'); } if (errs.length) { - content += "\n\n"; - content += "🐞 Errors \n========================\n"; - content += [...new Set(errs)].join("\n"); + content += '\n\n'; + content += '🐞 Errors \n========================\n'; + content += [...new Set(errs)].join('\n'); } } @@ -798,7 +827,7 @@ ${topLevelContractsText}`; vscode.window.showTextDocument(doc, { viewColumn: vscode.ViewColumn.Beside, preview: true, - }), + }) ); } @@ -807,9 +836,9 @@ ${topLevelContractsText}`; const content = writer.export(contractObj); vscode.workspace - .openTextDocument({ content: content, language: "csv" }) + .openTextDocument({ content: content, language: 'csv' }) .then((doc) => - vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside), + vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside) ); } @@ -818,29 +847,29 @@ ${topLevelContractsText}`; const content = writer.export(contractObjects); vscode.workspace - .openTextDocument({ content: content, language: "plantuml" }) + .openTextDocument({ content: content, language: 'plantuml' }) .then((doc) => vscode.window .showTextDocument(doc, vscode.ViewColumn.Beside) .then((editor) => { vscode.extensions - .getExtension("jebbs.plantuml") + .getExtension('jebbs.plantuml') .activate() .then( (active) => { vscode.commands - .executeCommand("plantuml.preview") + .executeCommand('plantuml.preview') .catch((error) => { //command does not exist }); }, (err) => { console.warn( - `Solidity Auditor: Failed to activate "jebbs.plantuml". Make sure the extension is installed from the marketplace. Details: ${err}`, + `Solidity Auditor: Failed to activate "jebbs.plantuml". Make sure the extension is installed from the marketplace. Details: ${err}` ); - }, + } ); - }), + }) ); } } diff --git a/src/features/templates.js b/src/features/templates.js index fb50ea4..6179111 100644 --- a/src/features/templates.js +++ b/src/features/templates.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; /** * @author github.com/tintinweb * @license GPLv3 @@ -6,112 +6,49 @@ * * */ -const vscode = require("vscode"); +const vscode = require('vscode'); +const { generateForgeTemplate } = require('./templates/forge'); +const { generateTruffleTemplate } = require('./templates/truffle'); +const { generateHardhatTemplate } = require('./templates/hardhat'); -function generateUnittestStubForContract(document, g_workspace, contractName) { +function generateTruffleUnittestStubForContract(document, g_workspace, contractObj) { let contract = { - name: contractName, + name: contractObj?.name, path: document.uri.fsPath, }; - if (!contractName) { + if (!contractObj) { //take first let sourceUnit = g_workspace.get(document.uri.fsPath); if (!sourceUnit || Object.keys(sourceUnit.contracts).length <= 0) { vscode.window.showErrorMessage( - `[Solidity VA] unable to create unittest stub for current contract. missing analysis for source-unit: ${document.uri.fsPath}`, + `[Solidity VA] unable to create unittest stub for current contract. missing analysis for source-unit: ${document.uri.fsPath}` ); return; } contract.name = Object.keys(sourceUnit.contracts)[0]; } - - let content = ` -/** - * - * autogenerated by solidity-visual-auditor - * - * execute with: - * #> truffle test - * - * */ -var ${contract.name} = artifacts.require("${contract.path}"); - -contract('${contract.name}', (accounts) => { - var creatorAddress = accounts[0]; - var firstOwnerAddress = accounts[1]; - var secondOwnerAddress = accounts[2]; - var externalAddress = accounts[3]; - var unprivilegedAddress = accounts[4] - /* create named accounts for contract roles */ - - before(async () => { - /* before tests */ - }) - - beforeEach(async () => { - /* before each context */ - }) - - it('should revert if ...', () => { - return ${contract.name}.deployed() - .then(instance => { - return instance.publicOrExternalContractMethod(argument1, argument2, {from:externalAddress}); - }) - .then(result => { - assert.fail(); - }) - .catch(error => { - assert.notEqual(error.message, "assert.fail()", "Reason ..."); - }); - }); - - context('testgroup - security tests - description...', () => { - //deploy a new contract - before(async () => { - /* before tests */ - const new${contract.name} = await ${contract.name}.new() - }) - - - beforeEach(async () => { - /* before each tests */ - }) - - - - it('fails on initialize ...', async () => { - return assertRevert(async () => { - await new${contract.name}.initialize() - }) - }) - - it('checks if method returns true', async () => { - assert.isTrue(await new${contract.name}.thisMethodShouldReturnTrue()) - }) - }) -}); -`; - return content; + + return generateTruffleTemplate(contract); } function generateHardhatUnittestStubForContract( document, g_parser, - contractName, + contractObj ) { let contract = { - name: contractName, + name: contractObj?.name, path: document.uri.fsPath, }; - if (!contractName) { + if (!contractObj) { //take first let sourceUnit = g_parser.sourceUnits[document.uri.fsPath]; if (!sourceUnit || Object.keys(sourceUnit.contracts).length <= 0) { vscode.window.showErrorMessage( - `[Solidity VA] unable to create hardhat-unittest stub for current contract. missing analysis for source-unit: ${document.uri.fsPath}`, + `[Solidity VA] unable to create hardhat-unittest stub for current contract. missing analysis for source-unit: ${document.uri.fsPath}` ); return; } @@ -119,102 +56,35 @@ function generateHardhatUnittestStubForContract( contract.name = Object.keys(sourceUnit.contracts)[0]; } - let content = ` -/** - * - * autogenerated by solidity-visual-auditor - * - * execute with: - * #> npx hardhat test - * - * */ -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { Artifact } from "hardhat/types"; -import { Signers } from "../types"; -import hre from "hardhat"; -import "@nomiclabs/hardhat-waffle"; -import { ethers } from "hardhat"; -import { chai, expect } from "chai"; -const { deployContract } = hre.waffle; - -var ${contract.name} = await hre.artifacts.readArtifact("${contract.path}"); - -describe('${contract.name}', () => { - - // Mocha has four functions that let you hook into the the test runner's - // lifecyle. These are: before, beforeEach, after, afterEach. - - // They're very useful to setup the environment for tests, and to clean it - // up after they run. - - // A common pattern is to declare some variables, and assign them in the - // before and beforeEach callbacks. - - let Token; - let hardhatToken; - let owner; - let addr1; - let addr2; - let addrs; - - /* create named accounts for contract roles */ - - before(async () => { - /* before tests */ - this.signers = {} as Signers; - const signers: SignerWithAddress[] = await hre.ethers.getSigners(); - this.signers.admin = signers[0]; - }) - - beforeEach(async () => { - /* before each context */ - }) - - it('should revert if ...', () => { - //Using .deploy() on artifact loaded (using ethers library) - return ${contract.name}.deploy() - .then(instance => { - return instance.publicOrExternalContractMethod(argument1, argument2, {from:externalAddress}); - }) - .then(result => { - assert.fail(); - }) - .catch(error => { - assert.notEqual(error.message, "assert.fail()", "Reason ..."); - }); - }); - - context('testgroup - security tests - description...', () => { - //deploy a new contract - before(async () => { - /* before tests */ - const new${contract.name} = await ${contract.name}.new() - }) - + return generateHardhatTemplate(contract); +} - beforeEach(async () => { - /* before each tests */ - }) +function generateForgeUnittestStubForContract(document, g_parser, contractObj, generateForkStub) { + let contract = { + name: contractObj?.name, + path: document.uri.fsPath, + pragma: contractObj?._parent.pragmas[0]?.value || '^0.8.17', + }; - + if (!contractObj) { + //take first + let sourceUnit = g_parser.sourceUnits[document.uri.fsPath]; + if (!sourceUnit || Object.keys(sourceUnit.contracts).length <= 0) { + vscode.window.showErrorMessage( + `[Solidity VA] unable to create forge unittest stub for current contract. missing analysis for source-unit: ${document.uri.fsPath}` + ); + return; + } - it('fails on initialize ...', async () => { - return assertRevert(async () => { - await new${contract.name}.initialize() - }) - }) + contract.name = Object.keys(sourceUnit.contracts)[0]; + contract.pragma = sourceUnit.pragmas[0].value || contract.pragma; + } - it('checks if method returns true', async () => { - assert.isTrue(await new${contract.name}.thisMethodShouldReturnTrue()) - }) - }) -}); -`; - return content; + return generateForgeTemplate(contract, generateForkStub); } module.exports = { - generateUnittestStubForContract: generateUnittestStubForContract, - generateHardhatUnittestStubForContract: - generateHardhatUnittestStubForContract, + generateTruffleUnittestStubForContract, + generateHardhatUnittestStubForContract, + generateForgeUnittestStubForContract, }; diff --git a/src/features/templates/forge.js b/src/features/templates/forge.js new file mode 100644 index 0000000..bb92245 --- /dev/null +++ b/src/features/templates/forge.js @@ -0,0 +1,123 @@ +const generateForgeTemplate = (contract, generateForkStub) => { + const contractTypeName = contract.name; + const contractInstanceName = contractTypeName.toLowerCase(); + const contractPath = contract.path; + const getTestContractName = (type) => + type == 'fork' ? `${contractTypeName}ForkTest` : `${contractTypeName}Test`; + let content = `\ +/** + * autogenerated by solidity-visual-auditor + * Run the tests with 'forge test -vvv' to see the console logs +**/ + +// SPDX-License-Identifier: UNLICENSED +pragma solidity ${contract.pragma}; + +import {Test, console2} from "forge-std/Test.sol"; +import ${contractTypeName} from "${contractPath}"; + +contract ${getTestContractName()} is Test { + + // https://book.getfoundry.sh/forge/writing-tests + + ${contractTypeName} public ${contractInstanceName}; + address immutable deployer = 0xaA96b71DA3E88aF887779056d0cc4A91C12BaAAA; + address immutable firstOwnerAddress = 0xaA96B71da3e88AF887779056D0CC4A91c12bbBBb; + address immutable secondOwnerAddress = 0xAA96b71DA3E88af887779056D0Cc4a91C12bCccc; + address immutable externalAddress = 0xAA96b71Da3e88af887779056d0CC4A91C12BDDdd; + + //the setUp function is invoked before each test case + function setUp() public { + ${contractInstanceName} = new ${contractTypeName}(); + } + + //test: Functions prefixed with test are run as a test case. + function test_CallFromDeployer() public { + ${contractInstanceName}.myFunction(); + } + + function test_CallFromExternalAddress() public { + vm.prank(externalAddress); + ${contractInstanceName}.myFunction(); + } + + // testFail: The inverse of the test prefix - if the function does not revert, the test fails + function testFail_CallFromExternalAddress() public { + vm.prank(externalAddress); + ${contractInstanceName}.myFunction(); + } + + // https://book.getfoundry.sh/cheatcodes/expect-revert + // A good practice is to use the pattern test_Revert[If|When]_Condition in combination with the expectRevert cheatcode + // Instead of using testFail, you know exactly what reverted and with which error + function testRevertFromExternalAddress() public { + vm.prank(firstOwnerAddress); + ${contractInstanceName}.myPrivilegedFunction(); + vm.prank(externalAddress); + vm.expectRevert("revertMsg"); + ${contractInstanceName}.myPrivilegedFunction(); + } + + // https://book.getfoundry.sh/forge/fuzz-testing + function testFuzz_CallMyFunction(uint256 x) public { + vm.prank(externalAddress); + x = bound(x, 100, 1e36); + bool res = ${contractInstanceName}.myFunction(x); + assertEq(res, true); + } +}`; + + if (generateForkStub) { + content += ` +contract ${getTestContractName('fork')} is Test { + // the identifiers of the forks + uint256 mainnetFork; + uint256 optimismFork; + + //Load RPC URLs from .env file + string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); + string OPTIMISM_RPC_URL = vm.envString("OPTIMISM_RPC_URL"); + + // create two _different_ forks during setup + function setUp() public { + mainnetFork = vm.createFork(MAINNET_RPC_URL); + optimismFork = vm.createFork(OPTIMISM_RPC_URL); + } + + // creates a new contract while a fork is active + function testCreateContract() public { + vm.selectFork(mainnetFork); + assertEq(vm.activeFork(), mainnetFork); + + //create contract + ${contractTypeName} ${contractInstanceName} = new ${contractTypeName}(); + + // and can be used as normal + ${contractInstanceName}.set(x); + assertEq(${contractInstanceName}.value(), 100); + } + + // creates a new _persistent_ contract while a fork is active + function testCreatePersistentContract() public { + vm.selectFork(mainnetFork); + ${contractTypeName} ${contractInstanceName} = new ${contractTypeName}(); + + // mark the contract as persistent so it is also available when other forks are active + vm.makePersistent(address(${contractInstanceName})); + assert(vm.isPersistent(address(${contractInstanceName}))); + + vm.selectFork(optimismFork); + assert(vm.isPersistent(address(${contractInstanceName}))); + + // This will succeed because the contract is now also available on the 'optimismFork' + assertEq(${contractInstanceName}.value(), 100); + } +} +`; + } + return content; +}; + +module.exports = { + generateForgeTemplate, +}; diff --git a/src/features/templates/hardhat.js b/src/features/templates/hardhat.js new file mode 100644 index 0000000..364d9dc --- /dev/null +++ b/src/features/templates/hardhat.js @@ -0,0 +1,96 @@ +const generateHardhatTemplate = (contract) => { + return `\ +/** + * + * autogenerated by solidity-visual-auditor + * + * execute with: + * #> npx hardhat test + * + * */ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { Artifact } from "hardhat/types"; +import { Signers } from "../types"; +import hre from "hardhat"; +import "@nomiclabs/hardhat-waffle"; +import { ethers } from "hardhat"; +import { chai, expect } from "chai"; +const { deployContract } = hre.waffle; + +var ${contract.name} = await hre.artifacts.readArtifact("${contract.path}"); + +describe('${contract.name}', () => { + + // Mocha has four functions that let you hook into the the test runner's + // lifecyle. These are: before, beforeEach, after, afterEach. + + // They're very useful to setup the environment for tests, and to clean it + // up after they run. + + // A common pattern is to declare some variables, and assign them in the + // before and beforeEach callbacks. + + let Token; + let hardhatToken; + let owner; + let addr1; + let addr2; + let addrs; + + /* create named accounts for contract roles */ + + before(async () => { + /* before tests */ + this.signers = {} as Signers; + const signers: SignerWithAddress[] = await hre.ethers.getSigners(); + this.signers.admin = signers[0]; + }) + + beforeEach(async () => { + /* before each context */ + }) + + it('should revert if ...', () => { + //Using .deploy() on artifact loaded (using ethers library) + return ${contract.name}.deploy() + .then(instance => { + return instance.publicOrExternalContractMethod(argument1, argument2, {from:externalAddress}); + }) + .then(result => { + assert.fail(); + }) + .catch(error => { + assert.notEqual(error.message, "assert.fail()", "Reason ..."); + }); + }); + + context('testgroup - security tests - description...', () => { + //deploy a new contract + before(async () => { + /* before tests */ + const new${contract.name} = await ${contract.name}.new() + }) + + + beforeEach(async () => { + /* before each tests */ + }) + + + + it('fails on initialize ...', async () => { + return assertRevert(async () => { + await new${contract.name}.initialize() + }) + }) + + it('checks if method returns true', async () => { + assert.isTrue(await new${contract.name}.thisMethodShouldReturnTrue()) + }) + }) +});`; +}; + +module.exports = { + generateHardhatTemplate, +}; diff --git a/src/features/templates/truffle.js b/src/features/templates/truffle.js new file mode 100644 index 0000000..ed5a99a --- /dev/null +++ b/src/features/templates/truffle.js @@ -0,0 +1,72 @@ +const generateTruffleTemplate = (contract) => { + return `\ +/** + * + * autogenerated by solidity-visual-auditor + * + * execute with: + * #> truffle test + * + * */ +var ${contract.name} = artifacts.require("${contract.path}"); + +contract('${contract.name}', (accounts) => { + var creatorAddress = accounts[0]; + var firstOwnerAddress = accounts[1]; + var secondOwnerAddress = accounts[2]; + var externalAddress = accounts[3]; + var unprivilegedAddress = accounts[4] + /* create named accounts for contract roles */ + + before(async () => { + /* before tests */ + }) + + beforeEach(async () => { + /* before each context */ + }) + + it('should revert if ...', () => { + return ${contract.name}.deployed() + .then(instance => { + return instance.publicOrExternalContractMethod(argument1, argument2, {from:externalAddress}); + }) + .then(result => { + assert.fail(); + }) + .catch(error => { + assert.notEqual(error.message, "assert.fail()", "Reason ..."); + }); + }); + + context('testgroup - security tests - description...', () => { + //deploy a new contract + before(async () => { + /* before tests */ + const new${contract.name} = await ${contract.name}.new() + }) + + + beforeEach(async () => { + /* before each tests */ + }) + + + + it('fails on initialize ...', async () => { + return assertRevert(async () => { + await new${contract.name}.initialize() + }) + }) + + it('checks if method returns true', async () => { + assert.isTrue(await new${contract.name}.thisMethodShouldReturnTrue()) + }) + }) +}); +`; +}; + +module.exports = { + generateTruffleTemplate, +};