├── .gitattributes ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── License.txt ├── README-DEVELOP.md ├── README-NeoVim.md ├── README-STANDALONE-SERVER.md ├── README.md ├── funding.json ├── images └── icon.png ├── package-lock.json ├── package.json ├── screenshots ├── abigeneration.png ├── autocompletedemo.gif ├── autocompletedemoDappSys.gif ├── autocompletedemoGlobal.gif ├── change-compiler-version-contextmenu.png ├── change-compiler-version-gui-setting.png ├── change-compiler-version-local-guisetting.png ├── change-compiler-version-selectversion.png ├── codeGenerateNethereumMultiSettings.png ├── compile-codegnerate-nethereum.png ├── ercautocomplete1.png ├── ercautocomplete2.png ├── select-linter.png ├── simpleDemoAutocomplete.gif ├── simpleProjectStructure.PNG ├── solidity-change-workspacecompiler-codeaction.gif ├── solidity-corrections.gif ├── solidity-etherscan-download.gif ├── solidity-hover.gif ├── solidity-nethereum-code-generation-from-dowloaded-contract.gif ├── solidity-references.gif ├── unigen1.png └── unigen2.png ├── scripts ├── add_cmd_wrapper.js ├── add_shebang.js ├── add_shebang.sh ├── post-standalone-build.js └── pre-standalone-build.js ├── snippets └── solidity.json ├── solidity-syntax-refactoring.json ├── solidity-versions.txt ├── solidity.configuration.json ├── src ├── client │ ├── codeActionProviders │ │ └── addressChecksumActionProvider.ts │ ├── codegen.ts │ ├── compileActive.ts │ ├── compileAll.ts │ ├── compiler.ts │ ├── formatter │ │ ├── forgeFormatter.ts │ │ ├── formatter.ts │ │ ├── formatterPrettierWorker.js │ │ └── prettierFormatter.ts │ ├── outputChannelService.ts │ ├── settingsService.ts │ ├── solErrorsToDiaganosticsClient.ts │ └── workspaceUtil.ts ├── common │ ├── model │ │ ├── package.ts │ │ ├── project.ts │ │ ├── remapping.ts │ │ ├── sourceDocument.ts │ │ └── sourceDocumentCollection.ts │ ├── projectService.ts │ ├── solcCompiler.ts │ ├── sourceCodeDownloader │ │ └── etherscanSourceCodeDownloader.ts │ └── util.ts ├── extension.ts ├── server.ts └── server │ ├── SolidityDefinitionProvider.ts │ ├── SolidityDefinitionProviderOld.ts │ ├── SolidityDocumentSymbolProvider.ts │ ├── SolidityHoverProvider.ts │ ├── SolidityReferencesProvider.ts │ ├── SolidityWorkspaceSymbolProvider.ts │ ├── completionService.ts │ ├── linter │ ├── linter.ts │ ├── solhint.ts │ ├── solium.ts │ └── soliumClientFixer.ts │ ├── parsedCodeModel │ ├── IParsedExpressionContainer.ts │ ├── ParsedCodeTypeHelper.ts │ ├── ParsedConstant.ts │ ├── ParsedContractIs.ts │ ├── ParsedCustomType.ts │ ├── ParsedDocument.ts │ ├── ParsedEnum.ts │ ├── ParsedError.ts │ ├── ParsedEvent.ts │ ├── ParsedExpression.ts │ ├── ParsedFunction.ts │ ├── ParsedFunctionVariable.ts │ ├── ParsedImport.ts │ ├── ParsedModifierArgument.ts │ ├── ParsedParameter.ts │ ├── ParsedStateVariable.ts │ ├── ParsedStruct.ts │ ├── ParsedStructVariable.ts │ ├── ParsedVariable.ts │ ├── codeCompletion │ │ ├── dotCompletionService.ts │ │ └── importCompletionService.ts │ ├── codeWalkerService.ts │ ├── parsedCode.ts │ ├── parsedContract.ts │ ├── parsedDeclarationType.ts │ └── parsedUsing.ts │ ├── solErrorsToDiagnostics.ts │ └── utils │ └── convertDocumentSymbolsToSymbolInformation.ts ├── syntaxes ├── solidity-markdown-injection.json └── solidity.json ├── tsconfig.json ├── tslint.json └── tsup.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vs 3 | .vscode-test 4 | node_modules 5 | out 6 | dist 7 | tmp 8 | .nyc_output 9 | coverage 10 | *.vsix 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "eg2.tslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "preLaunchTask": "npm: compile", 13 | "console": "integratedTerminal", 14 | "runtimeExecutable": "${execPath}", 15 | "args": [ 16 | "--extensionDevelopmentPath=${workspaceFolder}" 17 | ], 18 | "sourceMaps": true, 19 | "outFiles": [ 20 | "${workspaceFolder}/out/src/**/*.js" 21 | ], 22 | 23 | "stopOnEntry": false 24 | }, 25 | { 26 | "type": "node", 27 | "request": "attach", 28 | "name": "Attach to Server", 29 | "port": 6009, 30 | "restart": true, 31 | "outFiles": [ 32 | "${workspaceRoot}/out/src/**/*.js" 33 | ] 34 | }, 35 | { 36 | "type": "node", 37 | "request": "attach", 38 | "name": "Attach to Solidity Language Server (external)", 39 | "port": 9229, 40 | "restart": true, 41 | "protocol": "inspector", 42 | "outFiles": [ 43 | "${workspaceRoot}/out/src/**/*.js" 44 | ] 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "git.ignoreLimitWarning": true 12 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | *.vsix 8 | .gitignore 9 | tsconfig.json 10 | vsc-extension-quickstart.md 11 | tslint.json 12 | tsup.config.ts 13 | dist 14 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2025 Juan Blanco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /README-DEVELOP.md: -------------------------------------------------------------------------------- 1 | 2 | **Table of Contents** 3 | 4 | - [Prerequisites...](#prerequisites) 5 | - [How to run code in this github repository](#how-to-run-code-in-this-github-repository) 6 | - [Running](#running) 7 | - [Debugging](#debugging) 8 | - [See also:](#see-also) 9 | 10 | 11 | Perhaps you want to help work on this awesome project? Or run from the github repository? 12 | 13 | # Prerequisites... 14 | 15 | You need to have installed 16 | 17 | * VSCode (duh). Download [here](https://code.visualstudio.com/download). 18 | * [nodejs](https://nodejs.org/en/) 19 | * [npm](https://www.npmjs.com/get-npm) 20 | 21 | There are a number of nodejs packages are needed, like [typescript](https://www.typescriptlang.org/), but you can get those via `npm`, 22 | which is described in a below [section](#how-to-run-code-in-this-github-repository). 23 | 24 | # How to run code in this github repository 25 | 26 | Clone the repository: 27 | 28 | 29 | 30 | ```console 31 | $ git clone https://github.com/juanfranblanco/vscode-solidity 32 | Cloning into 'vscode-solidity'... 33 | remote: Enumerating objects: 169, done. 34 | ... 35 | $ cd vscode-solidity 36 | $ 37 | ``` 38 | 39 | Install dependent npm packages: 40 | 41 | ```console 42 | $ npm install 43 | ``` 44 | 45 | Now just run from inside the `vscode-solidity` folder 46 | 47 | ``` 48 | $ code . 49 | ``` 50 | 51 | ## Running 52 | 53 | If you just want to run the code, on the top there is a "Debug" menu item and under that, the second item is "Start Without Debugging", which is also bound the key sequence `Ctrl`-`F5`. 54 | 55 | After that, the VSCode window should turn from blue to orange and another window will pop up. In this window you will have the vscode extension installed and enabled. 56 | 57 | If you edit a solidity file, which is a file ending in `.sol` and looks like this: 58 | 59 | ```solidity 60 | /* 61 | * @source: http://blockchain.unica.it/projects/ethereum-survey/attacks.html#simpledao 62 | * @author: Atzei N., Bartoletti M., Cimoli T 63 | * Modified by Josselin Feist 64 | */ 65 | pragma solidity 0.4.25; 66 | 67 | contract SimpleDAO { 68 | mapping (address => uint) public credit; 69 | 70 | function donate(address to) payable public{ 71 | credit[to] += msg.value; 72 | } 73 | 74 | function withdraw(uint amount) public{ 75 | if (credit[msg.sender]>= amount) { 76 | require(msg.sender.call.value(amount)()); 77 | credit[msg.sender]-=amount; 78 | } 79 | } 80 | 81 | function queryCredit(address to) view public returns(uint){ 82 | return credit[to]; 83 | } 84 | } 85 | ``` 86 | 87 | This when rendered inside be colorized, for example the tokens "pragma", "solidity" and "0.4.25" may appear in a different color. If the all appear in the same color then the language extension mechanism is not working and there is probably something wrong in the extension. Look in the other VSCode window with the orange frame for what might be wrong. 88 | 89 | But if everything is good, enter `Ctrl`-`Shift`-`P` and a list of commands will pop up. If you type "Solidity", you should see those specific to this extension. 90 | 91 | ## Debugging 92 | 93 | You may want to extend this code of may find a problem and want to debug what is going wrong. For this, you start off from the "Debug" menu using the first item on the list "Start Debugging" which is also bound to the function key `F5`. As before the window will go orange a popup menu bar will appear at the top. 94 | 95 | __NOTE__: It may happen that after the window goes orange, another and VSCode window with the blue, the "play button" may go from a the parallel bars to "pause" back to "play". When that happens you may also be stopped in `bootstrap-fork.js` at some nonsensical like like line 9 which has no code on it. If this happens (and it may happen several times for each solidity file loaded) just hit the play button to make sure it goes back to the parallel bars. You have 10 seconds from the time this button goes from "Pause" to "Play" to hit the "Play" button. Otherwise in the other VSCode you will get a popup message that reads: 96 | 97 | > Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue. 98 | 99 | 100 | # See also: 101 | 102 | * [Extension API](https://code.visualstudio.com/api) 103 | * [Your First Extension](https://code.visualstudio.com/api/get-started/your-first-extension) 104 | -------------------------------------------------------------------------------- /README-NeoVim.md: -------------------------------------------------------------------------------- 1 | # Neovim Setup for VSCode-Solidity LSP 2 | 3 | This guide provides step-by-step instructions to set up **Neovim** as a Solidity development environment using the **VSCode-Solidity Language Server**. 4 | 5 | ## **1. Install Neovim** 6 | 7 | ### **Windows** 8 | Download and install Neovim from: 9 | - [Neovim Releases](https://github.com/neovim/neovim/releases) 10 | 11 | ### **Linux/macOS** 12 | ```sh 13 | sudo apt install neovim # Ubuntu/Debian 14 | brew install neovim # macOS 15 | ``` 16 | 17 | Verify installation: 18 | ```sh 19 | nvim --version 20 | ``` 21 | 22 | --- 23 | ## **2. Install Node.js (Required for Solidity LSP)** 24 | 25 | Ensure you have **Node.js** installed: 26 | ```sh 27 | node -v 28 | ``` 29 | If not installed, get it from: 30 | - [Node.js Download](https://nodejs.org/) 31 | 32 | For Linux/macOS: 33 | ```sh 34 | curl -fsSL https://fnm.vercel.app/install | bash 35 | fnm install 18 36 | ``` 37 | 38 | --- 39 | ## **3. Install Packer (Neovim Plugin Manager)** 40 | 41 | Run the following command in **PowerShell** or **Command Prompt**: 42 | ```sh 43 | git clone --depth 1 https://github.com/wbthomason/packer.nvim ^ 44 | %LOCALAPPDATA%\nvim-data\site\pack\packer\start\packer.nvim 45 | ``` 46 | 47 | --- 48 | ## **4. Configure Neovim (`init.lua`)** 49 | 50 | Create the Neovim configuration directory if it doesn't exist: 51 | ```sh 52 | mkdir %USERPROFILE%\AppData\Local\nvim\lua 53 | ``` 54 | 55 | Create and open `init.lua`: 56 | ```sh 57 | nvim %USERPROFILE%\AppData\Local\nvim\init.lua 58 | ``` 59 | 60 | Paste the following setup: 61 | ```lua 62 | -- Ensure packer is installed 63 | local ensure_packer = function() 64 | local fn = vim.fn 65 | local install_path = fn.stdpath('data')..'/site/pack/packer/start/packer.nvim' 66 | if fn.empty(fn.glob(install_path)) > 0 then 67 | vim.cmd('!git clone --depth 1 https://github.com/wbthomason/packer.nvim '..install_path) 68 | vim.cmd('packadd packer.nvim') 69 | return true 70 | end 71 | return false 72 | end 73 | 74 | ensure_packer() 75 | 76 | -- Install plugins 77 | require('packer').startup(function(use) 78 | use 'wbthomason/packer.nvim' -- Plugin manager 79 | use 'neovim/nvim-lspconfig' -- LSP support 80 | use 'hrsh7th/nvim-cmp' -- Autocompletion 81 | use 'hrsh7th/cmp-nvim-lsp' -- LSP completion source 82 | use 'williamboman/mason.nvim' -- LSP manager 83 | end) 84 | 85 | -- Setup Solidity LSP 86 | local lspconfig = require('lspconfig') 87 | 88 | local lspconfig = require('lspconfig') 89 | 90 | lspconfig.solidity_ls.setup{ 91 | cmd = { "vscode-solidity-server", "--stdio" }, 92 | filetypes = { "solidity" }, 93 | root_dir = lspconfig.util.root_pattern("hardhat.config.js", "foundry.toml", ".git"), 94 | settings = { 95 | solidity = { 96 | compileUsingRemoteVersion = 'latest', 97 | defaultCompiler = 'remote', 98 | enabledAsYouTypeCompilationErrorCheck = true, 99 | }, 100 | } 101 | } 102 | 103 | ``` 104 | Or Local debug etc 105 | 106 | ```lua 107 | lspconfig.solidity_ls.setup{ 108 | cmd = { 109 | "node", 110 | "--inspect-brk=9229", 111 | "(%PATH%)vscode-solidity/out/src/server.js", 112 | "--stdio" 113 | }, 114 | filetypes = {"solidity"}, 115 | root_dir = lspconfig.util.root_pattern("foundry.toml", "hardhat.config.js", ".git"), 116 | settings = { 117 | solidity = { 118 | compileUsingRemoteVersion = 'latest', 119 | defaultCompiler = 'remote', 120 | enabledAsYouTypeCompilationErrorCheck = true, 121 | }, 122 | } 123 | } 124 | ``` 125 | 126 | Save the file and exit Neovim. 127 | 128 | --- 129 | ## **5. Install Plugins** 130 | Open Neovim and run: 131 | ```sh 132 | :PackerSync 133 | ``` 134 | 135 | --- 136 | ## **6. Verify Solidity LSP is Working** 137 | ### **Start Neovim with a Solidity File** 138 | ```sh 139 | nvim test.sol 140 | ``` 141 | 142 | ### **Check Active LSPs** 143 | ```sh 144 | :LspInfo 145 | ``` 146 | You should see `solidity_ls` running. 147 | 148 | ### **Test Autocompletion** 149 | Start typing a Solidity function or keyword and press: 150 | ```sh 151 | 152 | ``` 153 | 154 | ### **Restart LSP if Needed** 155 | ```sh 156 | :LspRestart 157 | ``` 158 | 159 | --- 160 | ## **7. Useful Commands** 161 | | Command | Action | 162 | |---------|--------| 163 | | `gd` | Go to definition | 164 | | `gr` | Find references | 165 | | `K` | Show documentation | 166 | | `` | Trigger autocomplete | 167 | | `:LspInfo` | Show LSP status | 168 | | `:LspRestart` | Restart LSP | 169 | 170 | --- 171 | ## **8. Debugging LSP** 172 | If LSP is not working, check logs: 173 | ```sh 174 | :lua print(vim.inspect(vim.lsp.get_active_clients())) 175 | ``` 176 | Or manually start the Solidity LSP: 177 | ```sh 178 | node --inspect-brk=9229 (PATH)vscode-solidity/out/src/server.js --stdio 179 | ``` 180 | 181 | --- 182 | ### 🎉 **Neovim is now fully configured for Solidity development!** 🚀 -------------------------------------------------------------------------------- /README-STANDALONE-SERVER.md: -------------------------------------------------------------------------------- 1 | When deploying the standalone server, or running it. The server.ts 2 | ```const standAloneServerSide = true; ``` needs to set to true. Otherwise false. 3 | 4 | This enables to load the default settings of the package.json, so if a user doesnt use the ones provided by their enviroment (ie nvim) it continues to work. 5 | 6 | There is a pre-standalone-build that does this, and post-standalone-build that reverts it to normal. 7 | 8 | These are run when publishing to NPM, but not when using vsce. 9 | 10 | 11 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x819775803938d78eaa95809971ce94cae6a54b4df58505fa36153ffa1d55e12c" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/images/icon.png -------------------------------------------------------------------------------- /screenshots/abigeneration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/abigeneration.png -------------------------------------------------------------------------------- /screenshots/autocompletedemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/autocompletedemo.gif -------------------------------------------------------------------------------- /screenshots/autocompletedemoDappSys.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/autocompletedemoDappSys.gif -------------------------------------------------------------------------------- /screenshots/autocompletedemoGlobal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/autocompletedemoGlobal.gif -------------------------------------------------------------------------------- /screenshots/change-compiler-version-contextmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/change-compiler-version-contextmenu.png -------------------------------------------------------------------------------- /screenshots/change-compiler-version-gui-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/change-compiler-version-gui-setting.png -------------------------------------------------------------------------------- /screenshots/change-compiler-version-local-guisetting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/change-compiler-version-local-guisetting.png -------------------------------------------------------------------------------- /screenshots/change-compiler-version-selectversion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/change-compiler-version-selectversion.png -------------------------------------------------------------------------------- /screenshots/codeGenerateNethereumMultiSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/codeGenerateNethereumMultiSettings.png -------------------------------------------------------------------------------- /screenshots/compile-codegnerate-nethereum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/compile-codegnerate-nethereum.png -------------------------------------------------------------------------------- /screenshots/ercautocomplete1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/ercautocomplete1.png -------------------------------------------------------------------------------- /screenshots/ercautocomplete2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/ercautocomplete2.png -------------------------------------------------------------------------------- /screenshots/select-linter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/select-linter.png -------------------------------------------------------------------------------- /screenshots/simpleDemoAutocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/simpleDemoAutocomplete.gif -------------------------------------------------------------------------------- /screenshots/simpleProjectStructure.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/simpleProjectStructure.PNG -------------------------------------------------------------------------------- /screenshots/solidity-change-workspacecompiler-codeaction.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/solidity-change-workspacecompiler-codeaction.gif -------------------------------------------------------------------------------- /screenshots/solidity-corrections.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/solidity-corrections.gif -------------------------------------------------------------------------------- /screenshots/solidity-etherscan-download.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/solidity-etherscan-download.gif -------------------------------------------------------------------------------- /screenshots/solidity-hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/solidity-hover.gif -------------------------------------------------------------------------------- /screenshots/solidity-nethereum-code-generation-from-dowloaded-contract.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/solidity-nethereum-code-generation-from-dowloaded-contract.gif -------------------------------------------------------------------------------- /screenshots/solidity-references.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/solidity-references.gif -------------------------------------------------------------------------------- /screenshots/unigen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/unigen1.png -------------------------------------------------------------------------------- /screenshots/unigen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/screenshots/unigen2.png -------------------------------------------------------------------------------- /scripts/add_cmd_wrapper.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const cliPath = path.resolve('dist/cli/server.js'); // CLI script path 5 | const cmdPath = cliPath.replace('.js', '.cmd'); // Convert to Windows `.cmd` file 6 | 7 | const cmdContent = `@echo off\nnode "%~dp0/server.js" %*`; 8 | 9 | if (process.platform === 'win32') { 10 | fs.writeFile(cmdPath, cmdContent, (err) => { 11 | if (err) { 12 | console.error(`Error writing CMD wrapper: ${err}`); 13 | process.exit(1); 14 | } 15 | console.log(`CMD wrapper created: ${cmdPath}`); 16 | }); 17 | } -------------------------------------------------------------------------------- /scripts/add_shebang.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const filePath = process.argv[2]; // Get the file path argument 5 | if (!filePath) { 6 | console.error("Usage: node scripts/add_shebang.js "); 7 | process.exit(1); 8 | } 9 | 10 | const resolvedPath = path.resolve(filePath); 11 | const shebang = '#!/usr/bin/env node\n'; 12 | 13 | fs.readFile(resolvedPath, 'utf8', (err, data) => { 14 | if (err) { 15 | console.error(`Error reading file: ${err}`); 16 | process.exit(1); 17 | } 18 | 19 | // If the shebang is already there, do nothing 20 | if (data.startsWith(shebang)) { 21 | console.log(`Shebang already exists in ${filePath}`); 22 | return; 23 | } 24 | 25 | // Write the shebang to the top 26 | const updatedData = shebang + data; 27 | fs.writeFile(resolvedPath, updatedData, 'utf8', (err) => { 28 | if (err) { 29 | console.error(`Error writing file: ${err}`); 30 | process.exit(1); 31 | } 32 | console.log(`Shebang added to ${filePath}`); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /scripts/add_shebang.sh: -------------------------------------------------------------------------------- 1 | echo '#!/usr/bin/env node' | cat - "$1" > "$1.bak" && mv "$1.bak" "$1" 2 | -------------------------------------------------------------------------------- /scripts/post-standalone-build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // Paths to package.json and server source file 5 | const packageJsonPath = path.resolve(__dirname, '../package.json'); 6 | const serverFilePath = path.resolve(__dirname, '../src/server.ts'); // Adjust path if necessary 7 | 8 | // Read and modify package.json (revert name) 9 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 10 | packageJson.name = "solidity"; // Replace with the original name 11 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8'); 12 | 13 | // Read and modify the server file (revert constant) 14 | let serverCode = fs.readFileSync(serverFilePath, 'utf8'); 15 | serverCode = serverCode.replace(/const standAloneServerSide\s*=\s*true;/, 'const standAloneServerSide = false;'); 16 | fs.writeFileSync(serverFilePath, serverCode, 'utf8'); 17 | 18 | console.log("🔄 Package name reverted and standAloneServerSide set back to false!"); -------------------------------------------------------------------------------- /scripts/pre-standalone-build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // Paths to package.json and server source file 5 | const packageJsonPath = path.resolve(__dirname, '../package.json'); 6 | const serverFilePath = path.resolve(__dirname, '../src/server.ts'); // Adjust path if necessary 7 | 8 | // Read and modify package.json 9 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 10 | packageJson.name = "vscode-solidity-server"; 11 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8'); 12 | 13 | // Read and modify the server file 14 | let serverCode = fs.readFileSync(serverFilePath, 'utf8'); 15 | serverCode = serverCode.replace(/const standAloneServerSide\s*=\s*false;/, 'const standAloneServerSide = true;'); 16 | fs.writeFileSync(serverFilePath, serverCode, 'utf8'); 17 | 18 | console.log("✅ Package renamed and standAloneServerSide set to true!"); -------------------------------------------------------------------------------- /solidity-versions.txt: -------------------------------------------------------------------------------- 1 | 0.8.28: soljson-v0.8.28+commit.7893614a 2 | 0.8.27: soljson-v0.8.27+commit.40a35a09 3 | 0.8.26: soljson-v0.8.26+commit.8a97fa7a 4 | 0.8.25: soljson-v0.8.25+commit.b61c2a91 5 | 0.8.24: soljson-v0.8.24+commit.e11b9ed9 6 | 0.8.23: soljson-v0.8.23+commit.f704f362 7 | 0.8.22: soljson-v0.8.22+commit.4fc1097e 8 | 0.8.21: soljson-v0.8.21+commit.d9974bed 9 | 0.8.20: soljson-v0.8.20+commit.a1b79de6 10 | 0.8.19: soljson-v0.8.19+commit.7dd6d404 11 | 0.8.18: soljson-v0.8.18+commit.87f61d96 12 | 0.8.17: soljson-v0.8.17+commit.8df45f5f 13 | 0.8.16: soljson-v0.8.16+commit.07a7930e 14 | 0.8.15: soljson-v0.8.15+commit.e14f2714 15 | 0.8.14: soljson-v0.8.14+commit.80d49f37 16 | 0.8.13: soljson-v0.8.13+commit.abaa5c0e 17 | 0.8.12: soljson-v0.8.12+commit.f00d7308 18 | 0.8.11: soljson-v0.8.11+commit.d7f03943 19 | 0.8.10: soljson-v0.8.10+commit.fc410830 20 | 0.8.9: soljson-v0.8.9+commit.e5eed63a 21 | 0.8.8: soljson-v0.8.8+commit.dddeac2f 22 | 0.8.7: soljson-v0.8.7+commit.e28d00a7 23 | 0.8.6: soljson-v0.8.6+commit.11564f7e 24 | 0.8.5: soljson-v0.8.5+commit.a4f2e591 25 | 0.8.4: soljson-v0.8.4+commit.c7e474f2 26 | 0.8.3: soljson-v0.8.3+commit.8d00100c 27 | 0.8.2: soljson-v0.8.2+commit.661d1103 28 | 0.8.1: soljson-v0.8.1+commit.df193b15 29 | 0.8.0: soljson-v0.8.0+commit.c7dfd78e 30 | 0.7.6: soljson-v0.7.6+commit.7338295f 31 | 0.7.5: soljson-v0.7.5+commit.eb77ed08 32 | 0.7.4: soljson-v0.7.4+commit.3f05b770 33 | 0.7.3: soljson-v0.7.3+commit.9bfce1f6 34 | 0.7.2: soljson-v0.7.2+commit.51b20bc0 35 | 0.7.1: soljson-v0.7.1+commit.f4a555be 36 | 0.7.0: soljson-v0.7.0+commit.9e61f92b 37 | 0.6.12: soljson-v0.6.12+commit.27d51765 38 | 0.6.11: soljson-v0.6.11+commit.5ef660b1 39 | 0.6.10: soljson-v0.6.10+commit.00c0fcaf 40 | 0.6.9: soljson-v0.6.9+commit.3e3065ac 41 | 0.6.8: soljson-v0.6.8+commit.0bbfe453 42 | 0.6.7: soljson-v0.6.7+commit.b8d736ae 43 | 0.6.6: soljson-v0.6.6+commit.6c089d02 44 | 0.6.5: soljson-v0.6.5+commit.f956cc89 45 | 0.6.4: soljson-v0.6.4+commit.1dca32f3 46 | 0.6.3: soljson-v0.6.3+commit.8dda9521 47 | 0.6.2: soljson-v0.6.2+commit.bacdbe57 48 | 0.6.1: soljson-v0.6.1+commit.e6f7d5a4 49 | 0.6.0: soljson-v0.6.0+commit.26b70077 50 | 0.5.17: soljson-v0.5.17+commit.d19bba13 51 | 0.5.16: soljson-v0.5.16+commit.9c3226ce 52 | 0.5.15: soljson-v0.5.15+commit.6a57276f 53 | 0.5.14: soljson-v0.5.14+commit.01f1aaa4 54 | 0.5.13: soljson-v0.5.13+commit.5b0b510c 55 | 0.5.12: soljson-v0.5.12+commit.7709ece9 56 | 0.5.11: soljson-v0.5.11+commit.c082d0b4 57 | 0.5.10: soljson-v0.5.10+commit.5a6ea5b1 58 | 0.5.9: soljson-v0.5.9+commit.e560f70d 59 | 0.5.8: soljson-v0.5.8+commit.23d335f2 60 | 0.5.7: soljson-v0.5.7+commit.6da8b019 61 | 0.5.6: soljson-v0.5.6+commit.b259423e 62 | 0.5.5: soljson-v0.5.5+commit.47a71e8f 63 | 0.5.4: soljson-v0.5.4+commit.9549d8ff 64 | 0.5.3: soljson-v0.5.3+commit.10d17f24 65 | 0.5.2: soljson-v0.5.2+commit.1df8f40c 66 | 0.5.1: soljson-v0.5.1+commit.c8a2cb62 67 | 0.5.0: soljson-v0.5.0+commit.1d4f565a 68 | 0.4.26: soljson-v0.4.26+commit.4563c3fc 69 | 0.4.25: soljson-v0.4.25+commit.59dbf8f1 70 | 0.4.24: soljson-v0.4.24+commit.e67f0147 71 | 0.4.23: soljson-v0.4.23+commit.124ca40d 72 | 0.4.22: soljson-v0.4.22+commit.4cb486ee 73 | 0.4.21: soljson-v0.4.21+commit.dfe3193c 74 | 0.4.20: soljson-v0.4.20+commit.3155dd80 75 | 0.4.19: soljson-v0.4.19+commit.c4cbbb05 76 | 0.4.18: soljson-v0.4.18+commit.9cf6e910 77 | 0.4.17: soljson-v0.4.17+commit.bdeb9e52 78 | 0.4.16: soljson-v0.4.16+commit.d7661dd9 79 | 0.4.15: soljson-v0.4.15+commit.bbb8e64f 80 | 0.4.14: soljson-v0.4.14+commit.c2215d46 81 | 0.4.13: soljson-v0.4.13+commit.0fb4cb1a 82 | 0.4.12: soljson-v0.4.12+commit.194ff033 83 | 0.4.11: soljson-v0.4.11+commit.68ef5810 84 | 0.4.10: soljson-v0.4.10+commit.f0d539ae 85 | 0.4.9: soljson-v0.4.9+commit.364da425 86 | 0.4.8: soljson-v0.4.8+commit.60cc1668 87 | 0.4.7: soljson-v0.4.7+commit.822622cf 88 | 0.4.6: soljson-v0.4.6+commit.2dabbdf0 89 | 0.4.5: soljson-v0.4.5+commit.b318366e 90 | 0.4.4: soljson-v0.4.4+commit.4633f3de 91 | 0.4.3: soljson-v0.4.3+commit.2353da71 92 | 0.4.2: soljson-v0.4.2+commit.af6afb04 93 | 0.4.1: soljson-v0.4.1+commit.4fc6fc2c 94 | 0.4.0: soljson-v0.4.0+commit.acd334c9 95 | 0.3.6: soljson-v0.3.6+commit.3fc68da5 96 | 0.3.5: soljson-v0.3.5+commit.5f97274a 97 | 0.3.4: soljson-v0.3.4+commit.7dab8902 98 | 0.3.3: soljson-v0.3.3+commit.4dc1cb14 99 | 0.3.2: soljson-v0.3.2+commit.81ae2a78 100 | 0.3.1: soljson-v0.3.1+commit.c492d9be 101 | 0.3.0: soljson-v0.3.0+commit.11d67369 102 | 0.2.2: soljson-v0.2.2+commit.ef92f566 103 | 0.2.1: soljson-v0.2.1+commit.91a6b35f 104 | 0.2.0: soljson-v0.2.0+commit.4dc2445e 105 | 0.1.7: soljson-v0.1.7+commit.b4e666cc 106 | 0.1.6: soljson-v0.1.6+commit.d41f8b7c 107 | 0.1.5: soljson-v0.1.5+commit.23865e39 108 | 0.1.4: soljson-v0.1.4+commit.5f6c3cdf 109 | 0.1.3: soljson-v0.1.3+commit.028f561d 110 | 0.1.2: soljson-v0.1.2+commit.d0d36e3 111 | 0.1.1: soljson-v0.1.1+commit.6ff4cd6 112 | -------------------------------------------------------------------------------- /solidity.configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "//", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": [ "/*", "*/" ] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["[", "]"], 12 | ["(", ")"] 13 | ], 14 | "autoClosingPairs": [ 15 | { "open": "{", "close": "}" }, 16 | { "open": "[", "close": "]" }, 17 | { "open": "(", "close": ")" }, 18 | { "open": "/**", "close": " */", "notIn": ["string"] } 19 | ], 20 | "surroundingPairs": [ 21 | ["{", "}"], 22 | ["[", "]"], 23 | ["(", ")"] 24 | ], 25 | // markers used to folding code regions 26 | "folding": { 27 | "markers": { 28 | "start": "^\\s*//\\s*#?region\\b", 29 | "end": "^\\s*//\\s*#?endregion\\b" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/client/codeActionProviders/addressChecksumActionProvider.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as vscode from 'vscode'; 3 | 4 | export class AddressChecksumCodeActionProvider implements vscode.CodeActionProvider { 5 | 6 | 7 | public static readonly providedCodeActionKinds = [ 8 | vscode.CodeActionKind.QuickFix, 9 | vscode.CodeActionKind.SourceFixAll, 10 | vscode.CodeActionKind.Empty, 11 | ]; 12 | 13 | public static ADDRESS_CHECKSUM_ERRORCODE = '9429'; 14 | 15 | private static regex = /Correct checksummed address: "0x(?
[0-9a-fA-F]*)"/gm; 16 | 17 | 18 | public static createFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction { 19 | const match = this.regex.exec(diagnostic.message); 20 | if (match) { 21 | if (match.groups['address']) { 22 | const fixedAddress = match.groups['address']; 23 | const fix = new vscode.CodeAction(`Convert address to checksummed address: 0x${fixedAddress}`, vscode.CodeActionKind.QuickFix); 24 | 25 | fix.edit = new vscode.WorkspaceEdit(); 26 | fix.edit.replace(document.uri, new vscode.Range(diagnostic.range.start, diagnostic.range.start.translate(0, fixedAddress.length + 2)), '0x' + fixedAddress); 27 | fix.isPreferred = true; 28 | fix.diagnostics = [diagnostic]; 29 | return fix; 30 | } 31 | } 32 | return null; 33 | } 34 | 35 | // tslint:disable-next-line:max-line-length 36 | public provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): vscode.CodeAction[] { 37 | // for each diagnostic entry that has the matching `code`, create a code action command 38 | return context.diagnostics 39 | .filter(diagnostic => diagnostic.code === AddressChecksumCodeActionProvider.ADDRESS_CHECKSUM_ERRORCODE) 40 | .map(diagnostic => AddressChecksumCodeActionProvider.createFix(document, diagnostic)); 41 | } 42 | 43 | 44 | } 45 | 46 | 47 | export class ChangeCompilerVersionActionProvider implements vscode.CodeActionProvider { 48 | 49 | 50 | public static readonly providedCodeActionKinds = [ 51 | vscode.CodeActionKind.QuickFix, 52 | vscode.CodeActionKind.Empty, 53 | ]; 54 | public static COMPILER_ERRORCODE = '5333'; 55 | 56 | public static createFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction { 57 | 58 | const fix = new vscode.CodeAction(`Change workspace compiler version`, vscode.CodeActionKind.QuickFix); 59 | fix.command = { command: 'solidity.selectWorkspaceRemoteSolcVersion', 60 | title: 'Change the workspace remote compiler version', 61 | tooltip: 'This will open a prompt with the solidity version' }; 62 | fix.diagnostics = [diagnostic]; 63 | return fix; 64 | } 65 | 66 | // tslint:disable-next-line:max-line-length 67 | public provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): vscode.CodeAction[] { 68 | const results: vscode.CodeAction[] = []; 69 | const diagnostics = context.diagnostics 70 | .filter(diagnostic => diagnostic.code === ChangeCompilerVersionActionProvider.COMPILER_ERRORCODE); 71 | diagnostics.forEach(diagnostic => { 72 | results.push(ChangeCompilerVersionActionProvider.createFix(document, diagnostic)); 73 | } ); 74 | return results; 75 | } 76 | 77 | 78 | } 79 | 80 | 81 | export class SPDXCodeActionProvider implements vscode.CodeActionProvider { 82 | 83 | 84 | public static readonly providedCodeActionKinds = [ 85 | vscode.CodeActionKind.QuickFix, 86 | vscode.CodeActionKind.Empty, 87 | ]; 88 | public static SPDX_ERRORCODE = '1878'; 89 | public static licenses: string[] = ['MIT', 'UNKNOWN', 'UNLICENSED']; 90 | public static preferredLicense = 'MIT'; 91 | 92 | public static createFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic, license: string, isPreferred = false): vscode.CodeAction { 93 | 94 | const fix = new vscode.CodeAction(`Add SPDX License ` + license, vscode.CodeActionKind.QuickFix); 95 | const licenseText = '// SPDX-License-Identifier: ' + license + ' \n'; 96 | fix.edit = new vscode.WorkspaceEdit(); 97 | fix.edit.insert(document.uri, diagnostic.range.start, licenseText); 98 | fix.isPreferred = isPreferred; 99 | fix.diagnostics = [diagnostic]; 100 | return fix; 101 | } 102 | 103 | // tslint:disable-next-line:max-line-length 104 | public provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): vscode.CodeAction[] { 105 | const results: vscode.CodeAction[] = []; 106 | const diagnostics = context.diagnostics 107 | .filter(diagnostic => diagnostic.code === SPDXCodeActionProvider.SPDX_ERRORCODE); 108 | diagnostics.forEach(diagnostic => { 109 | results.push(SPDXCodeActionProvider.createFix(document, diagnostic, SPDXCodeActionProvider.preferredLicense, true)); 110 | SPDXCodeActionProvider.licenses.forEach(license => { 111 | if (license !== SPDXCodeActionProvider.preferredLicense) { 112 | results.push(SPDXCodeActionProvider.createFix(document, diagnostic, license, false)); 113 | } 114 | }); 115 | 116 | } ); 117 | return results; 118 | } 119 | 120 | 121 | } 122 | 123 | -------------------------------------------------------------------------------- /src/client/compileActive.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | import {Compiler} from './compiler'; 5 | import {SourceDocumentCollection} from '../common/model/sourceDocumentCollection'; 6 | import { initialiseProject } from '../common/projectService'; 7 | import { formatPath } from '../common/util'; 8 | import { compilerType } from '../common/solcCompiler'; 9 | import * as workspaceUtil from './workspaceUtil'; 10 | import { SettingsService } from './settingsService'; 11 | 12 | 13 | 14 | let diagnosticCollection: vscode.DiagnosticCollection; 15 | 16 | export function initDiagnosticCollection(diagnostics: vscode.DiagnosticCollection) { 17 | diagnosticCollection = diagnostics; 18 | } 19 | 20 | export function compileActiveContract(compiler: Compiler, overrideDefaultCompiler: compilerType = null): Promise> { 21 | const editor = vscode.window.activeTextEditor; 22 | 23 | if (!editor) { 24 | return; // We need something open 25 | } 26 | 27 | if (path.extname(editor.document.fileName) !== '.sol') { 28 | vscode.window.showWarningMessage('This not a solidity file (*.sol)'); 29 | return; 30 | } 31 | 32 | // Check if is folder, if not stop we need to output to a bin folder on rootPath 33 | if (workspaceUtil.getCurrentWorkspaceRootFolder() === undefined) { 34 | vscode.window.showWarningMessage('Please open a folder in Visual Studio Code as a workspace'); 35 | return; 36 | } 37 | 38 | const contractsCollection = new SourceDocumentCollection(); 39 | const contractCode = editor.document.getText(); 40 | const contractPath = editor.document.fileName; 41 | 42 | const packageDefaultDependenciesDirectory = SettingsService.getPackageDefaultDependenciesDirectories(); 43 | const packageDefaultDependenciesContractsDirectory = SettingsService.getPackageDefaultDependenciesContractsDirectory(); 44 | const compilationOptimisation = SettingsService.getCompilerOptimisation(); 45 | const evmVersion = SettingsService.getEVMVersion(); 46 | const viaIR = SettingsService.getViaIR(); 47 | const remappings = workspaceUtil.getSolidityRemappings(); 48 | const project = initialiseProject(workspaceUtil.getCurrentProjectInWorkspaceRootFsPath(), 49 | packageDefaultDependenciesDirectory, 50 | packageDefaultDependenciesContractsDirectory, 51 | remappings); 52 | const contract = contractsCollection.addSourceDocumentAndResolveImports(contractPath, contractCode, project); 53 | 54 | const packagesPath: string[] = []; 55 | if (project.packagesDir != null) { 56 | project.packagesDir.forEach(x => packagesPath.push(formatPath(x))); 57 | } 58 | 59 | return compiler.compile(contractsCollection.getDefaultSourceDocumentsForCompilation(compilationOptimisation, evmVersion, viaIR), 60 | diagnosticCollection, 61 | project.projectPackage.build_dir, 62 | project.projectPackage.absoluletPath, 63 | null, 64 | packagesPath, 65 | contract.absolutePath, 66 | overrideDefaultCompiler); 67 | } 68 | -------------------------------------------------------------------------------- /src/client/compileAll.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import {Compiler} from './compiler'; 6 | import {SourceDocumentCollection} from '../common/model/sourceDocumentCollection'; 7 | import { initialiseProject } from '../common/projectService'; 8 | import { formatPath, isPathSubdirectory } from '../common/util'; 9 | import * as workspaceUtil from './workspaceUtil'; 10 | import { SettingsService } from './settingsService'; 11 | 12 | export function compileAllContracts(compiler: Compiler, diagnosticCollection: vscode.DiagnosticCollection) { 13 | 14 | // Check if is folder, if not stop we need to output to a bin folder on rootPath 15 | if (workspaceUtil.getCurrentWorkspaceRootFolder() === undefined) { 16 | vscode.window.showWarningMessage('Please open a folder in Visual Studio Code as a workspace'); 17 | return; 18 | } 19 | const rootPath = workspaceUtil.getCurrentProjectInWorkspaceRootFsPath(); 20 | const packageDefaultDependenciesDirectory = SettingsService.getPackageDefaultDependenciesDirectories(); 21 | const packageDefaultDependenciesContractsDirectory = SettingsService.getPackageDefaultDependenciesContractsDirectory(); 22 | const compilationOptimisation = SettingsService.getCompilerOptimisation(); 23 | const evmVersion = SettingsService.getEVMVersion(); 24 | const viaIR = SettingsService.getViaIR(); 25 | const remappings = workspaceUtil.getSolidityRemappings(); 26 | 27 | const contractsCollection = new SourceDocumentCollection(); 28 | const project = initialiseProject(rootPath, packageDefaultDependenciesDirectory, packageDefaultDependenciesContractsDirectory, remappings); 29 | 30 | // Process open Text Documents first as it is faster (We might need to save them all first? Is this assumed?) 31 | vscode.workspace.textDocuments.forEach(document => { 32 | if (isPathSubdirectory(rootPath, document.fileName)) { 33 | if (path.extname(document.fileName) === '.sol' ) { 34 | const contractPath = document.fileName; 35 | const contractCode = document.getText(); 36 | contractsCollection.addSourceDocumentAndResolveImports(contractPath, contractCode, project); 37 | } 38 | } 39 | }); 40 | 41 | const documents = project.getAllSolFilesIgnoringDependencyFolders(); 42 | 43 | documents.forEach(document => { 44 | const contractPath = document; 45 | if (!contractsCollection.containsSourceDocument(contractPath)) { 46 | const contractCode = fs.readFileSync(contractPath, 'utf8'); 47 | contractsCollection.addSourceDocumentAndResolveImports(contractPath, contractCode, project); 48 | } 49 | }); 50 | const sourceDirPath = formatPath(project.projectPackage.getSolSourcesAbsolutePath()); 51 | const packagesPath: string[] = []; 52 | if (project.packagesDir != null) { 53 | project.packagesDir.forEach(x => packagesPath.push(formatPath(x))); 54 | } 55 | 56 | compiler.compile(contractsCollection.getDefaultSourceDocumentsForCompilation(compilationOptimisation, evmVersion, viaIR), 57 | diagnosticCollection, 58 | project.projectPackage.build_dir, 59 | project.projectPackage.absoluletPath, 60 | sourceDirPath, 61 | packagesPath); 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/client/formatter/forgeFormatter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as cp from 'child_process'; 3 | import * as workspaceUtil from '../workspaceUtil'; 4 | 5 | export async function formatDocument(document: vscode.TextDocument, context: vscode.ExtensionContext): Promise { 6 | const firstLine = document.lineAt(0); 7 | const lastLine = document.lineAt(document.lineCount - 1); 8 | const fullTextRange = new vscode.Range(firstLine.range.start, lastLine.range.end); 9 | const rootPath = workspaceUtil.getCurrentProjectInWorkspaceRootFsPath(); 10 | const formatted = await formatDocumentInternal(document.getText(), rootPath); 11 | return [vscode.TextEdit.replace(fullTextRange, formatted)]; 12 | } 13 | 14 | async function formatDocumentInternal(documentText, rootPath): Promise { 15 | return await new Promise((resolve, reject) => { 16 | const forge = cp.execFile( 17 | 'forge', 18 | ['fmt', '--raw', '-'], 19 | { cwd: rootPath }, 20 | (err, stdout) => { 21 | if (err !== null) { 22 | console.error(err); 23 | return reject(err); 24 | } 25 | 26 | resolve(stdout); 27 | }, 28 | ); 29 | 30 | forge.stdin?.write(documentText); 31 | forge.stdin?.end(); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/client/formatter/formatter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as prettier from './prettierFormatter'; 3 | import * as forge from './forgeFormatter'; 4 | 5 | export async function formatDocument(document: vscode.TextDocument, context: vscode.ExtensionContext): Promise { 6 | const formatter = vscode.workspace.getConfiguration('solidity').get('formatter'); 7 | console.log(formatter); 8 | switch (formatter) { 9 | case 'prettier': 10 | return await prettier.formatDocument(document, context); 11 | case 'forge': 12 | return forge.formatDocument(document, context); 13 | default: 14 | return null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/formatter/formatterPrettierWorker.js: -------------------------------------------------------------------------------- 1 | const { parentPort } = require('worker_threads'); 2 | 3 | parentPort.on('message', async (task) => { 4 | try { 5 | // Dynamically import prettier and the plugin 6 | const prettier = require(task.prettierPath); 7 | const pluginPath = require(task.pluginPath); 8 | 9 | // Resolve config 10 | const config = await prettier.resolveConfig(task.documentPath); 11 | if (config !== null) { 12 | await prettier.clearConfigCache(); 13 | } 14 | 15 | // Merge user config with default options 16 | const options = { ...task.options, ...config, plugins: [pluginPath] }; 17 | const formatted = prettier.format(task.source, options); 18 | 19 | parentPort.postMessage({ success: true, formatted }); 20 | } catch (error) { 21 | parentPort.postMessage({ success: false, error: error.message }); 22 | } 23 | }); -------------------------------------------------------------------------------- /src/client/formatter/prettierFormatter.ts: -------------------------------------------------------------------------------- 1 | 2 | const { Worker } = require('worker_threads'); 3 | 4 | import * as vscode from 'vscode'; 5 | import * as path from 'path'; 6 | 7 | export async function formatDocument(document, context) : Promise { 8 | const source = document.getText(); 9 | const documentPath = document.uri.fsPath; 10 | const pluginPathFile = path.join(context.extensionPath, 'node_modules', 'prettier-plugin-solidity', 'dist','standalone.cjs'); 11 | const prettierPathFile = path.join(context.extensionPath, 'node_modules', 'prettier'); 12 | const pluginPath = pluginPathFile ; 13 | const prettierPath = prettierPathFile; 14 | const options = { 15 | parser: 'solidity-parse', 16 | pluginSearchDirs: [context.extensionPath], 17 | }; 18 | 19 | return new Promise((resolve, reject) => { 20 | const workerPath = path.join(__dirname, 'formatterPrettierWorker.js'); 21 | let uri = vscode.Uri.file(workerPath).fsPath; 22 | const worker = new Worker(uri.toString()); 23 | worker.on('message', (response) => { 24 | worker.terminate(); 25 | if (response.success) { 26 | const firstLine = document.lineAt(0); 27 | const lastLine = document.lineAt(document.lineCount - 1); 28 | const fullTextRange = new vscode.Range(firstLine.range.start, lastLine.range.end); 29 | resolve([vscode.TextEdit.replace(fullTextRange, response.formatted)]); 30 | } else { 31 | console.error(response.error); 32 | resolve([]); 33 | } 34 | }); 35 | worker.on('error', (err) => { 36 | worker.terminate(); 37 | console.error(err); 38 | resolve([]); 39 | }); 40 | 41 | worker.postMessage({ source, options, documentPath, prettierPath, pluginPath }); 42 | }); 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/client/outputChannelService.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export class OutputChannelService { 4 | // Singleton instance 5 | private static instance: OutputChannelService | null = null; 6 | 7 | // Output channel reference 8 | private nethereumCodeGenerationOuputChannel: vscode.OutputChannel; 9 | private solidityCompilerOutputChannel: vscode.OutputChannel; 10 | 11 | // Get the singleton instance of the OutputChannelService 12 | public static getInstance(): OutputChannelService { 13 | if (!this.instance) { 14 | this.instance = new OutputChannelService(); 15 | } 16 | return this.instance; 17 | } 18 | 19 | public getNethereumCodeGenerationOutputChannel(): vscode.OutputChannel { 20 | return this.nethereumCodeGenerationOuputChannel; 21 | } 22 | 23 | public getSolidityCompilerOutputChannel(): vscode.OutputChannel { 24 | return this.solidityCompilerOutputChannel; 25 | } 26 | 27 | // Method to dispose of the output channel (useful during extension deactivation) 28 | public dispose(): void { 29 | if (this.nethereumCodeGenerationOuputChannel) { 30 | this.nethereumCodeGenerationOuputChannel.dispose(); 31 | OutputChannelService.instance = null; // Reset instance for future usage if needed 32 | } 33 | } 34 | 35 | // Private constructor to prevent direct instantiation 36 | private constructor() { 37 | // Create the output channel upon instantiation 38 | this.nethereumCodeGenerationOuputChannel = vscode.window.createOutputChannel('Nethereum Code Generation'); 39 | this.solidityCompilerOutputChannel = vscode.window.createOutputChannel('Solidity Compiler'); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/client/settingsService.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { EtherscanDomainChainMapper } from '../common/sourceCodeDownloader/etherscanSourceCodeDownloader'; 5 | 6 | export class SettingsService { 7 | 8 | public static getPackageDefaultDependenciesDirectories(): string[] { 9 | const packageDefaultDependenciesDirectory = vscode.workspace.getConfiguration('solidity').get('packageDefaultDependenciesDirectory'); 10 | if (typeof packageDefaultDependenciesDirectory === 'string') {return [packageDefaultDependenciesDirectory]; } 11 | return packageDefaultDependenciesDirectory; 12 | } 13 | 14 | public static getPackageDefaultDependenciesContractsDirectory(): string[] { 15 | const packageDefaultDependenciesContractsDirectory = vscode.workspace.getConfiguration('solidity').get('packageDefaultDependenciesContractsDirectory'); 16 | if (typeof packageDefaultDependenciesContractsDirectory === 'string') {return [packageDefaultDependenciesContractsDirectory]; } 17 | return packageDefaultDependenciesContractsDirectory; 18 | } 19 | 20 | public static getCompilerOptimisation(): number { 21 | return vscode.workspace.getConfiguration('solidity').get('compilerOptimization'); 22 | } 23 | 24 | public static getEVMVersion(): string { 25 | return vscode.workspace.getConfiguration('solidity').get('evmVersion'); 26 | } 27 | 28 | public static getViaIR(): boolean { 29 | return vscode.workspace.getConfiguration('solidity').get('viaIR'); 30 | } 31 | 32 | 33 | public static getRemappings(): string[] { 34 | return vscode.workspace.getConfiguration('solidity').get('remappings'); 35 | } 36 | 37 | public static getRemappingsWindows(): string[] { 38 | return vscode.workspace.getConfiguration('solidity').get('remappingsWindows'); 39 | } 40 | 41 | public static getRemappingsUnix(): string[] { 42 | return vscode.workspace.getConfiguration('solidity').get('remappingsUnix'); 43 | } 44 | 45 | public static getMonoRepoSupport(): boolean { 46 | return vscode.workspace.getConfiguration('solidity').get('monoRepoSupport'); 47 | } 48 | 49 | public static getExplorerEtherscanBasedApiKey(server: string): string { 50 | 51 | const key = EtherscanDomainChainMapper.getApiKeyMappings()[server]; 52 | return vscode.workspace.getConfiguration('solidity').get(key); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/client/solErrorsToDiaganosticsClient.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import {errorToDiagnostic} from '../server/solErrorsToDiagnostics'; 4 | import { DiagnosticSeverity } from 'vscode-languageserver'; 5 | 6 | interface ErrorWarningCounts { 7 | errors: number; 8 | warnings: number; 9 | } 10 | 11 | export function errorsToDiagnostics(diagnosticCollection: vscode.DiagnosticCollection, errors: any): ErrorWarningCounts { 12 | const errorWarningCounts: ErrorWarningCounts = {errors: 0, warnings: 0}; 13 | const diagnosticMap: Map = new Map(); 14 | 15 | errors.forEach(error => { 16 | const {diagnostic, fileName} = errorToDiagnostic(error); 17 | 18 | const targetUri = vscode.Uri.file(fileName); 19 | let diagnostics = diagnosticMap.get(targetUri); 20 | 21 | if (!diagnostics) { 22 | diagnostics = []; 23 | } 24 | 25 | diagnostics.push(diagnostic); 26 | diagnosticMap.set(targetUri, diagnostics); 27 | }); 28 | 29 | const entries: [vscode.Uri, vscode.Diagnostic[]][] = []; 30 | 31 | diagnosticMap.forEach((diags, uri) => { 32 | errorWarningCounts.errors += diags.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error).length; 33 | errorWarningCounts.warnings += diags.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Warning).length; 34 | 35 | entries.push([uri, diags]); 36 | }); 37 | 38 | diagnosticCollection.set(entries); 39 | 40 | return errorWarningCounts; 41 | } 42 | -------------------------------------------------------------------------------- /src/client/workspaceUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { replaceRemappings } from '../common/util'; 3 | import { findFirstRootProjectFile } from '../common/projectService'; 4 | import { SettingsService } from './settingsService'; 5 | 6 | 7 | export function getCurrentProjectInWorkspaceRootFsPath() { 8 | const monoreposupport = SettingsService.getMonoRepoSupport(); 9 | const currentRootPath = getCurrentWorkspaceRootFsPath(); 10 | if ( monoreposupport ) { 11 | const editor = vscode.window.activeTextEditor; 12 | const currentDocument = editor.document.uri; 13 | const projectFolder = findFirstRootProjectFile(currentRootPath, currentDocument.fsPath); 14 | if (projectFolder == null) { 15 | return currentRootPath; 16 | } else { 17 | return projectFolder; 18 | } 19 | } else { 20 | return currentRootPath; 21 | } 22 | 23 | } 24 | 25 | export function getCurrentWorkspaceRootFsPath() { 26 | return getCurrentWorkspaceRootFolder()?.uri?.fsPath; 27 | } 28 | 29 | export function getCurrentWorkspaceRootFolder() { 30 | const editor = vscode.window.activeTextEditor; 31 | const currentDocument = editor.document.fileName; 32 | return vscode.workspace.getWorkspaceFolder(vscode.Uri.file(currentDocument)); 33 | 34 | } 35 | 36 | export function getSolidityRemappings(): string[] { 37 | const remappings = SettingsService.getRemappings(); 38 | if (process.platform === 'win32') { 39 | return replaceRemappings(remappings, SettingsService.getRemappingsWindows()); 40 | } else { 41 | return replaceRemappings(remappings, SettingsService.getRemappingsUnix()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/common/model/package.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { Remapping } from './remapping'; 5 | 6 | export class Package { 7 | public name: string; 8 | public version: string; 9 | public sol_sources: string; 10 | public build_dir: string; 11 | public absoluletPath: string; 12 | public dependencies: any; 13 | private sol_sources_alternative_directories: string[] = []; 14 | 15 | public appendToSolSourcesAternativeDirectories(extraSolSourcesAlternativeDirectories: string[]) { 16 | this.sol_sources_alternative_directories = [...new Set(this.sol_sources_alternative_directories.concat(extraSolSourcesAlternativeDirectories))]; 17 | } 18 | 19 | constructor(solidityDirectory: string[]) { 20 | this.build_dir = 'bin'; 21 | if (solidityDirectory !== null && solidityDirectory.length > 0 ) { 22 | this.sol_sources = solidityDirectory[0]; 23 | this.sol_sources_alternative_directories = solidityDirectory; 24 | } else { 25 | this.sol_sources = ''; 26 | } 27 | } 28 | 29 | public getSolSourcesAbsolutePath() { 30 | if (this.sol_sources !== undefined || this.sol_sources === '') { 31 | return path.join(this.absoluletPath, this.sol_sources); 32 | } 33 | return this.absoluletPath; 34 | } 35 | 36 | public isImportForThis(contractDependencyImport: string) { 37 | const splitDirectories = contractDependencyImport.split('/'); 38 | if (splitDirectories.length === 1) { 39 | return false; 40 | } 41 | return splitDirectories[0] === this.name; 42 | } 43 | 44 | public resolveImport(contractDependencyImport: string) { 45 | if (this.isImportForThis(contractDependencyImport)) { 46 | const defaultPath = path.join(this.getSolSourcesAbsolutePath(), contractDependencyImport.substring(this.name.length)); 47 | if (fs.existsSync(defaultPath)) { 48 | return defaultPath; 49 | } else { 50 | for (let index = 0; index < this.sol_sources_alternative_directories.length; index++) { 51 | const directory = this.sol_sources_alternative_directories[index]; 52 | if (directory !== undefined || directory === '') { 53 | const fullpath = path.join(this.absoluletPath, directory, contractDependencyImport.substring(this.name.length)); 54 | if (fs.existsSync(fullpath)) { 55 | return fullpath; 56 | } 57 | } 58 | } 59 | } 60 | } 61 | return null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/common/model/project.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {Package} from './package'; 3 | import { Remapping, importRemappings, importRemappingArray } from './remapping'; 4 | import * as path from 'path'; 5 | import { glob } from 'glob'; 6 | 7 | export class Project { 8 | public projectPackage: Package; 9 | public dependencies: Array; 10 | public packagesDir: string[]; 11 | public remappings: Remapping[]; 12 | 13 | constructor(projectPackage: Package, dependencies: Array, packagesDir: string[], remappings: string[]) { 14 | this.projectPackage = projectPackage; 15 | this.dependencies = dependencies; 16 | this.packagesDir = packagesDir; 17 | this.remappings = importRemappingArray(remappings, this); 18 | } 19 | // This will need to add the current package as a parameter to resolve version dependencies 20 | public findDependencyPackage(contractDependencyImport: string) { 21 | return this.dependencies.find((depPack: Package) => depPack.isImportForThis(contractDependencyImport)); 22 | } 23 | 24 | public getAllSolFilesIgnoringDependencyFolders() { 25 | const solPath = this.projectPackage.getSolSourcesAbsolutePath() + '/**/*.sol'; 26 | const exclusions: string[] = []; 27 | this.packagesDir.forEach(x => { 28 | exclusions.push(path.join(this.projectPackage.getSolSourcesAbsolutePath(), x, '**')); 29 | }); 30 | exclusions.push(path.join(this.projectPackage.getSolSourcesAbsolutePath(), this.projectPackage.build_dir, '**')); 31 | this.getAllRelativeLibrariesAsExclusionsFromRemappings().forEach(x => exclusions.push(x)); 32 | return glob.sync( solPath, { ignore: exclusions, nodir: true }); 33 | } 34 | 35 | public getAllRelativeLibrariesAsExclusionsFromRemappings(): string[] { 36 | return this.getAllRelativeLibrariesRootDirsFromRemappingsAbsolutePaths().map(x => path.join(x, '**')); 37 | } 38 | 39 | public getAllRelativeLibrariesRootDirsFromRemappings(): string[] { 40 | const results: string[] = []; 41 | this.remappings.forEach(element => { 42 | const dirLib = element.getLibraryPathIfRelative(this.projectPackage.getSolSourcesAbsolutePath()); 43 | if (dirLib !== null && results.find(x => x === dirLib) === undefined) { 44 | results.push(dirLib); 45 | } 46 | }); 47 | return results; 48 | } 49 | 50 | public getAllRelativeLibrariesRootDirsFromRemappingsAbsolutePaths() { 51 | return this.getAllRelativeLibrariesRootDirsFromRemappings().map(x => path.resolve(this.projectPackage.getSolSourcesAbsolutePath(), x)); 52 | } 53 | 54 | public findImportRemapping(contractDependencyImport: string): Remapping { 55 | // const remappings = importRemappings("@openzeppelin/=lib/openzeppelin-contracts//\r\nds-test/=lib/ds-test/src/", this); 56 | const foundRemappings = []; 57 | this.remappings.forEach(element => { 58 | if ( element.isImportForThis(contractDependencyImport)) { 59 | foundRemappings.push(element); 60 | } 61 | }); 62 | 63 | if (foundRemappings.length > 0) { 64 | return this.sortByLength(foundRemappings)[foundRemappings.length - 1]; 65 | } 66 | return null; 67 | } 68 | 69 | public findRemappingForFile(filePath: string): Remapping { 70 | const foundRemappings = []; 71 | this.remappings.forEach(element => { 72 | if ( element.isFileForThis(filePath)) { 73 | foundRemappings.push(element); 74 | } 75 | }); 76 | 77 | if (foundRemappings.length > 0) { 78 | return this.sortByLength(foundRemappings)[foundRemappings.length - 1]; 79 | } 80 | return null; 81 | } 82 | 83 | private sortByLength(array) { 84 | return array.sort(function(a, b) { 85 | return a.length - b.length; 86 | }); 87 | } 88 | } 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/common/model/remapping.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as path from 'path'; 3 | import { Project } from './project'; 4 | import { isPathSubdirectory } from '../util'; 5 | 6 | export class Remapping { 7 | public context: string; 8 | public prefix: string; 9 | public target: string; 10 | public basePath: string; 11 | 12 | public isImportForThis(contractDependencyImport: string) { 13 | if (this.context !== undefined) { 14 | return contractDependencyImport.startsWith(this.context + ':' + this.prefix); 15 | } 16 | return contractDependencyImport.startsWith(this.prefix); 17 | } 18 | 19 | public getLibraryPathIfRelative(projectPath: string) { 20 | if (!path.isAbsolute(this.target)) { 21 | const fullPath = path.join(this.basePath, this.target); 22 | if (isPathSubdirectory(projectPath, fullPath)) { 23 | return path.dirname(this.target).split(path.sep)[0]; 24 | } 25 | } 26 | return null; 27 | } 28 | 29 | public createImportFromFile(filePath: string) { 30 | if (this.isFileForThis(filePath)) { 31 | if (path.isAbsolute(this.target)) { 32 | if (this.context === undefined) { 33 | return path.join(this.prefix, filePath.substring(this.target.length)); 34 | } 35 | if (this.context !== undefined) { 36 | return path.join(this.context + ':' + this.prefix, filePath.substring(this.target.length)); 37 | } 38 | } else { 39 | 40 | if (this.context === undefined) { 41 | return path.join(this.prefix, filePath.substring(path.join(this.basePath, this.target).length)); 42 | } 43 | if (this.context !== undefined) { 44 | return path.join(this.context + ':' + this.prefix, filePath.substring(path.join(this.basePath, this.target).length)); 45 | } 46 | } 47 | } 48 | } 49 | 50 | public isFileForThis(filePath: string) { 51 | if (path.isAbsolute(this.target)) { 52 | return filePath.startsWith(this.target); 53 | } else { 54 | return filePath.startsWith(path.join(this.basePath, this.target)); 55 | } 56 | } 57 | 58 | public resolveImport(contractDependencyImport: string) { 59 | if (contractDependencyImport === null || contractDependencyImport === undefined) { return null; } 60 | const validImport = this.isImportForThis(contractDependencyImport); 61 | if (path.isAbsolute(this.target)) { 62 | if (validImport && this.context === undefined) { 63 | return path.join(this.target, contractDependencyImport.substring(this.prefix.length)); 64 | } 65 | 66 | if (validImport && this.context !== undefined) { 67 | return path.join(this.target, contractDependencyImport.substring((this.context + ':' + this.prefix).length)); 68 | } 69 | } else { 70 | if (validImport && this.context === undefined) { 71 | return path.join(this.basePath, this.target, contractDependencyImport.substring(this.prefix.length)); 72 | } 73 | 74 | if (validImport && this.context !== undefined) { 75 | return path.join(this.basePath, this.target, contractDependencyImport.substring((this.context + ':' + this.prefix).length)); 76 | } 77 | } 78 | return null; 79 | } 80 | } 81 | 82 | export function importRemappings(remappings: string, project: Project): Array { 83 | const remappingArray = remappings.split(/\r\n|\r|\n/); // split lines 84 | return importRemappingArray(remappingArray, project); 85 | } 86 | 87 | export function importRemappingArray(remappings: string[], project: Project): Array { 88 | const remappingsList = new Array(); 89 | if (remappings !== undefined && remappings.length > 0) { 90 | remappings.forEach(remappingElement => { 91 | const remapping = new Remapping(); 92 | remapping.basePath = project.projectPackage.absoluletPath; 93 | const regex = /((?[\S]+)\:)?(?[\S]+)=(?.+)/g; 94 | const match = regex.exec(remappingElement); 95 | if (match) { 96 | if (match.groups['context']) { 97 | remapping.context = match.groups['context']; 98 | } 99 | 100 | if (match.groups['prefix']) { 101 | remapping.prefix = match.groups['prefix']; 102 | remapping.target = match.groups['target']; 103 | remappingsList.push(remapping); 104 | } 105 | } 106 | }); 107 | } 108 | return remappingsList; 109 | } -------------------------------------------------------------------------------- /src/common/model/sourceDocument.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as path from 'path'; 3 | import { formatPath } from '../util'; 4 | import { Project } from './project'; 5 | 6 | export class SourceDocument { 7 | public code: string; 8 | public unformattedCode: string; 9 | // TODO: Import needs to be a class including if is local, absolutePath, module etc 10 | public imports: Array; 11 | public absolutePath: string; 12 | public packagePath: string; 13 | public abi: string; 14 | public project: Project; 15 | 16 | public static getAllLibraryImports(code: string): string[] { 17 | const importRegEx = /^\s?import\s+[^'"]*['"](.*)['"]\s*/gm; 18 | const imports: string[] = []; 19 | let foundImport = importRegEx.exec(code); 20 | while (foundImport != null) { 21 | const importPath = foundImport[1]; 22 | 23 | if (!this.isImportLocal(importPath)) { 24 | imports.push(importPath); 25 | } 26 | 27 | foundImport = importRegEx.exec(code); 28 | } 29 | return imports; 30 | } 31 | 32 | public static isImportLocal(importPath: string) { 33 | return importPath.startsWith('.'); 34 | } 35 | 36 | constructor(absoulePath: string, code: string, project: Project) { 37 | this.absolutePath = this.formatDocumentPath(absoulePath); 38 | this.code = code; 39 | this.unformattedCode = code; 40 | this.project = project; 41 | this.imports = new Array(); 42 | } 43 | 44 | /** 45 | * Resolve import statement to absolute file path 46 | * 47 | * @param {string} importPath import statement in *.sol contract 48 | * @param {SourceDocument} contract the contract where the import statement belongs 49 | * @returns {string} the absolute path of the imported file 50 | */ 51 | public resolveImportPath(importPath: string): string { 52 | if (this.isImportLocal(importPath)) { 53 | return this.formatDocumentPath(path.resolve(path.dirname(this.absolutePath), importPath)); 54 | } else if (this.project !== undefined && this.project !== null) { 55 | const remapping = this.project.findImportRemapping(importPath); 56 | if (remapping !== undefined && remapping != null) { 57 | return this.formatDocumentPath(remapping.resolveImport(importPath)); 58 | } else { 59 | const depPack = this.project.findDependencyPackage(importPath); 60 | if (depPack !== undefined) { 61 | return this.formatDocumentPath(depPack.resolveImport(importPath)); 62 | } 63 | } 64 | } 65 | return path.resolve(this.project.projectPackage.absoluletPath, importPath); 66 | } 67 | 68 | public getAllImportFromPackages() { 69 | const importsFromPackages = new Array(); 70 | this.imports.forEach(importElement => { 71 | if (!this.isImportLocal(importElement)) { 72 | importsFromPackages.push(importElement); 73 | } 74 | }); 75 | return importsFromPackages; 76 | } 77 | 78 | public isImportLocal(importPath: string) { 79 | return SourceDocument.isImportLocal(importPath); 80 | } 81 | 82 | public formatDocumentPath(contractPath: string) { 83 | return formatPath(contractPath); 84 | } 85 | 86 | public replaceDependencyPath(importPath: string, depImportAbsolutePath: string) { 87 | const importRegEx = /(^\s?import\s+[^'"]*['"])(.*)(['"]\s*)/gm; 88 | this.code = this.code.replace(importRegEx, (match, p1, p2, p3) => { 89 | if (p2 === importPath) { 90 | return p1 + depImportAbsolutePath + p3; 91 | } else { 92 | return match; 93 | } 94 | }); 95 | } 96 | 97 | public resolveImports() { 98 | const importRegEx = /^\s?import\s+[^'"]*['"](.*)['"]\s*/gm; 99 | let foundImport = importRegEx.exec(this.code); 100 | while (foundImport != null) { 101 | const importPath = foundImport[1]; 102 | 103 | if (this.isImportLocal(importPath)) { 104 | const importFullPath = this.formatDocumentPath(path.resolve(path.dirname(this.absolutePath), foundImport[1])); 105 | this.imports.push(importFullPath); 106 | } else { 107 | this.imports.push(importPath); 108 | } 109 | 110 | foundImport = importRegEx.exec(this.code); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/common/model/sourceDocumentCollection.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as fs from 'fs'; 3 | import { SourceDocument } from './sourceDocument'; 4 | import { Project } from './project'; 5 | import { formatPath } from '../util'; 6 | 7 | export class SourceDocumentCollection { 8 | public documents: Array; 9 | 10 | public static getAllLibraryImports(codeFiles: string[]): string[] { 11 | let imports: string[] = []; 12 | codeFiles.forEach(x => imports = imports.concat(SourceDocument.getAllLibraryImports(x))); 13 | return [...new Set(imports)]; 14 | } 15 | 16 | constructor() { 17 | this.documents = new Array(); 18 | } 19 | 20 | public isDocumentPathTheSame(contract: SourceDocument, contractPath: string) { 21 | return contract.absolutePath === contractPath; 22 | } 23 | 24 | public containsSourceDocument(contractPath: string) { 25 | return this.documents.findIndex((contract: SourceDocument) => { return contract.absolutePath === contractPath; }) > -1; 26 | } 27 | 28 | public getDefaultSourceDocumentsForCompilation(optimizeCompilationRuns = 200, evmVersion = "", viaIR: boolean = false) { 29 | const compilerOutputSelection = { 30 | '*': { 31 | '': ['ast'], 32 | '*': ['abi', 'devdoc', 'userdoc', 'storageLayout', 'metadata', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'evm.gasEstimates'], 33 | }, 34 | }; 35 | 36 | return this.getSourceDocumentsForCompilation(true, optimizeCompilationRuns, evmVersion, viaIR, compilerOutputSelection); 37 | } 38 | 39 | public getDefaultSourceDocumentsForCompilationDiagnostics(evmVersion: string = "", viaIR: boolean = false) { 40 | const compilerOutputSelection = { 41 | '*': { 42 | '': [], 43 | '*': [], 44 | }, 45 | }; 46 | 47 | return this.getSourceDocumentsForCompilation(false, 0, evmVersion, viaIR, compilerOutputSelection); 48 | } 49 | 50 | public getSourceDocumentsForCompilation(optimizeCompilation: boolean, optimizeCompilationRuns: number, evmVersion: string = "", viaIR: boolean = false, outputSelection) { 51 | const contractsForCompilation = {}; 52 | this.documents.forEach(contract => { 53 | contractsForCompilation[contract.absolutePath] = { content: contract.code }; 54 | }); 55 | 56 | if (evmVersion === "" || evmVersion === undefined || evmVersion === null) { 57 | const compilation = { 58 | language: 'Solidity', 59 | settings: 60 | { 61 | optimizer: { 62 | enabled: optimizeCompilation, 63 | runs: optimizeCompilationRuns, 64 | }, 65 | outputSelection: outputSelection, 66 | viaIR: true, 67 | }, 68 | 69 | sources: contractsForCompilation, 70 | }; 71 | return compilation; 72 | } else { 73 | const compilation = { 74 | language: 'Solidity', 75 | settings: 76 | { 77 | optimizer: { 78 | enabled: optimizeCompilation, 79 | runs: optimizeCompilationRuns, 80 | }, 81 | outputSelection: outputSelection, 82 | evmVersion: evmVersion, 83 | 84 | }, 85 | 86 | sources: contractsForCompilation, 87 | }; 88 | return compilation; 89 | } 90 | } 91 | 92 | 93 | public addSourceDocumentAndResolveImports(contractPath: string, code: string, project: Project) { 94 | const contract = this.addSourceDocument(contractPath, code, project); 95 | if (contract !== null) { 96 | contract.resolveImports(); 97 | contract.imports.forEach(foundImport => { 98 | if (fs.existsSync(foundImport)) { 99 | if (!this.containsSourceDocument(foundImport)) { 100 | const importContractCode = this.readContractCode(foundImport); 101 | if (importContractCode != null) { 102 | this.addSourceDocumentAndResolveImports(foundImport, importContractCode, project); 103 | } 104 | } 105 | } else { 106 | this.addSourceDocumentAndResolveDependencyImport(foundImport, contract, project); 107 | } 108 | }); 109 | } 110 | return contract; 111 | } 112 | 113 | private addSourceDocument(contractPath: string, code: string, project: Project) { 114 | if (!this.containsSourceDocument(contractPath)) { 115 | const contract = new SourceDocument(contractPath, code, project); 116 | this.documents.push(contract); 117 | return contract; 118 | } 119 | return null; 120 | } 121 | 122 | private formatContractPath(contractPath: string) { 123 | return formatPath(contractPath); 124 | } 125 | 126 | private getAllImportFromPackages() { 127 | const importsFromPackages = new Array(); 128 | this.documents.forEach(contract => { 129 | const contractImports = contract.getAllImportFromPackages(); 130 | contractImports.forEach(contractImport => { 131 | if (importsFromPackages.indexOf(contractImport) < 0) { 132 | importsFromPackages.push(contractImport); 133 | } 134 | }); 135 | }); 136 | return importsFromPackages; 137 | } 138 | 139 | private readContractCode(contractPath: string) { 140 | if (fs.existsSync(contractPath)) { 141 | return fs.readFileSync(contractPath, 'utf8'); 142 | } 143 | return null; 144 | } 145 | 146 | private addSourceDocumentAndResolveDependencyImport(dependencyImport: string, contract: SourceDocument, project: Project) { 147 | // find re-mapping 148 | const remapping = project.findImportRemapping(dependencyImport); 149 | if (remapping !== undefined && remapping !== null) { 150 | const importPath = this.formatContractPath(remapping.resolveImport(dependencyImport)); 151 | this.addSourceDocumentAndResolveDependencyImportFromContractFullPath(importPath, project, contract, dependencyImport); 152 | } else { 153 | const depPack = project.findDependencyPackage(dependencyImport); 154 | if (depPack !== undefined) { 155 | const depImportPath = this.formatContractPath(depPack.resolveImport(dependencyImport)); 156 | this.addSourceDocumentAndResolveDependencyImportFromContractFullPath(depImportPath, project, contract, dependencyImport); 157 | } 158 | } 159 | } 160 | 161 | private addSourceDocumentAndResolveDependencyImportFromContractFullPath(importPath: string, project: Project, contract: SourceDocument, dependencyImport: string) { 162 | if (!this.containsSourceDocument(importPath)) { 163 | const importContractCode = this.readContractCode(importPath); 164 | if (importContractCode != null) { 165 | this.addSourceDocumentAndResolveImports(importPath, importContractCode, project); 166 | contract.replaceDependencyPath(dependencyImport, importPath); 167 | } 168 | } else { 169 | contract.replaceDependencyPath(dependencyImport, importPath); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/common/projectService.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as fs from 'fs'; 3 | import * as os from 'os'; 4 | import * as toml from '@iarna/toml'; 5 | import * as path from 'path'; 6 | import * as yaml from 'yaml-js'; 7 | import {Package} from './model/package'; 8 | import {Project} from './model/project'; 9 | import * as util from './util'; 10 | import { Remapping } from './model/remapping'; 11 | 12 | // TODO: These are temporary constants until standard agreed 13 | // A project standard is needed so each project can define where it store its project dependencies 14 | // and if are relative or at project source 15 | // also versioning (as it was defined years ago) 16 | 17 | 18 | const packageConfigFileName = 'dappFile'; 19 | const remappingConfigFileName = 'remappings.txt'; 20 | const brownieConfigFileName = 'brownie-config.yaml'; 21 | const hardhatConfigJsFileName = 'hardhat.config.js'; 22 | const hardhatConfigTsFileName = 'hardhat.config.ts'; 23 | const truffleConfigFileName = 'truffle-config.js'; 24 | const foundryConfigFileName = 'foundry.toml'; 25 | 26 | const projectFilesAtRoot = [remappingConfigFileName, brownieConfigFileName, foundryConfigFileName, 27 | hardhatConfigJsFileName, hardhatConfigTsFileName, truffleConfigFileName, packageConfigFileName]; 28 | 29 | // These are set using user configuration settings 30 | const defaultPackageDependenciesDirectory = ['lib', 'node_modules']; 31 | let packageDependenciesContractsDirectory = ['', 'src', 'contracts']; 32 | const defaultPackageDependenciesContractsDirectories = ['', 'src', 'contracts']; 33 | 34 | export function findFirstRootProjectFile(rootPath: string, currentDocument: string) { 35 | return util.findDirUpwardsToCurrentDocumentThatContainsAtLeastFileNameSync(projectFilesAtRoot, currentDocument, rootPath); 36 | } 37 | 38 | function createPackage(rootPath: string, packageContractsDirectory: string[]) { 39 | const projectPackageFile = path.join(rootPath, packageConfigFileName); 40 | if (fs.existsSync(projectPackageFile)) { 41 | // TODO: automapper 42 | const packageConfig = readYamlSync(projectPackageFile); 43 | // TODO: throw expection / warn user of invalid package file 44 | const projectPackage = new Package(packageContractsDirectory); 45 | projectPackage.absoluletPath = rootPath; 46 | if (packageConfig) { 47 | if (packageConfig.layout !== undefined) { 48 | if (projectPackage.build_dir !== undefined) { 49 | projectPackage.build_dir = packageConfig.layout.build_dir; 50 | } 51 | if (projectPackage.sol_sources !== undefined) { 52 | projectPackage.sol_sources = packageConfig.layout.sol_sources; 53 | } 54 | } 55 | if (projectPackage.name !== undefined) { 56 | projectPackage.name = packageConfig.name; 57 | } else { 58 | projectPackage.name = path.basename(rootPath); 59 | } 60 | 61 | if (projectPackage.version !== undefined) { 62 | projectPackage.version = packageConfig.name; 63 | } 64 | 65 | if (projectPackage.dependencies !== undefined) { 66 | projectPackage.dependencies = packageConfig.dependencies; 67 | } 68 | } 69 | return projectPackage; 70 | } 71 | return null; 72 | } 73 | 74 | function readYamlSync(filePath: string) { 75 | const fileContent = fs.readFileSync(filePath); 76 | return yaml.load(fileContent); 77 | } 78 | 79 | export function initialiseProject(rootPath: string, 80 | packageDefaultDependenciesDirectories: string[], 81 | packageDefaultDependenciesContractsDirectory: string[], 82 | remappings: string[]): Project { 83 | 84 | packageDependenciesContractsDirectory = packageDefaultDependenciesContractsDirectory; 85 | const projectPackage = createProjectPackage(rootPath, packageDefaultDependenciesContractsDirectory); 86 | // adding defaults to packages 87 | const packegesContractsDirectories = [...new Set(defaultPackageDependenciesContractsDirectories.concat(packageDefaultDependenciesContractsDirectory))]; 88 | const packageDependencies: Package[] = loadAllPackageDependencies(packageDefaultDependenciesDirectories, rootPath, projectPackage, packegesContractsDirectories); 89 | remappings = loadRemappings(rootPath, remappings); 90 | return new Project(projectPackage, packageDependencies, packageDefaultDependenciesDirectories, remappings); 91 | } 92 | 93 | function loadAllPackageDependencies(packageDefaultDependenciesDirectories: string[], rootPath: string, projectPackage: Package, packageDependenciesContractsDirectories: string[]) { 94 | let packageDependencies: Package[] = []; 95 | packageDefaultDependenciesDirectories.forEach(packageDirectory => { 96 | packageDependencies = packageDependencies.concat(loadDependencies(rootPath, projectPackage, packageDirectory, 97 | packageDependenciesContractsDirectories)); 98 | }); 99 | return packageDependencies; 100 | } 101 | 102 | function getRemappingsFromFoundryConfig(rootPath: string): string[] { 103 | const foundryConfigFile = path.join(rootPath, foundryConfigFileName); 104 | if (fs.existsSync(foundryConfigFile)) { 105 | 106 | try { 107 | const fileContent = fs.readFileSync(foundryConfigFile, 'utf8'); 108 | const configOutput = toml.parse(fileContent); 109 | let remappingsLoaded: string[]; 110 | remappingsLoaded = configOutput['profile']['default']['remappings']; 111 | if (!remappingsLoaded) { 112 | return null; 113 | } 114 | if (remappingsLoaded.length === 0) { 115 | return null; 116 | } 117 | return remappingsLoaded; 118 | } catch (error) { 119 | // ignore error 120 | console.log(error.message); 121 | console.log(error.stack); 122 | } 123 | return ; 124 | } 125 | return null; 126 | } 127 | 128 | 129 | function getRemappingsFromBrownieConfig(rootPath: string): string[] { 130 | const brownieConfigFile = path.join(rootPath, brownieConfigFileName); 131 | if (fs.existsSync(brownieConfigFile)) { 132 | const config = readYamlSync(brownieConfigFile); 133 | let remappingsLoaded: string[]; 134 | try { 135 | remappingsLoaded = config.compiler.solc.remappings; 136 | if (!remappingsLoaded) { 137 | return; 138 | } 139 | } catch (TypeError) { 140 | return; 141 | } 142 | const remappings = remappingsLoaded.map(i => { 143 | const [alias, packageID] = i.split('=') ; 144 | if (packageID.startsWith('/')) { // correct processing for imports defined with global path 145 | return `${alias}=${packageID}`; 146 | } else { 147 | return `${alias}=${path.join(os.homedir(), '.brownie', 'packages', packageID)}`; 148 | } 149 | }); 150 | return remappings; 151 | } 152 | return null; 153 | } 154 | 155 | function getRemappingsFromRemappingsFile(rootPath) { 156 | const remappingsFile = path.join(rootPath, remappingConfigFileName); 157 | if (fs.existsSync(remappingsFile)) { 158 | const remappings = []; 159 | const fileContent = fs.readFileSync(remappingsFile, 'utf8'); 160 | const remappingsLoaded = fileContent.split(/\r\n|\r|\n/); // split lines 161 | if (remappingsLoaded) { 162 | remappingsLoaded.forEach(element => { 163 | remappings.push(element); 164 | }); 165 | } 166 | return remappings; 167 | } 168 | return null; 169 | } 170 | 171 | export function loadRemappings(rootPath: string, remappings: string[]): string[] { 172 | if (remappings === undefined) { remappings = []; } 173 | 174 | // Brownie prioritezes brownie-config.yml over remappings.txt 175 | // but changing to remappings over foundry 176 | remappings = getRemappingsFromBrownieConfig(rootPath) ?? 177 | getRemappingsFromRemappingsFile(rootPath) ?? 178 | getRemappingsFromFoundryConfig(rootPath) ?? 179 | remappings; 180 | 181 | return remappings; 182 | } 183 | 184 | function loadDependencies(rootPath: string, projectPackage: Package, 185 | packageDirectory: string, 186 | dependencyAlternativeSmartContractDirectories: string[], 187 | depPackages: Array = new Array()) { 188 | if (projectPackage.dependencies !== undefined) { 189 | Object.keys(projectPackage.dependencies).forEach(dependency => { 190 | if (!depPackages.some((existingDepPack: Package) => existingDepPack.name === dependency)) { 191 | const depPackageDependencyPath = path.join(rootPath, packageDirectory, dependency); 192 | const depPackage = createPackage(depPackageDependencyPath, ['']); 193 | depPackage.appendToSolSourcesAternativeDirectories(dependencyAlternativeSmartContractDirectories); 194 | 195 | if (depPackage !== null) { 196 | depPackages.push(depPackage); 197 | // Assumed the package manager will install all the dependencies at root so adding all the existing ones 198 | loadDependencies(rootPath, depPackage, packageDirectory, dependencyAlternativeSmartContractDirectories, depPackages); 199 | } else { 200 | // should warn user of a package dependency missing 201 | } 202 | } 203 | }); 204 | } 205 | // lets not skip packages in lib 206 | const depPackagePath = path.join(projectPackage.absoluletPath, packageDirectory); 207 | if (fs.existsSync(depPackagePath)) { 208 | const depPackagesDirectories = getDirectories(depPackagePath); 209 | depPackagesDirectories.forEach(depPackageDir => { 210 | const fullPath = path.join(depPackagePath, depPackageDir); 211 | let depPackage = createPackage(fullPath, null); 212 | if (depPackage == null) { 213 | depPackage = createDefaultPackage(fullPath); 214 | depPackage.appendToSolSourcesAternativeDirectories(dependencyAlternativeSmartContractDirectories); 215 | } 216 | if (!depPackages.some((existingDepPack: Package) => existingDepPack.name === depPackage.name)) { 217 | depPackages.push(depPackage); 218 | loadDependencies(rootPath, depPackage, packageDirectory, dependencyAlternativeSmartContractDirectories, depPackages); 219 | } 220 | }); 221 | } 222 | return depPackages; 223 | } 224 | 225 | function getDirectories(dirPath: string): string[] { 226 | return fs.readdirSync(dirPath).filter(function (file) { 227 | const subdirPath = path.join(dirPath, file); 228 | return fs.statSync(subdirPath).isDirectory(); 229 | }); 230 | } 231 | 232 | function createDefaultPackage(packagePath: string, packageDependencySmartContractDirectory: string[] = ['']): Package { 233 | const defaultPackage = new Package(packageDependencySmartContractDirectory); 234 | defaultPackage.absoluletPath = packagePath; 235 | defaultPackage.name = path.basename(packagePath); 236 | return defaultPackage; 237 | } 238 | 239 | function createProjectPackage(rootPath: string, packageDependencySmartContractDirectory: string[] = ['']): Package { 240 | let projectPackage = createPackage(rootPath, packageDependencySmartContractDirectory); 241 | // Default project package,this could be passed as a function 242 | if (projectPackage === null) { 243 | projectPackage = createDefaultPackage(rootPath, packageDependencySmartContractDirectory); 244 | } 245 | return projectPackage; 246 | } 247 | -------------------------------------------------------------------------------- /src/common/sourceCodeDownloader/etherscanSourceCodeDownloader.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as fs from 'fs'; 3 | import * as fse from 'fs-extra'; 4 | import * as path from 'path'; 5 | import * as vscode from 'vscode'; 6 | import * as workspaceUtil from '../../client/workspaceUtil'; 7 | import { SourceDocumentCollection } from '../model/sourceDocumentCollection'; 8 | import { SettingsService } from '../../client/settingsService'; 9 | 10 | export class EtherscanDomainChainMapper { 11 | // public static apiKey = 'YourApiKey'; 12 | public static getMappings(): any { 13 | return {'ethereum' : 'api.etherscan.io', 14 | 'optimism' : 'api-optimistic.etherscan.io', 15 | 'binance': 'api.bscscan.com', 16 | 'polygon': 'api.polygonscan.com' }; 17 | } 18 | 19 | public static getApiKeyMappings(): any { 20 | return {'ethereum' : 'explorer_etherscan_apikey', 21 | 'optimism' : 'explorer_etherscan_optimism_apikey', 22 | 'binance': 'explorer_bscscan_apikey', 23 | 'polygon': 'explorer_polygonscan_apikey' }; 24 | } 25 | 26 | public static getDomain(chain: string ) { 27 | 28 | return this.getMappings()[chain]; 29 | } 30 | 31 | public static getChains(): string[] { 32 | return Object.keys(this.getMappings()); 33 | } 34 | } 35 | 36 | 37 | export class EtherscanContractDownloader { 38 | 39 | 40 | public static isValiAddressMessage(address: string): string | vscode.InputBoxValidationMessage | Thenable { 41 | const invalidAddress = { message: 'Invalid address', severity: vscode.InputBoxValidationSeverity.Error}; 42 | if (address === null || address === undefined) {return invalidAddress; } 43 | address = address.toLowerCase(); 44 | if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) { 45 | return invalidAddress; 46 | } else if (/^(0x)?[0-9a-f]{40}$/.test(address) || /^(0x)?[0-9A-F]{40}$/.test(address)) { 47 | // If it's all small caps or all all caps, return true 48 | return null; 49 | } 50 | return invalidAddress; 51 | } 52 | 53 | public static hexLenth(hex: string): number { 54 | if (hex.startsWith('0x')) { return hex.length - 2; } 55 | return hex.length; 56 | } 57 | 58 | public static ensureHexPrefix(hex: string): string { 59 | if (hex.startsWith('0x')) { return hex; } 60 | return hex; 61 | } 62 | 63 | public static isValiAddress(address: string): boolean { 64 | if (address === null || address === undefined) {return false; } 65 | address = address.toLowerCase(); 66 | if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) { 67 | return false; 68 | } else if (/^(0x)?[0-9a-f]{40}$/.test(address) || /^(0x)?[0-9A-F]{40}$/.test(address)) { 69 | return true; 70 | } 71 | return false; 72 | } 73 | 74 | public static async downloadContractWithPrompts() { 75 | 76 | if (vscode.window.activeTextEditor) { 77 | try { 78 | const chains = EtherscanDomainChainMapper.getChains(); 79 | const selectedChain: string = await vscode.window.showQuickPick(chains); 80 | const inputBox: vscode.InputBoxOptions = {}; 81 | inputBox.title = 'Please enter the contract address:'; 82 | inputBox.prompt = 'Please enter the contract address'; 83 | inputBox.ignoreFocusOut = true; 84 | inputBox.validateInput = this.isValiAddressMessage; 85 | 86 | let selectedAddress: string = await vscode.window.showInputBox(inputBox); 87 | if (selectedAddress !== undefined) { // cancelled 88 | if (!this.isValiAddress(selectedAddress)) { throw 'Invalid address'; } 89 | selectedAddress = this.ensureHexPrefix(selectedAddress); 90 | const pathProject = workspaceUtil.getCurrentProjectInWorkspaceRootFsPath(); 91 | const downloadedFiles = await EtherscanContractDownloader.downloadContract(selectedChain, selectedAddress, pathProject); 92 | vscode.window.showInformationMessage('Contract downloaded:' + downloadedFiles[0]); 93 | const openPath = vscode.Uri.file(downloadedFiles[0]); 94 | vscode.workspace.openTextDocument(openPath).then(doc => { 95 | vscode.window.showTextDocument(doc); 96 | }); 97 | } 98 | } catch (e) { 99 | vscode.window.showErrorMessage('Error downloading contract: ' + e); 100 | } 101 | } else { 102 | throw 'Please open a file to identify the worspace'; 103 | } 104 | } 105 | 106 | 107 | public static async downloadContract(chain: string, address: string, 108 | projectPath: string, subfolder = 'chainContracts'): Promise { 109 | const apiKey = SettingsService.getExplorerEtherscanBasedApiKey(chain); 110 | const info = await EtherscanContractInfoService.getContractInfo(chain, address, apiKey); 111 | const downloadedFiles: string[] = []; 112 | if (info.result.length > 0) { 113 | // one contract.. 114 | const contractInfo = info.result[0]; 115 | if(contractInfo.SourceCode === '') {throw 'Contract has not been verified or found'; } 116 | const subfolderFullPath = path.join(projectPath, contractInfo.ContractName); 117 | fse.ensureDirSync(subfolderFullPath); 118 | const abiFileName = contractInfo.ContractName + '.abi'; 119 | fs.writeFileSync(path.join(subfolderFullPath, abiFileName), contractInfo.ABI); 120 | const sourceCodeCollection: string[] = []; 121 | if (contractInfo.SourceCode.startsWith('{')) { 122 | let sourceInfoString = contractInfo.SourceCode.trim(); 123 | if (sourceInfoString.startsWith('{{')) { 124 | sourceInfoString = sourceInfoString.substring(1, sourceInfoString.length - 1); 125 | } 126 | const sourceInfo = JSON.parse(sourceInfoString); 127 | const fileNames = Object.keys(sourceInfo.sources); 128 | fileNames.forEach(fileName => { 129 | const fullPathContractFile = path.join(subfolderFullPath, fileName); 130 | fse.ensureDirSync(path.dirname(fullPathContractFile)); 131 | sourceCodeCollection.push(sourceInfo.sources[fileName].content); 132 | fs.writeFileSync(fullPathContractFile, sourceInfo.sources[fileName].content); 133 | downloadedFiles.push(fullPathContractFile); 134 | }); 135 | const libraryImports = SourceDocumentCollection.getAllLibraryImports(sourceCodeCollection); 136 | const remappingContents = libraryImports.map( x => `${x}=${x}`).join('\n'); 137 | fs.writeFileSync(path.join(subfolderFullPath, 'remappings.txt'), remappingContents); 138 | } else { 139 | const solidityFileName = contractInfo.ContractName + '.sol'; 140 | const fullPathContractFile = path.join(subfolderFullPath, solidityFileName); 141 | fs.writeFileSync(fullPathContractFile, contractInfo.SourceCode); 142 | downloadedFiles.push(fullPathContractFile); 143 | } 144 | return downloadedFiles; 145 | } 146 | } 147 | } 148 | 149 | 150 | export class EtherscanContractInfoService { 151 | public static async getContractInfo(chain: string, address: string, apiKey = 'YourApiKeyToken'): Promise { 152 | const domain = EtherscanDomainChainMapper.getDomain(chain); 153 | const url = `https://${domain}/api?module=contract&action=getsourcecode&address=${address}&apikey=${apiKey}`; 154 | const response = await axios.get(url); 155 | return response.data as EtherscanContractInfoResponse; 156 | } 157 | } 158 | 159 | 160 | export interface EtherscanContractInfoResponse { 161 | status: string; 162 | message: string; 163 | result: EtherscanContractInfo[]; 164 | } 165 | 166 | export interface EtherscanContractInfo { 167 | SourceCode: string; 168 | ABI: string; 169 | ContractName: string; 170 | CompilerVersion: string; 171 | OptimizationUsed: string; 172 | Runs: string; 173 | ConstructorArguments: string; 174 | EVMVersion: string; 175 | Library: string; 176 | LicenseType: string; 177 | Proxy: string; 178 | Implementation: string; 179 | SwarmSource: string; 180 | } 181 | -------------------------------------------------------------------------------- /src/common/util.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | export function formatPath(contractPath: string) { 6 | if (contractPath !== null) { 7 | return contractPath.replace(/\\/g, '/'); 8 | } 9 | return contractPath; 10 | } 11 | 12 | /** 13 | * Replaces remappings in the first array with matches from the second array, 14 | * then it concatenates only the unique strings from the 2 arrays. 15 | * 16 | * It splits the strings by '=' and checks the prefix of each element 17 | * @param remappings first array of remappings strings 18 | * @param replacer second array of remappings strings 19 | * @returns an array containing unique remappings 20 | */ 21 | export function replaceRemappings(remappings: string[], replacer: string[]): string[] { 22 | remappings.forEach(function (remapping, index) { 23 | const prefix = remapping.split('=')[0]; 24 | for (const replaceRemapping of replacer) { 25 | const replacePrefix = replaceRemapping.split('=')[0]; 26 | if (prefix === replacePrefix) { 27 | remappings[index] = replaceRemapping; 28 | break; 29 | } 30 | } 31 | }); 32 | return [...new Set([...remappings, ...replacer])]; 33 | } 34 | 35 | export function findDirUpwardsToCurrentDocumentThatContainsAtLeastFileNameSync(filenames: string[], currentDocument: string, rootPath: string) { 36 | let currentDir = path.dirname(path.resolve(currentDocument)); 37 | 38 | while (currentDir !== rootPath) { 39 | 40 | if (exitsAnyFileSync(filenames, currentDir)) { 41 | return currentDir; 42 | } 43 | 44 | currentDir = path.dirname(currentDir); 45 | } 46 | 47 | return null; 48 | } 49 | 50 | export function exitsAnyFileSync(filenames: string[], dir: string) { 51 | for (const fileName of filenames) { 52 | const file = path.join(dir, fileName); 53 | if (fs.existsSync(file)) { 54 | return true; 55 | } 56 | } 57 | return false; 58 | } 59 | 60 | export function isPathSubdirectory(parent: string, dir: string) { 61 | const relative = path.relative(parent, dir); 62 | return relative && !relative.startsWith('..') && !path.isAbsolute(relative); 63 | } 64 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as path from 'path'; 3 | import * as vscode from 'vscode'; 4 | import { compileAllContracts } from './client/compileAll'; 5 | import { Compiler } from './client/compiler'; 6 | import { compileActiveContract, initDiagnosticCollection } from './client/compileActive'; 7 | import { 8 | generateNethereumCodeSettingsFile, codeGenerateNethereumCQSCsharp, codeGenerateNethereumCQSFSharp, codeGenerateNethereumCQSVbNet, 9 | codeGenerateNethereumCQSCSharpAll, codeGenerateNethereumCQSFSharpAll, codeGenerateNethereumCQSVbAll, autoCodeGenerateAfterCompilation, 10 | codeGenerateCQS, codeGenerateAllFilesFromAbiInCurrentFolder, codeGenerateAllFilesFromNethereumGenAbisFile, 11 | } from './client/codegen'; 12 | import { LanguageClientOptions, RevealOutputChannelOn } from 'vscode-languageclient'; 13 | import { 14 | LanguageClient, 15 | ServerOptions, 16 | TransportKind, 17 | } from 'vscode-languageclient/node'; 18 | 19 | import { lintAndfixCurrentDocument } from './server/linter/soliumClientFixer'; 20 | // tslint:disable-next-line:no-duplicate-imports 21 | import { workspace, WorkspaceFolder } from 'vscode'; 22 | import { formatDocument } from './client/formatter/formatter'; 23 | import { compilerType } from './common/solcCompiler'; 24 | import * as workspaceUtil from './client/workspaceUtil'; 25 | import { AddressChecksumCodeActionProvider, ChangeCompilerVersionActionProvider, SPDXCodeActionProvider } from './client/codeActionProviders/addressChecksumActionProvider'; 26 | import { EtherscanContractDownloader } from './common/sourceCodeDownloader/etherscanSourceCodeDownloader'; 27 | 28 | let diagnosticCollection: vscode.DiagnosticCollection; 29 | let compiler: Compiler; 30 | 31 | export async function activate(context: vscode.ExtensionContext) { 32 | const ws = workspace.workspaceFolders; 33 | diagnosticCollection = vscode.languages.createDiagnosticCollection('solidity'); 34 | compiler = new Compiler(context.extensionPath); 35 | 36 | context.subscriptions.push(diagnosticCollection); 37 | 38 | initDiagnosticCollection(diagnosticCollection); 39 | 40 | context.subscriptions.push(vscode.commands.registerCommand('solidity.compile.active', async () => { 41 | const compiledResults = await compileActiveContract(compiler); 42 | autoCodeGenerateAfterCompilation(compiledResults, null, diagnosticCollection); 43 | return compiledResults; 44 | })); 45 | 46 | context.subscriptions.push(vscode.commands.registerCommand('solidity.compile.activeUsingRemote', async () => { 47 | const compiledResults = await compileActiveContract(compiler, compilerType.remote); 48 | autoCodeGenerateAfterCompilation(compiledResults, null, diagnosticCollection); 49 | return compiledResults; 50 | })); 51 | 52 | context.subscriptions.push(vscode.commands.registerCommand('solidity.compile.activeUsingLocalFile', async () => { 53 | const compiledResults = await compileActiveContract(compiler, compilerType.localFile); 54 | autoCodeGenerateAfterCompilation(compiledResults, null, diagnosticCollection); 55 | return compiledResults; 56 | })); 57 | 58 | context.subscriptions.push(vscode.commands.registerCommand('solidity.compile.activeUsingNodeModule', async () => { 59 | const compiledResults = await compileActiveContract(compiler, compilerType.localNodeModule); 60 | autoCodeGenerateAfterCompilation(compiledResults, null, diagnosticCollection); 61 | return compiledResults; 62 | })); 63 | 64 | 65 | context.subscriptions.push(vscode.commands.registerCommand('solidity.compile', () => { 66 | compileAllContracts(compiler, diagnosticCollection); 67 | })); 68 | 69 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenCSharpProject', (args: any[]) => { 70 | codeGenerateNethereumCQSCsharp(args, diagnosticCollection); 71 | })); 72 | 73 | context.subscriptions.push(vscode.commands.registerCommand('solidity.compileAndCodegenCSharpProject', async (args: any[]) => { 74 | const compiledResults = await compileActiveContract(compiler); 75 | compiledResults.forEach(file => { 76 | codeGenerateCQS(file, 0, args, diagnosticCollection); 77 | }); 78 | })); 79 | 80 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenNethereumCodeGenSettings', (args: any[]) => { 81 | generateNethereumCodeSettingsFile(); 82 | })); 83 | 84 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenVbNetProject', (args: any[]) => { 85 | codeGenerateNethereumCQSVbNet(args, diagnosticCollection); 86 | })); 87 | 88 | context.subscriptions.push(vscode.commands.registerCommand('solidity.compileAndCodegenVbNetProject', async (args: any[]) => { 89 | const compiledResults = await compileActiveContract(compiler); 90 | compiledResults.forEach(file => { 91 | codeGenerateCQS(file, 1, args, diagnosticCollection); 92 | }); 93 | })); 94 | 95 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenFSharpProject', (args: any[]) => { 96 | codeGenerateNethereumCQSFSharp(args, diagnosticCollection); 97 | })); 98 | 99 | context.subscriptions.push(vscode.commands.registerCommand('solidity.compileAndCodegenFSharpProject', async (args: any[]) => { 100 | const compiledResults = await compileActiveContract(compiler); 101 | compiledResults.forEach(file => { 102 | codeGenerateCQS(file, 3, args, diagnosticCollection); 103 | }); 104 | })); 105 | 106 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenCSharpProjectAll', (args: any[]) => { 107 | codeGenerateNethereumCQSCSharpAll(args, diagnosticCollection); 108 | })); 109 | 110 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenVbNetProjectAll', (args: any[]) => { 111 | codeGenerateNethereumCQSVbAll(args, diagnosticCollection); 112 | })); 113 | 114 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenFSharpProjectAll', (args: any[]) => { 115 | codeGenerateNethereumCQSFSharpAll(args, diagnosticCollection); 116 | })); 117 | 118 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenCSharpProjectAllAbiCurrent', (args: any[]) => { 119 | codeGenerateAllFilesFromAbiInCurrentFolder(0, args, diagnosticCollection); 120 | })); 121 | 122 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenVbNetProjectAllAbiCurrent', (args: any[]) => { 123 | codeGenerateAllFilesFromAbiInCurrentFolder(1, args, diagnosticCollection); 124 | })); 125 | 126 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codegenFSharpProjectAllAbiCurrent', (args: any[]) => { 127 | codeGenerateAllFilesFromAbiInCurrentFolder(3, args, diagnosticCollection); 128 | })); 129 | 130 | context.subscriptions.push(vscode.commands.registerCommand('solidity.codeGenFromNethereumGenAbisFile', (args: any[]) => { 131 | codeGenerateAllFilesFromNethereumGenAbisFile(args, diagnosticCollection); 132 | })); 133 | 134 | context.subscriptions.push(vscode.commands.registerCommand('solidity.fixDocument', () => { 135 | lintAndfixCurrentDocument(); 136 | })); 137 | 138 | context.subscriptions.push(vscode.commands.registerCommand('solidity.compilerInfo', async () => { 139 | await compiler.outputCompilerInfoEnsuringInitialised(); 140 | })); 141 | 142 | context.subscriptions.push(vscode.commands.registerCommand('solidity.solcReleases', async () => { 143 | compiler.outputSolcReleases(); 144 | })); 145 | 146 | context.subscriptions.push(vscode.commands.registerCommand('solidity.selectWorkspaceRemoteSolcVersion', async () => { 147 | compiler.selectRemoteVersion(vscode.ConfigurationTarget.Workspace); 148 | })); 149 | 150 | context.subscriptions.push(vscode.commands.registerCommand('solidity.downloadRemoteSolcVersion', async () => { 151 | const root = workspaceUtil.getCurrentWorkspaceRootFolder(); 152 | compiler.downloadRemoteVersion(root.uri.fsPath); 153 | })); 154 | 155 | context.subscriptions.push(vscode.commands.registerCommand('solidity.downloadVerifiedSmartContractEtherscan', async () => { 156 | await EtherscanContractDownloader.downloadContractWithPrompts(); 157 | })); 158 | 159 | context.subscriptions.push(vscode.commands.registerCommand('solidity.downloadRemoteVersionAndSetLocalPathSetting', async () => { 160 | const root = workspaceUtil.getCurrentWorkspaceRootFolder(); 161 | compiler.downloadRemoteVersionAndSetLocalPathSetting(vscode.ConfigurationTarget.Workspace, root.uri.fsPath); 162 | })); 163 | 164 | 165 | context.subscriptions.push(vscode.commands.registerCommand('solidity.selectGlobalRemoteSolcVersion', async () => { 166 | compiler.selectRemoteVersion(vscode.ConfigurationTarget.Global); 167 | })); 168 | 169 | context.subscriptions.push(vscode.commands.registerCommand('solidity.changeDefaultCompilerType', async () => { 170 | compiler.changeDefaultCompilerType(vscode.ConfigurationTarget.Workspace); 171 | })); 172 | 173 | 174 | context.subscriptions.push( 175 | vscode.languages.registerDocumentFormattingEditProvider('solidity', { 176 | async provideDocumentFormattingEdits(document: vscode.TextDocument): Promise { 177 | return await formatDocument(document, context); 178 | }, 179 | })); 180 | 181 | context.subscriptions.push( 182 | vscode.languages.registerCodeActionsProvider('solidity', new AddressChecksumCodeActionProvider(), { 183 | providedCodeActionKinds: AddressChecksumCodeActionProvider.providedCodeActionKinds, 184 | }), 185 | ); 186 | 187 | context.subscriptions.push( 188 | vscode.languages.registerCodeActionsProvider('solidity', new SPDXCodeActionProvider(), { 189 | providedCodeActionKinds: SPDXCodeActionProvider.providedCodeActionKinds, 190 | }), 191 | ); 192 | 193 | 194 | context.subscriptions.push( 195 | vscode.languages.registerCodeActionsProvider('solidity', new ChangeCompilerVersionActionProvider(), { 196 | providedCodeActionKinds: ChangeCompilerVersionActionProvider.providedCodeActionKinds, 197 | }), 198 | ); 199 | 200 | const serverModule = path.join(__dirname, 'server.js'); 201 | const serverOptions: ServerOptions = { 202 | debug: { 203 | module: serverModule, 204 | options: { 205 | execArgv: ['--nolazy', '--inspect=6009'], 206 | }, 207 | transport: TransportKind.ipc, 208 | }, 209 | run: { 210 | module: serverModule, 211 | transport: TransportKind.ipc, 212 | }, 213 | }; 214 | 215 | const clientOptions: LanguageClientOptions = { 216 | documentSelector: [ 217 | { language: 'solidity', scheme: 'file' }, 218 | { language: 'solidity', scheme: 'untitled' }, 219 | ], 220 | revealOutputChannelOn: RevealOutputChannelOn.Never, 221 | synchronize: { 222 | // Synchronize the setting section 'solidity' to the server 223 | configurationSection: 'solidity', 224 | // Notify the server about file changes to '.sol.js files contain in the workspace (TODO node, linter) 225 | fileEvents: vscode.workspace.createFileSystemWatcher('{**/remappings.txt,**/.solhint.json,**/.soliumrc.json,**/brownie-config.yaml}'), 226 | }, 227 | initializationOptions: context.extensionPath, 228 | }; 229 | 230 | let clientDisposable; 231 | 232 | if (ws) { 233 | clientDisposable = new LanguageClient( 234 | 'solidity', 235 | 'Solidity Language Server', 236 | serverOptions, 237 | clientOptions).start(); 238 | } 239 | // Push the disposable to the context's subscriptions so that the 240 | // client can be deactivated on extension deactivation 241 | context.subscriptions.push(clientDisposable); 242 | } 243 | 244 | 245 | -------------------------------------------------------------------------------- /src/server/SolidityDefinitionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode-languageserver'; 2 | import { CodeWalkerService } from './parsedCodeModel/codeWalkerService'; 3 | 4 | 5 | 6 | export class SolidityDefinitionProvider { 7 | 8 | public provideDefinition( 9 | document: vscode.TextDocument, 10 | position: vscode.Position, 11 | walker: CodeWalkerService 12 | ): Thenable { 13 | 14 | const offset = document.offsetAt(position); 15 | const documentContractSelected = walker.getSelectedDocument(document, position); 16 | const references = documentContractSelected.getSelectedTypeReferenceLocation(offset); 17 | const foundLocations = references.filter(x => x.location !== null).map(x => x.location); 18 | const keys = ['range', 'uri']; 19 | const result = this.removeDuplicates(foundLocations, keys); 20 | 21 | return Promise.resolve(result); 22 | } 23 | 24 | public removeDuplicates(foundLocations: any[], keys: string[]) { 25 | return Object.values(foundLocations.reduce((r, o: any) => { 26 | const key = keys.map(k => o[k]).join('|'); 27 | // tslint:disable-next-line:curly 28 | if (r[key]) 29 | r[key].condition = [].concat(r[key].condition, o.condition); 30 | 31 | 32 | 33 | // tslint:disable-next-line:curly 34 | else 35 | r[key] = { ...o }; 36 | return r; 37 | }, {})); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/server/SolidityDocumentSymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode-languageserver'; 2 | import { CodeWalkerService } from './parsedCodeModel/codeWalkerService'; 3 | 4 | export class SolidityDocumentSymbolProvider { 5 | /** 6 | * Provides DocumentSymbols for the given document. 7 | */ 8 | public provideDocumentSymbols( 9 | document: vscode.TextDocument, 10 | walker: CodeWalkerService, 11 | ): vscode.DocumentSymbol[] | undefined { 12 | // Always use position (0, 0) for reusability 13 | const startOfDocument = vscode.Position.create(0, 0); 14 | const selectedDocument = walker.getSelectedDocument(document, startOfDocument); 15 | if (selectedDocument) { 16 | const documentSymbol = selectedDocument.toDocumentSymbol(); 17 | return documentSymbol?.children || []; 18 | } 19 | 20 | return undefined; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/server/SolidityHoverProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode-languageserver'; 2 | import { CodeWalkerService } from './parsedCodeModel/codeWalkerService'; 3 | 4 | 5 | 6 | export class SolidityHoverProvider { 7 | 8 | public provideHover( 9 | document: vscode.TextDocument, 10 | position: vscode.Position, 11 | walker: CodeWalkerService ): vscode.Hover | undefined { 12 | 13 | const offset = document.offsetAt(position); 14 | const documentContractSelected = walker.getSelectedDocument(document, position); 15 | if (documentContractSelected !== null) { 16 | const item = documentContractSelected.getSelectedItem(offset); 17 | if (item !== null) { return item.getHover(); } 18 | } 19 | return undefined; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/server/SolidityReferencesProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode-languageserver'; 2 | import { CodeWalkerService } from './parsedCodeModel/codeWalkerService'; 3 | 4 | 5 | 6 | export class SolidityReferencesProvider { 7 | 8 | public provideReferences( 9 | document: vscode.TextDocument, 10 | position: vscode.Position, 11 | walker: CodeWalkerService 12 | ): vscode.Location[] { 13 | 14 | const offset = document.offsetAt(position); 15 | walker.initialiseChangedDocuments(); 16 | 17 | const documentContractSelected = walker.getSelectedDocument(document, position); 18 | const references = documentContractSelected.getAllReferencesToSelected(offset, [].concat(documentContractSelected, walker.parsedDocumentsCache)); 19 | const foundLocations = references.filter(x => x != null && x.location !== null).map(x => x.location); 20 | return foundLocations; 21 | } 22 | 23 | public removeDuplicates(foundLocations: any[], keys: string[]) { 24 | return Object.values(foundLocations.reduce((r, o: any) => { 25 | const key = keys.map(k => o[k]).join('|'); 26 | // tslint:disable-next-line:curly 27 | if (r[key]) 28 | r[key].condition = [].concat(r[key].condition, o.condition); 29 | 30 | 31 | 32 | // tslint:disable-next-line:curly 33 | else 34 | r[key] = { ...o }; 35 | return r; 36 | }, {})); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/server/SolidityWorkspaceSymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode-languageserver'; 2 | import { URI } from 'vscode-uri'; 3 | import { CodeWalkerService } from './parsedCodeModel/codeWalkerService'; 4 | import { convertDocumentSymbolsToSymbolInformation } from './utils/convertDocumentSymbolsToSymbolInformation'; 5 | import { SymbolInformation } from 'vscode-languageserver-types'; 6 | 7 | export class SolidityWorkspaceSymbolProvider { 8 | public provideWorkspaceSymbols( 9 | query: string, 10 | walker: CodeWalkerService, 11 | ): SymbolInformation[] { 12 | 13 | walker.initialiseChangedDocuments(); 14 | 15 | const allSymbols: SymbolInformation[] = []; 16 | 17 | for (const parsed of walker.getParsedDocumentsCache()) { 18 | const uri = URI.file(parsed.sourceDocument.absolutePath); 19 | const documentSymbol = parsed.toDocumentSymbol(); 20 | if (documentSymbol) { 21 | allSymbols.push( 22 | ...convertDocumentSymbolsToSymbolInformation([documentSymbol], uri), 23 | ); 24 | } 25 | } 26 | 27 | return allSymbols.filter(symbol => 28 | symbol.name.toLowerCase().includes(query.toLowerCase()), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/server/linter/linter.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic } from 'vscode-languageserver'; 2 | 3 | export default interface Linter { 4 | setIdeRules(rules: any); 5 | validate(filePath: string, documentText: string): Diagnostic[]; 6 | loadFileConfig(rootPath: string); 7 | } 8 | -------------------------------------------------------------------------------- /src/server/linter/solhint.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | import * as linter from 'solhint/lib/index'; 4 | import { Diagnostic, Range, DiagnosticSeverity as Severity } from 'vscode-languageserver'; 5 | 6 | import Linter from './linter'; 7 | 8 | export default class SolhintService implements Linter { 9 | private config: ValidationConfig; 10 | 11 | constructor(rootPath: string, rules: any) { 12 | this.config = new ValidationConfig(rootPath, rules); 13 | } 14 | 15 | public loadFileConfig(rootPath: string) { 16 | this.config.loadFileConfig(rootPath); 17 | } 18 | 19 | public setIdeRules(rules: any) { 20 | this.config.setIdeRules(rules); 21 | } 22 | 23 | public validate(filePath: string, documentText: string): Diagnostic[] { 24 | return linter 25 | .processStr(documentText, this.config.build()) 26 | .messages 27 | .map(e => this.toDiagnostic(e)); 28 | } 29 | 30 | private toDiagnostic(error) { 31 | return { 32 | message: `Linter: ${error.message} [${error.ruleId}]`, 33 | range: this.rangeOf(error), 34 | severity: this.severity(error), 35 | }; 36 | } 37 | 38 | private severity(error: any): Severity { 39 | return (error.severity === 3) ? Severity.Warning : Severity.Error; 40 | } 41 | 42 | private rangeOf(error: any): Range { 43 | const line = error.line - 1; 44 | const character = error.column - 1; 45 | 46 | return { 47 | start: { line, character }, 48 | // tslint:disable-next-line:object-literal-sort-keys 49 | end: { line, character: character + 1 }, 50 | }; 51 | } 52 | } 53 | 54 | 55 | class ValidationConfig { 56 | public static readonly DEFAULT_RULES = {'func-visibility': false}; 57 | public static readonly EMPTY_CONFIG = {rules: {}}; 58 | 59 | private ideRules: any; 60 | private fileConfig: any; 61 | private currentWatchFile: string; 62 | 63 | constructor(rootPath: string, ideRules: any) { 64 | this.setIdeRules(ideRules); 65 | this.loadFileConfig(rootPath); 66 | } 67 | 68 | public setIdeRules(rules: any) { 69 | this.ideRules = rules || {}; 70 | } 71 | 72 | public build() { 73 | let extendsConfig = ['solhint:recommended']; 74 | if (this.fileConfig.extends !== 'undefined' && this.fileConfig.extends !== null) { 75 | extendsConfig = this.fileConfig.extends; 76 | } 77 | 78 | return { 79 | extends: extendsConfig, 80 | // plugins: ["prettier"], // removed plugins as it crashes the extension until this is fully supported path etc loading in solhint 81 | rules: Object.assign( 82 | ValidationConfig.DEFAULT_RULES, 83 | this.ideRules, 84 | this.fileConfig.rules, 85 | ), 86 | }; 87 | } 88 | 89 | public isRootPathSet(rootPath: string): boolean { 90 | return typeof rootPath !== 'undefined' && rootPath !== null; 91 | } 92 | 93 | public loadFileConfig(rootPath: string) { 94 | 95 | if (this.isRootPathSet(rootPath)) { 96 | const filePath = `${rootPath}/.solhint.json`; 97 | const readConfig = this.readFileConfig.bind(this, filePath); 98 | 99 | readConfig(); 100 | this.currentWatchFile = filePath; 101 | // fs.watchFile(filePath, {persistent: false}, readConfig); 102 | } else { 103 | this.fileConfig = ValidationConfig.EMPTY_CONFIG; 104 | } 105 | } 106 | 107 | private readFileConfig(filePath: string) { 108 | this.fileConfig = ValidationConfig.EMPTY_CONFIG; 109 | if (fs.existsSync(filePath)) { 110 | this.fileConfig = JSON.parse(fs.readFileSync(filePath, 'utf-8')); 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/server/linter/solium.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as Solium from 'solium'; 3 | import { DiagnosticSeverity, Connection, 4 | } from 'vscode-languageserver'; 5 | import Linter from './linter'; 6 | import * as fs from 'fs'; 7 | 8 | export const defaultSoliumRules = { 9 | }; 10 | 11 | export default class SoliumService implements Linter { 12 | 13 | public static readonly EMPTY_CONFIG = {rules: {}}; 14 | 15 | private fileConfig: any; 16 | private soliumRules; 17 | private vsConnection: Connection; 18 | private currentWatchFile: string; 19 | 20 | constructor(rootPath: string, soliumRules: any, vsConnection: Connection) { 21 | this.vsConnection = vsConnection; 22 | this.loadFileConfig(rootPath); 23 | this.setIdeRules(soliumRules); 24 | } 25 | 26 | public isRootPathSet(rootPath: string): boolean { 27 | return typeof rootPath !== 'undefined' && rootPath !== null; 28 | } 29 | 30 | public setIdeRules(soliumRules: any) { 31 | if (typeof soliumRules === 'undefined' || soliumRules === null) { 32 | this.soliumRules = defaultSoliumRules; 33 | } else { 34 | this.soliumRules = soliumRules; 35 | } 36 | 37 | if (typeof this.soliumRules['indentation'] === 'undefined' || this.soliumRules['indentation'] === null) { 38 | this.soliumRules['indentation'] = 'false'; 39 | } 40 | 41 | if (process.platform === 'win32') { 42 | if (typeof this.soliumRules['linebreak-style'] === 'undefined' || this.soliumRules['linebreak-style'] === null) { 43 | this.soliumRules['linebreak-style'] = 'off'; 44 | } 45 | } 46 | } 47 | 48 | public lintAndFix(documentText) { 49 | return Solium.lintAndFix(documentText, this.getAllSettings()); 50 | } 51 | 52 | public getAllSettings() { 53 | if (this.fileConfig !== SoliumService.EMPTY_CONFIG && this.fileConfig !== false) { 54 | return this.fileConfig; 55 | } 56 | return { 57 | 'extends': 'solium:recommended', 58 | 'options': { 'returnInternalIssues': true }, 59 | 'plugins': ['security'], 60 | 'rules': this.soliumRules, 61 | }; 62 | } 63 | 64 | public validate(filePath, documentText) { 65 | let items = []; 66 | try { 67 | items = Solium.lint(documentText, this.getAllSettings()); 68 | } catch (err) { 69 | const match = /An error .*?\nSyntaxError: (.*?) Line: (\d+), Column: (\d+)/.exec(err.message); 70 | 71 | if (match) { 72 | const line = parseInt(match[2], 10) - 1; 73 | const character = parseInt(match[3], 10) - 1; 74 | 75 | return [ 76 | { 77 | message: `Syntax error: ${match[1]}`, 78 | range: { 79 | end: { 80 | character: character, 81 | line: line, 82 | }, 83 | start: { 84 | character: character, 85 | line: line, 86 | }, 87 | }, 88 | severity: DiagnosticSeverity.Error, 89 | }, 90 | ]; 91 | } else { 92 | // this.vsConnection.window.showErrorMessage('solium error: ' + err); 93 | this.vsConnection.console.error('solium error: ' + err); 94 | } 95 | } 96 | return items.map(this.soliumLintResultToDiagnostic); 97 | } 98 | 99 | public soliumLintResultToDiagnostic(lintResult) { 100 | const severity = lintResult.type === 'warning' ? 101 | DiagnosticSeverity.Warning : 102 | DiagnosticSeverity.Error; 103 | 104 | const line = lintResult.line - 1; 105 | 106 | return { 107 | message: `Linter: ${lintResult.ruleName}: ${lintResult.message}`, 108 | range: { 109 | end: { 110 | character: lintResult.node.end, 111 | line: line, 112 | }, 113 | start: { 114 | character: lintResult.column, 115 | line: line, 116 | }, 117 | }, 118 | severity: severity, 119 | }; 120 | } 121 | 122 | public loadFileConfig(rootPath: string) { 123 | if (this.isRootPathSet(rootPath)) { 124 | const filePath = `${rootPath}/.soliumrc.json`; 125 | const readConfig = this.readFileConfig.bind(this, filePath); 126 | 127 | readConfig(); 128 | this.currentWatchFile = filePath; 129 | } else { 130 | this.fileConfig = SoliumService.EMPTY_CONFIG; 131 | } 132 | } 133 | 134 | private readFileConfig(filePath: string) { 135 | this.fileConfig = SoliumService.EMPTY_CONFIG; 136 | fs.readFile(filePath, 'utf-8', this.onConfigLoaded.bind(this)); 137 | } 138 | 139 | private onConfigLoaded(err: any, data: string) { 140 | this.fileConfig = (!err) && JSON.parse(data); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/server/linter/soliumClientFixer.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import SoliumService from './solium'; 3 | import * as vscode from 'vscode'; 4 | import * as workspaceUtil from '../../client/workspaceUtil'; 5 | 6 | export function lintAndfixCurrentDocument() { 7 | const linterType = vscode.workspace.getConfiguration('solidity').get('linter'); 8 | if (linterType === 'solium') { 9 | const soliumRules = vscode.workspace.getConfiguration('solidity').get('soliumRules'); 10 | const linter = new SoliumService( 11 | workspaceUtil.getCurrentProjectInWorkspaceRootFsPath(), soliumRules, null); 12 | const editor = vscode.window.activeTextEditor; 13 | const sourceCode = editor.document.getText(); 14 | const fullRange = new vscode.Range( 15 | editor.document.positionAt(0), 16 | editor.document.positionAt(sourceCode.length), 17 | ); 18 | 19 | const result = linter.lintAndFix(sourceCode); 20 | const edit = new vscode.WorkspaceEdit(); 21 | edit.replace(editor.document.uri, fullRange, result.fixedSourceCode); 22 | return vscode.workspace.applyEdit(edit); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/IParsedExpressionContainer.ts: -------------------------------------------------------------------------------- 1 | import { ParsedCode } from './parsedCode'; 2 | import { ParsedExpression } from './ParsedExpression'; 3 | 4 | 5 | 6 | export interface IParsedExpressionContainer extends ParsedCode { 7 | expressions: ParsedExpression[]; 8 | initialiseVariablesMembersEtc(statement: any, parentStatement: any, child: ParsedExpression); 9 | } 10 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedCodeTypeHelper.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export class ParsedCodeTypeHelper { 4 | 5 | public static getTypeString(literal: any) { 6 | const isArray = literal.array_parts.length > 0; 7 | let isMapping = false; 8 | let literalType: any; 9 | let parentType: string = null; 10 | if (literal.members !== undefined && literal.members.length > 0) { 11 | literalType = literal.members[0]; 12 | parentType = literal.literal; 13 | } else { 14 | literalType = literal.literal; 15 | } 16 | 17 | let suffixType = ''; 18 | 19 | if (typeof literalType.type !== 'undefined') { 20 | isMapping = literalType.type === 'MappingExpression'; 21 | if (isMapping) { 22 | suffixType = '(' + this.getTypeString(literalType.from) + ' => ' + this.getTypeString(literalType.to) + ')'; 23 | } 24 | } 25 | 26 | if (isArray) { 27 | suffixType = suffixType + '[]'; 28 | } 29 | 30 | if (isMapping) { 31 | return 'mapping' + suffixType; 32 | } 33 | 34 | if (parentType !== null) { 35 | return parentType + '.' + literalType + suffixType; 36 | } 37 | 38 | return literalType + suffixType; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedConstant.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind } from 'vscode-languageserver'; 2 | import { ParsedDeclarationType } from './parsedDeclarationType'; 3 | import { ParsedDocument } from './ParsedDocument'; 4 | import { ParsedVariable } from './ParsedVariable'; 5 | import { ParsedParameter } from './ParsedParameter'; 6 | 7 | 8 | export class ParsedConstant extends ParsedVariable { 9 | public from: string; 10 | private completionItem: CompletionItem = null; 11 | public override initialise(element: any, document: ParsedDocument) { 12 | super.initialise(element, document); 13 | this.name = element.name; 14 | this.type = ParsedDeclarationType.create(element.literal, null, document); 15 | } 16 | 17 | public override createCompletionItem(): CompletionItem { 18 | if (this.completionItem === null) { 19 | const completionItem = CompletionItem.create(this.name); 20 | completionItem.kind = CompletionItemKind.Field; 21 | const info = this.document.getGlobalPathInfo(); 22 | completionItem.insertText = this.name; 23 | completionItem.documentation = this.getMarkupInfo(); 24 | this.completionItem = completionItem; 25 | } 26 | return this.completionItem; 27 | } 28 | 29 | 30 | public override getParsedObjectType(): string { 31 | return 'Constant'; 32 | } 33 | 34 | public override getInfo(): string { 35 | return '### ' + this.getParsedObjectType() + ': ' + this.name + '\n' + 36 | '#### ' + this.getContractNameOrGlobal() + '\n' + 37 | '\t' + this.getSignature() + ' \n\n' + 38 | '### Type Info: \n' + 39 | this.type.getInfo() + '\n'; 40 | } 41 | 42 | public getSignature(): string { 43 | return ParsedParameter.getParamInfo(this.element); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedContractIs.ts: -------------------------------------------------------------------------------- 1 | import { Location } from 'vscode-languageserver'; 2 | import { ParsedDocument } from './ParsedDocument'; 3 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 4 | import { ParsedContract } from './parsedContract'; 5 | 6 | 7 | export class ParsedContractIs extends ParsedCode { 8 | 9 | private contractReference: ParsedContract = null; 10 | public override initialise(element: any, document: ParsedDocument, contract: ParsedContract, isGlobal: boolean) { 11 | super.initialise(element, document, contract, isGlobal); 12 | this.name = element.name; 13 | } 14 | 15 | public initialiseContractReference(): ParsedContract { 16 | if (this.contractReference !== null) { return this.contractReference; } 17 | this.contractReference = this.document.findContractByName(this.name); 18 | if (this.contractReference !== undefined && this.contractReference !== null) { 19 | this.contractReference.initialiseExtendContracts(); 20 | } 21 | return this.contractReference; 22 | } 23 | 24 | public getContractReference(): ParsedContract { 25 | return this.initialiseContractReference(); 26 | } 27 | 28 | public getContractReferenceLocation(): Location { 29 | return this.getContractReference().getLocation(); 30 | } 31 | 32 | public override getSelectedTypeReferenceLocation(offset: number): FindTypeReferenceLocationResult[] { 33 | if (this.isCurrentElementedSelected(offset)) { 34 | return [FindTypeReferenceLocationResult.create(true, this.getContractReferenceLocation())]; 35 | } 36 | return [FindTypeReferenceLocationResult.create(false)]; 37 | } 38 | 39 | public override getAllReferencesToThis(): FindTypeReferenceLocationResult[] { 40 | const results: FindTypeReferenceLocationResult[] = []; 41 | results.push(this.createFoundReferenceLocationResult()); 42 | return results.concat(this.document.getAllReferencesToObject(this.getContractReference())); 43 | } 44 | 45 | public override getAllReferencesToObject(parsedCode: ParsedCode): FindTypeReferenceLocationResult[] { 46 | if (this.isTheSame(parsedCode)) { 47 | return [this.createFoundReferenceLocationResult()]; 48 | } else { 49 | const reference = this.getContractReference(); 50 | if (reference !== null && reference.isTheSame(parsedCode)) { 51 | return [this.createFoundReferenceLocationResult()]; 52 | } 53 | } 54 | } 55 | 56 | public override getInfo(): string { 57 | const reference = this.getContractReference(); 58 | if (reference !== null) { 59 | return reference.getInfo(); 60 | } else { 61 | return '### Contract: ' + this.name; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedCustomType.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, DocumentSymbol, SymbolKind } from 'vscode-languageserver'; 2 | import { ParsedDocument } from './ParsedDocument'; 3 | import { ParsedCode } from './parsedCode'; 4 | import { ParsedContract } from './parsedContract'; 5 | 6 | export class ParsedCustomType extends ParsedCode { 7 | public isType: string; 8 | private completionItem: CompletionItem = null; 9 | 10 | public override initialise(element: any, document: ParsedDocument, contract: ParsedContract, isGlobal: boolean) { 11 | super.initialise(element, document, contract, isGlobal); 12 | this.element = element; 13 | this.isType = element.isType; 14 | } 15 | 16 | public toDocumentSymbol(): DocumentSymbol { 17 | const range = this.getRange(); 18 | const name = this.name || 'Unnamed'; 19 | const symbol = DocumentSymbol.create( 20 | name, 21 | this.getInfo(), 22 | SymbolKind.Class, 23 | range, 24 | range, 25 | ); 26 | return symbol; 27 | } 28 | 29 | public override createCompletionItem(): CompletionItem { 30 | if (this.completionItem === null) { 31 | const completionItem = CompletionItem.create(this.name); 32 | completionItem.kind = CompletionItemKind.Field; 33 | let contractName = ''; 34 | if (!this.isGlobal) { 35 | contractName = this.contract.name; 36 | } else { 37 | contractName = this.document.getGlobalPathInfo(); 38 | } 39 | const typeString = this.isType; 40 | completionItem.insertText = this.name; 41 | completionItem.documentation = this.getMarkupInfo(); 42 | this.completionItem = completionItem; 43 | } 44 | return this.completionItem; 45 | } 46 | 47 | 48 | public override getParsedObjectType(): string { 49 | return 'Custom Type'; 50 | } 51 | 52 | public override getInfo(): string { 53 | return '### ' + this.getParsedObjectType() + ': ' + this.name + '\n' + 54 | '#### ' + this.getContractNameOrGlobal() + '\n' + 55 | '### Type Info: \n' + 56 | this.isType + '\n'; 57 | } 58 | 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedEnum.ts: -------------------------------------------------------------------------------- 1 | import { ParsedContract } from './parsedContract'; 2 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 3 | import { ParsedDocument } from './ParsedDocument'; 4 | import { CompletionItem, CompletionItemKind, DocumentSymbol, SymbolKind } from 'vscode-languageserver'; 5 | 6 | 7 | 8 | 9 | export class ParsedEnum extends ParsedCode { 10 | public items: string[] = []; 11 | public id: any; 12 | private completionItem: CompletionItem = null; 13 | public override initialise(element: any, document: ParsedDocument, contract: ParsedContract, isGlobal: boolean) { 14 | super.initialise(element, document, contract, isGlobal); 15 | this.name = element.name; 16 | this.id = element.id; 17 | element.members.forEach(member => { this.items.push(member); }); 18 | } 19 | 20 | public toDocumentSymbol(): DocumentSymbol { 21 | const enumRange = this.getRange(); 22 | const enumSymbol = DocumentSymbol.create( 23 | this.name, 24 | this.getSimpleInfo(), 25 | SymbolKind.Enum, 26 | enumRange, 27 | enumRange, 28 | ); 29 | return enumSymbol; 30 | } 31 | 32 | public override getSimpleInfo(): string { 33 | const members = this.items.map(member => member).join(', '); 34 | return `Enum ${this.name} { ${members} }`; 35 | } 36 | 37 | public override createCompletionItem(): CompletionItem { 38 | if (this.completionItem === null) { 39 | const completionItem = CompletionItem.create(this.name); 40 | completionItem.kind = CompletionItemKind.Enum; 41 | let contractName = ''; 42 | if (!this.isGlobal) { 43 | contractName = this.contract.name; 44 | } else { 45 | contractName = this.document.getGlobalPathInfo(); 46 | } 47 | completionItem.insertText = this.name; 48 | completionItem.documentation = this.getMarkupInfo(); 49 | this.completionItem = completionItem; 50 | } 51 | return this.completionItem; 52 | } 53 | 54 | public override getInnerCompletionItems(): CompletionItem[] { 55 | const completionItems: CompletionItem[] = []; 56 | this.items.forEach(property => completionItems.push(CompletionItem.create(property))); 57 | return completionItems; 58 | } 59 | 60 | public override getParsedObjectType(): string { 61 | return 'Enum'; 62 | } 63 | 64 | public override getInfo(): string { 65 | return '### ' + this.getParsedObjectType() + ': ' + this.name + '\n' + 66 | '#### ' + this.getContractNameOrGlobal() + '\n' + 67 | this.getComment(); 68 | } 69 | 70 | 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedError.ts: -------------------------------------------------------------------------------- 1 | import { ParsedContract } from './parsedContract'; 2 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 3 | import { ParsedParameter } from './ParsedParameter'; 4 | import { ParsedDocument } from './ParsedDocument'; 5 | import { CompletionItem, CompletionItemKind, DocumentSymbol, SymbolKind } from 'vscode-languageserver'; 6 | 7 | 8 | export class ParsedError extends ParsedCode { 9 | public input: ParsedParameter[] = []; 10 | public id: any; 11 | private completionItem: CompletionItem = null; 12 | 13 | public override initialise(element: any, document: ParsedDocument, contract: ParsedContract, isGlobal: boolean) { 14 | super.initialise(element, document, contract, isGlobal); 15 | this.name = element.name; 16 | this.initialiseParamters(); 17 | this.id = element.id; 18 | } 19 | 20 | public toDocumentSymbol(): DocumentSymbol { 21 | const errorRange = this.getRange(); 22 | const errorSymbol = DocumentSymbol.create( 23 | this.name, 24 | this.getSimpleInfo(), 25 | SymbolKind.Class, 26 | errorRange, 27 | errorRange, 28 | ); 29 | errorSymbol.children = this.input.map(param => param.toDocumentSymbolType('Input Parameter')); 30 | return errorSymbol; 31 | } 32 | 33 | public override getSimpleInfo(): string { 34 | const params = this.input 35 | .map(param => `${param.name}: ${param.type.getSimpleInfo()}`) 36 | .join(', '); 37 | return `Error ${this.name}(${params})`; 38 | } 39 | 40 | public initialiseParamters() { 41 | this.input = ParsedParameter.extractParameters(this.element.params, this.contract, this.document, this); 42 | } 43 | 44 | public override createCompletionItem(): CompletionItem { 45 | if (this.completionItem === null) { 46 | const completionItem = CompletionItem.create(this.name); 47 | completionItem.kind = CompletionItemKind.Function; 48 | 49 | const paramsSnippet = ParsedParameter.createFunctionParamsSnippet(this.element.params, false); 50 | completionItem.insertTextFormat = 2; 51 | completionItem.insertText = this.name + '(' + paramsSnippet + ');'; 52 | 53 | completionItem.documentation = this.getMarkupInfo(); 54 | 55 | this.completionItem = completionItem; 56 | } 57 | return this.completionItem; 58 | } 59 | 60 | public override getSelectedTypeReferenceLocation(offset: number): FindTypeReferenceLocationResult[] { 61 | if (this.isCurrentElementedSelected(offset)) { 62 | let results: FindTypeReferenceLocationResult[] = []; 63 | this.input.forEach(x => results = this.mergeArrays(results, x.getSelectedTypeReferenceLocation(offset))); 64 | const foundResult = FindTypeReferenceLocationResult.filterFoundResults(results); 65 | if (foundResult.length > 0) { 66 | return foundResult; 67 | } else { 68 | return [FindTypeReferenceLocationResult.create(true)]; 69 | } 70 | } 71 | return [FindTypeReferenceLocationResult.create(false)]; 72 | } 73 | 74 | public override getSelectedItem(offset: number): ParsedCode { 75 | let selectedItem: ParsedCode = null; 76 | if (this.isCurrentElementedSelected(offset)) { 77 | let allItems: ParsedCode[] = []; 78 | allItems = allItems.concat(this.input); 79 | selectedItem = allItems.find(x => x.getSelectedItem(offset)); 80 | if (selectedItem !== undefined && selectedItem !== null) { return selectedItem; } 81 | return this; 82 | } 83 | return selectedItem; 84 | } 85 | 86 | public override getParsedObjectType(): string { 87 | return 'Error'; 88 | } 89 | 90 | public override getInfo(): string { 91 | const elementType = this.getParsedObjectType(); 92 | return '### ' + elementType + ': ' + this.name + '\n' + 93 | '#### ' + this.getContractNameOrGlobal() + '\n' + 94 | '\t' + this.getSignature() + ' \n\n' + 95 | this.getComment(); 96 | } 97 | 98 | public getDeclaration(): string { 99 | return 'error'; 100 | } 101 | public getSignature(): string { 102 | const paramsInfo = ParsedParameter.createParamsInfo(this.element.params); 103 | return this.getDeclaration() + ' ' + this.name + '(' + paramsInfo + ') \n\t\t'; 104 | } 105 | 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedEvent.ts: -------------------------------------------------------------------------------- 1 | import { ParsedContract } from './parsedContract'; 2 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 3 | import { ParsedParameter } from './ParsedParameter'; 4 | import { ParsedDocument } from './ParsedDocument'; 5 | import { CompletionItem, CompletionItemKind, DocumentSymbol, SymbolKind } from 'vscode-languageserver'; 6 | 7 | 8 | export class ParsedEvent extends ParsedCode { 9 | 10 | public input: ParsedParameter[] = []; 11 | public contract: ParsedContract; 12 | public isGlobal: boolean; 13 | public id: any; 14 | private completionItem: CompletionItem = null; 15 | 16 | public override initialise(element: any, document: ParsedDocument, contract: ParsedContract, isGlobal = false) { 17 | super.initialise(element, document, contract, isGlobal); 18 | this.name = element.name; 19 | this.id = element.id; 20 | this.initialiseParamters(); 21 | } 22 | 23 | public initialiseParamters() { 24 | this.input = ParsedParameter.extractParameters(this.element.params, this.contract, this.document, this); 25 | } 26 | 27 | public toDocumentSymbol(): DocumentSymbol { 28 | const eventRange = this.getRange(); 29 | const eventSymbol = DocumentSymbol.create( 30 | this.name, 31 | this.getSimpleInfo(), 32 | SymbolKind.Event, 33 | eventRange, 34 | eventRange, 35 | ); 36 | eventSymbol.children = this.input.map(param => param.toDocumentSymbolType('Input Parameter')); 37 | 38 | return eventSymbol; 39 | } 40 | 41 | public override getSimpleInfo(): string { 42 | const params = this.input 43 | .map(param => `${param.name}: ${param.type.getSimpleInfo()}`) 44 | .join(', '); 45 | return `Event ${this.name}(${params})`; 46 | } 47 | 48 | public override createCompletionItem(skipFirstParamSnipppet = false): CompletionItem { 49 | if (this.completionItem === null) { 50 | const completionItem = CompletionItem.create(this.name); 51 | completionItem.kind = CompletionItemKind.Event; 52 | const paramsSnippet = ParsedParameter.createFunctionParamsSnippet(this.element.params, skipFirstParamSnipppet); 53 | completionItem.insertTextFormat = 2; 54 | completionItem.insertText = this.name + '(' + paramsSnippet + ');'; 55 | completionItem.documentation = this.getMarkupInfo(); 56 | this.completionItem = completionItem; 57 | } 58 | return this.completionItem; 59 | } 60 | 61 | public override getSelectedTypeReferenceLocation(offset: number): FindTypeReferenceLocationResult[] { 62 | if (this.isCurrentElementedSelected(offset)) { 63 | let results: FindTypeReferenceLocationResult[] = []; 64 | this.input.forEach(x => results = this.mergeArrays(results, x.getSelectedTypeReferenceLocation(offset))); 65 | const foundResult = FindTypeReferenceLocationResult.filterFoundResults(results); 66 | if (foundResult.length > 0) { 67 | return foundResult; 68 | } else { 69 | return [FindTypeReferenceLocationResult.create(true)]; 70 | } 71 | } 72 | return [FindTypeReferenceLocationResult.create(false)]; 73 | } 74 | 75 | public override getSelectedItem(offset: number): ParsedCode { 76 | let selectedItem: ParsedCode = null; 77 | if (this.isCurrentElementedSelected(offset)) { 78 | let allItems: ParsedCode[] = []; 79 | allItems = allItems.concat(this.input); 80 | selectedItem = allItems.find(x => x.getSelectedItem(offset)); 81 | if (selectedItem !== undefined && selectedItem !== null) { return selectedItem; } 82 | return this; 83 | } 84 | return selectedItem; 85 | } 86 | 87 | public override getParsedObjectType(): string { 88 | return 'Event'; 89 | } 90 | 91 | public override getInfo(): string { 92 | const elementType = this.getParsedObjectType(); 93 | return '### ' + elementType + ': ' + this.name + '\n' + 94 | '#### ' + this.getContractNameOrGlobal() + '\n' + 95 | '\t' + this.getSignature() + ' \n\n' + 96 | this.getComment(); 97 | } 98 | 99 | public getDeclaration(): string { 100 | return 'event'; 101 | } 102 | public getSignature(): string { 103 | const paramsInfo = ParsedParameter.createParamsInfo(this.element.params); 104 | return this.getDeclaration() + ' ' + this.name + '(' + paramsInfo + ') \n\t\t'; 105 | } 106 | 107 | } 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedFunctionVariable.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, DocumentSymbol, SymbolKind } from 'vscode-languageserver'; 2 | import { ParsedCodeTypeHelper } from './ParsedCodeTypeHelper'; 3 | import { ParsedFunction } from './ParsedFunction'; 4 | import { ParsedVariable } from './ParsedVariable'; 5 | import { FindTypeReferenceLocationResult } from './parsedCode'; 6 | import { ParsedDocument } from './ParsedDocument'; 7 | import { ParsedParameter } from './ParsedParameter'; 8 | 9 | 10 | export class ParsedFunctionVariable extends ParsedVariable { 11 | public function: ParsedFunction; 12 | private completionItem: CompletionItem = null; 13 | 14 | public override createCompletionItem(): CompletionItem { 15 | 16 | if (this.completionItem === null) { 17 | const completionItem = CompletionItem.create(this.name); 18 | completionItem.kind = CompletionItemKind.Field; 19 | let name = ''; 20 | if (this.function.isGlobal) { 21 | name = this.document.getGlobalPathInfo(); 22 | } else { 23 | name = this.function.contract.name; 24 | } 25 | const typeString = ParsedCodeTypeHelper.getTypeString(this.element.literal); 26 | completionItem.detail = '(Function variable in ' + this.function.name + ') ' 27 | + typeString + ' ' + name; 28 | this.completionItem = completionItem; 29 | } 30 | return this.completionItem; 31 | } 32 | 33 | public override getAllReferencesToThis(): FindTypeReferenceLocationResult[] { 34 | const results: FindTypeReferenceLocationResult[] = []; 35 | results.push(this.createFoundReferenceLocationResult()); 36 | return results.concat(this.function.getAllReferencesToObject(this)); 37 | } 38 | 39 | public override getAllReferencesToSelected(offset: number, documents: ParsedDocument[]): FindTypeReferenceLocationResult[] { 40 | if (this.isCurrentElementedSelected(offset)) { 41 | if (this.type.isCurrentElementedSelected(offset)) { 42 | return this.type.getAllReferencesToSelected(offset, documents); 43 | } else { 44 | return this.getAllReferencesToThis(); 45 | } 46 | } 47 | return []; 48 | } 49 | 50 | public override getParsedObjectType(): string { 51 | return 'Function Variable'; 52 | } 53 | 54 | public override getInfo(): string { 55 | return '### ' + this.getParsedObjectType() + ': ' + this.name + '\n' + 56 | '#### ' + this.function.getParsedObjectType() + ': ' + this.function.name + '\n' + 57 | '#### ' + this.getContractNameOrGlobal() + '\n' + 58 | '### Type Info: \n' + 59 | this.type.getInfo() + '\n'; 60 | } 61 | 62 | public getSignature(): string { 63 | return ParsedParameter.getParamInfo(this.element); 64 | } 65 | 66 | public toDocumentSymbolType(): DocumentSymbol { 67 | const name = this.name || 'Unnamed'; 68 | const varRange = this.getRange(); 69 | return DocumentSymbol.create( 70 | name, 71 | `Variable: ${this.type.getSimpleInfo()}`, 72 | SymbolKind.Variable, 73 | varRange, 74 | varRange, 75 | ); 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedImport.ts: -------------------------------------------------------------------------------- 1 | import { DocumentSymbol, Location, Range, SymbolKind } from 'vscode-languageserver'; 2 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 3 | import { ParsedDocument } from './ParsedDocument'; 4 | import { URI } from 'vscode-uri'; 5 | 6 | 7 | export class ParsedImport extends ParsedCode { 8 | public from: string; 9 | public documentReference: ParsedDocument = null; 10 | public resolvedImportPath: string = null; 11 | 12 | public initialise(element: any, document: ParsedDocument) { 13 | this.document = document; 14 | this.element = element; 15 | this.from = element.from; 16 | } 17 | 18 | public override getSelectedTypeReferenceLocation(offset: number): FindTypeReferenceLocationResult[] { 19 | if (this.isCurrentElementedSelected(offset)) { 20 | return [FindTypeReferenceLocationResult.create(true, this.getReferenceLocation())]; 21 | } 22 | return [FindTypeReferenceLocationResult.create(false)]; 23 | } 24 | 25 | public initialiseDocumentReference(parsedDocuments: ParsedDocument[]) { 26 | if(this.resolvedImportPath === null) { 27 | this.resolvedImportPath = this.document.sourceDocument.resolveImportPath(this.from); 28 | } 29 | for (let index = 0; index < parsedDocuments.length; index++) { 30 | const element = parsedDocuments[index]; 31 | if (element.sourceDocument.absolutePath === this.resolvedImportPath) { 32 | this.documentReference = element; 33 | if (this.document.importedDocuments.indexOf(element) < 0) { 34 | this.document.addImportedDocument(element); 35 | } 36 | } 37 | } 38 | } 39 | 40 | 41 | public getDocumentsThatReference(document: ParsedDocument, processedDocuments: Set = new Set()): ParsedDocument[] { 42 | if (this.documentReference !== null) { 43 | return this.documentReference.getDocumentsThatReference(document, processedDocuments); 44 | } 45 | return []; 46 | } 47 | 48 | public getAllReferencesToSelected(offset: number, documents: ParsedDocument[]): FindTypeReferenceLocationResult[] { 49 | if (this.isCurrentElementedSelected(offset)) { 50 | return this.getAllReferencesToObject(this.documentReference); 51 | } 52 | return []; 53 | } 54 | 55 | public getReferenceLocation(): Location { 56 | if(this.resolvedImportPath === null) { 57 | this.resolvedImportPath = this.document.sourceDocument.resolveImportPath(this.from); 58 | } 59 | // note: we can use the path to find the referenced source document too. 60 | return Location.create( 61 | URI.file(this.resolvedImportPath).toString(), 62 | Range.create(0, 0, 0, 0), 63 | ); 64 | } 65 | 66 | public toDocumentSymbol(): DocumentSymbol { 67 | const importRange = this.getRange(); 68 | // Display the import details 69 | const resolvedPath = this.getResolvedImportPath(); 70 | const detail = `Import from: ${this.from}\nResolved to: ${resolvedPath}`; 71 | 72 | return DocumentSymbol.create( 73 | `import "${this.from}"`, 74 | detail, // Additional metadata 75 | SymbolKind.File, // Represent imports as files 76 | importRange, 77 | importRange, 78 | ); 79 | } 80 | 81 | private getResolvedImportPath(): string { 82 | if (this.resolvedImportPath === null) { 83 | this.resolvedImportPath = this.document.sourceDocument.resolveImportPath(this.from); 84 | } 85 | return this.resolvedImportPath; 86 | } 87 | } 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedModifierArgument.ts: -------------------------------------------------------------------------------- 1 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 2 | import { ParsedDocument } from './ParsedDocument'; 3 | import { ParsedFunction } from './ParsedFunction'; 4 | 5 | 6 | export class ParsedModifierArgument extends ParsedCode { 7 | public functionParent: ParsedFunction; 8 | public params: any; 9 | 10 | public initialiseModifier(element: any, functionParent: ParsedFunction, document: ParsedDocument) { 11 | this.functionParent = functionParent; 12 | this.contract = functionParent.contract; 13 | this.element = element; 14 | this.name = element.name; 15 | this.document = document; 16 | } 17 | 18 | public isPublic(): boolean { 19 | return this.name === 'public'; 20 | } 21 | 22 | public isPrivate(): boolean { 23 | return this.name === 'private'; 24 | } 25 | 26 | public isExternal(): boolean { 27 | return this.name === 'external'; 28 | } 29 | 30 | public isInternal(): boolean { 31 | return this.name === 'internal'; 32 | } 33 | 34 | public isView(): boolean { 35 | return this.name === 'pure'; 36 | } 37 | 38 | public isPure(): boolean { 39 | return this.name === 'view'; 40 | } 41 | 42 | public isPayeable(): boolean { 43 | return this.name === 'payeable'; 44 | } 45 | 46 | public IsCustomModifier(): boolean { 47 | return !(this.isPublic() || this.isExternal() || this.isPrivate() || this.isView() || this.isPure() || this.isPayeable() || this.isInternal()); 48 | } 49 | 50 | public getSelectedTypeReferenceLocation(offset: number): FindTypeReferenceLocationResult[] { 51 | if (this.isCurrentElementedSelected(offset)) { 52 | const results: FindTypeReferenceLocationResult[] = []; 53 | if (this.IsCustomModifier()) { 54 | const foundResults = this.findMethodsInScope(this.name); 55 | if (foundResults.length > 0) { 56 | foundResults.forEach(x => { 57 | results.push(FindTypeReferenceLocationResult.create(true, x.getLocation())); 58 | }); 59 | } 60 | return results; 61 | } 62 | return [FindTypeReferenceLocationResult.create(true)]; 63 | } 64 | return [FindTypeReferenceLocationResult.create(false)]; 65 | } 66 | 67 | public getAllReferencesToObject(parsedCode: ParsedCode): FindTypeReferenceLocationResult[] { 68 | if (this.IsCustomModifier()) { 69 | if (parsedCode instanceof ParsedFunction) { 70 | const functModifider = parsedCode; 71 | if (functModifider.isModifier && functModifider.name === this.name) { 72 | return [this.createFoundReferenceLocationResult()]; 73 | } 74 | } 75 | } 76 | return []; 77 | } 78 | 79 | public override getAllReferencesToSelected(offset: number, documents: ParsedDocument[]): FindTypeReferenceLocationResult[] { 80 | if (this.isCurrentElementedSelected(offset)) { 81 | let results: FindTypeReferenceLocationResult[] = []; 82 | if (this.IsCustomModifier()) { 83 | const foundResults = this.findMethodsInScope(this.name); 84 | foundResults.forEach(x => results = results.concat(x.getAllReferencesToThis(documents))); 85 | return results; 86 | } 87 | } 88 | return []; 89 | } 90 | 91 | public override getParsedObjectType(): string { 92 | return 'Modifier Argument'; 93 | } 94 | 95 | public override getInfo(): string { 96 | 97 | if (this.IsCustomModifier()) { 98 | const foundResults = this.findMethodsInScope(this.name); 99 | if (foundResults.length > 0 ) { 100 | return '### ' + this.getParsedObjectType() + ': ' + this.name + '\n' + 101 | '#### ' + this.functionParent.getParsedObjectType() + ': ' + this.functionParent.name + '\n' + 102 | '#### ' + this.getContractNameOrGlobal() + '\n' + 103 | '### Type Info: \n' + 104 | foundResults[0].getInfo() + '\n'; 105 | } 106 | 107 | return '### ' + this.getParsedObjectType() + ': ' + this.name + '\n' + 108 | '#### ' + this.functionParent.getParsedObjectType() + ': ' + this.functionParent.name + '\n' + 109 | '#### ' + this.getContractNameOrGlobal() + '\n' 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedParameter.ts: -------------------------------------------------------------------------------- 1 | import { ParsedDeclarationType } from './parsedDeclarationType'; 2 | import { ParsedVariable } from './ParsedVariable'; 3 | import { ParsedCodeTypeHelper } from './ParsedCodeTypeHelper'; 4 | import { CompletionItem, CompletionItemKind, DocumentSymbol, Hover, MarkupContent, MarkupKind, SymbolKind } from 'vscode-languageserver'; 5 | import { ParsedDocument } from './ParsedDocument'; 6 | import { ParsedContract } from './parsedContract'; 7 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 8 | 9 | 10 | export class ParsedParameter extends ParsedVariable { 11 | public parent: ParsedCode; 12 | private completionItem: CompletionItem = null; 13 | 14 | public static extractParameters(params: any, contract: ParsedContract, document: ParsedDocument, parent: ParsedCode): ParsedParameter[] { 15 | const parameters: ParsedParameter[] = []; 16 | if (typeof params !== 'undefined' && params !== null) { 17 | if (params.hasOwnProperty('params')) { 18 | params = params.params; 19 | } 20 | params.forEach(parameterElement => { 21 | const parameter: ParsedParameter = new ParsedParameter(); 22 | parameter.initialiseParameter(parameterElement, contract, document, parent); 23 | parameters.push(parameter); 24 | 25 | }); 26 | } 27 | return parameters; 28 | } 29 | 30 | 31 | public static createParamsInfo(params: any): string { 32 | let paramsInfo = ''; 33 | if (typeof params !== 'undefined' && params !== null) { 34 | if (params.hasOwnProperty('params')) { 35 | params = params.params; 36 | } 37 | params.forEach( parameterElement => { 38 | 39 | const currentParamInfo = ParsedParameter.getParamInfo(parameterElement); 40 | if (paramsInfo === '') { 41 | paramsInfo = currentParamInfo; 42 | } else { 43 | paramsInfo = paramsInfo + ',\n\t\t\t\t' + currentParamInfo; 44 | } 45 | }); 46 | } 47 | return paramsInfo; 48 | } 49 | 50 | public static getParamInfo(parameterElement: any) { 51 | const typeString = ParsedCodeTypeHelper.getTypeString(parameterElement.literal); 52 | 53 | let currentParamInfo = ''; 54 | if (typeof parameterElement.id !== 'undefined' && parameterElement.id !== null) { // no name on return parameters 55 | currentParamInfo = typeString + ' ' + parameterElement.id; 56 | } else { 57 | currentParamInfo = typeString; 58 | } 59 | return currentParamInfo; 60 | } 61 | 62 | public static createFunctionParamsSnippet(params: any, skipFirst = false): string { 63 | let paramsSnippet = ''; 64 | let counter = 0; 65 | if (typeof params !== 'undefined' && params !== null) { 66 | params.forEach( parameterElement => { 67 | if (skipFirst && counter === 0) { 68 | skipFirst = false; 69 | } else { 70 | const typeString = ParsedCodeTypeHelper.getTypeString(parameterElement.literal); 71 | counter = counter + 1; 72 | const currentParamSnippet = '${' + counter + ':' + parameterElement.id + '}'; 73 | if (paramsSnippet === '') { 74 | paramsSnippet = currentParamSnippet; 75 | } else { 76 | paramsSnippet = paramsSnippet + ', ' + currentParamSnippet; 77 | } 78 | } 79 | }); 80 | } 81 | return paramsSnippet; 82 | } 83 | 84 | public override getAllReferencesToSelected(offset: number, documents: ParsedDocument[]): FindTypeReferenceLocationResult[] { 85 | if (this.isCurrentElementedSelected(offset)) { 86 | if (this.type.isCurrentElementedSelected(offset)) { 87 | return this.type.getAllReferencesToSelected(offset, documents); 88 | } else { 89 | return this.getAllReferencesToThis(documents); 90 | } 91 | } 92 | return []; 93 | } 94 | 95 | public override getAllReferencesToObject(parsedCode: ParsedCode): FindTypeReferenceLocationResult[] { 96 | if (this.isTheSame(parsedCode)) { 97 | return [this.createFoundReferenceLocationResult()]; 98 | } else { 99 | return this.type.getAllReferencesToObject(parsedCode); 100 | } 101 | } 102 | 103 | public override getAllReferencesToThis(documents: ParsedDocument[]): FindTypeReferenceLocationResult[] { 104 | const results: FindTypeReferenceLocationResult[] = []; 105 | results.push(this.createFoundReferenceLocationResult()); 106 | return results.concat(this.parent.getAllReferencesToObject(this)); 107 | } 108 | 109 | public initialiseParameter(element: any, contract: ParsedContract, document: ParsedDocument, parent: ParsedCode) { 110 | this.element = element; 111 | this.name = element.name; 112 | this.document = document; 113 | this.contract = contract; 114 | this.parent = parent; 115 | 116 | const type = ParsedDeclarationType.create(element.literal, contract, document); 117 | this.element = element; 118 | this.type = type; 119 | if (typeof element.id !== 'undefined' && element.id !== null) { // no name on return parameters 120 | this.name = element.id; 121 | } 122 | } 123 | 124 | public createParamCompletionItem(type: string, contractName: string): CompletionItem { 125 | if (this.completionItem === null) { 126 | let id = '[parameter name not set]'; 127 | if (this.element.id !== null) { 128 | id = this.element.id; 129 | } 130 | const completionItem = CompletionItem.create(id); 131 | completionItem.kind = CompletionItemKind.Variable; 132 | completionItem.documentation = this.getMarkupInfo(); 133 | this.completionItem = completionItem; 134 | } 135 | return this.completionItem; 136 | } 137 | 138 | public override getParsedObjectType(): string { 139 | return 'Parameter'; 140 | } 141 | 142 | public override getInfo(): string { 143 | let name = 'Name not set'; 144 | if (this.name !== undefined) { 145 | name = this.name; 146 | } 147 | return '### ' + this.getParsedObjectType() + ': ' + name + '\n' + 148 | '#### ' + this.parent.getParsedObjectType() + ': ' + this.parent.name + '\n' + 149 | '#### ' + this.getContractNameOrGlobal() + '\n' + 150 | '### Type Info: \n' + 151 | this.type.getInfo() + '\n'; 152 | } 153 | 154 | public getSignature(): string { 155 | return ParsedParameter.getParamInfo(this.element); 156 | } 157 | 158 | public toDocumentSymbolType(parameterType: string): DocumentSymbol { 159 | const name = this.name || 'Unnamed'; 160 | const paramRange = this.getRange(); 161 | return DocumentSymbol.create( 162 | name, 163 | `${parameterType}: ${this.type.getSimpleInfo()}`, 164 | SymbolKind.Variable, 165 | paramRange, 166 | paramRange, 167 | ); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedStateVariable.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, DocumentSymbol, SymbolKind } from 'vscode-languageserver'; 2 | import { ParsedContract } from './parsedContract'; 3 | import { ParsedDeclarationType } from './parsedDeclarationType'; 4 | import { ParsedDocument } from './ParsedDocument'; 5 | import { ParsedVariable } from './ParsedVariable'; 6 | import { ParsedCodeTypeHelper } from './ParsedCodeTypeHelper'; 7 | 8 | 9 | export class ParsedStateVariable extends ParsedVariable { 10 | private completionItem: CompletionItem = null; 11 | 12 | public initialise(element: any, document: ParsedDocument, contract: ParsedContract) { 13 | super.initialise(element, document, contract); 14 | this.name = element.name; 15 | this.type = ParsedDeclarationType.create(element.literal, contract, document); 16 | } 17 | 18 | public createCompletionItem(): CompletionItem { 19 | if (this.completionItem === null) { 20 | const completionItem = CompletionItem.create(this.name); 21 | completionItem.kind = CompletionItemKind.Field; 22 | completionItem.documentation = this.getMarkupInfo(); 23 | this.completionItem = completionItem; 24 | } 25 | return this.completionItem; 26 | } 27 | 28 | public toDocumentSymbolType(): DocumentSymbol { 29 | const name = this.name || 'Unnamed'; 30 | const range = this.getRange(); 31 | const symbol = DocumentSymbol.create( 32 | name, 33 | this.type.getSimpleInfo(), 34 | SymbolKind.Variable, 35 | range, 36 | range, 37 | ); 38 | return symbol; 39 | } 40 | 41 | public override getParsedObjectType(): string { 42 | return 'State Variable'; 43 | } 44 | 45 | public override getInfo(): string { 46 | return '### ' + this.getParsedObjectType() + ': ' + this.name + '\n' + 47 | '#### ' + this.getContractNameOrGlobal() + '\n' + 48 | this.getComment() + '\n' + 49 | '### Type Info: \n' + 50 | this.type.getInfo() + '\n'; 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedStruct.ts: -------------------------------------------------------------------------------- 1 | import { ParsedContract } from './parsedContract'; 2 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 3 | import { ParsedDeclarationType } from './parsedDeclarationType'; 4 | import { ParsedStructVariable } from './ParsedStructVariable'; 5 | import { ParsedDocument } from './ParsedDocument'; 6 | import { CompletionItem, CompletionItemKind, DocumentSymbol, Location, SymbolKind } from 'vscode-languageserver'; 7 | 8 | export class ParsedStruct extends ParsedCode { 9 | public properties: ParsedStructVariable[] = []; 10 | public id: any; 11 | private completionItem: CompletionItem = null; 12 | 13 | public initialise(element: any, document: ParsedDocument, contract: ParsedContract, isGlobal: boolean) { 14 | this.contract = contract; 15 | this.element = element; 16 | this.id = element.id; 17 | this.name = element.name; 18 | this.document = document; 19 | this.isGlobal = isGlobal; 20 | 21 | if (this.element.body !== 'undefined') { 22 | this.element.body.forEach(structBodyElement => { 23 | if (structBodyElement.type === 'DeclarativeExpression') { 24 | const variable = new ParsedStructVariable(); 25 | variable.initialiseStructVariable(structBodyElement, this.contract, this.document, this); 26 | this.properties.push(variable); 27 | } 28 | }); 29 | } 30 | } 31 | 32 | public toDocumentSymbol(): DocumentSymbol { 33 | const name = this.name || 'Unnamed'; 34 | const structRange = this.getRange(); 35 | const structSymbol = DocumentSymbol.create( 36 | name, 37 | this.getSimpleInfo(), 38 | SymbolKind.Struct, 39 | structRange, 40 | structRange, 41 | ); 42 | structSymbol.children = this.properties.map(property => property.toDocumentSymbolType()); 43 | 44 | return structSymbol; 45 | } 46 | 47 | public override getSimpleInfo(): string { 48 | const properties = this.properties 49 | .map(prop => `${prop.name}: ${prop.type.getSimpleInfo()}`) 50 | .join(', '); 51 | return `Struct ${this.name} { ${properties} }`; 52 | } 53 | 54 | public getInnerMembers(): ParsedCode[] { 55 | return this.properties; 56 | } 57 | 58 | public getVariableSelected(offset: number) { 59 | return this.properties.find(x => { 60 | return x.isCurrentElementedSelected(offset); 61 | }); 62 | } 63 | 64 | public override getSelectedItem(offset: number): ParsedCode { 65 | if (this.isCurrentElementedSelected(offset)) { 66 | const variableSelected = this.getVariableSelected(offset); 67 | if (variableSelected !== undefined) { 68 | return variableSelected; 69 | } else { 70 | return this; 71 | } 72 | } 73 | return null; 74 | } 75 | 76 | public override getSelectedTypeReferenceLocation(offset: number): FindTypeReferenceLocationResult[] { 77 | if (this.isCurrentElementedSelected(offset)) { 78 | const variableSelected = this.getVariableSelected(offset); 79 | if (variableSelected !== undefined) { 80 | return variableSelected.getSelectedTypeReferenceLocation(offset); 81 | } else { 82 | return [FindTypeReferenceLocationResult.create(true)]; 83 | } 84 | } 85 | return [FindTypeReferenceLocationResult.create(false)]; 86 | } 87 | 88 | public createCompletionItem(): CompletionItem { 89 | if (this.completionItem === null) { 90 | const completionItem = CompletionItem.create(this.name); 91 | completionItem.kind = CompletionItemKind.Struct; 92 | completionItem.insertText = this.name; 93 | completionItem.documentation = this.getMarkupInfo(); 94 | this.completionItem = completionItem; 95 | } 96 | return this.completionItem; 97 | } 98 | 99 | public override getInnerCompletionItems(): CompletionItem[] { 100 | const completionItems: CompletionItem[] = []; 101 | this.properties.forEach(x => completionItems.push(x.createCompletionItem())); 102 | return completionItems; 103 | } 104 | 105 | public getAllReferencesToSelected(offset: number, documents: ParsedDocument[]): FindTypeReferenceLocationResult[] { 106 | if (this.isCurrentElementedSelected(offset)) { 107 | const selectedProperty = this.getSelectedProperty(offset); 108 | if (selectedProperty !== undefined) { 109 | return selectedProperty.getAllReferencesToThis(documents); 110 | } else { 111 | return this.getAllReferencesToThis(documents); 112 | } 113 | } 114 | return []; 115 | } 116 | 117 | public getSelectedProperty(offset: number) { 118 | return this.properties.find(x => x.isCurrentElementedSelected(offset)); 119 | } 120 | 121 | public override getParsedObjectType(): string { 122 | return 'Struct'; 123 | } 124 | 125 | 126 | public override getInfo(): string { 127 | return '### ' + this.getParsedObjectType() + ': ' + this.name + '\n' + 128 | '#### ' + this.getContractNameOrGlobal() + '\n' + 129 | this.getComment(); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedStructVariable.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, DocumentSymbol, SymbolKind } from 'vscode-languageserver'; 2 | import { ParsedCodeTypeHelper } from './ParsedCodeTypeHelper'; 3 | import { ParsedStruct } from './ParsedStruct'; 4 | import { ParsedVariable } from './ParsedVariable'; 5 | import { ParsedContract } from './parsedContract'; 6 | import { ParsedDocument } from './ParsedDocument'; 7 | import { ParsedDeclarationType } from './parsedDeclarationType'; 8 | import { FindTypeReferenceLocationResult } from './parsedCode'; 9 | 10 | 11 | export class ParsedStructVariable extends ParsedVariable { 12 | public struct: ParsedStruct; 13 | private completionItem: CompletionItem = null; 14 | 15 | public initialiseStructVariable(element: any, contract: ParsedContract, document: ParsedDocument, struct: ParsedStruct ) { 16 | this.element = element; 17 | this.name = element.name; 18 | this.document = document; 19 | this.type = ParsedDeclarationType.create(element.literal, contract, document); 20 | this.struct = struct; 21 | } 22 | public createCompletionItem(): CompletionItem { 23 | if (this.completionItem === null) { 24 | const completitionItem = CompletionItem.create(this.name); 25 | completitionItem.documentation = this.getMarkupInfo(); 26 | this.completionItem = completitionItem; 27 | } 28 | return this.completionItem; 29 | } 30 | 31 | public toDocumentSymbolType(): DocumentSymbol { 32 | const name = this.name || 'Unnamed'; 33 | const range = this.getRange(); 34 | const symbol = DocumentSymbol.create( 35 | name, 36 | this.type.getSimpleInfo(), 37 | SymbolKind.Variable, 38 | range, 39 | range, 40 | ); 41 | return symbol; 42 | } 43 | 44 | public override getParsedObjectType(): string { 45 | return 'Struct Property'; 46 | } 47 | 48 | public override getInfo(): string { 49 | 50 | return '### ' + this.getParsedObjectType() + ': ' + this.name + '\n' + 51 | '#### ' + this.struct.getParsedObjectType() + ': ' + this.struct.name + '\n' + 52 | '#### ' + this.getContractNameOrGlobal() + '\n' + 53 | // '\t' + this.getSignature() + ' \n\n' + 54 | '### Type Info: \n' + 55 | this.type.getInfo() + '\n'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/ParsedVariable.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, DocumentSymbol, Location, SymbolKind } from 'vscode-languageserver'; 2 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 3 | import { ParsedDeclarationType } from './parsedDeclarationType'; 4 | import { ParsedCodeTypeHelper } from './ParsedCodeTypeHelper'; 5 | import { ParsedParameter } from './ParsedParameter'; 6 | 7 | export class ParsedVariable extends ParsedCode { 8 | public type: ParsedDeclarationType; 9 | 10 | public override getSelectedTypeReferenceLocation(offset: number): FindTypeReferenceLocationResult[] { 11 | if (this.isCurrentElementedSelected(offset)) { 12 | const foundType = this.type.findType(); 13 | if (foundType !== undefined) { 14 | return [foundType.createFoundReferenceLocationResult()]; 15 | } 16 | return [this.createFoundReferenceLocationResultNoLocation()]; 17 | } 18 | return [this.createNotFoundReferenceLocationResult()]; 19 | } 20 | 21 | public toDocumentSymbol(): DocumentSymbol { 22 | const name = this.name || 'Unnamed'; 23 | const range = this.getRange(); 24 | const symbol = DocumentSymbol.create( 25 | name, 26 | this.type.getSimpleInfo(), 27 | SymbolKind.Variable, 28 | range, 29 | range, 30 | ); 31 | return symbol; 32 | } 33 | 34 | public override getAllReferencesToObject(parsedCode: ParsedCode): FindTypeReferenceLocationResult[] { 35 | if (this.isTheSame(parsedCode)) { 36 | return [this.createFoundReferenceLocationResult()]; 37 | } else { 38 | return this.type.getAllReferencesToObject(parsedCode); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/codeCompletion/importCompletionService.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/5198201a23874e79248e6b09558ca30e5bf5cdcf/src/server/parsedCodeModel/codeCompletion/importCompletionService.ts -------------------------------------------------------------------------------- /src/server/parsedCodeModel/parsedCode.ts: -------------------------------------------------------------------------------- 1 | import { ParsedDocument } from './ParsedDocument'; 2 | import { CompletionItem, Location, Range, Position, Hover, MarkupKind, MarkupContent } from 'vscode-languageserver'; 3 | import { TextDocument } from 'vscode-languageserver-textdocument'; 4 | import { URI } from 'vscode-uri'; 5 | import { ParsedContract } from './parsedContract'; 6 | import { HoverFeature } from 'vscode-languageclient/lib/common/hover'; 7 | 8 | export class FindTypeReferenceLocationResult { 9 | public isCurrentElementSelected: boolean; 10 | public location: Location; 11 | public reference: ParsedCode; 12 | 13 | public static create(isSelected: boolean, location: Location = null, reference: ParsedCode = null) { 14 | const result = new FindTypeReferenceLocationResult(); 15 | result.location = location; 16 | result.isCurrentElementSelected = isSelected; 17 | result.reference = reference; 18 | return result; 19 | } 20 | 21 | public static filterFoundResults(results: FindTypeReferenceLocationResult[]): FindTypeReferenceLocationResult[] { 22 | const foundResult = results.filter(x => x.isCurrentElementSelected === true); 23 | if (foundResult.length > 0) { 24 | const foundLocations = foundResult.filter(x => x.location !== null); 25 | if (foundLocations.length > 0) { 26 | return foundLocations; 27 | } else { 28 | return [FindTypeReferenceLocationResult.create(true)]; 29 | } 30 | } else { 31 | return []; 32 | } 33 | } 34 | } 35 | 36 | export class ParsedCode { 37 | public element: any; 38 | public name = ''; 39 | public document: ParsedDocument; 40 | public contract: ParsedContract = null; 41 | public isGlobal: boolean; 42 | public supportsNatSpec = true; 43 | public comment: string = null; 44 | 45 | public initialise(element: any, document: ParsedDocument, contract: ParsedContract = null, isGlobal = false) { 46 | this.contract = contract; 47 | this.element = element; 48 | this.name = element.name; 49 | this.document = document; 50 | this.isGlobal = isGlobal; // need to remove is global 51 | if (contract !== null && isGlobal === false) { 52 | this.isGlobal = true; 53 | } 54 | } 55 | 56 | public getHover(): Hover { 57 | const doc: MarkupContent = this.getMarkupInfo(); 58 | return { 59 | contents: doc, 60 | }; } 61 | 62 | public getMarkupInfo(): MarkupContent { 63 | return { 64 | kind: MarkupKind.Markdown, 65 | value: this.getInfo(), 66 | }; 67 | } 68 | 69 | public getInfo(): string { 70 | return '### ' + this.name + '\n' + this.getComment(); 71 | } 72 | 73 | public getSelectedItem(offset: number): ParsedCode { 74 | if (this.isCurrentElementedSelected(offset)) { return this; } 75 | return null; 76 | } 77 | 78 | public generateNatSpec(): string { 79 | return null; 80 | } 81 | 82 | public isCommentLine(document: TextDocument, line: number): boolean { 83 | if (line === 0) { 84 | return false; 85 | } 86 | const txt: string = document.getText(this.getLineRange(line)).trimStart(); 87 | if (!txt.startsWith('///') && !txt.startsWith('*') && 88 | !txt.startsWith('/**') && !txt.startsWith('/*!') && !txt.startsWith('*/') ) { 89 | return false; 90 | } else { 91 | return true; 92 | } 93 | } 94 | 95 | public getSimpleInfo(): string { 96 | const name = this.name || 'Unnamed'; 97 | return name; 98 | } 99 | 100 | public extractContractName(text: string): string | null { 101 | const pattern = /@inheritdoc\s+(\w+)/; 102 | const matches = text.match(pattern); 103 | if (matches && matches.length > 1) { 104 | // The second element in the array will be the contract/interface name 105 | return matches[1]; 106 | } 107 | return null; 108 | } 109 | 110 | public getLineRange(lineNumber: number){ 111 | return Range.create(Position.create(lineNumber, 0), Position.create(lineNumber + 1, 0)); 112 | } 113 | 114 | public getContractNameOrGlobal(): string { 115 | if (this.contract != null) { 116 | return this.contract.getContractTypeName(this.contract.contractType) + ': ' + this.contract.name; 117 | } else { 118 | return 'Global'; 119 | } 120 | } 121 | 122 | 123 | 124 | public getComment(): string { 125 | if (this.comment === null && this.supportsNatSpec) { 126 | const uri = URI.file(this.document.sourceDocument.absolutePath).toString(); 127 | const document = TextDocument.create(uri, null, null, this.document.sourceDocument.unformattedCode); 128 | const position = document.positionAt(this.element.start); 129 | let comment = ''; 130 | let currentLine = position.line - 1; 131 | while (this.isCommentLine(document, currentLine)) { 132 | let lineText = document.getText(this.getLineRange(currentLine)).trimStart() 133 | 134 | currentLine = currentLine - 1; 135 | let contractName = this.extractContractName(lineText); 136 | if(contractName && this.contract !== null) { 137 | comment = '\t' + lineText + this.contract.getInheritedComment(this.name, contractName) + comment; 138 | } else { 139 | comment = '\t' + lineText + comment; 140 | } 141 | } 142 | this.comment = comment; 143 | } 144 | return this.comment; 145 | } 146 | 147 | public createFoundReferenceLocationResult(): FindTypeReferenceLocationResult { 148 | return FindTypeReferenceLocationResult.create(true, this.getLocation(), this); 149 | } 150 | 151 | public createNotFoundReferenceLocationResult(): FindTypeReferenceLocationResult { 152 | return FindTypeReferenceLocationResult.create(false); 153 | } 154 | 155 | public createFoundReferenceLocationResultNoLocation(): FindTypeReferenceLocationResult { 156 | return FindTypeReferenceLocationResult.create(true, null, this); 157 | } 158 | 159 | public isTheSame(parsedCode: ParsedCode): boolean { 160 | try { 161 | const sameObject = parsedCode === this; 162 | const sameDocReference = 163 | (this.document.sourceDocument.absolutePath === parsedCode.document.sourceDocument.absolutePath 164 | && this.name === parsedCode.name && this.element.start === parsedCode.element.start && this.element.end === parsedCode.element.end); 165 | return sameObject || sameDocReference; 166 | } catch(error) { 167 | // console.log(error); 168 | } 169 | } 170 | 171 | public getAllReferencesToObject(parsedCode: ParsedCode): FindTypeReferenceLocationResult[] { 172 | if (this.isTheSame(parsedCode)) { 173 | return [this.createFoundReferenceLocationResult()]; 174 | } 175 | return []; 176 | } 177 | 178 | public findElementByOffset(elements: Array, offset: number): any { 179 | return elements.find( 180 | element => element.start <= offset && offset <= element.end, 181 | ); 182 | } 183 | 184 | public isElementedSelected(element: any, offset: number): boolean { 185 | if (element !== undefined && element !== null) { 186 | if (element.start <= offset && offset <= element.end) { 187 | return true; 188 | } 189 | } 190 | return false; 191 | } 192 | 193 | public createCompletionItem(): CompletionItem { 194 | return null; 195 | } 196 | 197 | public isCurrentElementedSelected(offset: number): boolean { 198 | return this.isElementedSelected(this.element, offset); 199 | } 200 | 201 | public getLocation(): Location { 202 | const uri = URI.file(this.document.sourceDocument.absolutePath).toString(); 203 | const document = TextDocument.create(uri, null, null, this.document.sourceDocument.unformattedCode); 204 | return Location.create( 205 | document.uri, 206 | Range.create(document.positionAt(this.element.start), document.positionAt(this.element.end)), 207 | ); 208 | } 209 | 210 | public getRange(): Range { 211 | const uri = URI.file(this.document.sourceDocument.absolutePath).toString(); 212 | const document = TextDocument.create(uri, null, null, this.document.sourceDocument.unformattedCode); 213 | return Range.create(document.positionAt(this.element.start), document.positionAt(this.element.end)); 214 | } 215 | 216 | public getSelectedTypeReferenceLocation(offset: number): FindTypeReferenceLocationResult[] { 217 | if (this.isCurrentElementedSelected(offset)) { 218 | 219 | return [FindTypeReferenceLocationResult.create(true)]; 220 | 221 | } 222 | return [FindTypeReferenceLocationResult.create(false)]; 223 | } 224 | 225 | public getAllReferencesToSelected(offset: number, documents: ParsedDocument[]): FindTypeReferenceLocationResult[] { 226 | if (this.isCurrentElementedSelected(offset)) { 227 | return this.getAllReferencesToThis(documents); 228 | } 229 | return []; 230 | } 231 | 232 | public getAllReferencesToThis(documents: ParsedDocument[]): FindTypeReferenceLocationResult[] { 233 | let results: FindTypeReferenceLocationResult[] = []; 234 | results.push(this.createFoundReferenceLocationResult()); 235 | let documentsToSearch: ParsedDocument[] = []; 236 | 237 | documents.forEach(x => { 238 | documentsToSearch.push(...x.getDocumentsThatReference(this.document)); 239 | }); 240 | documentsToSearch = [...new Set(documentsToSearch)]; 241 | 242 | documentsToSearch.forEach(x => { 243 | results.push(...x.getAllReferencesToObject(this)); 244 | }); 245 | 246 | return results; 247 | } 248 | 249 | public findTypeInScope(name: string): ParsedCode { 250 | if (this.contract === null) { 251 | return this.document.findType(name); 252 | } else { 253 | return this.contract.findType(name); 254 | } 255 | } 256 | 257 | public findMethodsInScope(name: string): ParsedCode[] { 258 | if (this.contract === null) { 259 | return this.document.findMethodCalls(name); 260 | } else { 261 | return this.contract.findMethodCalls(name); 262 | } 263 | } 264 | 265 | public findMembersInScope(name: string): ParsedCode[] { 266 | if (this.contract === null) { 267 | return this.document.findMembersInScope(name); 268 | } else { 269 | return this.contract.findMembersInScope(name); 270 | } 271 | } 272 | 273 | public getInnerCompletionItems(): CompletionItem[] { 274 | return []; 275 | } 276 | 277 | public getInnerMembers(): ParsedCode[] { 278 | return []; 279 | } 280 | 281 | public getInnerMethodCalls(): ParsedCode[] { 282 | return []; 283 | } 284 | 285 | public getParsedObjectType(): string { 286 | return ''; 287 | } 288 | 289 | protected mergeArrays(first: Type[], second: Type[]): Type[] { 290 | for (let i = 0; i < second.length; i++) { 291 | if (first.indexOf(second[i]) === -1) { 292 | first.push(second[i]); 293 | } 294 | } 295 | return first; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/parsedDeclarationType.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem } from 'vscode-languageserver'; 2 | import { ParsedDocument } from './ParsedDocument'; 3 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 4 | import { ParsedContract } from './parsedContract'; 5 | import { ParsedUsing } from './parsedUsing'; 6 | 7 | export class ParsedDeclarationType extends ParsedCode { 8 | public isArray: boolean; 9 | public isMapping: boolean; 10 | public parentTypeName: any = null; 11 | public type: ParsedCode = null; 12 | 13 | public static create(literal: any, contract: ParsedContract, document: ParsedDocument): ParsedDeclarationType { 14 | const declarationType = new ParsedDeclarationType(); 15 | declarationType.initialise(literal, document, contract); 16 | return declarationType; 17 | } 18 | 19 | public override initialise(element: any, document: ParsedDocument, contract: ParsedContract, isGlobal = false) { 20 | super.initialise(element, document, contract, isGlobal); 21 | if (element.members !== undefined && element.members.length > 0) { 22 | this.name = element.members[0]; 23 | this.parentTypeName = element.literal; 24 | } else { 25 | if (element.literal.literal !== undefined ) { 26 | this.name = element.literal.literal; 27 | } else { 28 | this.name = element.literal; 29 | } 30 | } 31 | this.isArray = element.array_parts.length > 0; 32 | this.isMapping = false; 33 | const literalType = element.literal; 34 | if (typeof literalType.type !== 'undefined') { 35 | this.isMapping = literalType.type === 'MappingExpression'; 36 | this.name = 'mapping'; // do something here 37 | // suffixType = '(' + this.getTypeString(literalType.from) + ' => ' + this.getTypeString(literalType.to) + ')'; 38 | } 39 | } 40 | 41 | public override getInnerCompletionItems(): CompletionItem[] { 42 | const result: CompletionItem[] = []; 43 | this.getExtendedMethodCallsFromUsing().forEach(x => result.push(x.createCompletionItem())); 44 | const type = this.findType(); 45 | if (type === null || type === undefined) { 46 | return result; 47 | } 48 | return result.concat(type.getInnerCompletionItems()); 49 | } 50 | 51 | public override getInnerMembers(): ParsedCode[] { 52 | const type = this.findType(); 53 | if (type === null || type === undefined) { return []; } 54 | return type.getInnerMembers(); 55 | } 56 | 57 | public override getInnerMethodCalls(): ParsedCode[] { 58 | let result: ParsedCode[] = []; 59 | result = result.concat(this.getExtendedMethodCallsFromUsing()); 60 | const type = this.findType(); 61 | if (type === null || type === undefined) { 62 | return result; 63 | } 64 | return result.concat(type.getInnerMethodCalls()); 65 | } 66 | 67 | public getExtendedMethodCallsFromUsing(): ParsedCode[] { 68 | 69 | let usings: ParsedUsing[] = []; 70 | if (this.contract !== null) { 71 | usings = this.contract.getAllUsing(this); 72 | } else { 73 | usings = this.document.getAllGlobalUsing(this); 74 | } 75 | 76 | let result: ParsedCode[] = []; 77 | usings.forEach(usingItem => { 78 | const foundLibrary = this.document.getAllContracts().find(x => x.name === usingItem.name); 79 | if (foundLibrary !== undefined) { 80 | const allfunctions = foundLibrary.getAllFunctions(); 81 | const filteredFunctions = allfunctions.filter( x => { 82 | if (x.input.length > 0 ) { 83 | const typex = x.input[0].type; 84 | let validTypeName = false; 85 | if (typex.name === this.name || (this.name === 'address_payable' && typex.name === 'address')) { 86 | validTypeName = true; 87 | } 88 | return typex.isArray === this.isArray && validTypeName && typex.isMapping === this.isMapping; 89 | } 90 | return false; 91 | }); 92 | result = result.concat(filteredFunctions); 93 | } 94 | }); 95 | return result; 96 | } 97 | 98 | public findType(): ParsedCode { 99 | if (this.type === null) { 100 | if (this.parentTypeName !== null) { 101 | const parentType = this.findTypeInScope(this.parentTypeName); 102 | if (parentType !== undefined) { 103 | this.type = parentType.findTypeInScope(this.name); 104 | } 105 | } else { 106 | this.type = this.findTypeInScope(this.name); 107 | } 108 | } 109 | return this.type; 110 | } 111 | 112 | public override getAllReferencesToSelected(offset: number, documents: ParsedDocument[]): FindTypeReferenceLocationResult[] { 113 | if (this.isCurrentElementedSelected(offset)) { 114 | const type = this.findType(); 115 | return type.getAllReferencesToThis(documents); 116 | } 117 | return []; 118 | } 119 | 120 | public override getAllReferencesToObject(parsedCode: ParsedCode): FindTypeReferenceLocationResult[] { 121 | if (this.isTheSame(parsedCode)) { 122 | return [this.createFoundReferenceLocationResult()]; 123 | } 124 | const type = this.findType(); 125 | if (this.type != null && this.type.isTheSame(parsedCode)) { 126 | return [this.createFoundReferenceLocationResult()]; 127 | } 128 | return []; 129 | } 130 | 131 | public getInfo(): string { 132 | let returnString = ''; 133 | if (this.isArray) { returnString = '### Array \n'; } 134 | const type = this.findType(); 135 | if (this.type != null) { 136 | return returnString + type.getInfo(); 137 | } 138 | return returnString + '### ' + this.name; 139 | } 140 | 141 | public override getSimpleInfo(): string { 142 | let returnString = ''; 143 | if (this.isArray) { returnString = 'Array:'; } 144 | const type = this.findType(); 145 | if (this.type != null) { 146 | return returnString + type.getSimpleInfo(); 147 | } 148 | return returnString + ' ' + this.name; 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/server/parsedCodeModel/parsedUsing.ts: -------------------------------------------------------------------------------- 1 | 2 | import { FindTypeReferenceLocationResult, ParsedCode } from './parsedCode'; 3 | import { ParsedDeclarationType } from './parsedDeclarationType'; 4 | import { ParsedContract } from './parsedContract'; 5 | import { ParsedDocument } from './ParsedDocument'; 6 | import { DocumentSymbol, SymbolKind } from 'vscode-languageserver'; 7 | 8 | export class ParsedUsing extends ParsedCode { 9 | public for: ParsedDeclarationType; 10 | public forStar = false; 11 | 12 | public initialise(element: any, document: ParsedDocument, contract: ParsedContract, isGlobal: boolean) { 13 | this.contract = contract; 14 | this.element = element; 15 | this.name = element.library.literal; 16 | this.document = document; 17 | this.isGlobal = isGlobal; 18 | if (element.for === null) { 19 | this.for = null; 20 | } else { 21 | if (element.for === '*') { 22 | this.forStar = true; 23 | this.for = null; 24 | } else { 25 | this.for = ParsedDeclarationType.create(element.for, this.contract, this.document); 26 | } 27 | } 28 | } 29 | 30 | public override getSelectedTypeReferenceLocation(offset: number): FindTypeReferenceLocationResult[] { 31 | if (this.isCurrentElementedSelected(offset)) { 32 | if (this.for !== null) { 33 | const foundType = this.for.findType(); 34 | if (foundType !== undefined) { 35 | return [foundType.createFoundReferenceLocationResult()]; 36 | } 37 | return [this.createFoundReferenceLocationResultNoLocation()]; 38 | } 39 | } 40 | return [this.createNotFoundReferenceLocationResult()]; 41 | } 42 | 43 | public toDocumentSymbol(): DocumentSymbol { 44 | const usingRange = this.getRange(); 45 | 46 | // Detail about the `for` type or `*` for global applicability 47 | const forTypeDetail = this.forStar 48 | ? 'for *' 49 | : `for ${this.for?.name || 'unknown'}`; 50 | 51 | return DocumentSymbol.create( 52 | `using ${this.name} ${forTypeDetail}`, // Display name in Outline view 53 | `Library: ${this.name}, ${forTypeDetail}`, // Additional details 54 | SymbolKind.Namespace, // `using` is closely related to a namespace 55 | usingRange, 56 | usingRange, 57 | ); 58 | } 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/server/solErrorsToDiagnostics.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { DiagnosticSeverity } from 'vscode-languageserver'; 3 | 4 | export interface CompilerError { 5 | diagnostic: any; 6 | fileName: string; 7 | } 8 | 9 | export function getDiagnosticSeverity(severity: string): DiagnosticSeverity { 10 | switch (severity) { 11 | case 'error': 12 | return DiagnosticSeverity.Error; 13 | case 'warning': 14 | return DiagnosticSeverity.Warning; 15 | case 'info': 16 | return DiagnosticSeverity.Information; 17 | default: 18 | return DiagnosticSeverity.Error; 19 | } 20 | } 21 | 22 | export function errorToDiagnostic(error: any): CompilerError { 23 | if(error.formattedMessage !== undefined && 24 | error.sourceLocation === undefined) { 25 | return { 26 | diagnostic: { 27 | message: error.formattedMessage, 28 | code: error.errorCode, 29 | range: { 30 | end: { 31 | character: 0, 32 | line: 0, 33 | }, 34 | start: { 35 | character: 0, 36 | line: 0, 37 | }, 38 | }, 39 | severity: getDiagnosticSeverity(error.severity), 40 | }, 41 | fileName: '', 42 | }; 43 | } 44 | 45 | if (error.sourceLocation.file !== undefined && error.sourceLocation.file !== null) { 46 | const fileName = error.sourceLocation.file; 47 | 48 | const errorSplit = error.formattedMessage.substr(error.formattedMessage.indexOf(fileName)).split(':'); 49 | let index = 1; 50 | // a full path in windows includes a : for the drive 51 | if (process.platform === 'win32') { 52 | index = 2; 53 | } 54 | 55 | return splitErrorToDiagnostic(error, errorSplit, index, fileName); 56 | 57 | } else { 58 | 59 | const errorSplit = error.formattedMessage.split(':'); 60 | let fileName = errorSplit[0]; 61 | let index = 1; 62 | 63 | // a full path in windows includes a : for the drive 64 | if (process.platform === 'win32') { 65 | fileName = errorSplit[0] + ':' + errorSplit[1]; 66 | index = 2; 67 | } 68 | 69 | return splitErrorToDiagnostic(error, errorSplit, index, fileName); 70 | 71 | } 72 | } 73 | 74 | export function splitErrorToDiagnostic(error: any, errorSplit: any, index: number, fileName: any): CompilerError { 75 | const severity = getDiagnosticSeverity(error.severity); 76 | const errorMessage = error.message; 77 | // tslint:disable-next-line:radix 78 | let line = parseInt(errorSplit[index]); 79 | if (Number.isNaN(line)) { line = 1; } 80 | // tslint:disable-next-line:radix 81 | let column = parseInt(errorSplit[index + 1]); 82 | if (Number.isNaN(column)) { column = 1; } 83 | 84 | let startCharacter = column - 1; 85 | 86 | let endCharacter = column + error.sourceLocation.end - error.sourceLocation.start - 1; 87 | if (endCharacter < 0) { endCharacter = 1; } 88 | 89 | let endLine = line - 1; 90 | let startLine = line - 1; 91 | 92 | if (error.code === '1878') { 93 | startLine = 0; 94 | endLine = 2; 95 | endCharacter = 0; 96 | startCharacter = 1; 97 | } 98 | return { 99 | diagnostic: { 100 | message: errorMessage, 101 | code: error.errorCode, 102 | range: { 103 | end: { 104 | character: endCharacter, 105 | line: endLine, 106 | }, 107 | start: { 108 | character: startCharacter, 109 | line: startLine, 110 | }, 111 | }, 112 | severity: severity, 113 | }, 114 | fileName: fileName, 115 | }; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /src/server/utils/convertDocumentSymbolsToSymbolInformation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentSymbol, 3 | SymbolInformation, 4 | SymbolKind, 5 | Location, 6 | Range, 7 | } from 'vscode-languageserver-types'; 8 | 9 | import { URI } from 'vscode-uri'; 10 | 11 | export function convertDocumentSymbolsToSymbolInformation( 12 | symbols: DocumentSymbol[], 13 | uri: URI, 14 | containerName?: string, 15 | ): SymbolInformation[] { 16 | const result: SymbolInformation[] = []; 17 | 18 | for (const symbol of symbols) { 19 | result.push({ 20 | name: symbol.name, 21 | kind: symbol.kind, 22 | location: Location.create(uri.toString(), symbol.selectionRange), 23 | containerName, 24 | }); 25 | 26 | if (symbol.children && symbol.children.length > 0) { 27 | result.push( 28 | ...convertDocumentSymbolsToSymbolInformation( 29 | symbol.children, 30 | uri, 31 | symbol.name, 32 | ), 33 | ); 34 | } 35 | } 36 | 37 | return result; 38 | } 39 | -------------------------------------------------------------------------------- /syntaxes/solidity-markdown-injection.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:text.html.markdown", 4 | "patterns": [ 5 | { 6 | "include": "#solidity-code-block" 7 | } 8 | ], 9 | "repository": { 10 | "solidity-code-block": { 11 | "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(solidity)(\\s+[^`~]*)?$)", 12 | "name": "markup.fenced_code.block.markdown", 13 | "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", 14 | "beginCaptures": { 15 | "3": { 16 | "name": "punctuation.definition.markdown" 17 | }, 18 | "4": { 19 | "name": "fenced_code.block.language.markdown" 20 | }, 21 | "5": { 22 | "name": "fenced_code.block.language.attributes.markdown" 23 | } 24 | }, 25 | "endCaptures": { 26 | "3": { 27 | "name": "punctuation.definition.markdown" 28 | } 29 | }, 30 | "patterns": [ 31 | { 32 | "begin": "(^|\\G)(\\s*)(.*)", 33 | "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", 34 | "contentName": "meta.embedded.block.solidity", 35 | "patterns": [ 36 | { 37 | "include": "source.solidity" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | }, 44 | "scopeName": "markdown.solidity.codeblock" 45 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es2017", "dom" 8 | ], 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true, 12 | "rootDir": ".", 13 | "allowJs": false 14 | }, 15 | "include": [ 16 | "src/**/*" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | ".vscode-test" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, "check-space"], 5 | "curly": true, 6 | "eofline": true, 7 | "forin": true, 8 | "no-duplicate-imports":true, 9 | "import-spacing":true, 10 | "indent": [true, "spaces"], 11 | "label-position": true, 12 | "label-undefined": true, 13 | "max-line-length": [true, 180], 14 | "member-access": true, 15 | "member-ordering": [true, 16 | "public-before-private", 17 | "static-before-instance", 18 | "variables-before-functions" 19 | ], 20 | "no-arg": true, 21 | "no-bitwise": true, 22 | "no-console": [true, 23 | "debug", 24 | "info", 25 | "time", 26 | "timeEnd", 27 | "trace" 28 | ], 29 | "no-construct": true, 30 | "no-debugger": true, 31 | "no-duplicate-key": true, 32 | "no-duplicate-variable": true, 33 | "no-empty": true, 34 | "no-eval": true, 35 | "no-inferrable-types": true, 36 | "no-shadowed-variable": true, 37 | "no-string-literal": false, 38 | "no-switch-case-fall-through": true, 39 | "no-trailing-whitespace": true, 40 | "no-unused-expression": true, 41 | "no-unused-variable": true, 42 | "no-unreachable": true, 43 | "no-use-before-declare": true, 44 | "no-var-keyword": true, 45 | "object-literal-sort-keys": false, 46 | "prefer-const": true, 47 | "one-line": [true, 48 | "check-open-brace", 49 | "check-catch", 50 | "check-else", 51 | "check-finally", 52 | "check-whitespace" 53 | ], 54 | "quotemark": [true, "single", "avoid-escape"], 55 | "radix": true, 56 | "semicolon": [true, "always"], 57 | "trailing-comma": [true, { 58 | "singleline": "never", 59 | "multiline": "always" 60 | }], 61 | "triple-equals": [true, "allow-null-check"], 62 | "typedef-whitespace": [true, { 63 | "call-signature": "nospace", 64 | "index-signature": "nospace", 65 | "parameter": "nospace", 66 | "property-declaration": "nospace", 67 | "variable-declaration": "nospace" 68 | }], 69 | "variable-name": false, 70 | "whitespace": [true, 71 | "check-branch", 72 | "check-decl", 73 | "check-operator", 74 | "check-separator", 75 | "check-type" 76 | ] 77 | } 78 | } -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type Options } from "tsup"; 2 | 3 | export const baseOptions = (options: Options) => ({ 4 | splitting: true, 5 | clean: true, 6 | bundle: !options.watch, 7 | minify: !options.watch, 8 | }) 9 | 10 | export default defineConfig(baseOptions); 11 | --------------------------------------------------------------------------------