├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── contracts │ ├── Bar.sol │ ├── Foo.sol │ ├── IExampleContract.sol │ ├── excluded │ │ └── Nope.sol │ └── subfolder │ │ └── AnotherContract.sol ├── docs │ ├── Bar.json │ ├── Bar.md │ ├── Foo.json │ ├── Foo.md │ ├── IBar.json │ ├── IBar.md │ ├── IExampleContract.json │ ├── IExampleContract.md │ ├── IFoo.json │ ├── IFoo.md │ └── subfolder │ │ ├── AnotherContract.json │ │ └── AnotherContract.md └── templates │ └── docusaurus.sqrl ├── hardhat.config.ts ├── package.json ├── src ├── abiDecoder.ts ├── dodocTypes.ts ├── index.ts ├── template.sqrl └── type-extensions.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | 'airbnb-base', 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaVersion: 12, 14 | sourceType: 'module', 15 | }, 16 | plugins: [ 17 | '@typescript-eslint', 18 | ], 19 | rules: { 20 | 'import/extensions': ['error', 'never'], 21 | }, 22 | settings: { 23 | 'import/resolver': { 24 | node: { 25 | paths: ['./src'], 26 | extensions: ['.ts'], 27 | }, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cache 2 | /typechain 3 | /artifacts 4 | /flatten 5 | .vscode 6 | /dist 7 | .npmrc 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # next.js build output 66 | .next 67 | 68 | # environment variables 69 | .env 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Primitive 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 | # Dodoc 2 | 3 | ![version](https://img.shields.io/npm/v/@primitivefi/hardhat-dodoc) ![npm](https://img.shields.io/npm/dt/@primitivefi/hardhat-dodoc) ![license](https://img.shields.io/npm/l/@primitivefi/hardhat-dodoc) 4 | 5 | Zero-config Hardhat plugin to generate documentation for all your Solidity contracts. 6 | 7 | - 🤪 Zero-configuration required 8 | - ✅ Compatible with latest Solidity versions (>= 0.8.0) 9 | - 🔍 Supports events, errors and external / public functions 10 | - 📖 Default output to Markdown 11 | - 🔧 Extendable using custom templates 12 | 13 | Want to see a live example? Check out [Primitive documentation](https://docs.primitive.finance/)! 14 | 15 | ## 📦 Installation 16 | 17 | First thing to do is to install the plugin in your Hardhat project: 18 | 19 | ```bash 20 | # Using yarn 21 | yarn add @primitivefi/hardhat-dodoc 22 | 23 | # Or using npm 24 | npm i @primitivefi/hardhat-dodoc 25 | ``` 26 | 27 | Next step is simply to include the plugin into your `hardhat.config.js` or `hardhat.config.ts` file: 28 | 29 | ```typescript 30 | // Using JavaScript 31 | require('@primitivefi/hardhat-dodoc'); 32 | 33 | // Using ES6 or TypeScript 34 | import '@primitivefi/hardhat-dodoc'; 35 | ``` 36 | 37 | And you're done! Documentation will be automatically generated on the next compilation and saved into the `docs` folder at the root of your project. 38 | 39 | ## 📝 Usage 40 | 41 | The only requirement to use Dodoc is to comment your Solidity contracts using [NatSpec](https://docs.soliditylang.org/en/v0.8.9/natspec-format.html) format. For example, given the following function: 42 | 43 | ```solidity 44 | /// @notice Does another thing when the function is called. 45 | /// @dev More info about doing another thing when the function is called. 46 | /// @param num A random number 47 | /// @return A random variable 48 | function anotherThing(uint256 num) external pure returns (uint256); 49 | ``` 50 | 51 | Dodoc will take care of everything and will generate the following output: 52 | 53 | > ## Methods 54 | > 55 | > ### anotherThing 56 | > 57 | > ```solidity 58 | > function anotherThing(uint256 num) external pure returns (uint256) 59 | > ``` 60 | > 61 | > Does another thing when the function is called. 62 | > 63 | > *More info about doing another thing when the function is called.* 64 | > 65 | > #### Parameters 66 | > 67 | > | Name | Type | Description | 68 | > |---|---|---| 69 | > | num | uint256 | A random number | 70 | > 71 | > #### Returns 72 | > 73 | > | Name | Type | Description | 74 | > |---|---|---| 75 | > | _0 | uint256 | A random variable | 76 | 77 | Dodoc is compatible with all the NatSpec tags (except custom ones for now), and can generate documentation for events, custom errors and external / public functions. 78 | 79 | By default Dodoc generates new documentation after each compilation, but you can also trigger the task with the following command: 80 | 81 | ```bash 82 | # Using yarn 83 | yarn hardhat dodoc 84 | 85 | # Or using npx 86 | npx hardhat dodoc 87 | ``` 88 | 89 | ## 🔧 Config 90 | 91 | Dodoc comes with a default configuration but you can still tweak some parameters. To do it, change your Hardhat config file like this: 92 | 93 | ```typescript 94 | import { HardhatUserConfig } from 'hardhat/config'; 95 | import '@nomiclabs/hardhat-waffle'; 96 | import '@nomiclabs/hardhat-ethers'; 97 | import '@primitivefi/hardhat-dodoc'; 98 | 99 | const config: HardhatUserConfig = { 100 | // Your Hardhat config... 101 | dodoc: { 102 | runOnCompile: true, 103 | debugMode: true, 104 | // More options... 105 | }, 106 | }; 107 | 108 | export default config; 109 | ``` 110 | 111 | Here are all the configuration parameters that are currently available, but as said above, all of them are entirely optional: 112 | 113 | | Parameter | Description | Default value | 114 | |---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------| 115 | | `runOnCompile` | True if the plugin should generate the documentation on every compilation | `true` | 116 | | `include` | List of all the contract / interface / library / folder names to include in the documentation generation. An empty array will generate documentation for everything | `[]` | 117 | | `exclude` | List of all the contract / interface / library / folder names to exclude from the documentation generation | `[]` | 118 | | `outputDir` | Output directory of the documentation | `docs` | 119 | | `templatePath` | Path to the documentation template | `./template.sqrl` | 120 | | `debugMode` | Test mode generating additional JSON files used for debugging | `false` | 121 | | `keepFileStructure` | True if you want to preserve your contracts file structure in the output directory | `true` | 122 | | `freshOutput` | True if you want to clean the output directory before generating new documentation | `true` | 123 | 124 | ## 💅 Customize 125 | 126 | Dodoc integrates a super cool template engine called [SquirrellyJS](https://github.com/squirrellyjs/squirrelly), allowing anyone to create new output formats easily. 127 | 128 | You can checkout the [default Markdown template](https://) to get some inspiration, as well as [SquirrellyJS documentation](https://squirrelly.js.org/docs) to learn more about it. Feel free to be creative, any kind of output such as Markdown, MDX, HTML or even JSON is supported! 129 | 130 | Once you're satisfied, simply refer to your template using the `templatePath` parameter in your configuration and Dodoc will use it to output the documentation! 131 | 132 | ## ⛑ Help 133 | 134 | Feel free to open an issue if you need help or if you encounter a problem! Here are some already known problems though: 135 | - Due to the technical limitations of the Solidity compiler, the documentation of `private` and `internal` functions is not rendered. Hence, the documentation of libraries might be close to empty! 136 | - Functions that are not commented at all might not be rendered. 137 | - State variables overriding functions defined by an interface might "erase" the name of the parameters. A current workaround is to name the function parameters using the `_0`, `_1`, ... format. 138 | - Special functions such as `constructor`, `fallback` and `receive` might not be rendered. 139 | - Custom NatSpec tags `@custom:...` are not rendered (yet). 140 | -------------------------------------------------------------------------------- /examples/contracts/Bar.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.8.9; 3 | 4 | interface IBar { 5 | /// @notice Notice of T 6 | /// @dev Dev of T 7 | /// @param paramA A number 8 | /// @param paramB An address 9 | struct T { 10 | uint256 paramA; 11 | address paramB; 12 | } 13 | 14 | /// @notice Sets a T 15 | /// @dev Uses a struct 16 | /// @param t T struct FTW 17 | function set(T memory t) external; 18 | 19 | function boop(uint256 bar) external; 20 | 21 | /// @notice Emitted when transfer 22 | /// @dev Transfer some stuff 23 | /// @param foo Amount of stuff 24 | event Transfer(uint256 foo); 25 | 26 | /// @notice Thrown when doh 27 | /// @dev Bad doh error 28 | /// @param yay A bool 29 | error Doh(bool yay); 30 | } 31 | 32 | /// @title Bar contract 33 | /// @author Primitive 34 | /// @notice Manages the bar 35 | /// @dev Blablou 36 | contract Bar is IBar { 37 | /// @inheritdoc IBar 38 | function set(T memory t) external { } 39 | 40 | /// @notice Cool function bro 41 | function boop(uint256 bar) external { } 42 | 43 | /// @notice Alt cool function bro 44 | function boop(uint256 bar, uint256 bar2) external { } 45 | 46 | /// @notice Baaps the yaps 47 | /// @param bar Number of bar 48 | /// @param aar Address of aar 49 | function baap(uint256 bar, address aar) external { } 50 | } 51 | -------------------------------------------------------------------------------- /examples/contracts/Foo.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.8.9; 3 | 4 | interface IFoo { 5 | /// @notice Returns the nonce of an address 6 | /// @dev Nonces much 7 | /// @param _0 Address to inspect 8 | /// @return Current nonce of the address 9 | function nonces(address _0) external view returns (uint256); 10 | } 11 | 12 | contract Foo is IFoo { 13 | /// @inheritdoc IFoo 14 | mapping(address => uint256) public override nonces; 15 | } 16 | -------------------------------------------------------------------------------- /examples/contracts/IExampleContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.6; 3 | 4 | /// @title Interface of our ExampleContract 5 | /// @author 0xAn0n 6 | /// @notice Put a simple description of the contract here. 7 | /// @dev And then a more complicated and tech oriented description of the contract there. 8 | interface IExampleContract { 9 | /// @notice Emitted when the function doSomething is called. 10 | /// @dev More info about the event can be added here. 11 | /// @param a Address of someone 12 | /// @param b A random number 13 | event DoSomething( 14 | address indexed a, 15 | uint256 b 16 | ); 17 | 18 | /// @notice Thrown when an error happens. 19 | /// @dev More info about the error. 20 | /// @param expected Expected address 21 | /// @param actual Actual address 22 | error RandomError(address expected, address actual); 23 | 24 | /// @notice Does something when this function is called. 25 | /// @dev More info about the doSomething, and this even works 26 | /// when the explanation is on two lines. 27 | /// @param a Address to do something 28 | /// @param b Number to do something 29 | /// @return foo First return variable 30 | /// @return bar Second return variable 31 | function doSomething(address a, uint256 b) external returns ( 32 | uint256 foo, 33 | uint256 bar 34 | ); 35 | 36 | /// @notice A bad documented payable function. 37 | function pay() external payable; 38 | 39 | /// @notice Does another thing when the function is called. 40 | /// @dev More info about doing another thing when the function is called. 41 | /// @param num A random number 42 | /// @return A random variable 43 | function anotherThing(uint256 num) external pure returns (uint256); 44 | 45 | /// @notice Poorly documented function starting with weird spaces. 46 | function boop() external view returns (address); 47 | } 48 | -------------------------------------------------------------------------------- /examples/contracts/excluded/Nope.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.8.9; 3 | 4 | /// @title Excluded contract from the doc generation! 5 | contract AnotherContract { 6 | } 7 | -------------------------------------------------------------------------------- /examples/contracts/subfolder/AnotherContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.8.9; 3 | 4 | contract AnotherContract { 5 | /// @notice A strange function 6 | /// @dev Someone wrote this weird function... 7 | /// @param much How much 8 | /// @param mop No idea what this is 9 | function tip(uint256 much, address mop) external { 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/docs/Bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "methods": { 3 | "baap(uint256,address)": { 4 | "stateMutability": "nonpayable", 5 | "code": "function baap(uint256 bar, address aar) external nonpayable", 6 | "inputs": { 7 | "bar": { 8 | "type": "uint256", 9 | "description": "Number of bar" 10 | }, 11 | "aar": { 12 | "type": "address", 13 | "description": "Address of aar" 14 | } 15 | }, 16 | "outputs": {}, 17 | "notice": "Baaps the yaps" 18 | }, 19 | "boop(uint256)": { 20 | "stateMutability": "nonpayable", 21 | "code": "function boop(uint256 bar) external nonpayable", 22 | "inputs": { 23 | "bar": { 24 | "type": "uint256" 25 | } 26 | }, 27 | "outputs": {}, 28 | "notice": "Cool function bro" 29 | }, 30 | "boop(uint256,uint256)": { 31 | "stateMutability": "nonpayable", 32 | "code": "function boop(uint256 bar, uint256 bar2) external nonpayable", 33 | "inputs": { 34 | "bar": { 35 | "type": "uint256" 36 | }, 37 | "bar2": { 38 | "type": "uint256" 39 | } 40 | }, 41 | "outputs": {}, 42 | "notice": "Alt cool function bro" 43 | }, 44 | "set(tuple)": { 45 | "stateMutability": "nonpayable", 46 | "code": "function set(IBar.T t) external nonpayable", 47 | "inputs": { 48 | "t": { 49 | "type": "IBar.T" 50 | } 51 | }, 52 | "outputs": {} 53 | } 54 | }, 55 | "events": { 56 | "Transfer": { 57 | "code": "event Transfer(uint256 foo)", 58 | "inputs": { 59 | "foo": { 60 | "type": "uint256", 61 | "indexed": false 62 | } 63 | }, 64 | "notice": "Emitted when transfer" 65 | } 66 | }, 67 | "errors": { 68 | "Doh": { 69 | "code": "error Doh(bool yay)", 70 | "inputs": { 71 | "yay": { 72 | "type": "bool", 73 | "description": "A bool" 74 | } 75 | }, 76 | "notice": "Thrown when doh", 77 | "details": "Bad doh error" 78 | } 79 | }, 80 | "path": "", 81 | "title": "Bar contract", 82 | "notice": "Manages the bar", 83 | "details": "Blablou", 84 | "author": "Primitive", 85 | "name": "Bar" 86 | } -------------------------------------------------------------------------------- /examples/docs/Bar.md: -------------------------------------------------------------------------------- 1 | # Bar 2 | 3 | *Primitive* 4 | 5 | > Bar contract 6 | 7 | Manages the bar 8 | 9 | *Blablou* 10 | 11 | ## Methods 12 | 13 | ### baap 14 | 15 | ```solidity 16 | function baap(uint256 bar, address aar) external nonpayable 17 | ``` 18 | 19 | Baaps the yaps 20 | 21 | 22 | 23 | #### Parameters 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | bar | uint256 | Number of bar | 28 | | aar | address | Address of aar | 29 | 30 | ### boop 31 | 32 | ```solidity 33 | function boop(uint256 bar) external nonpayable 34 | ``` 35 | 36 | Cool function bro 37 | 38 | 39 | 40 | #### Parameters 41 | 42 | | Name | Type | Description | 43 | |---|---|---| 44 | | bar | uint256 | undefined | 45 | 46 | ### boop 47 | 48 | ```solidity 49 | function boop(uint256 bar, uint256 bar2) external nonpayable 50 | ``` 51 | 52 | Alt cool function bro 53 | 54 | 55 | 56 | #### Parameters 57 | 58 | | Name | Type | Description | 59 | |---|---|---| 60 | | bar | uint256 | undefined | 61 | | bar2 | uint256 | undefined | 62 | 63 | ### set 64 | 65 | ```solidity 66 | function set(IBar.T t) external nonpayable 67 | ``` 68 | 69 | 70 | 71 | 72 | 73 | #### Parameters 74 | 75 | | Name | Type | Description | 76 | |---|---|---| 77 | | t | IBar.T | undefined | 78 | 79 | 80 | 81 | ## Events 82 | 83 | ### Transfer 84 | 85 | ```solidity 86 | event Transfer(uint256 foo) 87 | ``` 88 | 89 | Emitted when transfer 90 | 91 | 92 | 93 | #### Parameters 94 | 95 | | Name | Type | Description | 96 | |---|---|---| 97 | | foo | uint256 | undefined | 98 | 99 | 100 | 101 | ## Errors 102 | 103 | ### Doh 104 | 105 | ```solidity 106 | error Doh(bool yay) 107 | ``` 108 | 109 | Thrown when doh 110 | 111 | *Bad doh error* 112 | 113 | #### Parameters 114 | 115 | | Name | Type | Description | 116 | |---|---|---| 117 | | yay | bool | A bool | 118 | 119 | 120 | -------------------------------------------------------------------------------- /examples/docs/Foo.json: -------------------------------------------------------------------------------- 1 | { 2 | "methods": { 3 | "nonces(address)": { 4 | "stateMutability": "view", 5 | "code": "function nonces(address) external view returns (uint256)", 6 | "inputs": { 7 | "_0": { 8 | "type": "address", 9 | "description": "Address to inspect" 10 | } 11 | }, 12 | "outputs": { 13 | "_0": { 14 | "type": "uint256", 15 | "description": "Current nonce of the address" 16 | } 17 | }, 18 | "notice": "Returns the nonce of an address", 19 | "details": "Nonces much" 20 | } 21 | }, 22 | "events": {}, 23 | "errors": {}, 24 | "path": "", 25 | "name": "Foo" 26 | } -------------------------------------------------------------------------------- /examples/docs/Foo.md: -------------------------------------------------------------------------------- 1 | # Foo 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ## Methods 12 | 13 | ### nonces 14 | 15 | ```solidity 16 | function nonces(address) external view returns (uint256) 17 | ``` 18 | 19 | Returns the nonce of an address 20 | 21 | *Nonces much* 22 | 23 | #### Parameters 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | _0 | address | Address to inspect | 28 | 29 | #### Returns 30 | 31 | | Name | Type | Description | 32 | |---|---|---| 33 | | _0 | uint256 | Current nonce of the address | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/docs/IBar.json: -------------------------------------------------------------------------------- 1 | { 2 | "methods": { 3 | "boop(uint256)": { 4 | "stateMutability": "nonpayable", 5 | "code": "function boop(uint256 bar) external nonpayable", 6 | "inputs": { 7 | "bar": { 8 | "type": "uint256" 9 | } 10 | }, 11 | "outputs": {} 12 | }, 13 | "set(tuple)": { 14 | "stateMutability": "nonpayable", 15 | "code": "function set(IBar.T t) external nonpayable", 16 | "inputs": { 17 | "t": { 18 | "type": "IBar.T" 19 | } 20 | }, 21 | "outputs": {} 22 | } 23 | }, 24 | "events": { 25 | "Transfer": { 26 | "code": "event Transfer(uint256 foo)", 27 | "inputs": { 28 | "foo": { 29 | "type": "uint256", 30 | "indexed": false, 31 | "description": "Amount of stuff" 32 | } 33 | }, 34 | "notice": "Emitted when transfer", 35 | "details": "Transfer some stuff" 36 | } 37 | }, 38 | "errors": { 39 | "Doh": { 40 | "code": "error Doh(bool yay)", 41 | "inputs": { 42 | "yay": { 43 | "type": "bool", 44 | "description": "A bool" 45 | } 46 | }, 47 | "notice": "Thrown when doh", 48 | "details": "Bad doh error" 49 | } 50 | }, 51 | "path": "", 52 | "name": "IBar" 53 | } -------------------------------------------------------------------------------- /examples/docs/IBar.md: -------------------------------------------------------------------------------- 1 | # IBar 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ## Methods 12 | 13 | ### boop 14 | 15 | ```solidity 16 | function boop(uint256 bar) external nonpayable 17 | ``` 18 | 19 | 20 | 21 | 22 | 23 | #### Parameters 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | bar | uint256 | undefined | 28 | 29 | ### set 30 | 31 | ```solidity 32 | function set(IBar.T t) external nonpayable 33 | ``` 34 | 35 | 36 | 37 | 38 | 39 | #### Parameters 40 | 41 | | Name | Type | Description | 42 | |---|---|---| 43 | | t | IBar.T | undefined | 44 | 45 | 46 | 47 | ## Events 48 | 49 | ### Transfer 50 | 51 | ```solidity 52 | event Transfer(uint256 foo) 53 | ``` 54 | 55 | Emitted when transfer 56 | 57 | *Transfer some stuff* 58 | 59 | #### Parameters 60 | 61 | | Name | Type | Description | 62 | |---|---|---| 63 | | foo | uint256 | Amount of stuff | 64 | 65 | 66 | 67 | ## Errors 68 | 69 | ### Doh 70 | 71 | ```solidity 72 | error Doh(bool yay) 73 | ``` 74 | 75 | Thrown when doh 76 | 77 | *Bad doh error* 78 | 79 | #### Parameters 80 | 81 | | Name | Type | Description | 82 | |---|---|---| 83 | | yay | bool | A bool | 84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/docs/IExampleContract.json: -------------------------------------------------------------------------------- 1 | { 2 | "methods": { 3 | "anotherThing(uint256)": { 4 | "stateMutability": "pure", 5 | "code": "function anotherThing(uint256 num) external pure returns (uint256)", 6 | "inputs": { 7 | "num": { 8 | "type": "uint256", 9 | "description": "A random number" 10 | } 11 | }, 12 | "outputs": { 13 | "_0": { 14 | "type": "uint256", 15 | "description": "A random variable" 16 | } 17 | }, 18 | "notice": "Does another thing when the function is called.", 19 | "details": "More info about doing another thing when the function is called." 20 | }, 21 | "boop()": { 22 | "stateMutability": "view", 23 | "code": "function boop() external view returns (address)", 24 | "inputs": {}, 25 | "outputs": { 26 | "_0": { 27 | "type": "address" 28 | } 29 | }, 30 | "notice": "Poorly documented function starting with weird spaces." 31 | }, 32 | "doSomething(address,uint256)": { 33 | "stateMutability": "nonpayable", 34 | "code": "function doSomething(address a, uint256 b) external nonpayable returns (uint256 foo, uint256 bar)", 35 | "inputs": { 36 | "a": { 37 | "type": "address", 38 | "description": "Address to do something" 39 | }, 40 | "b": { 41 | "type": "uint256", 42 | "description": "Number to do something" 43 | } 44 | }, 45 | "outputs": { 46 | "foo": { 47 | "type": "uint256", 48 | "description": "First return variable" 49 | }, 50 | "bar": { 51 | "type": "uint256", 52 | "description": "Second return variable" 53 | } 54 | }, 55 | "notice": "Does something when this function is called.", 56 | "details": "More info about the doSomething, and this even works when the explanation is on two lines." 57 | }, 58 | "pay()": { 59 | "stateMutability": "payable", 60 | "code": "function pay() external payable", 61 | "inputs": {}, 62 | "outputs": {}, 63 | "notice": "A bad documented payable function." 64 | } 65 | }, 66 | "events": { 67 | "DoSomething": { 68 | "code": "event DoSomething(address indexed a, uint256 b)", 69 | "inputs": { 70 | "a": { 71 | "type": "address", 72 | "indexed": true, 73 | "description": "Address of someone" 74 | }, 75 | "b": { 76 | "type": "uint256", 77 | "indexed": false, 78 | "description": "A random number" 79 | } 80 | }, 81 | "notice": "Emitted when the function doSomething is called.", 82 | "details": "More info about the event can be added here." 83 | } 84 | }, 85 | "errors": { 86 | "RandomError": { 87 | "code": "error RandomError(address expected, address actual)", 88 | "inputs": { 89 | "expected": { 90 | "type": "address", 91 | "description": "Expected address" 92 | }, 93 | "actual": { 94 | "type": "address", 95 | "description": "Actual address" 96 | } 97 | }, 98 | "notice": "Thrown when an error happens.", 99 | "details": "More info about the error." 100 | } 101 | }, 102 | "path": "", 103 | "title": "Interface of our ExampleContract", 104 | "notice": "Put a simple description of the contract here.", 105 | "details": "And then a more complicated and tech oriented description of the contract there.", 106 | "author": "0xAn0n", 107 | "name": "IExampleContract" 108 | } -------------------------------------------------------------------------------- /examples/docs/IExampleContract.md: -------------------------------------------------------------------------------- 1 | # IExampleContract 2 | 3 | *0xAn0n* 4 | 5 | > Interface of our ExampleContract 6 | 7 | Put a simple description of the contract here. 8 | 9 | *And then a more complicated and tech oriented description of the contract there.* 10 | 11 | ## Methods 12 | 13 | ### anotherThing 14 | 15 | ```solidity 16 | function anotherThing(uint256 num) external pure returns (uint256) 17 | ``` 18 | 19 | Does another thing when the function is called. 20 | 21 | *More info about doing another thing when the function is called.* 22 | 23 | #### Parameters 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | num | uint256 | A random number | 28 | 29 | #### Returns 30 | 31 | | Name | Type | Description | 32 | |---|---|---| 33 | | _0 | uint256 | A random variable | 34 | 35 | ### boop 36 | 37 | ```solidity 38 | function boop() external view returns (address) 39 | ``` 40 | 41 | Poorly documented function starting with weird spaces. 42 | 43 | 44 | 45 | 46 | #### Returns 47 | 48 | | Name | Type | Description | 49 | |---|---|---| 50 | | _0 | address | undefined | 51 | 52 | ### doSomething 53 | 54 | ```solidity 55 | function doSomething(address a, uint256 b) external nonpayable returns (uint256 foo, uint256 bar) 56 | ``` 57 | 58 | Does something when this function is called. 59 | 60 | *More info about the doSomething, and this even works when the explanation is on two lines.* 61 | 62 | #### Parameters 63 | 64 | | Name | Type | Description | 65 | |---|---|---| 66 | | a | address | Address to do something | 67 | | b | uint256 | Number to do something | 68 | 69 | #### Returns 70 | 71 | | Name | Type | Description | 72 | |---|---|---| 73 | | foo | uint256 | First return variable | 74 | | bar | uint256 | Second return variable | 75 | 76 | ### pay 77 | 78 | ```solidity 79 | function pay() external payable 80 | ``` 81 | 82 | A bad documented payable function. 83 | 84 | 85 | 86 | 87 | 88 | 89 | ## Events 90 | 91 | ### DoSomething 92 | 93 | ```solidity 94 | event DoSomething(address indexed a, uint256 b) 95 | ``` 96 | 97 | Emitted when the function doSomething is called. 98 | 99 | *More info about the event can be added here.* 100 | 101 | #### Parameters 102 | 103 | | Name | Type | Description | 104 | |---|---|---| 105 | | a `indexed` | address | Address of someone | 106 | | b | uint256 | A random number | 107 | 108 | 109 | 110 | ## Errors 111 | 112 | ### RandomError 113 | 114 | ```solidity 115 | error RandomError(address expected, address actual) 116 | ``` 117 | 118 | Thrown when an error happens. 119 | 120 | *More info about the error.* 121 | 122 | #### Parameters 123 | 124 | | Name | Type | Description | 125 | |---|---|---| 126 | | expected | address | Expected address | 127 | | actual | address | Actual address | 128 | 129 | 130 | -------------------------------------------------------------------------------- /examples/docs/IFoo.json: -------------------------------------------------------------------------------- 1 | { 2 | "methods": { 3 | "nonces(address)": { 4 | "stateMutability": "view", 5 | "code": "function nonces(address _0) external view returns (uint256)", 6 | "inputs": { 7 | "_0": { 8 | "type": "address", 9 | "description": "Address to inspect" 10 | } 11 | }, 12 | "outputs": { 13 | "_0": { 14 | "type": "uint256", 15 | "description": "Current nonce of the address" 16 | } 17 | }, 18 | "notice": "Returns the nonce of an address", 19 | "details": "Nonces much" 20 | } 21 | }, 22 | "events": {}, 23 | "errors": {}, 24 | "path": "", 25 | "name": "IFoo" 26 | } -------------------------------------------------------------------------------- /examples/docs/IFoo.md: -------------------------------------------------------------------------------- 1 | # IFoo 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ## Methods 12 | 13 | ### nonces 14 | 15 | ```solidity 16 | function nonces(address _0) external view returns (uint256) 17 | ``` 18 | 19 | Returns the nonce of an address 20 | 21 | *Nonces much* 22 | 23 | #### Parameters 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | _0 | address | Address to inspect | 28 | 29 | #### Returns 30 | 31 | | Name | Type | Description | 32 | |---|---|---| 33 | | _0 | uint256 | Current nonce of the address | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/docs/subfolder/AnotherContract.json: -------------------------------------------------------------------------------- 1 | { 2 | "methods": { 3 | "tip(uint256,address)": { 4 | "stateMutability": "nonpayable", 5 | "code": "function tip(uint256 much, address mop) external nonpayable", 6 | "inputs": { 7 | "much": { 8 | "type": "uint256", 9 | "description": "How much" 10 | }, 11 | "mop": { 12 | "type": "address", 13 | "description": "No idea what this is" 14 | } 15 | }, 16 | "outputs": {}, 17 | "notice": "A strange function", 18 | "details": "Someone wrote this weird function..." 19 | } 20 | }, 21 | "events": {}, 22 | "errors": {}, 23 | "path": "subfolder", 24 | "name": "AnotherContract" 25 | } -------------------------------------------------------------------------------- /examples/docs/subfolder/AnotherContract.md: -------------------------------------------------------------------------------- 1 | # AnotherContract 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ## Methods 12 | 13 | ### tip 14 | 15 | ```solidity 16 | function tip(uint256 much, address mop) external nonpayable 17 | ``` 18 | 19 | A strange function 20 | 21 | *Someone wrote this weird function...* 22 | 23 | #### Parameters 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | much | uint256 | How much | 28 | | mop | address | No idea what this is | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/templates/docusaurus.sqrl: -------------------------------------------------------------------------------- 1 | --- 2 | description: {{@if (it.title)}}{{it.title}}{{/if}} 3 | 4 | --- 5 | 6 | {{@if (it.name)}}# {{it.name}}.sol{{/if}} 7 | 8 | 9 | {{@if (it.notice)}}{{it.notice}}{{/if}} 10 | 11 | 12 | {{@if (it.details)}} 13 | :::note Details 14 | {{it.details}} 15 | 16 | ::: 17 | {{/if}} 18 | 19 | 20 | {{@if (Object.keys(it.methods).length > 0)}} 21 | ## Methods 22 | 23 | {{@foreach(it.methods) => key, val}} 24 | ### {{key.split('(')[0]}} 25 | 26 | 27 | {{@if (val.notice)}}{{val.notice}}{{/if}} 28 | 29 | 30 | ```solidity title="Solidity" 31 | {{val.code}} 32 | 33 | ``` 34 | 35 | 36 | {{@if (val.details)}} 37 | :::note Details 38 | {{val.details}} 39 | 40 | ::: 41 | {{/if}} 42 | 43 | 44 | {{@if (Object.keys(val.inputs).length > 0)}} 45 | #### Parameters 46 | 47 | | Name | Type | Description | 48 | |---|---|---| 49 | {{@foreach(val.inputs) => key, val}} 50 | | {{key}} | {{val.type}} | {{val.description}} | 51 | {{/foreach}} 52 | {{/if}} 53 | 54 | {{@if (Object.keys(val.outputs).length > 0)}} 55 | #### Returns 56 | 57 | | Name | Type | Description | 58 | |---|---|---| 59 | {{@foreach(val.outputs) => key, val}} 60 | | {{key}} | {{val.type}} | {{val.description}} | 61 | {{/foreach}} 62 | 63 | {{/if}} 64 | {{/foreach}} 65 | 66 | {{/if}} 67 | 68 | {{@if (Object.keys(it.events).length > 0)}} 69 | ## Events 70 | 71 | {{@foreach(it.events) => key, val}} 72 | ### {{key}} 73 | 74 | 75 | {{@if (val.notice)}}{{val.notice}}{{/if}} 76 | 77 | 78 | ```solidity title="Solidity" 79 | {{val.code}} 80 | 81 | ``` 82 | 83 | 84 | {{@if (val.details)}} 85 | :::note Details 86 | {{val.details}} 87 | 88 | ::: 89 | {{/if}} 90 | 91 | 92 | {{@if (Object.keys(val.inputs).length > 0)}} 93 | #### Parameters 94 | 95 | | Name | Type | Description | 96 | |---|---|---| 97 | {{@foreach(val.inputs) => key, val}} 98 | | {{key}} {{@if (val.indexed)}}`indexed`{{/if}} | {{val.type}} | {{val.description}} | 99 | {{/foreach}} 100 | {{/if}} 101 | 102 | {{/foreach}} 103 | 104 | {{/if}} 105 | 106 | {{@if (Object.keys(it.errors).length > 0)}} 107 | ## Errors 108 | 109 | {{@foreach(it.errors) => key, val}} 110 | ### {{key}} 111 | 112 | 113 | {{@if (val.notice)}}{{val.notice}}{{/if}} 114 | 115 | 116 | ```solidity title="Solidity" 117 | {{val.code}} 118 | 119 | ``` 120 | 121 | 122 | {{@if (val.details)}} 123 | :::note Details 124 | {{val.details}} 125 | 126 | ::: 127 | {{/if}} 128 | 129 | 130 | {{@if (Object.keys(val.inputs).length > 0)}} 131 | #### Parameters 132 | 133 | | Name | Type | Description | 134 | |---|---|---| 135 | {{@foreach(val.inputs) => key, val}} 136 | | {{key}} | {{val.type}} | {{val.description}} | 137 | {{/foreach}} 138 | {{/if}} 139 | 140 | {{/foreach}} 141 | 142 | {{/if}} 143 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from 'hardhat/config'; 2 | import '@nomiclabs/hardhat-waffle'; 3 | import '@nomiclabs/hardhat-ethers'; 4 | import './src'; 5 | 6 | const config: HardhatUserConfig = { 7 | solidity: '0.8.9', 8 | paths: { 9 | sources: './examples/contracts/', 10 | }, 11 | dodoc: { 12 | debugMode: true, 13 | outputDir: './examples/docs', 14 | exclude: ['excluded'], 15 | runOnCompile: true, 16 | }, 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@primitivefi/hardhat-dodoc", 3 | "version": "0.2.3", 4 | "description": "Zero-config Hardhat plugin to generate documentation for all your Solidity contracts", 5 | "repository": "github:primitivefinance/primitive-dodoc", 6 | "author": "Primitive", 7 | "license": "MIT", 8 | "main": "dist/src/index.js", 9 | "types": "dist/src/index.d.ts", 10 | "keywords": [ 11 | "ethereum", 12 | "smart-contracts", 13 | "solidity", 14 | "hardhat", 15 | "hardhat-plugin", 16 | "natspec" 17 | ], 18 | "scripts": { 19 | "build": "rm -rf dist && tsc && cp ./src/template.sqrl ./dist/src", 20 | "compile": "hardhat compile", 21 | "prepublishOnly": "npm run build" 22 | }, 23 | "files": [ 24 | "LICENSE", 25 | "README.md", 26 | "dist/src/" 27 | ], 28 | "peerDependencies": { 29 | "hardhat": "^2.6.4", 30 | "squirrelly": "^8.0.8" 31 | }, 32 | "devDependencies": { 33 | "@typescript-eslint/eslint-plugin": "^4.21.0", 34 | "@typescript-eslint/parser": "^4.21.0", 35 | "eslint": "^7.24.0", 36 | "eslint-config-airbnb-base": "^14.2.1", 37 | "eslint-plugin-import": "^2.22.1", 38 | "@nomiclabs/hardhat-ethers": "^2.0.2", 39 | "@nomiclabs/hardhat-waffle": "^2.0.1", 40 | "@types/chai": "^4.2.22", 41 | "@types/mocha": "^9.0.0", 42 | "@types/node": "^16.10.1", 43 | "chai": "^4.3.4", 44 | "ethereum-waffle": "^3.4.0", 45 | "ethers": "^5.4.7", 46 | "hardhat": "^2.6.4", 47 | "ts-node": "^10.2.1", 48 | "typescript": "^4.2.4" 49 | }, 50 | "dependencies": { 51 | "squirrelly": "^8.0.8" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/abiDecoder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbiElement, Doc, Method, Error, Event, 3 | } from './dodocTypes'; 4 | 5 | export function getCodeFromAbi(element: AbiElement): string { 6 | let code; 7 | 8 | if (element.type === 'constructor') { 9 | code = 'constructor('; 10 | } else { 11 | code = `${element.type} ${element.name}(`; 12 | } 13 | 14 | if (element.inputs) { 15 | for (let i = 0; i < element.inputs.length; i += 1) { 16 | if (element.inputs[i].internalType.includes('struct')) { 17 | code += element.inputs[i].internalType.substr(7); 18 | } else { 19 | code += element.inputs[i].internalType; 20 | } 21 | 22 | if (element.inputs[i].name) code += ' '; 23 | 24 | if (element.type === 'event' && element.inputs[i].indexed) { 25 | code += 'indexed '; 26 | } 27 | 28 | code += element.inputs[i].name; 29 | 30 | if (i + 1 < element.inputs.length) code += ', '; 31 | } 32 | } 33 | 34 | code += ')'; 35 | 36 | if (element.type === 'function') { 37 | code += ` external ${element.stateMutability}`; 38 | } 39 | 40 | if (element.outputs && element.outputs.length > 0) { 41 | code += ' returns ('; 42 | 43 | for (let i = 0; i < element.outputs.length; i += 1) { 44 | code += element.outputs[i].internalType; 45 | 46 | if (element.outputs[i].name) code += ' '; 47 | 48 | code += element.outputs[i].name; 49 | 50 | if (i + 1 < element.outputs.length) code += ', '; 51 | } 52 | 53 | code += ')'; 54 | } 55 | 56 | return code; 57 | } 58 | 59 | export function decodeAbi(abi: AbiElement[]): Doc { 60 | const doc: Doc = { 61 | methods: {}, 62 | events: {}, 63 | errors: {}, 64 | }; 65 | 66 | for (let i = 0; i < abi.length; i += 1) { 67 | const el = abi[i]; 68 | 69 | /* 70 | if (el.type === 'constructor') { 71 | const func: Method = { 72 | stateMutability: el.stateMutability, 73 | code: getCodeFromAbi(el), 74 | inputs: {}, 75 | outputs: {}, 76 | }; 77 | 78 | el.inputs.forEach((input, index) => { 79 | const name = input.name.length !== 0 ? input.name : `_${index}`; 80 | 81 | func.inputs[name] = { 82 | type: input.internalType.includes('struct') ? input.internalType.substr(7) : input.internalType, 83 | }; 84 | }); 85 | 86 | doc.methods['constructor'] = func; 87 | } 88 | */ 89 | 90 | if (el.type === 'function') { 91 | const func: Method = { 92 | stateMutability: el.stateMutability, 93 | code: getCodeFromAbi(el), 94 | inputs: {}, 95 | outputs: {}, 96 | }; 97 | 98 | el.inputs.forEach((input, index) => { 99 | const name = input.name.length !== 0 ? input.name : `_${index}`; 100 | 101 | func.inputs[name] = { 102 | type: input.internalType.includes('struct') ? input.internalType.substr(7) : input.internalType, 103 | }; 104 | }); 105 | 106 | el.outputs.forEach((output, index) => { 107 | const name = output.name.length !== 0 ? output.name : `_${index}`; 108 | 109 | func.outputs[name] = { 110 | type: output.internalType.includes('struct') ? output.internalType.substr(7) : output.internalType, 111 | }; 112 | }); 113 | 114 | doc.methods[`${el.name}(${ 115 | el.inputs ? el.inputs.map((inp) => inp.type).join(',') : '' 116 | })`] = func; 117 | } 118 | 119 | if (el.type === 'event') { 120 | const event: Event = { 121 | code: getCodeFromAbi(el), 122 | inputs: {}, 123 | }; 124 | 125 | el.inputs.forEach((input, index) => { 126 | const name = input.name.length !== 0 ? input.name : `_${index}`; 127 | 128 | event.inputs[name] = { 129 | type: input.internalType.includes('struct') ? input.internalType.substr(7) : input.internalType, 130 | indexed: input.indexed, 131 | }; 132 | }); 133 | 134 | doc.events[el.name] = event; 135 | } 136 | 137 | if (el.type === 'error') { 138 | const error: Error = { 139 | code: getCodeFromAbi(el), 140 | inputs: {}, 141 | }; 142 | 143 | el.inputs.forEach((input, index) => { 144 | const name = input.name.length !== 0 ? input.name : `_${index}`; 145 | 146 | error.inputs[name] = { 147 | type: input.internalType.includes('struct') ? input.internalType.substr(7) : input.internalType, 148 | }; 149 | }); 150 | 151 | doc.errors[el.name] = error; 152 | } 153 | } 154 | 155 | return doc; 156 | } 157 | -------------------------------------------------------------------------------- /src/dodocTypes.ts: -------------------------------------------------------------------------------- 1 | import { CompilerOutputContract } from 'hardhat/types'; 2 | 3 | export interface ErrorDevdocArrayItem { 4 | details?: string; 5 | params?: { 6 | [key: string]: string; 7 | } 8 | } 9 | 10 | declare interface ErrorUserdocArrayItem { 11 | notice?: string; 12 | } 13 | 14 | export interface CompilerOutputContractWithDocumentation extends CompilerOutputContract { 15 | devdoc?: { 16 | author?: string; 17 | details?: string; 18 | title?: string; 19 | errors?: { 20 | [key: string]: ErrorDevdocArrayItem[] 21 | } 22 | events?: { 23 | [key: string]: { 24 | details: string; 25 | params: { 26 | [key: string]: string; 27 | } 28 | } 29 | } 30 | methods?: { 31 | [key: string]: { 32 | details?: string; 33 | params: { 34 | [key: string]: string; 35 | }, 36 | returns: { 37 | [key: string]: string; 38 | } 39 | } 40 | }, 41 | returns?: { 42 | [key: string]: { 43 | details?: string; 44 | params: { 45 | [key: string]: string; 46 | } 47 | } 48 | } 49 | stateVariables?: { 50 | [key: string]: { 51 | details?: string; 52 | params: { 53 | [key: string]: string; 54 | } 55 | returns: { 56 | [key: string]: string; 57 | } 58 | } 59 | } 60 | }, 61 | userdoc?: { 62 | errors?: { 63 | [key: string]: ErrorUserdocArrayItem[] 64 | }, 65 | events?: { 66 | [key: string]: { 67 | notice: string; 68 | }, 69 | }, 70 | methods?: { 71 | [key: string]: { 72 | notice: string; 73 | }, 74 | }, 75 | notice?: string; 76 | } 77 | } 78 | 79 | export interface AbiElementPut { 80 | internalType: string; 81 | name: string; 82 | type: string; 83 | indexed?: boolean; 84 | } 85 | 86 | export interface AbiElement { 87 | type: 'constructor' | 'function' | 'event' | 'error'; 88 | name: string; 89 | stateMutability?: string; 90 | inputs: AbiElementPut[]; 91 | outputs: AbiElementPut[]; 92 | } 93 | 94 | export interface Param { 95 | type?: string; 96 | description?: string; 97 | indexed?: boolean; 98 | } 99 | 100 | export interface Method { 101 | code?: string; 102 | stateMutability?: string; 103 | notice?: string; 104 | details?: string; 105 | inputs: { 106 | [key: string]: Param; 107 | } 108 | outputs: { 109 | [key: string]: Param; 110 | } 111 | } 112 | 113 | export interface Event { 114 | code?: string; 115 | notice?: string; 116 | details?: string; 117 | inputs: { 118 | [key: string]: Param; 119 | } 120 | } 121 | 122 | export interface Error { 123 | code?: string; 124 | description?: string; 125 | details?: string; 126 | inputs: { 127 | [key: string]: Param; 128 | } 129 | } 130 | 131 | export interface Doc { 132 | path?: string; 133 | name?: string; 134 | title?: string; 135 | notice?: string; 136 | details?: string; 137 | author?: string; 138 | methods: { 139 | [key: string]: Method; 140 | } 141 | events: { 142 | [key: string]: Event; 143 | } 144 | errors: { 145 | [key: string]: Event; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable guard-for-in, max-len, no-await-in-loop, no-restricted-syntax */ 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { extendConfig, task } from 'hardhat/config'; 5 | import { TASK_COMPILE } from 'hardhat/builtin-tasks/task-names'; 6 | import { 7 | HardhatConfig, 8 | HardhatRuntimeEnvironment, 9 | HardhatUserConfig, 10 | } from 'hardhat/types'; 11 | import * as Sqrl from 'squirrelly'; 12 | 13 | import { CompilerOutputContractWithDocumentation, Doc } from './dodocTypes'; 14 | import { decodeAbi } from './abiDecoder'; 15 | import './type-extensions'; 16 | 17 | extendConfig((config: HardhatConfig, userConfig: Readonly) => { 18 | // eslint-disable-next-line no-param-reassign 19 | config.dodoc = { 20 | include: userConfig.dodoc?.include || [], 21 | exclude: userConfig.dodoc?.exclude || [], 22 | runOnCompile: userConfig.dodoc?.runOnCompile !== undefined ? userConfig.dodoc?.runOnCompile : true, 23 | debugMode: userConfig.dodoc?.debugMode || false, 24 | outputDir: userConfig.dodoc?.outputDir || './docs', 25 | templatePath: userConfig.dodoc?.templatePath || path.join(__dirname, './template.sqrl'), 26 | keepFileStructure: userConfig.dodoc?.keepFileStructure ?? true, 27 | freshOutput: userConfig.dodoc?.freshOutput ?? true, 28 | }; 29 | }); 30 | 31 | async function generateDocumentation( 32 | hre: HardhatRuntimeEnvironment, 33 | ): Promise { 34 | const config = hre.config.dodoc; 35 | const docs: Doc[] = []; 36 | 37 | const qualifiedNames = await hre.artifacts.getAllFullyQualifiedNames(); 38 | const filteredQualifiedNames = qualifiedNames.filter((filePath: string) => { 39 | // Checks if the documentation has to be generated for this contract 40 | const includesPath = config.include.some((str) => filePath.includes(str)); 41 | const excludesPath = config.exclude.some((str) => filePath.includes(str)); 42 | return (config.include.length === 0 || includesPath) && !excludesPath; 43 | }); 44 | 45 | // Loops through all the qualified names to get all the compiled contracts 46 | const sourcesPath = hre.config.paths.sources.substr(process.cwd().length + 1); // trick to get relative path to files, and trim the first / 47 | 48 | for (const qualifiedName of filteredQualifiedNames) { 49 | const [source, name] = qualifiedName.split(':'); 50 | 51 | const buildInfo = await hre.artifacts.getBuildInfo(qualifiedName); 52 | const info = buildInfo?.output.contracts[source][name] as CompilerOutputContractWithDocumentation; 53 | 54 | if (config.debugMode) { 55 | console.log('ABI:\n'); 56 | console.log(JSON.stringify(info.abi, null, 4)); 57 | console.log('\n\n'); 58 | console.log('User doc:\n'); 59 | console.log(JSON.stringify(info.userdoc, null, 4)); 60 | console.log('\n\n'); 61 | console.log('Dev doc:\n'); 62 | console.log(JSON.stringify(info.devdoc, null, 4)); 63 | } 64 | 65 | const doc = { ...decodeAbi(info.abi), path: source.substr(sourcesPath.length).split('/').slice(0, -1).join('/') }; // get file path without filename 66 | 67 | // Fetches info from userdoc 68 | for (const errorSig in info.userdoc?.errors) { 69 | const [errorName] = errorSig.split('('); 70 | const error = info.userdoc?.errors[errorSig][0]; 71 | 72 | if (doc.errors[errorName] !== undefined) doc.errors[errorName].notice = error?.notice; 73 | } 74 | 75 | for (const eventSig in info.userdoc?.events) { 76 | const [eventName] = eventSig.split('('); 77 | const event = info.userdoc?.events[eventSig]; 78 | 79 | if (doc.events[eventName] !== undefined) doc.events[eventName].notice = event?.notice; 80 | } 81 | 82 | for (const methodSig in info.userdoc?.methods) { 83 | // const [methodName] = methodSig.split('('); 84 | const method = info.userdoc?.methods[methodSig]; 85 | 86 | if (doc.methods[methodSig] !== undefined) doc.methods[methodSig].notice = method?.notice; 87 | } 88 | 89 | // Fetches info from devdoc 90 | for (const errorSig in info.devdoc?.errors) { 91 | const [errorName] = errorSig.split('('); 92 | const error = info.devdoc?.errors[errorSig][0]; 93 | 94 | if (doc.errors[errorName] !== undefined) doc.errors[errorName].details = error?.details; 95 | 96 | for (const param in error?.params) { 97 | if (doc.errors[errorName].inputs[param]) doc.errors[errorName].inputs[param].description = error?.params[param]; 98 | } 99 | } 100 | 101 | for (const eventSig in info.devdoc?.events) { 102 | const [eventName] = eventSig.split('('); 103 | const event = info.devdoc?.events[eventSig]; 104 | 105 | if (doc.events[eventName] !== undefined) doc.events[eventName].details = event?.details; 106 | 107 | for (const param in event?.params) { 108 | if (doc.events[eventName].inputs[param]) doc.events[eventName].inputs[param].description = event?.params[param]; 109 | } 110 | } 111 | 112 | for (const methodSig in info.devdoc?.methods) { 113 | const [methodName] = methodSig.split('('); 114 | const method = info.devdoc?.methods[methodSig]; 115 | 116 | if (doc.methods[methodSig] !== undefined && methodName !== 'constructor') { 117 | doc.methods[methodSig].details = method?.details; 118 | 119 | for (const param in method?.params) { 120 | if (doc.methods[methodSig].inputs[param]) doc.methods[methodSig].inputs[param].description = method?.params[param]; 121 | } 122 | 123 | for (const output in method?.returns) { 124 | if (doc.methods[methodSig].outputs[output]) doc.methods[methodSig].outputs[output].description = method?.returns[output]; 125 | } 126 | } 127 | } 128 | 129 | for (const varName in info.devdoc?.stateVariables) { 130 | const variable = info.devdoc?.stateVariables[varName]; 131 | const abiInfo = info.abi.find((a:any) => a.name === varName); 132 | 133 | const varNameWithParams = `${varName}(${ 134 | abiInfo?.inputs ? abiInfo.inputs.map((inp:any) => inp.type).join(',') : '' 135 | })`; 136 | 137 | if (doc.methods[varNameWithParams]) doc.methods[varNameWithParams].details = variable?.details; 138 | 139 | for (const param in variable?.params) { 140 | if (doc.methods[varNameWithParams].inputs[param]) doc.methods[varNameWithParams].inputs[param].description = variable?.params[param]; 141 | } 142 | 143 | for (const output in variable?.returns) { 144 | if (doc.methods[varNameWithParams].outputs[output]) doc.methods[varNameWithParams].outputs[output].description = variable?.returns[output]; 145 | } 146 | } 147 | 148 | // Fetches global info 149 | if (info.devdoc?.title) doc.title = info.devdoc.title; 150 | if (info.userdoc?.notice) doc.notice = info.userdoc.notice; 151 | if (info.devdoc?.details) doc.details = info.devdoc.details; 152 | if (info.devdoc?.author) doc.author = info.devdoc.author; 153 | 154 | doc.name = name; 155 | docs.push(doc); 156 | } 157 | 158 | try { 159 | await fs.promises.access(config.outputDir); 160 | 161 | if (config.freshOutput) { 162 | await fs.promises.rm(config.outputDir, { 163 | recursive: true, 164 | }); 165 | await fs.promises.mkdir(config.outputDir); 166 | } 167 | } catch (e) { 168 | await fs.promises.mkdir(config.outputDir); 169 | } 170 | 171 | const template = await fs.promises.readFile(config.templatePath, { 172 | encoding: 'utf-8', 173 | }); 174 | 175 | for (let i = 0; i < docs.length; i += 1) { 176 | const result = Sqrl.render(template, docs[i]); 177 | let docfileName = `${docs[i].name}.md`; 178 | let testFileName = `${docs[i].name}.json`; 179 | if (config.keepFileStructure && docs[i].path !== undefined) { 180 | if (!fs.existsSync(path.join(config.outputDir, docs[i].path))) await fs.promises.mkdir(path.join(config.outputDir, docs[i].path), { recursive: true }); 181 | docfileName = path.join(docs[i].path, docfileName); 182 | testFileName = path.join(docs[i].path, testFileName); 183 | } 184 | await fs.promises.writeFile( 185 | path.join(config.outputDir, docfileName), 186 | result, { 187 | encoding: 'utf-8', 188 | }, 189 | ); 190 | 191 | if (config.debugMode) { 192 | await fs.promises.writeFile( 193 | path.join(config.outputDir, testFileName), 194 | JSON.stringify(docs[i], null, 4), { 195 | encoding: 'utf-8', 196 | }, 197 | ); 198 | } 199 | } 200 | 201 | console.log('✅ Generated documentation for', docs.length, docs.length > 1 ? 'contracts' : 'contract'); 202 | } 203 | 204 | // Custom standalone task 205 | task('dodoc', 'Generates NatSpec documentation for the project') 206 | .addFlag('noCompile', 'Prevents compiling before running this task') 207 | .setAction(async (args, hre) => { 208 | if (!args.noCompile) { 209 | await hre.run(TASK_COMPILE, { noDodoc: true }); 210 | } 211 | 212 | await generateDocumentation(hre); 213 | }); 214 | 215 | // Overriding task triggered when COMPILE is called 216 | task(TASK_COMPILE) 217 | .addFlag('noDodoc', 'Prevents generating NatSpec documentation for the project') 218 | .setAction(async (args, hre, runSuper) => { 219 | // Updates the compiler settings 220 | for (const compiler of hre.config.solidity.compilers) { 221 | compiler.settings.outputSelection['*']['*'].push('devdoc'); 222 | compiler.settings.outputSelection['*']['*'].push('userdoc'); 223 | } 224 | 225 | // Compiles the contracts 226 | await runSuper(); 227 | 228 | if (hre.config.dodoc.runOnCompile && !args.noDodoc) { 229 | await hre.run('dodoc', { noCompile: true }); 230 | } 231 | }); 232 | -------------------------------------------------------------------------------- /src/template.sqrl: -------------------------------------------------------------------------------- 1 | {{@if (it.name)}}# {{it.name}}{{/if}} 2 | 3 | 4 | {{@if (it.author)}}*{{it.author}}*{{/if}} 5 | 6 | 7 | {{@if (it.title)}}> {{it.title}}{{/if}} 8 | 9 | 10 | {{@if (it.notice)}}{{it.notice}}{{/if}} 11 | 12 | 13 | {{@if (it.details)}}*{{it.details}}*{{/if}} 14 | 15 | 16 | {{@if (Object.keys(it.methods).length > 0)}} 17 | ## Methods 18 | 19 | {{@foreach(it.methods) => key, val}} 20 | ### {{key.split('(')[0]}} 21 | 22 | 23 | ```solidity 24 | {{val.code}} 25 | 26 | ``` 27 | 28 | {{@if (val.notice)}}{{val.notice}}{{/if}} 29 | 30 | 31 | {{@if (val.details)}}*{{val.details}}*{{/if}} 32 | 33 | 34 | {{@if (Object.keys(val.inputs).length > 0)}} 35 | #### Parameters 36 | 37 | | Name | Type | Description | 38 | |---|---|---| 39 | {{@foreach(val.inputs) => key, val}} 40 | | {{key}} | {{val.type}} | {{val.description}} | 41 | {{/foreach}} 42 | {{/if}} 43 | 44 | {{@if (Object.keys(val.outputs).length > 0)}} 45 | #### Returns 46 | 47 | | Name | Type | Description | 48 | |---|---|---| 49 | {{@foreach(val.outputs) => key, val}} 50 | | {{key}} | {{val.type}} | {{val.description}} | 51 | {{/foreach}} 52 | 53 | {{/if}} 54 | {{/foreach}} 55 | 56 | {{/if}} 57 | 58 | {{@if (Object.keys(it.events).length > 0)}} 59 | ## Events 60 | 61 | {{@foreach(it.events) => key, val}} 62 | ### {{key}} 63 | 64 | 65 | ```solidity 66 | {{val.code}} 67 | 68 | ``` 69 | 70 | {{@if (val.notice)}}{{val.notice}}{{/if}} 71 | 72 | 73 | {{@if (val.details)}}*{{val.details}}*{{/if}} 74 | 75 | 76 | {{@if (Object.keys(val.inputs).length > 0)}} 77 | #### Parameters 78 | 79 | | Name | Type | Description | 80 | |---|---|---| 81 | {{@foreach(val.inputs) => key, val}} 82 | | {{key}} {{@if (val.indexed)}}`indexed`{{/if}} | {{val.type}} | {{val.description}} | 83 | {{/foreach}} 84 | {{/if}} 85 | 86 | {{/foreach}} 87 | 88 | {{/if}} 89 | 90 | {{@if (Object.keys(it.errors).length > 0)}} 91 | ## Errors 92 | 93 | {{@foreach(it.errors) => key, val}} 94 | ### {{key}} 95 | 96 | 97 | ```solidity 98 | {{val.code}} 99 | 100 | ``` 101 | 102 | {{@if (val.notice)}}{{val.notice}}{{/if}} 103 | 104 | 105 | {{@if (val.details)}}*{{val.details}}*{{/if}} 106 | 107 | 108 | {{@if (Object.keys(val.inputs).length > 0)}} 109 | #### Parameters 110 | 111 | | Name | Type | Description | 112 | |---|---|---| 113 | {{@foreach(val.inputs) => key, val}} 114 | | {{key}} | {{val.type}} | {{val.description}} | 115 | {{/foreach}} 116 | {{/if}} 117 | 118 | {{/foreach}} 119 | 120 | {{/if}} 121 | -------------------------------------------------------------------------------- /src/type-extensions.ts: -------------------------------------------------------------------------------- 1 | import 'hardhat/types/config'; 2 | 3 | declare module 'hardhat/types/config' { 4 | export interface HardhatUserConfig { 5 | dodoc?: { 6 | include?: string[]; 7 | exclude?: string[]; 8 | runOnCompile?: boolean; 9 | debugMode?: boolean; 10 | templatePath?: string; 11 | outputDir?: string; 12 | keepFileStructure?: boolean; 13 | freshOutput?: boolean; 14 | } 15 | } 16 | 17 | export interface HardhatConfig { 18 | dodoc: { 19 | include: string[]; 20 | exclude: string[]; 21 | runOnCompile: boolean; 22 | debugMode: boolean; 23 | templatePath: string; 24 | outputDir: string; 25 | keepFileStructure: boolean; 26 | freshOutput: boolean; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "strict": true, 10 | "rootDirs": ["./src", "./test"], 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true 13 | }, 14 | "exclude": ["dist", "node_modules"], 15 | "include": ["./src", "./test"], 16 | "files": ["./hardhat.config.ts"] 17 | } 18 | --------------------------------------------------------------------------------