├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
--------------------------------------------------------------------------------