├── .c8rc ├── .codecov.yml ├── .eslintrc.json ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── test.yml ├── .gitignore ├── .mocharc.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── ExampleERC721-Class.svg ├── package-lock.json ├── package.json ├── src ├── classes │ ├── base │ │ ├── __tests__ │ │ │ ├── indented.test.ts │ │ │ ├── line.test.ts │ │ │ ├── mermaid.test.ts │ │ │ └── utils │ │ │ │ ├── indented.behavior.ts │ │ │ │ └── mermaid.behavior.ts │ │ ├── indented.ts │ │ ├── line.ts │ │ └── mermaid.ts │ ├── diagrams │ │ └── class │ │ │ ├── __tests__ │ │ │ └── class.test.ts │ │ │ ├── index.ts │ │ │ └── processor.ts │ └── errors │ │ ├── __tests__ │ │ ├── ast.test.ts │ │ ├── format.test.ts │ │ ├── typed.test.ts │ │ └── utils │ │ │ └── typed.behavior.ts │ │ ├── ast.ts │ │ ├── format.ts │ │ └── typed.ts ├── index.ts ├── tests │ ├── class.integration.test.ts │ └── fixtures │ │ └── ERC721.output.json └── types │ └── index.d.ts ├── tsconfig.base.json └── tsconfig.json /.c8rc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "reporter": ["lcov", "text"] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "root": true, 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:@typescript-eslint/recommended" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | 3 | runs: 4 | using: composite 5 | steps: 6 | - uses: actions/setup-node@v3 7 | with: 8 | node-version: 16.x 9 | cache: npm 10 | - uses: actions/cache@v3 11 | id: cache 12 | with: 13 | path: '**/node_modules' 14 | key: npm-0-${{ hashFiles('**/package-lock.json') }} 15 | - name: Install dependencies 16 | run: npm ci --prefer-offline 17 | shell: bash 18 | if: steps.cache.outputs.cache-hit != 'true' 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: {} 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up environment 14 | uses: ./.github/actions/setup 15 | - run: npm run coverage --forbid-only 16 | - uses: codecov/codecov-action@v3 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | "ts" 4 | ], 5 | "spec": [ 6 | "src/classes/**/*.test.ts", 7 | "src/tests/**/*.test.ts" 8 | ], 9 | "require": "ts-node/register" 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Solidity Mermaid 2 | 3 | ## 0.1.0 4 | 5 | - `Class`: Solidity AST to Mermaid class now available 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Contributing to solidity-mermaid 4 | 5 | First off, thanks for taking the time to contribute! ❤️ 6 | 7 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 8 | 9 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 10 | > 11 | > - Star the project 12 | > - Tweet about it 13 | > - Refer this project in your project's readme 14 | > - Mention the project at local meetups and tell your friends/colleagues 15 | 16 | 17 | 18 | ## Table of Contents 19 | 20 | - [I Have a Question](#i-have-a-question) 21 | - [I Want To Contribute](#i-want-to-contribute) 22 | - [Reporting Bugs](#reporting-bugs) 23 | - [Suggesting Enhancements](#suggesting-enhancements) 24 | - [Your First Code Contribution](#your-first-code-contribution) 25 | - [Improving The Documentation](#improving-the-documentation) 26 | - [Styleguides](#styleguides) 27 | - [Commit Messages](#commit-messages) 28 | - [Join The Project Team](#join-the-project-team) 29 | 30 | ## I Have a Question 31 | 32 | > If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/ernestognw/solidity-mermaid#README). 33 | 34 | Before you ask a question, it is best to search for existing [Issues](https://github.com/ernestognw/solidity-mermaid/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. 35 | 36 | If you then still feel the need to ask a question and need clarification, we recommend the following: 37 | 38 | - Open an [Issue](https://github.com/ernestognw/solidity-mermaid/issues/new). 39 | - Provide as much context as you can about what you're running into. 40 | - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. 41 | 42 | We will then take care of the issue as soon as possible. 43 | 44 | ## I Want To Contribute 45 | 46 | > ### Legal Notice 47 | > 48 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. 49 | 50 | ### Reporting Bugs 51 | 52 | 53 | 54 | #### Before Submitting a Bug Report 55 | 56 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 57 | 58 | - Make sure that you are using the latest version. 59 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/ernestognw/solidity-mermaid#README). If you are looking for support, you might want to check [this section](#i-have-a-question)). 60 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/ernestognw/solidity-mermaidissues?q=label%3Abug). 61 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. 62 | - Collect information about the bug: 63 | - Stack trace (Traceback) 64 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 65 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 66 | - Possibly your input and the output 67 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 68 | 69 | 70 | 71 | #### How Do I Submit a Good Bug Report? 72 | 73 | > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <>. 74 | 75 | 76 | 77 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 78 | 79 | - Open an [Issue](https://github.com/ernestognw/solidity-mermaid/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) 80 | - Explain the behavior you would expect and the actual behavior. 81 | - Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. 82 | - Provide the information you collected in the previous section. 83 | 84 | Once it's filed: 85 | 86 | - The project team will label the issue accordingly. 87 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. 88 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). 89 | 90 | 91 | 92 | ### Suggesting Enhancements 93 | 94 | This section guides you through submitting an enhancement suggestion for solidity-mermaid, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 95 | 96 | #### Before Submitting an Enhancement 97 | 98 | - Make sure that you are using the latest version. 99 | - Read the [documentation](https://github.com/ernestognw/solidity-mermaid#README) carefully and find out if the functionality is already covered, maybe by an individual configuration. 100 | - Perform a [search](https://github.com/ernestognw/solidity-mermaid/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 101 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. 102 | 103 | #### How Do I Submit a Good Enhancement Suggestion? 104 | 105 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/ernestognw/solidity-mermaid/issues). 106 | 107 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 108 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 109 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 110 | - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 111 | - **Explain why this enhancement would be useful** to most solidity-mermaid users. You may also want to point out the other projects that solved it better and which could serve as inspiration. 112 | 113 | 114 | 115 | ### Your First Code Contribution 116 | 117 | To begin, clone the repo and then follow the next steps: 118 | 119 | ```sh 120 | npm install 121 | ``` 122 | 123 | ## Run tests 124 | 125 | Just run: 126 | 127 | ```sh 128 | npm run test 129 | ``` 130 | 131 | ### Improving The Documentation 132 | 133 | 137 | 138 | ## Styleguides 139 | 140 | ### Commit Messages 141 | 142 | 145 | 146 | ## Join The Project Team 147 | 148 | 149 | 150 | 151 | 152 | ## Attribution 153 | 154 | This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Ernesto García 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to solidity-mermaid 👋

2 |

3 | 4 | 5 | 6 | Version 7 | 8 | Documentation 9 | 10 | 11 | Maintenance 12 | 13 | 14 | License: MIT 15 | 16 |

17 | 18 | > A Solidity AST parser that allows to convert smart contracts into Github's Mermaid.js language for diagramming. 19 | 20 | [Solidity](https://docs.soliditylang.org/en/latest/index.html) is an object-oriented, high-level language for implementing smart contracts on top of the Ethereum Virtual Machine, while [Mermaid](https://mermaid.js.org/) is a Javascript library for diagramming that includes support for [Class Diagrams](https://mermaid.js.org/syntax/classDiagram.html). 21 | 22 | This package aims to be a tool to produce Mermaid definitions from Solidity code, which can be useful for high-level representations, usefulf for audits and security assesment or just putting them on your generated docs. See [solidity-docgen](https://github.com/OpenZeppelin/solidity-docgen). 23 | 24 | Take for example the following Solidity code: 25 | 26 | ```solidity 27 | // contracts/GameItem.sol 28 | // SPDX-License-Identifier: MIT 29 | pragma solidity ^0.8.0; 30 | 31 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; 32 | import "@openzeppelin/contracts/utils/Counters.sol"; 33 | 34 | contract GameItem is ERC721URIStorage { 35 | using Counters for Counters.Counter; 36 | Counters.Counter private _tokenIds; 37 | 38 | constructor() ERC721("GameItem", "ITM") {} 39 | 40 | function awardItem(address player, string memory tokenURI) 41 | public 42 | returns (uint256) 43 | { 44 | uint256 newItemId = _tokenIds.current(); 45 | _mint(player, newItemId); 46 | _setTokenURI(newItemId, tokenURI); 47 | 48 | _tokenIds.increment(); 49 | return newItemId; 50 | } 51 | } 52 | ``` 53 | 54 | It will output the following representation: 55 | 56 | ```mermaid 57 | classDiagram 58 | %% 216:471:12 59 | class GameItem { 60 | <> 61 | +constructor() 62 | +awardItem(address player, string memory tokenURI): (uint256) 63 | } 64 | 65 | GameItem --|> ERC721URIStorage 66 | 67 | %% 248:1623:3 68 | class ERC721URIStorage { 69 | <> 70 | +tokenURI(uint256 tokenId): (string memory) 71 | ~_setTokenURI(uint256 tokenId, string memory _tokenURI) 72 | ~_burn(uint256 tokenId) 73 | } 74 | 75 | ERC721URIStorage --|> ERC721 76 | 77 | %% 628:16327:0 78 | class ERC721 { 79 | <> 80 | +constructor(string memory name_, string memory symbol_) 81 | +supportsInterface(bytes4 interfaceId): (bool) 82 | +balanceOf(address owner): (uint256) 83 | +ownerOf(uint256 tokenId): (address) 84 | +name(): (string memory) 85 | +symbol(): (string memory) 86 | +tokenURI(uint256 tokenId): (string memory) 87 | ~_baseURI(): (string memory) 88 | +approve(address to, uint256 tokenId) 89 | +getApproved(uint256 tokenId): (address) 90 | +setApprovalForAll(address operator, bool approved) 91 | +isApprovedForAll(address owner, address operator): (bool) 92 | +transferFrom(address from, address to, uint256 tokenId) 93 | +safeTransferFrom(address from, address to, uint256 tokenId) 94 | +safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) 95 | ~_safeTransfer(address from, address to, uint256 tokenId, bytes memory data) 96 | ~_ownerOf(uint256 tokenId): (address) 97 | ~_exists(uint256 tokenId): (bool) 98 | ~_isApprovedOrOwner(address spender, uint256 tokenId): (bool) 99 | ~_safeMint(address to, uint256 tokenId) 100 | ~_safeMint(address to, uint256 tokenId, bytes memory data) 101 | ~_mint(address to, uint256 tokenId) 102 | ~_burn(uint256 tokenId) 103 | ~_transfer(address from, address to, uint256 tokenId) 104 | ~_approve(address to, uint256 tokenId) 105 | ~_setApprovalForAll(address owner, address operator, bool approved) 106 | ~_requireMinted(uint256 tokenId) 107 | -_checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data): (bool) 108 | ~_beforeTokenTransfer(address from, address to, uint256, uint256 batchSize) 109 | ~_afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) 110 | } 111 | 112 | ERC721 --|> Context 113 | 114 | %% 608:235:6 115 | class Context { 116 | <> 117 | ~_msgSender(): (address) 118 | ~_msgData(): (bytes calldata) 119 | } 120 | 121 | ERC721 --|> ERC165 122 | 123 | %% 726:260:9 124 | class ERC165 { 125 | <> 126 | +supportsInterface(bytes4 interfaceId): (bool) 127 | } 128 | 129 | ERC165 --|> IERC165 130 | 131 | %% 405:447:10 132 | class IERC165 { 133 | <> 134 | #supportsInterface(bytes4 interfaceId): (bool)$ 135 | } 136 | 137 | ERC721 --|> IERC721 138 | 139 | %% 250:4725:1 140 | class IERC721 { 141 | <> 142 | #balanceOf(address owner): (uint256 balance)$ 143 | #ownerOf(uint256 tokenId): (address owner)$ 144 | #safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data)$ 145 | #safeTransferFrom(address from, address to, uint256 tokenId)$ 146 | #transferFrom(address from, address to, uint256 tokenId)$ 147 | #approve(address to, uint256 tokenId)$ 148 | #setApprovalForAll(address operator, bool _approved)$ 149 | #getApproved(uint256 tokenId): (address operator)$ 150 | #isApprovedForAll(address owner, address operator): (bool)$ 151 | } 152 | 153 | IERC721 --|> IERC165 154 | 155 | %% 405:447:10 156 | class IERC165 { 157 | <> 158 | #supportsInterface(bytes4 interfaceId): (bool)$ 159 | } 160 | 161 | ERC721 --|> IERC721Metadata 162 | 163 | %% 297:463:4 164 | class IERC721Metadata { 165 | <> 166 | #name(): (string memory)$ 167 | #symbol(): (string memory)$ 168 | #tokenURI(uint256 tokenId): (string memory)$ 169 | } 170 | 171 | IERC721Metadata --|> IERC721 172 | 173 | %% 250:4725:1 174 | class IERC721 { 175 | <> 176 | #balanceOf(address owner): (uint256 balance)$ 177 | #ownerOf(uint256 tokenId): (address owner)$ 178 | #safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data)$ 179 | #safeTransferFrom(address from, address to, uint256 tokenId)$ 180 | #transferFrom(address from, address to, uint256 tokenId)$ 181 | #approve(address to, uint256 tokenId)$ 182 | #setApprovalForAll(address operator, bool _approved)$ 183 | #getApproved(uint256 tokenId): (address operator)$ 184 | #isApprovedForAll(address owner, address operator): (bool)$ 185 | } 186 | 187 | IERC721 --|> IERC165 188 | 189 | %% 405:447:10 190 | class IERC165 { 191 | <> 192 | #supportsInterface(bytes4 interfaceId): (bool)$ 193 | } 194 | ``` 195 | 196 | ## Getting started 197 | 198 | ``` 199 | npm install solidity-mermaid 200 | ``` 201 | 202 | ### Getting a Solc Output 203 | 204 | In order to get a Solc output, you can use a compilation artifact from your common development enviroment (such as [Hardhat](https://github.com/NomicFoundation/hardhat) or [Foundry](https://github.com/foundry-rs/foundry/)) 205 | 206 | If not, you can always get the output from scratch using [solc-js](https://github.com/ethereum/solc-js): 207 | 208 | ```js 209 | import solc from "solc"; 210 | 211 | const input = { 212 | language: "Solidity", 213 | sources: { 214 | "path/to/your/file.sol": { 215 | content: ` 216 | // SPDX-License-Identifier: MIT 217 | 218 | ... 219 | 220 | contract Example is ... { 221 | ... 222 | } 223 | `, 224 | }, 225 | }, 226 | settings: { 227 | outputSelection: { 228 | "*": { 229 | "*": ["*"], 230 | "": ["ast"], 231 | }, 232 | }, 233 | }, 234 | }; 235 | 236 | const output = JSON.parse(solc.compile(JSON.stringify(input))); 237 | ``` 238 | 239 | ### Solidity AST to Class Diagram 240 | 241 | To get a class diagram from your output, you'll need to pass the output, and an AST node with its type and id: 242 | 243 | ```js 244 | const classDiagram = new Class(output, "ContractDefinition", typeDef.id); 245 | 246 | // First run you'll need to use `processed` so the AST gets converted into text 247 | console.log(classDiagram.processed); 248 | 249 | // Afterwards, if no changes were made to the AST, you can just print its text 250 | console.log(classDiagram.text); 251 | ``` 252 | 253 | You can also use it with `solidity-ast/utils` 254 | 255 | ```js 256 | import { Class } from "solidity-mermaid"; 257 | import { findAll } from "solidity-ast/utils"; 258 | 259 | for (const [, { ast }] of Object.entries(output.sources)) { 260 | for (const typeDef of findAll(["ContractDefinition"], ast)) { 261 | const classDiagram = new Class(output, "ContractDefinition", typeDef.id); 262 | 263 | // ... 264 | } 265 | } 266 | ``` 267 | 268 | ## Solidity Versioning 269 | 270 | The Solidity AST should've been produce with a version that's supported in OpenZeppelin's [solidity-ast](https://github.com/OpenZeppelin/solidity-ast) package. 271 | 272 | ## 🤝 Contributing 273 | 274 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/ernestognw/solidity-mermaid/issues). You can also take a look at the [contributing guide](https://github.com/ernestognw/solidity-mermaid/blob/master/CONTRIBUTING.md). 275 | 276 | ## 📝 License 277 | 278 | Copyright © 2023 [Ernesto García ](https://github.com/ernestognw).
279 | This project is [MIT](https://github.com/ernestognw/solidity-mermaid/blob/master/LICENSE) licensed. 280 | -------------------------------------------------------------------------------- /assets/ExampleERC721-Class.svg: -------------------------------------------------------------------------------- 1 |
«Contract»
GameItem
+constructor()
+awardItem(address player, string memory tokenURI): (uint256)
«Contract»
ERC721URIStorage
+tokenURI(uint256 tokenId): (string memory)
~_setTokenURI(uint256 tokenId, string memory _tokenURI)
~_burn(uint256 tokenId)
«Contract»
ERC721
+constructor(string memory name_, string memory symbol_)
+supportsInterface(bytes4 interfaceId): (bool)
+balanceOf(address owner): (uint256)
+ownerOf(uint256 tokenId): (address)
+name(): (string memory)
+symbol(): (string memory)
+tokenURI(uint256 tokenId): (string memory)
~_baseURI(): (string memory)
+approve(address to, uint256 tokenId)
+getApproved(uint256 tokenId): (address)
+setApprovalForAll(address operator, bool approved)
+isApprovedForAll(address owner, address operator): (bool)
+transferFrom(address from, address to, uint256 tokenId)
+safeTransferFrom(address from, address to, uint256 tokenId)
+safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
~_safeTransfer(address from, address to, uint256 tokenId, bytes memory data)
~_ownerOf(uint256 tokenId): (address)
~_exists(uint256 tokenId): (bool)
~_isApprovedOrOwner(address spender, uint256 tokenId): (bool)
~_safeMint(address to, uint256 tokenId)
~_safeMint(address to, uint256 tokenId, bytes memory data)
~_mint(address to, uint256 tokenId)
~_burn(uint256 tokenId)
~_transfer(address from, address to, uint256 tokenId)
~_approve(address to, uint256 tokenId)
~_setApprovalForAll(address owner, address operator, bool approved)
~_requireMinted(uint256 tokenId)
-_checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data): (bool)
~_beforeTokenTransfer(address from, address to, uint256, uint256 batchSize)
~_afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize)
«Contract»
Context
~_msgSender(): (address)
~_msgData(): (bytes calldata)
«Contract»
ERC165
+supportsInterface(bytes4 interfaceId): (bool)
«Interface»
IERC165
#supportsInterface(bytes4 interfaceId): (bool)
#supportsInterface(bytes4 interfaceId): (bool)
#supportsInterface(bytes4 interfaceId): (bool)
«Interface»
IERC721
#balanceOf(address owner): (uint256 balance)
#ownerOf(uint256 tokenId): (address owner)
#safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data)
#safeTransferFrom(address from, address to, uint256 tokenId)
#transferFrom(address from, address to, uint256 tokenId)
#approve(address to, uint256 tokenId)
#setApprovalForAll(address operator, bool _approved)
#getApproved(uint256 tokenId): (address operator)
#isApprovedForAll(address owner, address operator): (bool)
#balanceOf(address owner): (uint256 balance)
#ownerOf(uint256 tokenId): (address owner)
#safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data)
#safeTransferFrom(address from, address to, uint256 tokenId)
#transferFrom(address from, address to, uint256 tokenId)
#approve(address to, uint256 tokenId)
#setApprovalForAll(address operator, bool _approved)
#getApproved(uint256 tokenId): (address operator)
#isApprovedForAll(address owner, address operator): (bool)
«Interface»
IERC721Metadata
#name(): (string memory)
#symbol(): (string memory)
#tokenURI(uint256 tokenId): (string memory)
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidity-mermaid", 3 | "version": "0.1.2", 4 | "description": "A Solidity AST parser that allows to convert smart contracts into Github's Mermaid.js language for diagramming.", 5 | "types": "dist/index.d.ts", 6 | "main": "dist/index.js", 7 | "author": "Ernesto García ", 8 | "license": "MIT", 9 | "private": false, 10 | "homepage": "https://www.npmjs.com/package/solidity-mermaid", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ernestognw/solidity-mermaid.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/ernestognw/solidity-mermaid/issues" 17 | }, 18 | "scripts": { 19 | "test": "mocha", 20 | "coverage": "c8 npm run test --", 21 | "prepublishOnly": "npm run clean", 22 | "prepare": "tsc && tsc-alias", 23 | "clean": "rm -rf dist" 24 | }, 25 | "files": [ 26 | "/src", 27 | "/dist", 28 | "!**/tests", 29 | "!**/__tests__", 30 | "!**/*.tsbuildinfo", 31 | "!**/*.test.*" 32 | ], 33 | "keywords": [ 34 | "solidity", 35 | "mermaid", 36 | "diagram", 37 | "ethereum" 38 | ], 39 | "devDependencies": { 40 | "@openzeppelin/contracts": "^4.8.1", 41 | "@types/chai": "^4.3.4", 42 | "@types/glob": "^8.0.1", 43 | "@types/mocha": "^10.0.1", 44 | "@types/node": "^18.11.18", 45 | "@types/sinon": "^10.0.13", 46 | "@typescript-eslint/eslint-plugin": "^5.49.0", 47 | "@typescript-eslint/parser": "^5.49.0", 48 | "axios": "^1.2.6", 49 | "c8": "^7.12.0", 50 | "chai": "^4.3.7", 51 | "eslint": "^8.32.0", 52 | "glob": "^8.1.0", 53 | "jison": "^0.4.18", 54 | "mocha": "^10.2.0", 55 | "sinon": "^15.0.1", 56 | "solc": "^0.8.17", 57 | "ts-node": "^10.9.1", 58 | "tsc-alias": "^1.8.2", 59 | "tsconfig-paths": "^4.1.2", 60 | "typescript": "^4.9.4" 61 | }, 62 | "dependencies": { 63 | "solidity-ast": "^0.4.43" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/classes/base/__tests__/indented.test.ts: -------------------------------------------------------------------------------- 1 | import Indented from "../indented"; 2 | import { shouldBehaveLikeIndented } from "./utils/indented.behavior"; 3 | 4 | function build(indentation?: number) { 5 | return new Indented(indentation); 6 | } 7 | 8 | describe(Indented.name, function () { 9 | shouldBehaveLikeIndented({ 10 | initialIndentation: 0, 11 | build, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/classes/base/__tests__/line.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import Line from "../line"; 3 | import { shouldBehaveLikeIndented } from "./utils/indented.behavior"; 4 | 5 | function buildLine(text?: string, indentation?: number) { 6 | return new Line(text, indentation); 7 | } 8 | 9 | describe(Line.name, function () { 10 | let line: Line; 11 | 12 | beforeEach(function () { 13 | line = buildLine("", 0); 14 | }); 15 | 16 | shouldBehaveLikeIndented({ 17 | initialIndentation: 0, 18 | build: (indentation) => buildLine("", indentation), 19 | }); 20 | 21 | describe("#constructor", function () { 22 | it("sets empty text with 0 indentation by deafault", function () { 23 | const line = buildLine(); 24 | expect(line.text).to.equal(""); 25 | expect(line["indentation"]).to.equal(0); 26 | }); 27 | 28 | it("sets initial text", function () { 29 | const text = "Hello world"; 30 | const line = buildLine(text); 31 | expect(line.text).to.equal(text); 32 | }); 33 | 34 | it("sets initial indentation", function () { 35 | const indentation = 12; 36 | const line = buildLine("", indentation); 37 | expect(line["indentation"]).to.equal(indentation); 38 | expect(line.text).to.include(line["_spaces"].repeat(indentation)); 39 | }); 40 | 41 | it("should throw with newlines", function () { 42 | expect(() => buildLine("\n")).to.throw("Line can't contain newline"); 43 | }); 44 | }); 45 | 46 | describe("+concat", function () { 47 | it("should add to text", function () { 48 | const text = "Hello world"; 49 | line.concat(text); 50 | expect(line.text).to.equal(text); 51 | }); 52 | 53 | it("should allow to chain calls", function () { 54 | line.concat("1").concat(" ").concat("1"); 55 | expect(line.text).to.equal("1 1"); 56 | }); 57 | 58 | it("should throw with newlines", function () { 59 | expect(line.concat.bind(line, "\n")).to.throw( 60 | "Line can't contain newline" 61 | ); 62 | }); 63 | }); 64 | 65 | describe("-_reset", function () { 66 | it("should go back to initial state", function () { 67 | line.concat("This should be removed"); 68 | const expected = "Test reset"; 69 | line["initialText"] = expected; 70 | line["_reset"](); 71 | expect(line["_text"]).to.equal(expected); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/classes/base/__tests__/mermaid.test.ts: -------------------------------------------------------------------------------- 1 | import Mermaid from "../mermaid"; 2 | import { shouldBehaveLikeMermaid } from "./utils/mermaid.behavior"; 3 | 4 | function buildMermaid(indentation?: number) { 5 | return new Mermaid(indentation); 6 | } 7 | 8 | describe(Mermaid.name, function () { 9 | shouldBehaveLikeMermaid({ 10 | initialIndentation: 0, 11 | build: buildMermaid, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/classes/base/__tests__/utils/indented.behavior.ts: -------------------------------------------------------------------------------- 1 | import Indented from "@classes/base/indented"; 2 | import { expect } from "chai"; 3 | 4 | export interface BehaveLikeIntendedParams { 5 | initialIndentation: number; 6 | build: (indentation?: number) => Indented; 7 | } 8 | 9 | export function shouldBehaveLikeIndented({ 10 | initialIndentation = 0, 11 | build, 12 | }: BehaveLikeIntendedParams) { 13 | describe(`extends ${Indented.name}`, () => { 14 | beforeEach(function () { 15 | this.indented = build(0); 16 | }); 17 | 18 | function call( 19 | indented: Indented, 20 | method: "indent" | "unindent", 21 | times: number 22 | ) { 23 | for (let i = 0; i < times; i++) indented[method](); 24 | } 25 | 26 | const indentations = new Array(5).fill("").map((_, index) => 2 ** index); 27 | 28 | describe("#constructor", function () { 29 | it(`has ${initialIndentation} indentation by default`, () => { 30 | const customIndented = build(); 31 | expect(customIndented["indentation"]).to.equal(initialIndentation); 32 | }); 33 | 34 | indentations.forEach((indentation) => { 35 | it(`sets ${indentation} initial indentation`, function () { 36 | const customIndented = build(indentation); 37 | expect(customIndented["indentation"]).to.equal( 38 | initialIndentation + indentation 39 | ); 40 | }); 41 | }); 42 | }); 43 | 44 | describe("+indent", function () { 45 | [0, ...indentations].forEach((indentation) => { 46 | it(`should add ${indentation} spaces of indentation`, function () { 47 | call(this.indented, "indent", indentation); 48 | expect(this.indented["indentation"]).to.equal( 49 | initialIndentation + indentation 50 | ); 51 | }); 52 | }); 53 | }); 54 | 55 | describe("+unindent", function () { 56 | const initialValue = 100; // Should be higher than indentations map 57 | 58 | [0, ...indentations].reverse().forEach((indentation) => { 59 | it(`should unindent ${indentation} spaces from initial (${initialValue})`, function () { 60 | const customIndented = build(initialValue); 61 | call(customIndented, "unindent", indentation); 62 | expect(customIndented["indentation"]).to.equal( 63 | initialValue - indentation + initialIndentation 64 | ); 65 | }); 66 | }); 67 | 68 | it("should avoid unindent from 0", function () { 69 | this.indented["_indentation"] = 0; 70 | this.indented.unindent(); 71 | expect(this.indented["_indentation"]).to.equal(0); 72 | }); 73 | }); 74 | 75 | describe("-_reset", function () { 76 | it("should go back to initial state", function () { 77 | call(this.indented, "indent", 10); 78 | const expected = 4; 79 | this.indented.initialIndentation = expected; 80 | this.indented["_reset"](); 81 | expect(this.indented["_indentation"]).to.equal(expected); 82 | }); 83 | }); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/classes/base/__tests__/utils/mermaid.behavior.ts: -------------------------------------------------------------------------------- 1 | import Line from "@classes/base/line"; 2 | import Mermaid from "@classes/base/mermaid"; 3 | import { expect } from "chai"; 4 | import { 5 | BehaveLikeIntendedParams, 6 | shouldBehaveLikeIndented, 7 | } from "./indented.behavior"; 8 | 9 | export function shouldBehaveLikeMermaid({ 10 | initialIndentation = 0, 11 | build, 12 | }: BehaveLikeIntendedParams) { 13 | describe(`extends ${Mermaid.name}`, () => { 14 | beforeEach(function () { 15 | this.mermaid = build(0); 16 | }); 17 | 18 | shouldBehaveLikeIndented({ 19 | initialIndentation, 20 | build, 21 | }); 22 | 23 | function push(mermaid: Mermaid, times: number) { 24 | for (let i = 0; i < times; i++) mermaid.push("Hello world"); 25 | } 26 | 27 | const pushes = new Array(5).fill("").map((_, index) => 2 ** index); 28 | 29 | describe("#constructor", function () { 30 | pushes.forEach((lines) => { 31 | it(`Adds ${lines} new lines`, function () { 32 | const initialLines = this.mermaid.lines.length; 33 | push(this.mermaid, lines); 34 | expect(this.mermaid.lines.length).to.equal(initialLines + lines); 35 | }); 36 | }); 37 | 38 | pushes.forEach((indentation) => { 39 | it(`Add current indentation to line (${indentation})`, function () { 40 | const initialLines = this.mermaid.lines.length; 41 | this.mermaid["_indentation"] = indentation; 42 | this.mermaid.push("Test"); 43 | expect(this.mermaid.lines[initialLines]["_indentation"]).to.equal( 44 | indentation 45 | ); 46 | }); 47 | }); 48 | }); 49 | 50 | describe("+indentAll", function () { 51 | beforeEach(function () { 52 | push(this.mermaid, 10); 53 | }); 54 | 55 | it("Indents all of the lines", function () { 56 | const currents = this.mermaid.lines.map( 57 | ({ _indentation }) => _indentation 58 | ); 59 | this.mermaid.indentAll(); 60 | this.mermaid.lines.forEach((line, i) => 61 | expect(line["_indentation"]).to.equal(currents[i] + 1) 62 | ); 63 | }); 64 | }); 65 | 66 | describe("+unindentAll", function () { 67 | beforeEach(function () { 68 | push(this.mermaid, 10); 69 | }); 70 | it("Unindents all of the lines", function () { 71 | this.mermaid.indentAll(); // So unindent is not skipped 72 | const currents = this.mermaid.lines.map( 73 | ({ _indentation }) => _indentation 74 | ); 75 | this.mermaid.unindentAll(); 76 | this.mermaid.lines.forEach((line, i) => 77 | expect(line["_indentation"]).to.equal(currents[i] - 1) 78 | ); 79 | }); 80 | }); 81 | 82 | describe("+text", function () { 83 | pushes.forEach((lines) => { 84 | it(`Print ${lines} lines added`, function () { 85 | const initialLines = this.mermaid.lines.length; 86 | this.mermaid.text; // Just to execute process 87 | push(this.mermaid, lines); 88 | expect(this.mermaid.text.split("\n").length).to.equal( 89 | initialLines + lines 90 | ); 91 | }); 92 | }); 93 | }); 94 | 95 | describe("-_reset", function () { 96 | it("should go back to initial state", function () { 97 | push(this.mermaid, 4); 98 | const expected = new Array(10) 99 | .fill({}) 100 | .map((_, i) => new Line(`Text ${i}`, i)); 101 | this.mermaid.initialLines = expected; 102 | this.mermaid["_reset"](); 103 | expect(this.mermaid["_lines"]).to.equal(expected); 104 | }); 105 | }); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /src/classes/base/indented.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_INDENTATION = 0; 2 | 3 | export default class Indented { 4 | private _initialIndentation: number; 5 | 6 | constructor(private _indentation = DEFAULT_INDENTATION) { 7 | this._initialIndentation = 0; 8 | } 9 | 10 | public get indentation() { 11 | return this._indentation; 12 | } 13 | 14 | indent() { 15 | this._indentation++; 16 | } 17 | 18 | unindent() { 19 | if (this._indentation > 0) this._indentation--; 20 | } 21 | 22 | protected _reset() { 23 | this._indentation = this._initialIndentation; 24 | } 25 | 26 | protected set initialIndentation(indentation: number) { 27 | this._initialIndentation = indentation; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/classes/base/line.ts: -------------------------------------------------------------------------------- 1 | import { ErrorType, FormatError } from "@classes/errors/format"; 2 | import Indented, { DEFAULT_INDENTATION } from "./indented"; 3 | 4 | export const DEFAULT_TEXT = ""; 5 | 6 | export default class Line extends Indented { 7 | private readonly _regex = /^.*$/g; 8 | private readonly _spaces = " "; 9 | 10 | private _initialText: string; 11 | 12 | constructor(private _text = DEFAULT_TEXT, indentation = DEFAULT_INDENTATION) { 13 | super(indentation); 14 | this._initialText = _text; 15 | this.text = _text; // Explicit so `_validate` runs 16 | } 17 | 18 | private set text(text: string) { 19 | this._text = text; 20 | this._validate(); 21 | } 22 | 23 | get text() { 24 | return `${this._spaces.repeat(this.indentation)}${this._text}`; 25 | } 26 | 27 | concat(text: string) { 28 | // Intentionally muting the variable so it's validated 29 | this.text = this._text.concat(text); 30 | 31 | return this; 32 | } 33 | 34 | private _validate() { 35 | if (!this.text.match(this._regex)) 36 | throw new FormatError("Line can't contain newline", ErrorType.BadLine); 37 | } 38 | 39 | protected _reset() { 40 | super._reset(); 41 | this._text = this._initialText; 42 | } 43 | 44 | protected set initialText(text: string) { 45 | this._initialText = text; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/classes/base/mermaid.ts: -------------------------------------------------------------------------------- 1 | import Indented, { DEFAULT_INDENTATION } from "./indented"; 2 | import Line from "./line"; 3 | 4 | export default class Mermaid extends Indented { 5 | private _lines: Line[] = []; 6 | private _initialLines: Line[] = []; 7 | 8 | constructor(_indentation = DEFAULT_INDENTATION) { 9 | super(_indentation); 10 | } 11 | 12 | get lines() { 13 | return this._lines; 14 | } 15 | 16 | get text() { 17 | return this.lines.map(({ text }) => text).join("\n"); 18 | } 19 | 20 | public push(text: string) { 21 | this._lines.push(new Line(text, this.indentation)); 22 | } 23 | 24 | indentAll() { 25 | this.lines.forEach((line) => line.indent()); 26 | } 27 | 28 | unindentAll() { 29 | this.lines.forEach((line) => line.unindent()); 30 | } 31 | 32 | indent() { 33 | super.indent(); 34 | } 35 | 36 | unindent() { 37 | super.unindent(); 38 | } 39 | 40 | protected _reset() { 41 | super._reset(); 42 | this._lines = this._initialLines; 43 | } 44 | 45 | protected set initialLines(lines: Line[]) { 46 | this._initialLines = lines; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/classes/diagrams/class/__tests__/class.test.ts: -------------------------------------------------------------------------------- 1 | import Class from "../"; 2 | import { shouldBehaveLikeMermaid } from "@classes/base/__tests__/utils/mermaid.behavior"; 3 | import ERC721Output from "@tests/fixtures/ERC721.output.json"; 4 | import { SolcOutput } from "solidity-ast/solc"; 5 | import { NodeType } from "solidity-ast/node"; 6 | import { expect } from "chai"; 7 | import sinon, { SinonStub } from "sinon"; 8 | import { astDereferencer } from "solidity-ast/utils"; 9 | import Processor from "../processor"; 10 | 11 | function buildClass( 12 | solcOuput: SolcOutput, 13 | nodeType: NodeType, 14 | id: number, 15 | indentation?: number 16 | ) { 17 | return new Class(solcOuput, nodeType, id, indentation); 18 | } 19 | 20 | describe(Class.name, function () { 21 | const solcOuput = ERC721Output as SolcOutput; 22 | const nodeType = "ContractDefinition"; 23 | const id = 2787; 24 | 25 | beforeEach(function () { 26 | this.classDiagram = buildClass(solcOuput, nodeType, id, 0); 27 | }); 28 | 29 | shouldBehaveLikeMermaid({ 30 | initialIndentation: 1, 31 | build: (indentation) => buildClass(solcOuput, nodeType, id, indentation), 32 | }); 33 | 34 | describe("#constructor", function () { 35 | it("sets node", function () { 36 | const dereference = astDereferencer(solcOuput); 37 | expect(this.classDiagram.node).to.equal(dereference(nodeType, id)); 38 | }); 39 | 40 | it("sets processor", function () { 41 | expect(this.classDiagram.processor).to.be.instanceOf(Processor); 42 | }); 43 | 44 | it("sets processor with Class context", function () { 45 | expect(this.classDiagram.processor.context).to.be.equal( 46 | this.classDiagram 47 | ); 48 | }); 49 | 50 | it("sets processor with dereferencer", function () { 51 | expect(this.classDiagram.processor.dereferencer).to.be.instanceOf( 52 | Function 53 | ); 54 | }); 55 | }); 56 | 57 | describe("+processed", function () { 58 | it("process AST into text", function () { 59 | const initialText = this.classDiagram.text; 60 | expect(this.classDiagram.processed.length).to.be.greaterThan( 61 | initialText.length 62 | ); 63 | }); 64 | 65 | it("is idempotent", function () { 66 | const text = this.classDiagram.text; 67 | new Array(10) 68 | .fill({}) 69 | .forEach(() => expect(this.classDiagram.text).to.be.equal(text)); 70 | }); 71 | }); 72 | 73 | describe("+print", function () { 74 | let stub: SinonStub; 75 | 76 | beforeEach(function () { 77 | stub = sinon.stub(console, "log"); 78 | }); 79 | 80 | afterEach(function () { 81 | stub.restore(); 82 | }); 83 | 84 | it("prints processed text", function () { 85 | const text = this.classDiagram.processed; 86 | this.classDiagram.print(); 87 | expect(stub.calledOnce).to.be.true; 88 | expect(stub.firstCall.args[0]).to.equal(text); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/classes/diagrams/class/index.ts: -------------------------------------------------------------------------------- 1 | import Mermaid from "@classes/base/mermaid"; 2 | import { Node, NodeType } from "solidity-ast/node"; 3 | import { SolcOutput } from "solidity-ast/solc"; 4 | import { astDereferencer } from "solidity-ast/utils"; 5 | import Processor from "./processor"; 6 | 7 | export default class Class extends Mermaid { 8 | private readonly _node: Node; 9 | private _processor: Processor; 10 | 11 | constructor( 12 | _solcOutput: SolcOutput, 13 | nodeType: NodeType, 14 | id: number, 15 | initialIndentation?: number 16 | ) { 17 | super(initialIndentation); 18 | 19 | const dereference = astDereferencer(_solcOutput); 20 | this._node = dereference(nodeType, id); 21 | this._processor = new Processor(this, dereference); 22 | 23 | super.push("classDiagram"); 24 | this.indent(); 25 | this._setInitialState(); 26 | } 27 | 28 | get node() { 29 | return this._node; 30 | } 31 | 32 | get processor() { 33 | return this._processor; 34 | } 35 | 36 | get text() { 37 | return super.text; 38 | } 39 | 40 | get processed() { 41 | this._process(this.node); 42 | return this.text; 43 | } 44 | 45 | unindentAll() { 46 | super.unindentAll(); 47 | } 48 | 49 | indentAll() { 50 | super.indentAll(); 51 | } 52 | 53 | push(text: string) { 54 | super.push(text); 55 | } 56 | 57 | print() { 58 | console.log(this.text); 59 | } 60 | 61 | private _process(node: Node) { 62 | this._reset(); 63 | this.processor.process(node); 64 | } 65 | 66 | protected _reset() { 67 | super._reset(); 68 | } 69 | 70 | private _setInitialState() { 71 | this.initialIndentation = this.indentation; 72 | this.initialLines = this.lines; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/classes/diagrams/class/processor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContractDefinition, 3 | FunctionDefinition, 4 | InheritanceSpecifier, 5 | VariableDeclaration, 6 | Visibility, 7 | } from "solidity-ast"; 8 | import { ASTProcessor, ProcessOptions } from "@types"; 9 | import Class from "."; 10 | import { ASTDereferencer, isNodeType } from "solidity-ast/utils"; 11 | import { Node } from "solidity-ast/node"; 12 | import { ASTError, ErrorType } from "@classes/errors/ast"; 13 | import Line from "@classes/base/line"; 14 | 15 | export default class Processor implements ASTProcessor { 16 | constructor( 17 | private _context: Class, 18 | private _dereferencer: ASTDereferencer 19 | ) {} 20 | 21 | get dereferencer() { 22 | return this._dereferencer; 23 | } 24 | 25 | get context() { 26 | return this._context; 27 | } 28 | 29 | process(node: Node) { 30 | this.processNode(node, { 31 | parent: {} as Node, // No parent 32 | }); 33 | } 34 | 35 | // Explicitly skipped. 36 | // See: https://github.com/ernestognw/solidity-mermaid/issues/16 37 | // eslint-disable-next-line @typescript-eslint/no-empty-function 38 | processSourceUnit() {} 39 | 40 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/11 41 | // eslint-disable-next-line @typescript-eslint/no-empty-function 42 | processArrayTypeName() {} 43 | 44 | // Explicitly skipped. 45 | // eslint-disable-next-line @typescript-eslint/no-empty-function 46 | processAssignment() {} 47 | 48 | // Explicitly skipped. 49 | // eslint-disable-next-line @typescript-eslint/no-empty-function 50 | processBinaryOperation() {} 51 | 52 | // Explicitly skipped. 53 | // eslint-disable-next-line @typescript-eslint/no-empty-function 54 | processBlock() {} 55 | 56 | // Explicitly skipped. 57 | // eslint-disable-next-line @typescript-eslint/no-empty-function 58 | processBreak() {} 59 | 60 | // Explicitly skipped. 61 | // eslint-disable-next-line @typescript-eslint/no-empty-function 62 | processConditional() {} 63 | 64 | // Explicitly skipped. 65 | // eslint-disable-next-line @typescript-eslint/no-empty-function 66 | processContinue() {} 67 | 68 | processContractDefinition(node: ContractDefinition) { 69 | if (node.documentation) 70 | this.processSubNodes([node.documentation], { parent: node }); 71 | 72 | this.comment(`${node.src}`); 73 | this.context.push(`class ${node.name} {`); 74 | this.context.indent(); 75 | 76 | switch (node.contractKind) { 77 | case "contract": 78 | this.context.push("<>"); 79 | break; 80 | case "interface": 81 | this.context.push("<>"); 82 | break; 83 | case "library": 84 | this.context.push("<>"); 85 | } 86 | 87 | this.processSubNodes(node.nodes, { parent: node }); 88 | 89 | this.context.unindent(); 90 | this.context.push("}"); 91 | this.context.push(""); 92 | 93 | this.processSubNodes(node.baseContracts, { parent: node }); 94 | } 95 | 96 | // Explicitly skipped. 97 | // eslint-disable-next-line @typescript-eslint/no-empty-function 98 | processDoWhileStatement() {} 99 | 100 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/11 101 | // eslint-disable-next-line @typescript-eslint/no-empty-function 102 | processElementaryTypeName() {} 103 | 104 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/11 105 | // eslint-disable-next-line @typescript-eslint/no-empty-function 106 | processElementaryTypeNameExpression() {} 107 | 108 | // Explicitly skipped. 109 | // eslint-disable-next-line @typescript-eslint/no-empty-function 110 | processEmitStatement() {} 111 | 112 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/15 113 | // eslint-disable-next-line @typescript-eslint/no-empty-function 114 | processEnumDefinition() {} 115 | 116 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/15 117 | // eslint-disable-next-line @typescript-eslint/no-empty-function 118 | processEnumValue() {} 119 | 120 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/14 121 | // eslint-disable-next-line @typescript-eslint/no-empty-function 122 | processErrorDefinition() {} 123 | 124 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/13 125 | // eslint-disable-next-line @typescript-eslint/no-empty-function 126 | processEventDefinition() {} 127 | 128 | // Explicitly skipped. 129 | // eslint-disable-next-line @typescript-eslint/no-empty-function 130 | processExpressionStatement() {} 131 | 132 | // Explicitly skipped. 133 | // eslint-disable-next-line @typescript-eslint/no-empty-function 134 | processForStatement() {} 135 | 136 | // Explicitly skipped. 137 | // eslint-disable-next-line @typescript-eslint/no-empty-function 138 | processFunctionCall() {} 139 | 140 | // Explicitly skipped. 141 | // eslint-disable-next-line @typescript-eslint/no-empty-function 142 | processFunctionCallOptions() {} 143 | 144 | processFunctionDefinition(node: FunctionDefinition) { 145 | const visibilityMap: Record = { 146 | public: "+", 147 | external: "#", // Using Mermaid protected since there's no external 148 | internal: "~", 149 | private: "-", 150 | }; 151 | 152 | const visibility = visibilityMap[node.visibility]; 153 | 154 | const name = node.name || node.kind; 155 | 156 | // Should we handle this in `procesParameterList`? 157 | // See https://github.com/ernestognw/solidity-mermaid/issues/7 158 | const processParameters = (parameters: VariableDeclaration[]): string => 159 | parameters 160 | .map((parameter) => { 161 | const line = new Line(); 162 | 163 | const type = parameter.typeDescriptions.typeString; 164 | if (type) line.concat(type).concat(" "); 165 | 166 | if (parameter.storageLocation != "default") 167 | line.concat(parameter.storageLocation).concat(" "); 168 | 169 | line.concat(parameter.name).concat(" "); 170 | 171 | return line.text.trim(); 172 | }) 173 | .join(", "); 174 | 175 | const parameters = processParameters(node.parameters.parameters); 176 | const returnParameters = processParameters( 177 | node.returnParameters.parameters 178 | ); 179 | 180 | const line = new Line(`${visibility}${name}(${parameters})`); 181 | 182 | if (returnParameters) line.concat(`: (${returnParameters})`); 183 | if (!node.implemented) line.concat(`$`); // For representing virtual 184 | 185 | this.context.push(line.text); 186 | } 187 | 188 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/11 189 | // eslint-disable-next-line @typescript-eslint/no-empty-function 190 | processFunctionTypeName() {} 191 | 192 | // Explicitly skipped. 193 | // eslint-disable-next-line @typescript-eslint/no-empty-function 194 | processIdentifier() {} 195 | 196 | // Explicitly skipped. 197 | // eslint-disable-next-line @typescript-eslint/no-empty-function 198 | processIdentifierPath() {} 199 | 200 | // Explicitly skipped. 201 | // eslint-disable-next-line @typescript-eslint/no-empty-function 202 | processIfStatement() {} 203 | 204 | // Explicitly skipped. 205 | // See: https://github.com/ernestognw/solidity-mermaid/issues/12 206 | // eslint-disable-next-line @typescript-eslint/no-empty-function 207 | processImportDirective() {} 208 | 209 | // Explicitly skipped. 210 | // eslint-disable-next-line @typescript-eslint/no-empty-function 211 | processIndexAccess() {} 212 | 213 | // Explicitly skipped. 214 | // eslint-disable-next-line @typescript-eslint/no-empty-function 215 | processIndexRangeAccess() {} 216 | 217 | processInheritanceSpecifier( 218 | node: InheritanceSpecifier, 219 | options: ProcessOptions 220 | ) { 221 | const inheritFrom = this.dereferencer( 222 | // Verify other cases for 223 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/4 224 | "ContractDefinition", 225 | node.baseName.referencedDeclaration 226 | ); 227 | 228 | if (!isNodeType("ContractDefinition", options.parent)) 229 | throw new ASTError( 230 | "Parent of InheritanceSpecifier can only be ContractDefinition", 231 | ErrorType.BadParent 232 | ); 233 | 234 | this.context.push(`${options.parent.name} --|> ${inheritFrom.name}`); 235 | this.context.push(""); 236 | this.processSubNodes([inheritFrom], { parent: node }); 237 | } 238 | 239 | // Explicitly skipped. 240 | // eslint-disable-next-line @typescript-eslint/no-empty-function 241 | processInlineAssembly() {} 242 | 243 | // Explicitly skipped. 244 | // eslint-disable-next-line @typescript-eslint/no-empty-function 245 | processLiteral() {} 246 | 247 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/11 248 | // eslint-disable-next-line @typescript-eslint/no-empty-function 249 | processMapping() {} 250 | 251 | // Explicitly skipped. 252 | // eslint-disable-next-line @typescript-eslint/no-empty-function 253 | processMemberAccess() {} 254 | 255 | // Explicitly skipped. 256 | // eslint-disable-next-line @typescript-eslint/no-empty-function 257 | processModifierDefinition() {} 258 | 259 | // Explicitly skipped. 260 | // eslint-disable-next-line @typescript-eslint/no-empty-function 261 | processModifierInvocation() {} 262 | 263 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/10 264 | // eslint-disable-next-line @typescript-eslint/no-empty-function 265 | processNewExpression() {} 266 | 267 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/9 268 | // eslint-disable-next-line @typescript-eslint/no-empty-function 269 | processOverrideSpecifier() {} 270 | 271 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/7 272 | // eslint-disable-next-line @typescript-eslint/no-empty-function 273 | processParameterList() {} 274 | 275 | // Explicitly skipped. 276 | // eslint-disable-next-line @typescript-eslint/no-empty-function 277 | processPlaceholderStatement() {} 278 | 279 | // Explicitly skipped. 280 | // See: https://github.com/ernestognw/solidity-mermaid/issues/6 281 | // eslint-disable-next-line @typescript-eslint/no-empty-function 282 | processPragmaDirective() {} 283 | 284 | // Explicitly skipped. 285 | // eslint-disable-next-line @typescript-eslint/no-empty-function 286 | processReturn() {} 287 | 288 | // Explicitly skipped. 289 | // eslint-disable-next-line @typescript-eslint/no-empty-function 290 | processRevertStatement() {} 291 | 292 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/8 293 | // eslint-disable-next-line @typescript-eslint/no-empty-function 294 | processStructDefinition() {} 295 | 296 | // Explicitly skipped. 297 | // See: https://github.com/ernestognw/solidity-mermaid/issues/5 298 | // eslint-disable-next-line @typescript-eslint/no-empty-function 299 | processStructuredDocumentation() { 300 | // See https://mermaid.js.org/syntax/classDiagram.html#notes 301 | } 302 | 303 | // Explicitly skipped. 304 | // eslint-disable-next-line @typescript-eslint/no-empty-function 305 | processTryCatchClause() {} 306 | 307 | // Explicitly skipped. 308 | // eslint-disable-next-line @typescript-eslint/no-empty-function 309 | processTryStatement() {} 310 | 311 | // Explicitly skipped. 312 | // eslint-disable-next-line @typescript-eslint/no-empty-function 313 | processTupleExpression() {} 314 | 315 | // Explicitly skipped. 316 | // eslint-disable-next-line @typescript-eslint/no-empty-function 317 | processUnaryOperation() {} 318 | 319 | // Explicitly skipped. 320 | // eslint-disable-next-line @typescript-eslint/no-empty-function 321 | processUncheckedBlock() {} 322 | 323 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/4 324 | // eslint-disable-next-line @typescript-eslint/no-empty-function 325 | processUserDefinedTypeName() {} 326 | 327 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/4 328 | // eslint-disable-next-line @typescript-eslint/no-empty-function 329 | processUserDefinedValueTypeDefinition() {} 330 | 331 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/3 332 | // eslint-disable-next-line @typescript-eslint/no-empty-function 333 | processUsingForDirective() {} 334 | 335 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/2 336 | // eslint-disable-next-line @typescript-eslint/no-empty-function 337 | processVariableDeclaration() {} 338 | 339 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/2 340 | // eslint-disable-next-line @typescript-eslint/no-empty-function 341 | processVariableDeclarationStatement() {} 342 | 343 | // Explicitly skipped. 344 | // eslint-disable-next-line @typescript-eslint/no-empty-function 345 | processWhileStatement() {} 346 | 347 | private processSubNodes(nodes: Node[], options: ProcessOptions) { 348 | for (const subnode of nodes) { 349 | this.processNode(subnode, options); 350 | } 351 | } 352 | 353 | private processNode(node: Node, options: ProcessOptions) { 354 | // Couldn't find a way to successfully execute this. Failed. 355 | // TODO: https://github.com/ernestognw/solidity-mermaid/issues/1 356 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 357 | this[`process${node.nodeType}`]( 358 | node as never, // Cound't find a way to use template literals as type discriminators to avoid `never` intersection 359 | options 360 | ); 361 | } 362 | 363 | private comment(message: string) { 364 | this.context.push(`%% ${message}`); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/classes/errors/__tests__/ast.test.ts: -------------------------------------------------------------------------------- 1 | import { ErrorType, ASTError } from "../ast"; 2 | import { shouldBehaveLikeTypedError } from "./utils/typed.behavior"; 3 | 4 | describe("ASTError", () => { 5 | shouldBehaveLikeTypedError({ 6 | build: (message, type) => new ASTError(message, type as ErrorType), 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/classes/errors/__tests__/format.test.ts: -------------------------------------------------------------------------------- 1 | import { ErrorType, FormatError } from "../format"; 2 | import { shouldBehaveLikeTypedError } from "./utils/typed.behavior"; 3 | 4 | describe("FormatError", () => { 5 | shouldBehaveLikeTypedError({ 6 | build: (message, type) => new FormatError(message, type as ErrorType), 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/classes/errors/__tests__/typed.test.ts: -------------------------------------------------------------------------------- 1 | import { TypedError } from "../typed"; 2 | import { shouldBehaveLikeTypedError } from "./utils/typed.behavior"; 3 | 4 | describe("TypedError", () => { 5 | shouldBehaveLikeTypedError({ 6 | build: (message, type) => new TypedError(message, type), 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/classes/errors/__tests__/utils/typed.behavior.ts: -------------------------------------------------------------------------------- 1 | import { TypedError } from "@classes/errors/typed"; 2 | import { expect } from "chai"; 3 | import sinon, { SinonStub } from "sinon"; 4 | 5 | interface BehaveLikeTypedErrorParams { 6 | build>( 7 | ...args: T 8 | ): TypedError; 9 | } 10 | 11 | enum TestTypes { 12 | A, 13 | } 14 | 15 | export function shouldBehaveLikeTypedError({ 16 | build, 17 | }: BehaveLikeTypedErrorParams) { 18 | describe("#constructor", () => { 19 | it("sets message", () => { 20 | const message = "Hello world"; 21 | expect(new TypedError(message, "").message).to.equal(message); 22 | }); 23 | 24 | it("sets type", () => { 25 | expect(build("", TestTypes.A).type).to.equal(TestTypes.A); 26 | }); 27 | }); 28 | 29 | describe("+print", () => { 30 | let stub: SinonStub; 31 | 32 | beforeEach(function () { 33 | stub = sinon.stub(console, "error"); 34 | }); 35 | 36 | afterEach(function () { 37 | stub.restore(); 38 | }); 39 | 40 | it("includes name", () => { 41 | const typedError = build("", ""); 42 | typedError.print(); 43 | expect(stub.calledOnce).to.be.true; 44 | expect(stub.firstCall.args[0]).to.include(typedError.name); 45 | }); 46 | 47 | it("includes type", () => { 48 | const typedError = build("", ""); 49 | typedError.print(); 50 | expect(stub.calledOnce).to.be.true; 51 | expect(stub.firstCall.args[0]).to.include(typedError.type); 52 | }); 53 | 54 | it("includes message", () => { 55 | const message = "Testing error"; 56 | const typedError = build(message, ""); 57 | typedError.print(); 58 | expect(stub.calledOnce).to.be.true; 59 | expect(stub.firstCall.args[0]).to.include(message); 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /src/classes/errors/ast.ts: -------------------------------------------------------------------------------- 1 | import { TypedError } from "./typed"; 2 | 3 | export enum ErrorType { 4 | BadParent = "BadParent", 5 | } 6 | 7 | export class ASTError extends TypedError { 8 | constructor(message: string, type: ErrorType) { 9 | super(message, type); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/classes/errors/format.ts: -------------------------------------------------------------------------------- 1 | import { TypedError } from "./typed"; 2 | 3 | export enum ErrorType { 4 | BadLine = "BadLine", 5 | } 6 | 7 | export class FormatError extends TypedError { 8 | constructor(message: string, type: ErrorType) { 9 | super(message, type); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/classes/errors/typed.ts: -------------------------------------------------------------------------------- 1 | export class TypedError extends Error { 2 | private readonly _name: string; 3 | 4 | constructor(message: string, private readonly _type: T) { 5 | super(message); 6 | this._name = this.constructor.name; 7 | } 8 | 9 | get type() { 10 | return this._type; 11 | } 12 | 13 | get name() { 14 | return this._name; 15 | } 16 | 17 | print() { 18 | const error = `${this.name} [${this.type}]: ${this.message}`; 19 | console.error(error); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Class } from "@classes/diagrams/class"; 2 | -------------------------------------------------------------------------------- /src/tests/class.integration.test.ts: -------------------------------------------------------------------------------- 1 | import solc from "solc"; 2 | import glob from "glob"; 3 | import { join, relative } from "path"; 4 | import { readFileSync } from "fs"; 5 | import { SolcOutput } from "solidity-ast/solc"; 6 | import { findAll } from "solidity-ast/utils"; 7 | import { Class } from "@/index"; 8 | import axios from "axios"; 9 | import { Parser } from "jison"; 10 | import { expect } from "chai"; 11 | 12 | function getContracts() { 13 | const modulesPath = join(__dirname, "..", "..", "node_modules"); 14 | const ozPath = join(modulesPath, "@openzeppelin/contracts"); 15 | const contracts = glob.sync(join(ozPath, "/**/*.sol")).map((contract) => ({ 16 | name: contract.split("/").reverse()[0], 17 | content: readFileSync(relative(".", contract), "utf-8"), 18 | })); 19 | 20 | return contracts; 21 | } 22 | 23 | function getOutput(name: string, source: { content: string }): SolcOutput { 24 | const input = { 25 | language: "Solidity", 26 | sources: { [name]: source }, 27 | settings: { 28 | outputSelection: { 29 | "*": { 30 | "*": ["*"], 31 | "": ["ast"], 32 | }, 33 | }, 34 | }, 35 | }; 36 | 37 | return JSON.parse(solc.compile(JSON.stringify(input))); 38 | } 39 | 40 | const getParserFrom = (grammar) => { 41 | const YY_REGEX = /yy\.(\w*).?(?=\()/g; 42 | const yyMock = {}; 43 | for (const yyFunction of grammar.match(YY_REGEX)) { 44 | yyMock[yyFunction.replace("yy.", "")] = () => void 0; 45 | } 46 | const parser = new Parser(grammar); 47 | parser.yy = yyMock; 48 | return parser; 49 | }; 50 | 51 | describe("Class Diagram", function () { 52 | // Yes, I know this is horrible but the mermaid team doesn't publish the grammar within their npm package 53 | const MERMAID_9_3_0_GRAMMAR = 54 | "https://raw.githubusercontent.com/mermaid-js/mermaid/v9.3.0/packages/mermaid/src/diagrams/class/parser/classDiagram.jison"; 55 | 56 | describe("all @openzeppelin/contracts are valid", function () { 57 | let parser; 58 | 59 | before("load parser", async function () { 60 | const { data } = await axios.get(MERMAID_9_3_0_GRAMMAR); 61 | parser = getParserFrom(data); 62 | }); 63 | 64 | for (const { name, content } of getContracts()) { 65 | const output = getOutput(name, { content }); 66 | 67 | for (const [, { ast, id }] of Object.entries(output.sources)) { 68 | for (const typeDef of findAll(["ContractDefinition"], ast)) { 69 | it(`creates Class Diagram for ${name}`, async function () { 70 | const classDiagram = new Class( 71 | { 72 | sources: { 73 | contract: { 74 | ast, 75 | id, 76 | }, 77 | }, 78 | }, 79 | "ContractDefinition", 80 | typeDef.id 81 | ); 82 | parser.parse(classDiagram.processed); 83 | 84 | expect(true).to.be.true; // It just needs to not throw 85 | }); 86 | } 87 | } 88 | } 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeType, NodeTypeMap } from "solidity-ast/node"; 2 | 3 | export type ProcessOptions = { 4 | parent: Node; 5 | }; 6 | 7 | export type ProcessorKey = `process${T}`; 8 | 9 | export type Process = ( 10 | node: NodeTypeMap[K], 11 | options: ProcessOptions 12 | ) => void; 13 | 14 | export type ASTProcessor = { 15 | [Key in NodeType as ProcessorKey]: Process; 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "moduleResolution": "node", 5 | "target": "es2019", 6 | "downlevelIteration": true, 7 | "strict": true, 8 | "module": "commonjs", 9 | "esModuleInterop": true, 10 | "outDir": "dist" 11 | }, 12 | "ts-node": { 13 | "transpileOnly": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "noImplicitAny": false, 6 | "rootDir": "src", 7 | "baseUrl": "src", 8 | "paths": { 9 | "@*": ["./*"] 10 | }, 11 | "outDir": "./dist", 12 | "declaration": true 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "ts-node": { 18 | "require": ["tsconfig-paths/register"] 19 | } 20 | } 21 | --------------------------------------------------------------------------------