├── .github └── workflows │ ├── release.yml │ ├── test.yml │ └── update-solc.yml ├── .gitignore ├── .gitmodules ├── .mocharc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── netlify.toml ├── package-lock.json ├── package.json ├── scripts ├── build-finder.js ├── build-node-type.js ├── build-schema.js ├── build-types.js └── prepare.sh ├── solc.d.ts ├── src ├── ast-dereferencer.ts └── src-decoder.ts ├── test ├── helpers │ ├── assert-valid.js │ ├── solc-compile-helper.js │ ├── solc-compile.js │ └── solc-versions.js ├── openzeppelin-contracts.js ├── openzeppelin-contracts.json ├── solc.js ├── solidity-submodule.js ├── sources │ ├── asm-0.6.sol │ ├── asm-0.7.5.sol │ ├── asm-0.7.sol │ ├── ast-deref-2.sol │ ├── ast-deref.sol │ ├── basic.sol │ ├── catch-panic.sol │ ├── constructor-0.6.sol │ ├── constructor-0.7.sol │ ├── errors-require.sol │ ├── errors.sol │ ├── file-level-constant.sol │ ├── find-all.sol │ ├── finney-szabo.sol │ ├── free-function.sol │ ├── gwei.sol │ ├── identifier-path.sol │ ├── import.sol │ ├── modifier-no-body.sol │ ├── new-0.8.13.sol │ ├── new-0.8.14.sol │ ├── new-0.8.18.sol │ ├── new-0.8.19.sol │ ├── new-0.8.20.sol │ ├── new-0.8.21.sol │ ├── new-0.8.8.sol │ ├── only-0.8.13.sol │ ├── src-decoder.sol │ ├── stmt-docs-0.8.4.sol │ ├── stmt-docs.sol │ ├── transient.sol │ ├── unchecked.sol │ └── unicode-string.sol └── utils.js ├── tsconfig.base.json ├── tsconfig.docs.json ├── tsconfig.emit.json ├── tsconfig.json ├── typedoc.css ├── typedoc.json ├── utils.d.ts ├── utils.js └── utils ├── find-all.d.ts ├── find-all.js ├── is-node-type.d.ts └── is-node-type.js /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - uses: frangio/extract-changelog@v1 15 | id: changelog 16 | - name: Release 17 | uses: softprops/action-gh-release@v1 18 | with: 19 | body_path: ${{ steps.changelog.outputs.file }} 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: {} 7 | 8 | concurrency: 9 | group: test-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: true 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 18.x 22 | cache: npm 23 | - name: Install dependencies and build 24 | run: npm ci 25 | - name: Run tests 26 | run: npm test -- --exclude test/solc.js 27 | 28 | # Solc tests run significantly faster in Node 18 29 | test-solc: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v3 33 | with: 34 | submodules: true 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version: 18.x 38 | cache: npm 39 | - name: Install dependencies and build 40 | run: npm ci 41 | - name: Run tests 42 | run: npm test -- test/solc.js 43 | -------------------------------------------------------------------------------- /.github/workflows/update-solc.yml: -------------------------------------------------------------------------------- 1 | name: Update tested solc versions 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: {} 7 | 8 | jobs: 9 | update: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 22.x 18 | cache: npm 19 | - run: | 20 | set -euo pipefail 21 | all_versions() { 22 | npm view --json solc | jq -r '.versions[]' 23 | } 24 | exclude_unwanted() { 25 | grep -v -e - -e '^0\.[0-6]' | grep -vF -e 0.8.1 -e 0.8.7 -e 0.8.14 26 | } 27 | npm install -DE $(all_versions | exclude_unwanted | sed 's/\S*/solc-&@npm:solc@&/') 28 | - name: Create Pull Request 29 | uses: peter-evans/create-pull-request@v7 30 | with: 31 | token: ${{ secrets.PR_PAT }} 32 | branch: update-solc 33 | title: Update solc 34 | body: | 35 | - [ ] Review the [latest Solidity changes](https://github.com/ethereum/solidity/blob/develop/Changelog.md) and add test files for any new language constructs. 36 | - [ ] If tests are failing: fix the schema and update the changelog accordingly. 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /schema.json 3 | /types.d.ts 4 | /node.d.ts 5 | /finder.json 6 | /docs 7 | /.solc-bin 8 | /dist 9 | 10 | *.tsbuildinfo 11 | 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "solidity"] 2 | path = test/solidity 3 | url = https://github.com/ethereum/solidity.git 4 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch-files": [ 3 | "scripts/build-schema.js", 4 | "test" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.4.60 (2025-03-15) 4 | 5 | - Added `ContractDefinition.storageLayout`, available since Solidity 0.8.29, corresponding to `layout at`. 6 | 7 | ### 0.4.59 (2024-09-06) 8 | 9 | - Added `'transient'` as a possible value for `VariableDeclaration.storageLocation`, available since Solidity 0.8.27. 10 | - Added `'prague'` as a possible value for `InlineAssembly.evmVersion`. 11 | 12 | ### 0.4.58 (2024-08-29) 13 | 14 | - Fixed `YulFunctionDefinition.parameters` and `returnVariables`: made optional, used when empty. 15 | - Fixed `InlineAssembly.externalReferences[].suffix`: added `'length'` option. 16 | 17 | ### 0.4.57 (2024-07-16) 18 | 19 | - Fixed `ModifierDefinition.body`: made nullable to support Solidity 0.6.7 virtual modifiers with empty body. 20 | 21 | ### 0.4.56 (2024-03-14) 22 | 23 | - Added `'cancun'` as a possible value for `InlineAssembly.evmVersion`. 24 | 25 | ### 0.4.55 (2023-11-25) 26 | 27 | - Fixed `TupleExpression.components`: array members are nullable. 28 | 29 | 30 | ### 0.4.54 (2023-11-23) 31 | 32 | - Fixed `Literal.subdenomination`, previously typed as `null`, now typed with values `'seconds'`, `'minutes'`, etc. 33 | - Added `EnumDefinition.documentation`. 34 | 35 | ### 0.4.53 (2023-11-10) 36 | 37 | - Added `ForStatement.isSimpleCounterLoop`, available since Solidity 0.8.22. 38 | 39 | ### 0.4.52 (2023-08-24) 40 | 41 | - Fixed behavior of `findAll('*', ...)` around `UsingForDirective.functionList` and `ImportDirective.symbolAliases`, which missed some identifiers and returned non-nodes. 42 | 43 | ### 0.4.51 (2023-08-19) 44 | 45 | - Fixed bug in AST dereferencer reading a property of null. 46 | 47 | ### 0.4.50 (2023-08-19) 48 | 49 | - Fixed `findAll` exhaustivity around objects in the AST that are not AST nodes. Affected queries for `IdentifierPath` nodes under `UsingForDirective.functionList` and for `Identifier` nodes under `ImportDirective.symbolAliases.foreign`. 50 | - Significantly optimized `findAll` and `astDereferencer`. 51 | - Added ability to enumerate all nodes with `findAll('*', node)` or dereference an unknown node type with `deref('*', id)`. 52 | - Added `SourceUnit.experimentalSolidity`, available since Solidity 0.8.21. 53 | - Added `ContractDefinition.usedEvents` and `ContractDefinition.internalFunctionIDs`, available since Solidity 0.8.20. 54 | - Added `StructDefinition.documentation`, available since Solidity 0.8.20. 55 | - Added `'shanghai'` as a possible value for `InlineAssembly.evmVersion`. 56 | - Added `nativeSrc` to Yul AST nodes. 57 | 58 | ### 0.4.49 (2023-05-04) 59 | 60 | - Added a custom Error subclass for ASTDereferencer errors. 61 | 62 | ### 0.4.48 (2023-05-04) 63 | 64 | - Fixed return type of `ASTDereferencer.withSourceUnit`. 65 | 66 | ### 0.4.47 (2023-05-04) 67 | 68 | - Added `ASTDereferencer.withSourceUnit` to be able to obtain the source unit that contains a node with a given id. 69 | 70 | ### 0.4.46 (2023-02-22) 71 | 72 | - Added new variant of `UsingForDirective` for user-defined operators. 73 | - Added `BinaryOperation.function` and `UnaryOperation.function` for user-defined operators. 74 | 75 | ### 0.4.45 (2023-02-01) 76 | 77 | - Added `'paris'` as a possible value for `InlineAssembly.evmVersion`. 78 | - Added new `Mapping` fields `keyName`, `keyNameLocation`, `valueName`, `valueNameLocation` from Solidity 0.8.18. 79 | 80 | ### 0.4.44 (2023-01-30) 81 | 82 | - Added `'~'` as a possible value for `UnaryOperation.operator`. 83 | 84 | ### 0.4.43 (2023-01-20) 85 | 86 | - Added `FunctionCall.nameLocations` and `IdentifierPath.nameLocations` from Solidity 0.8.16. 87 | - Added `MemberAccess.memberLocation` from Solidity 0.8.16. 88 | 89 | ### 0.4.42 (2023-01-20) 90 | 91 | - Reverted optimizations to `findAll` from 0.4.40. 92 | 93 | ### 0.4.41 (2023-01-20) 94 | 95 | - Optimized AST dereferencer. 96 | 97 | ### 0.4.40 (2022-12-29) 98 | 99 | - Further optimized `findAll` for multiple wanted node types. 100 | 101 | ### 0.4.39 (2022-12-16) 102 | 103 | - Optimized `findAll` for multiple wanted node types. 104 | 105 | ### 0.4.38 (2022-11-15) 106 | 107 | - Added missing types for `srcDecoder`. 108 | 109 | ### 0.4.37 (2022-11-15) 110 | 111 | - Added `srcDecoder`, a new util to decode source locations in AST nodes. 112 | 113 | ### 0.4.36 (2022-11-14) 114 | 115 | - Fixed error in `findAll` when the code contains user defined value types. 116 | 117 | ### 0.4.35 (2022-06-23) 118 | 119 | - Made `IndexAccess.indexExpression` nullable, which shows up when array types are used with `abi.decode`. 120 | 121 | ### 0.4.34 (2022-06-11) 122 | 123 | - Added missing files to package. 124 | 125 | ### 0.4.33 (2022-06-11) 126 | 127 | - Removed circular dependency from utils. 128 | 129 | ### 0.4.32 (2022-04-24) 130 | 131 | - Added `EventDefinition.eventSelector` and `ErrorDefinition.errorSelector`. 132 | - Added `InlineAssembly.flags` which can now indicate assembly as memory safe. 133 | - Added `UsingForDirective.global`. 134 | - In `UsingForDirective` added `functionList` and made `libraryName` optional. These properties are exclusive: exactly one of them should be present, though this is not encoded in the type. 135 | - Fixed `UsingForDirective.typeName`: it should have always been nullable to account for `using ... for *`. 136 | 137 | ### 0.4.31 (2022-03-20) 138 | 139 | - Added `SolcInput` interface. 140 | 141 | ### 0.4.30 (2022-01-27) 142 | 143 | - Added support for an array of node types in `isNodeType`. 144 | 145 | ### 0.4.29 (2021-12-24) 146 | 147 | - Made input array types `readonly` in `utils` module. 148 | 149 | ### 0.4.28 (2021-10-27) 150 | 151 | - Added `DoWhileStatement`, `Continue`, and `Break`. All of them kinds of `Statement`. 152 | 153 | ### 0.4.27 (2021-06-25) 154 | 155 | - Added `UserDefinedValueTypeDefinition` which can appear in `SourceUnit` and `ContractDefinition`. 156 | - Added `ContractDefinition.canonicalName` from Solidity 0.8.9. 157 | - Added new EVM version `'london'`. 158 | 159 | ### 0.4.26 (2021-06-25) 160 | 161 | - Added a new utility for looking up AST nodes based on their id and type. 162 | 163 | ### 0.4.25 (2021-06-01) 164 | 165 | - Removed Yul nodes from `Node` and `NodeType`, export them separately as `YulNode` and `YulNodeType`. 166 | 167 | ### 0.4.24 (2021-06-01) 168 | 169 | - Fixed duplicate type name in generated declaration file. 170 | 171 | ### 0.4.23 (2021-05-30) 172 | 173 | - Added missing values for `Assignment.operator`: `>>=`, `<<=` 174 | - Added missing values for `BinaryOperation.operator`: `&`, `|` 175 | - Added missing kind of `Expression`: `IndexRangeAccess` (e.g. `msg.data[start:end]`) 176 | - Added missing field in members of `InlineAssembly.externalReferences`: `suffix` (`'slot'` and `'offset'`) 177 | 178 | ### 0.4.22 (2021-05-30) 179 | 180 | - Added Yul types for typing `InlineAssembly.AST`. 181 | - Fixed `Identifier.overloadedDeclarations`: was `unknown` and is now `number`. 182 | 183 | ### 0.4.21 (2021-05-23) 184 | 185 | - Rewrote schema in JavaScript in a more modular way. 186 | - Fixed `IndexAccess.baseExpression`: was optional but is required. 187 | - Added missing values for `Assignment.operator`: `-=`, `*=`, `%=`, `|=`, `&=`, `^=`. 188 | - Added statement-level `documentation` available since Solidity 0.8.2. 189 | 190 | ### 0.4.20 (2021-04-22) 191 | 192 | - Added new Solidity 0.8.4 constructs: 193 | - `ErrorDefinition` as a new child of `SourceUnit` and `ContractDefinition` 194 | - `RevertStatement` as a new kind of statement 195 | - `ContractDefinition.usedErrors` 196 | 197 | ### 0.4.19 (2021-03-23) 198 | 199 | - Added new Solidity 0.8.3 field `ModifierInvocation.kind`. 200 | 201 | Note that there is a bug in 0.8.3 where `kind` never actually has the value 202 | `"baseConstructorSpecifier"`. This will presumably be fixed in the next 203 | release. 204 | 205 | ### 0.4.18 (2021-03-21) 206 | 207 | - Added new Solidity 0.8.2 field `nameLocation` in: 208 | - `EnumValue` 209 | - `EnumDefinition` 210 | - `EventDefinition` 211 | - `FunctionDefinition` 212 | - `ModifierDefinition` 213 | - `ImportDirective` (and entries of `ImportDirective.symbolAliases`) 214 | - `ContractDefinition` 215 | - `StructDefinition` 216 | - `VariableDeclaration` 217 | 218 | ### 0.4.17 (2021-01-05) 219 | 220 | - Added missing override-related types and fields: 221 | - `ModifierDefinition.baseModifiers: number[]` 222 | - `ModifierDefinition.overrides?: OverrideSpecifier` 223 | - `VariableDeclaration.baseFunctions: number[]` 224 | - `VariableDeclaration.overrides?: OverrideSpecifier` 225 | 226 | ### 0.4.16 (2020-12-17) 227 | 228 | - Added `Block` as a possible kind of `Statement`. 229 | - Added new Solidity 0.8 constructs: 230 | - `UncheckedBlock` is a new kind of `Statement`. 231 | - `IdentifierPath` is a new node type that replaces some instances of `UserDefinedTypeName` and `Identifier`, used in the following places: 232 | - `InheritanceSpecifier.baseName` 233 | - `ModifierInvocation.modifierName` 234 | - `OverrideSpecifier.overrides` 235 | - `UsingForDirective.libraryName` 236 | - `UserDefinedTypeName.pathNode` (new) 237 | 238 | ### 0.4.15 (2020-12-11) 239 | 240 | - Extended `findAll` to enumerate multiple node types simultaneously. 241 | 242 | ### 0.4.14 (2020-11-18) 243 | 244 | - Added `hexString`, `unicodeString` as possible values for `Literal.kind`. Available since Solidity 0.7.0. 245 | 246 | ### 0.4.13 (2020-11-02) 247 | 248 | - Fixed `findAll` crash when used with node type `'SourceUnit'`. 249 | 250 | ### 0.4.12 (2020-10-22) 251 | 252 | - Added an optional argument `prune` to `findAll`. 253 | 254 | > If the optional `prune: (node: Node) => boolean` argument is specified, 255 | > `findAll` will apply the function to each node, if the return value is truthy 256 | > the node will be ignored, neither yielding the node nor recursing into it. Note 257 | > that `prune` is not available when curried. 258 | 259 | ### 0.4.11 (2020-10-19) 260 | 261 | - Added support for file-level constant `VariableDeclaration` nodes, available since Solidity 0.7.4. 262 | 263 | ### 0.4.10 (2020-10-15) 264 | 265 | - Added `VariableDeclaration.documentation`, which is available since Solidity 0.6.9. 266 | 267 | ### 0.4.9 (2020-10-15) 268 | 269 | - Added `TryStatement` as a new type of statement node. 270 | 271 | ### 0.4.8 (2020-10-15) 272 | 273 | - Fixed types of `ForStatement` properties `condition`, `initializationExpression`, `loopExpression`, allowing them to be empty. 274 | - Fixed type of `ForStatement.initializationExpression` to also potentially contain an `ExpressionStatement`. 275 | 276 | ### 0.4.7 (2020-10-14) 277 | 278 | - `NewExpression.isLValue` and `FunctionCallOptions.isLValue` are now optional. 279 | 280 | Due to a bug in Solidity 0.7.2, these two properties are missing in the ASTs produced by that version. In order for the types to remain accurate, they have been made optional. When the property is missing its value should be assumed to be `false` (see [ethereum/solidity#9953](https://github.com/ethereum/solidity/pull/9953)). 281 | 282 | ### 0.4.6 (2020-10-14) 283 | 284 | - Fixed type of `ModifierInvocation.arguments`. 285 | 286 | ### 0.4.5 (2020-10-14) 287 | 288 | - Disabled `additionalProperties` in `Conditional` node. 289 | - Fixed `Return` node for empty return statements. 290 | 291 | ### 0.4.4 (2020-09-02) 292 | 293 | - Fixed `body` property of `ForStatement`: was `Block`, can also be `Statement`. 294 | - Added support for Solidity 0.7.1. 295 | - Made nullable properties optional. For TypeScript this means that `null` values can now be `undefined`. 296 | - Added support for free functions: `FunctionDefinition` is now a potential child in `SourceUnit.nodes`. 297 | 298 | ### 0.4.3 (2020-07-02) 299 | 300 | - Fixed `body` property of `WhileStatement`: was `Block`, can also be `Statement`. 301 | 302 | ### 0.4.2 (2020-07-02) 303 | 304 | - Fixed `length` property of `ArrayTypeName`: was `null`, can be any `Expression`. 305 | 306 | ### 0.4.1 (2020-06-18) 307 | 308 | - Added all EVM versions to `InlineAssembly` node. 309 | - Fixed `findAll` to check for null property values. 310 | 311 | ### 0.4.0 (2020-06-12) 312 | 313 | - Added `solidity-ast/utils` with the following utility functions: 314 | - `isNodeType(nodeType, node)`: a type predicate for type-safe filtering or 315 | any kind of narrowing. 316 | - `findAll(nodeType, node)`: a generator function that recursively enumerates 317 | all of a node's descendents of type `nodeType`. 318 | - Both of these functions can be partially applied by supplying only the 319 | `nodeType` argument. This is useful for higher order functions like 320 | `filter` or `map`, as in `nodes.filter(isNodeType('ContractDefinition'))`. 321 | - Removed `solidity-ast/predicates`. Use `isNodeType` from `solidity-ast/utils` instead. 322 | 323 | ### 0.3.2 (2020-06-12) 324 | 325 | - Fixed type for `ImportDirective.symbolAliases`. 326 | 327 | ### 0.3.1 (2020-06-09) 328 | 329 | - Added missing type for `ImportDirective.symbolAliases`. 330 | 331 | ### 0.3.0 (2020-06-04) 332 | 333 | - Added `solidity-ast/predicates` with type guards for type-safe filtering. 334 | 335 | ```typescript 336 | import { isContractDefinition } from "solidity-ast/predicates"; 337 | const contractDefs = sourceUnit.nodes.filter(isContractDefinition); 338 | // : ContractDefinition[] 339 | ``` 340 | 341 | - Removed `ParameterTypes`, which was a duplicate of `ParameterList`. The latter should be used instead. 342 | - Removed `ParameterTypeName`, which wasn't referenced anywhere. 343 | 344 | ### 0.2.1 (2020-06-01) 345 | 346 | - Added missing `>>` operator. 347 | 348 | ### 0.2.0 (2020-05-18) 349 | 350 | - Completed schema to successfully validate OpenZeppelin Contracts. 351 | 352 | ### 0.1.0 (2020-05-15) 353 | 354 | - Initial release with incomplete schema. 355 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 zOS Global Ltd. 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 | # Solidity AST Types 2 | 3 | [![Docs](https://img.shields.io/badge/docs-%F0%9F%93%84-blue)][docs] 4 | [![NPM Package](https://img.shields.io/npm/v/solidity-ast.svg)](https://www.npmjs.org/package/solidity-ast) 5 | 6 | **TypeScript types and a JSON Schema for the Solidity AST.** 7 | 8 | ``` 9 | npm install solidity-ast 10 | ``` 11 | 12 | 13 | ```typescript 14 | import type { SourceUnit, ContractDefinition } from 'solidity-ast'; 15 | ``` 16 | 17 | The types included in the NPM package are automatically generated from the JSON 18 | Schema, so you will not find them in the repository. You can see what they look 19 | like on [unpkg] or the [documentation][docs]. 20 | 21 | [unpkg]: https://unpkg.com/solidity-ast@latest/types.d.ts 22 | [docs]: https://solidity-ast.info/ 23 | 24 | ## Solidity Versioning 25 | 26 | The types are currently accurate and tested for Solidity >=0.6.6, but you can 27 | very likely use them safely for any version since 0.6.0. For simple traversals 28 | they will probably work well for 0.5.0 and up as well. 29 | 30 | The versioning story will be gradually improved upon and the ultimate goal is 31 | to be able to manipulate and traverse the AST in a uniform way that is as 32 | agnostic to the Solidity version as possible. 33 | 34 | ## Utilities 35 | 36 | Included in the package is a set of utility functions for type-safe interactions 37 | with nodes based on the node type. 38 | 39 | ### `isNodeType(nodeType, node)` 40 | 41 | A type predicate that can be used for narrowing the type of an 42 | unknown node, or combined with higher order functions like `filter`. 43 | 44 | An array of node types can be used as well to check if the node matches one of them. 45 | 46 | ```typescript 47 | import { isNodeType } from 'solidity-ast/utils'; 48 | 49 | if (isNodeType('ContractDefinition', node)) { 50 | // node: ContractDefinition 51 | } 52 | 53 | const contractDefs = sourceUnit.nodes.filter(isNodeType('ContractDefinition')); 54 | // contractDefs: ContractDefinition[] 55 | ``` 56 | 57 | ### `findAll(nodeType, node[, prune])` 58 | 59 | `findAll` is a generator function that will recursively enumerate all 60 | descendent nodes of a given node type. It does this in an efficient way by 61 | visiting only the nodes that are necessary for the searched node type. 62 | 63 | ```typescript 64 | import { findAll } from 'solidity-ast/utils'; 65 | 66 | for (const functionDef of findAll('FunctionDefinition', sourceUnit)) { 67 | // functionDef: FunctionDefinition 68 | } 69 | ``` 70 | 71 | If the optional `prune: (node: Node) => boolean` argument is specified, 72 | `findAll` will apply the function to each node, if the return value is truthy 73 | the node will be ignored, neither yielding the node nor recursing into it. Note 74 | that `prune` is not available when curried. 75 | 76 | To enumerate multiple node types at the same time, `nodeType` can be an array 77 | of node types such as `['EnumDefinition', 'StructDefinition']`. 78 | 79 | ```typescript 80 | for (const typeDef of findAll(['EnumDefinition', 'StructDefinition'], sourceUnit)) { 81 | // typeDef: EnumDefinition | StructDefinition 82 | } 83 | ``` 84 | 85 | To enumerate all subnodes regardless of node type, `nodeType` can be `'*'` (a 86 | string with a single asterisk). 87 | 88 | ### `astDereferencer(solcOutput) => (nodeType, id) => Node` 89 | 90 | `astDereferencer` looks up AST nodes based on their id. Notably, it works 91 | across multiple source files, which is why it needs the entire solc JSON output 92 | with the ASTs for all source files in a compilation. 93 | 94 | > On Hardhat, the solc JSON output can be found in [build info files]. 95 | 96 | [build info files]: https://hardhat.org/guides/compile-contracts.html#build-info-files 97 | 98 | ```typescript 99 | const deref = astDereferencer(solcOutput); 100 | 101 | deref('ContractDefinition', 4); 102 | 103 | for (const contractDef of findAll('ContractDefinition', sourceUnit)) { 104 | const baseContracts = contractDef.linearizedBaseContracts.map(deref('ContractDefinition')); 105 | ... 106 | } 107 | ``` 108 | 109 | It is also possible to obtain the source unit that contains the dereferenced node: 110 | 111 | ```typescript 112 | const deref = astDereferencer(solcOutput); 113 | 114 | const { node, sourceUnit } = deref.withSourceUnit('ContractDefinition', 4); 115 | ``` 116 | 117 | If the node type is unknown you can specify `'*'` for `nodeType`. 118 | 119 | ### `srcDecoder(solcInput, solcOutput, basePath = '.') => (node: Node) => string` 120 | 121 | `srcDecoder` allows decoding of the `src` property of a node, which looks 122 | something like `123:4:0`, into a human-readable description of the location of 123 | that node, such as `file.sol:10`. 124 | 125 | > On Hardhat, the solc JSON input and output can be found in [build info files]. 126 | 127 | [build info files]: https://hardhat.org/guides/compile-contracts.html#build-info-files 128 | 129 | ```typescript 130 | const decodeSrc = srcDecoder(solcInput, solcOutput); 131 | ... 132 | const location = decodeSrc(contractDefinition); 133 | console.log('found contract at ' + location); 134 | ``` 135 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "https://solidity-ast.info/:splat" 4 | status = 302 5 | force = true 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidity-ast", 3 | "version": "0.4.60", 4 | "description": "Solidity AST schema and type definitions", 5 | "author": "Francisco Giordano ", 6 | "homepage": "https://solidity-ast.info/", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/OpenZeppelin/solidity-ast.git" 10 | }, 11 | "types": "types.d.ts", 12 | "files": [ 13 | "/finder.json", 14 | "/*.d.ts", 15 | "/utils.js", 16 | "/utils", 17 | "/dist/**/*.{js,d.ts}{,.map}", 18 | "/src/**/*.ts", 19 | "/schema.json" 20 | ], 21 | "scripts": { 22 | "build:docs": "typedoc --tsconfig tsconfig.docs.json", 23 | "build:schema": "node scripts/build-schema.js", 24 | "build:types": "node scripts/build-types.js", 25 | "build:node-type": "node scripts/build-node-type.js", 26 | "build:finder": "node scripts/build-finder.js", 27 | "test": "mocha", 28 | "test:watch": "mocha -w", 29 | "prepare": "bash -x scripts/prepare.sh" 30 | }, 31 | "devDependencies": { 32 | "@types/lodash": "^4.17.7", 33 | "@types/node": "^18.14.10", 34 | "ajv": "^8.17.1", 35 | "chalk": "^4.1.2", 36 | "fast-check": "^3.20.0", 37 | "json-schema-to-typescript": "^14.1.0", 38 | "lodash": "^4.17.21", 39 | "mocha": "^10.6.0", 40 | "semver": "^7.6.2", 41 | "solc-0.6.10": "npm:solc@0.6.10", 42 | "solc-0.6.11": "npm:solc@0.6.11", 43 | "solc-0.6.12": "npm:solc@0.6.12", 44 | "solc-0.6.6": "npm:solc@0.6.6", 45 | "solc-0.6.7": "npm:solc@0.6.7", 46 | "solc-0.6.8": "npm:solc@0.6.8", 47 | "solc-0.6.9": "npm:solc@0.6.9", 48 | "solc-0.7.0": "npm:solc@0.7.0", 49 | "solc-0.7.1": "npm:solc@0.7.1", 50 | "solc-0.7.2": "npm:solc@0.7.2", 51 | "solc-0.7.3": "npm:solc@0.7.3", 52 | "solc-0.7.4": "npm:solc@0.7.4", 53 | "solc-0.7.5": "npm:solc@0.7.5", 54 | "solc-0.7.6": "npm:solc@0.7.6", 55 | "solc-0.8.0": "npm:solc@0.8.0", 56 | "solc-0.8.10": "npm:solc@0.8.10", 57 | "solc-0.8.11": "npm:solc@0.8.11", 58 | "solc-0.8.12": "npm:solc@0.8.12", 59 | "solc-0.8.13": "npm:solc@0.8.13", 60 | "solc-0.8.15": "npm:solc@0.8.15", 61 | "solc-0.8.16": "npm:solc@0.8.16", 62 | "solc-0.8.17": "npm:solc@0.8.17", 63 | "solc-0.8.18": "npm:solc@0.8.18", 64 | "solc-0.8.19": "npm:solc@0.8.19", 65 | "solc-0.8.2": "npm:solc@0.8.2", 66 | "solc-0.8.20": "npm:solc@0.8.20", 67 | "solc-0.8.21": "npm:solc@0.8.21", 68 | "solc-0.8.22": "npm:solc@0.8.22", 69 | "solc-0.8.23": "npm:solc@0.8.23", 70 | "solc-0.8.24": "npm:solc@0.8.24", 71 | "solc-0.8.25": "npm:solc@0.8.25", 72 | "solc-0.8.26": "npm:solc@0.8.26", 73 | "solc-0.8.27": "npm:solc@0.8.27", 74 | "solc-0.8.28": "npm:solc@0.8.28", 75 | "solc-0.8.29": "npm:solc@0.8.29", 76 | "solc-0.8.3": "npm:solc@0.8.3", 77 | "solc-0.8.4": "npm:solc@0.8.4", 78 | "solc-0.8.5": "npm:solc@0.8.5", 79 | "solc-0.8.6": "npm:solc@0.8.6", 80 | "solc-0.8.8": "npm:solc@0.8.8", 81 | "solc-0.8.9": "npm:solc@0.8.9", 82 | "typedoc": "^0.26.4", 83 | "typedoc-theme-hierarchy": "^5.0.3", 84 | "typescript": "^5.5.3" 85 | }, 86 | "license": "MIT" 87 | } 88 | -------------------------------------------------------------------------------- /scripts/build-finder.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // @ts-check 4 | 'use strict'; 5 | 6 | const fs = require('fs'); 7 | const _ = require('lodash'); 8 | 9 | const reachable = {}; 10 | 11 | const schema = require('../schema.json'); 12 | 13 | for (const def of [...Object.values(schema.definitions), schema]) { 14 | if ('properties' in def && 'nodeType' in def.properties) { 15 | const parentType = def.properties.nodeType.enum[0]; 16 | _.defaults(reachable, { [parentType]: {} }); 17 | 18 | for (const prop in def.properties) { 19 | for (const contained of getReachableNodeTypes(def.properties[prop])) { 20 | _.set(reachable, [contained.nodeType, parentType, prop], true); 21 | _.set(reachable, ['*', parentType, prop], true); 22 | for (const nonNodeParent of contained.nonNodeParents) { 23 | _.set(reachable, [contained.nodeType, '$other', nonNodeParent], true); 24 | _.set(reachable, ['*', '$other', nonNodeParent], true); 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | const finder = _.mapValues(reachable, f => _.mapValues(f, Object.keys)); 32 | 33 | fs.writeFileSync('finder.json', JSON.stringify(finder, null, 2)); 34 | 35 | function* getReachableNodeTypes(nodeSchema, nonNodeParents = [], visited = new Set()) { 36 | const nodeTypes = nodeSchema?.properties?.nodeType?.enum; 37 | 38 | if (nodeTypes) { 39 | yield* nodeTypes.map(nodeType => ({ nodeType, nonNodeParents })); 40 | } 41 | 42 | if ('$ref' in nodeSchema) { 43 | const [ ref ] = nodeSchema['$ref'].match(/[^\/]+$/); 44 | if (!visited.has(ref)) { 45 | visited.add(ref); 46 | yield* getReachableNodeTypes(schema.definitions[ref], nonNodeParents, visited); 47 | } 48 | } 49 | 50 | if ('anyOf' in nodeSchema) { 51 | for (const subSchema of nodeSchema['anyOf']) { 52 | yield* getReachableNodeTypes(subSchema, nonNodeParents, visited); 53 | } 54 | } 55 | 56 | if ('properties' in nodeSchema) { 57 | for (const [subprop, subpropSchema] of Object.entries(nodeSchema.properties)) { 58 | if (!nodeTypes) { 59 | nonNodeParents.push(subprop); 60 | } 61 | yield* getReachableNodeTypes(subpropSchema, nonNodeParents, visited); 62 | if (!nodeTypes) { 63 | nonNodeParents.pop(); 64 | } 65 | } 66 | } 67 | 68 | if ('items' in nodeSchema) { 69 | yield* getReachableNodeTypes(nodeSchema.items, nonNodeParents, visited); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scripts/build-node-type.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // @ts-check 4 | 'use strict'; 5 | 6 | const fs = require('fs'); 7 | const schema = require('../schema.json'); 8 | 9 | const nodeTypesSet = new Set(schema.properties.nodeType.enum); 10 | const yulNodeTypesSet = new Set(); 11 | 12 | for (const def of Object.values(schema.definitions)) { 13 | if ('properties' in def && 'nodeType' in def.properties) { 14 | for (const type of def.properties.nodeType.enum) { 15 | if (type.startsWith('Yul')) { 16 | yulNodeTypesSet.add(type); 17 | } else { 18 | nodeTypesSet.add(type); 19 | } 20 | } 21 | } 22 | } 23 | 24 | const nodeTypes = [...nodeTypesSet]; 25 | const yulNodeTypes = [...yulNodeTypesSet]; 26 | 27 | const lines = []; 28 | 29 | lines.push(`import type {${nodeTypes.join(', ')}} from './types';`); 30 | lines.push(`export type Node = ${nodeTypes.join(' | ')};`) 31 | lines.push(`export type NodeTypeMap = { ${nodeTypes.map(t => [t, t].join(': ')).join(', ')} };`); 32 | lines.push(`export type NodeType = keyof NodeTypeMap;`); 33 | 34 | lines.push(`import type {${yulNodeTypes.join(', ')}} from './types';`); 35 | lines.push(`export type YulNode = ${yulNodeTypes.join(' | ')};`) 36 | lines.push(`export type YulNodeTypeMap = { ${yulNodeTypes.map(t => [t, t].join(': ')).join(', ')} };`); 37 | lines.push(`export type YulNodeType = keyof YulNodeTypeMap;`); 38 | 39 | fs.writeFileSync('node.d.ts', lines.join('\n')); 40 | -------------------------------------------------------------------------------- /scripts/build-schema.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const boolean = { type: 'boolean' }; 6 | const string = { type: 'string' }; 7 | const integer = { type: 'integer' }; 8 | const _null = { type: 'null' }; 9 | 10 | const pattern = pat => ({ type: 'string', pattern: pat.source }); 11 | const literal = (...values) => ({ enum: values }); 12 | const array = items => ({ type: 'array', items }); 13 | const record = values => ({ type: 'object', additionalProperties: values }); 14 | const anyOf = (...types) => ({ 'anyOf': types }); 15 | const ref = id => ({ $ref: `#/definitions/${id}` }); 16 | 17 | const $optional = Symbol('optional'); 18 | const optional = schema => ({ ...schema, [$optional]: true }); 19 | 20 | // Older compiler versions returned null for missing fields, whereas newer 21 | // releases omit them. Generally `optional` should be used instead of `nullable`. 22 | const nullable = schema => schema ? optional(anyOf(schema, _null)) : optional(_null); 23 | 24 | const object = (properties = {}) => ({ 25 | type: 'object', 26 | additionalProperties: false, 27 | properties, 28 | required: Object.keys(properties).filter(p => !properties[p][$optional]), 29 | }); 30 | 31 | const baseNode = { 32 | id: integer, 33 | src: ref('SourceLocation'), 34 | }; 35 | 36 | const node = (type, props) => object({ ...baseNode, ...props, nodeType: literal(type) }); 37 | 38 | const baseYulNode = { 39 | src: ref('SourceLocation'), 40 | }; 41 | 42 | const yulNode = (type, props) => object({ 43 | ...baseYulNode, 44 | ...props, 45 | nodeType: literal(type), 46 | nativeSrc: optional(ref('SourceLocation')), 47 | }); 48 | 49 | const mapValues = (obj, fn) => 50 | Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(k, v)])); 51 | 52 | const nodes = (defs) => mapValues(defs, node); 53 | const yulNodes = (defs) => mapValues(defs, yulNode); 54 | 55 | const documentation = nullable(ref('StructuredDocumentation')); 56 | const typeDescriptions = ref('TypeDescriptions'); 57 | const overrides = nullable(ref('OverrideSpecifier')); 58 | const parameters = ref('ParameterList'); 59 | const visibility = ref('Visibility'); 60 | 61 | const baseDefinition = { 62 | name: string, 63 | nameLocation: optional(string), 64 | }; 65 | 66 | const baseExpression = { 67 | argumentTypes: nullable(array(ref('TypeDescriptions'))), 68 | isConstant: boolean, 69 | isLValue: optional(boolean), 70 | isPure: boolean, 71 | lValueRequested: boolean, 72 | typeDescriptions, 73 | }; 74 | 75 | const baseExpressionL = { 76 | ...baseExpression, 77 | isLValue: boolean, 78 | }; 79 | 80 | const baseStatement = { 81 | documentation: optional(string), 82 | }; 83 | 84 | const baseTypeName = { 85 | typeDescriptions: ref('TypeDescriptions'), 86 | }; 87 | 88 | const schema = { 89 | $schema: 'http://json-schema.org/draft-06/schema#', 90 | 91 | title: 'SourceUnit', 92 | 93 | ...node('SourceUnit', { 94 | absolutePath: string, 95 | exportedSymbols: record(array(integer)), 96 | experimentalSolidity: optional(boolean), 97 | license: nullable(string), 98 | nodes: array(anyOf( 99 | ref('ContractDefinition'), 100 | ref('EnumDefinition'), 101 | ref('ErrorDefinition'), 102 | ref('FunctionDefinition'), 103 | ref('ImportDirective'), 104 | ref('PragmaDirective'), 105 | ref('StructDefinition'), 106 | ref('UserDefinedValueTypeDefinition'), 107 | ref('UsingForDirective'), 108 | ref('VariableDeclaration'), 109 | )), 110 | }), 111 | 112 | definitions: { 113 | SourceLocation: pattern(/^\d+:\d+:\d+$/), 114 | 115 | Mutability: literal( 116 | 'mutable', 117 | 'immutable', 118 | 'constant', 119 | ), 120 | 121 | StateMutability: literal( 122 | 'payable', 123 | 'pure', 124 | 'nonpayable', 125 | 'view', 126 | ), 127 | 128 | StorageLocation: literal( 129 | 'calldata', 130 | 'default', 131 | 'memory', 132 | 'storage', 133 | 'transient', 134 | ), 135 | 136 | Visibility: literal( 137 | 'external', 138 | 'public', 139 | 'internal', 140 | 'private', 141 | ), 142 | 143 | TypeDescriptions: object({ 144 | typeIdentifier: nullable(string), 145 | typeString: nullable(string), 146 | }), 147 | 148 | Expression: anyOf( 149 | ref('Assignment'), 150 | ref('BinaryOperation'), 151 | ref('Conditional'), 152 | ref('ElementaryTypeNameExpression'), 153 | ref('FunctionCall'), 154 | ref('FunctionCallOptions'), 155 | ref('Identifier'), 156 | ref('IndexAccess'), 157 | ref('IndexRangeAccess'), 158 | ref('Literal'), 159 | ref('MemberAccess'), 160 | ref('NewExpression'), 161 | ref('TupleExpression'), 162 | ref('UnaryOperation'), 163 | ), 164 | 165 | Statement: anyOf( 166 | ref('Block'), 167 | ref('Break'), 168 | ref('Continue'), 169 | ref('DoWhileStatement'), 170 | ref('EmitStatement'), 171 | ref('ExpressionStatement'), 172 | ref('ForStatement'), 173 | ref('IfStatement'), 174 | ref('InlineAssembly'), 175 | ref('PlaceholderStatement'), 176 | ref('Return'), 177 | ref('RevertStatement'), 178 | ref('TryStatement'), 179 | ref('UncheckedBlock'), 180 | ref('VariableDeclarationStatement'), 181 | ref('WhileStatement'), 182 | ), 183 | 184 | TypeName: anyOf( 185 | ref('ArrayTypeName'), 186 | ref('ElementaryTypeName'), 187 | ref('FunctionTypeName'), 188 | ref('Mapping'), 189 | ref('UserDefinedTypeName'), 190 | ), 191 | 192 | ...nodes({ 193 | ArrayTypeName: { 194 | ...baseTypeName, 195 | baseType: ref('TypeName'), 196 | length: nullable(ref('Expression')), 197 | }, 198 | 199 | Assignment: { 200 | ...baseExpressionL, 201 | leftHandSide: ref('Expression'), 202 | operator: literal( 203 | '=', 204 | '+=', 205 | '-=', 206 | '*=', 207 | '/=', 208 | '%=', 209 | '|=', 210 | '&=', 211 | '^=', 212 | '>>=', 213 | '<<=' 214 | ), 215 | rightHandSide: ref('Expression'), 216 | }, 217 | 218 | BinaryOperation: { 219 | ...baseExpressionL, 220 | commonType: ref('TypeDescriptions'), 221 | leftExpression: ref('Expression'), 222 | operator: literal( 223 | '+', 224 | '-', 225 | '*', 226 | '/', 227 | '%', 228 | '**', 229 | '&&', 230 | '||', 231 | '!=', 232 | '==', 233 | '<', 234 | '<=', 235 | '>', 236 | '>=', 237 | '^', 238 | '&', 239 | '|', 240 | '<<', 241 | '>>' 242 | ), 243 | rightExpression: ref('Expression'), 244 | function: optional(integer), 245 | }, 246 | 247 | Block: { 248 | ...baseStatement, 249 | statements: nullable(array(ref('Statement'))), 250 | }, 251 | 252 | Break: { 253 | ...baseStatement, 254 | }, 255 | 256 | Conditional: { 257 | ...baseExpressionL, 258 | condition: ref('Expression'), 259 | falseExpression: ref('Expression'), 260 | trueExpression: ref('Expression'), 261 | }, 262 | 263 | Continue: { 264 | ...baseStatement, 265 | }, 266 | 267 | ContractDefinition: { 268 | ...baseDefinition, 269 | abstract: boolean, 270 | baseContracts: array(ref('InheritanceSpecifier')), 271 | canonicalName: optional(string), 272 | contractDependencies: array(integer), 273 | contractKind: literal('contract', 'interface', 'library'), 274 | documentation, 275 | fullyImplemented: boolean, 276 | linearizedBaseContracts: array(integer), 277 | nodes: array(anyOf( 278 | ref('EnumDefinition'), 279 | ref('ErrorDefinition'), 280 | ref('EventDefinition'), 281 | ref('FunctionDefinition'), 282 | ref('ModifierDefinition'), 283 | ref('StructDefinition'), 284 | ref('UserDefinedValueTypeDefinition'), 285 | ref('UsingForDirective'), 286 | ref('VariableDeclaration'), 287 | )), 288 | scope: integer, 289 | usedErrors: optional(array(integer)), 290 | usedEvents: optional(array(integer)), 291 | internalFunctionIDs: optional(record(integer)), 292 | storageLayout: optional(ref('StorageLayoutSpecifier')), 293 | }, 294 | 295 | StorageLayoutSpecifier: { 296 | baseSlotExpression: ref('Expression'), 297 | }, 298 | 299 | DoWhileStatement: { 300 | ...baseStatement, 301 | body: anyOf( 302 | ref('Block'), 303 | ref('Statement'), 304 | ), 305 | condition: ref('Expression'), 306 | }, 307 | 308 | ElementaryTypeName: { 309 | ...baseTypeName, 310 | name: string, 311 | stateMutability: optional(ref('StateMutability')), 312 | }, 313 | 314 | ElementaryTypeNameExpression: { 315 | ...baseExpressionL, 316 | typeName: ref('ElementaryTypeName'), 317 | }, 318 | 319 | EmitStatement: { 320 | ...baseStatement, 321 | eventCall: ref('FunctionCall'), 322 | }, 323 | 324 | EnumDefinition: { 325 | ...baseDefinition, 326 | canonicalName: string, 327 | members: array(ref('EnumValue')), 328 | documentation, // nullable but in practice just optional 329 | }, 330 | 331 | EnumValue: { 332 | ...baseDefinition, 333 | }, 334 | 335 | ErrorDefinition: { 336 | ...baseDefinition, 337 | documentation, 338 | errorSelector: optional(string), 339 | parameters, 340 | nameLocation: string, 341 | }, 342 | 343 | EventDefinition: { 344 | ...baseDefinition, 345 | anonymous: boolean, 346 | eventSelector: optional(string), 347 | documentation, 348 | parameters, 349 | }, 350 | 351 | ExpressionStatement: { 352 | ...baseStatement, 353 | expression: ref('Expression'), 354 | }, 355 | 356 | ForStatement: { 357 | ...baseStatement, 358 | body: anyOf( 359 | ref('Block'), 360 | ref('Statement'), 361 | ), 362 | condition: nullable(ref('Expression')), 363 | initializationExpression: nullable(anyOf( 364 | ref('ExpressionStatement'), 365 | ref('VariableDeclarationStatement'), 366 | )), 367 | loopExpression: nullable(ref('ExpressionStatement')), 368 | isSimpleCounterLoop: optional(boolean), 369 | }, 370 | 371 | FunctionCall: { 372 | ...baseExpressionL, 373 | arguments: array(ref('Expression')), 374 | expression: ref('Expression'), 375 | kind: literal( 376 | 'functionCall', 377 | 'typeConversion', 378 | 'structConstructorCall', 379 | ), 380 | names: array(string), 381 | nameLocations: optional(array(string)), 382 | tryCall: boolean, 383 | typeDescriptions, 384 | }, 385 | 386 | FunctionCallOptions: { 387 | ...baseExpression, 388 | expression: ref('Expression'), 389 | names: array(string), 390 | options: array(ref('Expression')), 391 | }, 392 | 393 | FunctionDefinition: { 394 | ...baseDefinition, 395 | baseFunctions: optional(array(integer)), 396 | body: nullable(ref('Block')), 397 | documentation, 398 | functionSelector: optional(string), 399 | implemented: boolean, 400 | kind: literal( 401 | 'function', 402 | 'receive', 403 | 'constructor', 404 | 'fallback', 405 | 'freeFunction', 406 | ), 407 | modifiers: array(ref('ModifierInvocation')), 408 | overrides, 409 | parameters, 410 | returnParameters: ref('ParameterList'), 411 | scope: integer, 412 | stateMutability: ref('StateMutability'), 413 | virtual: boolean, 414 | visibility, 415 | }, 416 | 417 | FunctionTypeName: { 418 | ...baseTypeName, 419 | parameterTypes: ref('ParameterList'), 420 | returnParameterTypes: ref('ParameterList'), 421 | stateMutability: ref('StateMutability'), 422 | visibility, 423 | }, 424 | 425 | Identifier: { 426 | argumentTypes: nullable(array(ref('TypeDescriptions'))), 427 | name: string, 428 | overloadedDeclarations: array(integer), 429 | referencedDeclaration: nullable(integer), 430 | typeDescriptions, 431 | }, 432 | 433 | IdentifierPath: { 434 | name: string, 435 | nameLocations: optional(array(string)), 436 | referencedDeclaration: integer, 437 | }, 438 | 439 | IfStatement: { 440 | ...baseStatement, 441 | condition: ref('Expression'), 442 | falseBody: nullable(anyOf( 443 | ref('Statement'), 444 | ref('Block'), 445 | )), 446 | trueBody: anyOf( 447 | ref('Statement'), 448 | ref('Block'), 449 | ), 450 | }, 451 | 452 | ImportDirective: { 453 | absolutePath: string, 454 | file: string, 455 | nameLocation: optional(string), 456 | scope: integer, 457 | sourceUnit: integer, 458 | symbolAliases: array(object({ 459 | foreign: ref('Identifier'), 460 | local: nullable(string), 461 | nameLocation: optional(string), 462 | })), 463 | unitAlias: string, 464 | }, 465 | 466 | IndexAccess: { 467 | ...baseExpressionL, 468 | baseExpression: ref('Expression'), 469 | indexExpression: nullable(ref('Expression')), 470 | }, 471 | 472 | IndexRangeAccess: { 473 | ...baseExpressionL, 474 | baseExpression: ref('Expression'), 475 | endExpression: nullable(ref('Expression')), 476 | startExpression: nullable(ref('Expression')), 477 | }, 478 | 479 | InheritanceSpecifier: { 480 | arguments: nullable(array(ref('Expression'))), 481 | baseName: anyOf( 482 | ref('UserDefinedTypeName'), 483 | ref('IdentifierPath'), 484 | ), 485 | }, 486 | 487 | InlineAssembly: { 488 | ...baseStatement, 489 | AST: ref('YulBlock'), 490 | evmVersion: literal( 491 | 'homestead', 492 | 'tangerineWhistle', 493 | 'spuriousDragon', 494 | 'byzantium', 495 | 'constantinople', 496 | 'petersburg', 497 | 'istanbul', 498 | 'berlin', 499 | 'london', 500 | 'paris', 501 | 'shanghai', 502 | 'cancun', 503 | 'prague', 504 | ), 505 | externalReferences: array(object({ 506 | declaration: integer, 507 | isOffset: boolean, 508 | isSlot: boolean, 509 | src: ref('SourceLocation'), 510 | valueSize: integer, 511 | suffix: optional(literal('slot', 'offset', 'length')), 512 | })), 513 | flags: optional(array(literal('memory-safe'))), 514 | }, 515 | 516 | Literal: { 517 | ...baseExpressionL, 518 | hexValue: pattern(/^[0-9a-f]*$/), 519 | kind: literal( 520 | 'bool', 521 | 'number', 522 | 'string', 523 | 'hexString', 524 | 'unicodeString' 525 | ), 526 | subdenomination: nullable(literal( 527 | 'seconds', 528 | 'minutes', 529 | 'hours', 530 | 'days', 531 | 'weeks', 532 | 'wei', 533 | 'gwei', 534 | 'ether', 535 | 'finney', 536 | 'szabo', 537 | )), 538 | value: nullable(string), 539 | }, 540 | 541 | Mapping: { 542 | ...baseTypeName, 543 | keyType: ref('TypeName'), 544 | valueType: ref('TypeName'), 545 | keyName: optional(string), 546 | keyNameLocation: optional(string), 547 | valueName: optional(string), 548 | valueNameLocation: optional(string), 549 | }, 550 | 551 | MemberAccess: { 552 | ...baseExpressionL, 553 | expression: ref('Expression'), 554 | memberName: string, 555 | memberLocation: optional(string), 556 | referencedDeclaration: nullable(integer), 557 | }, 558 | 559 | ModifierDefinition: { 560 | ...baseDefinition, 561 | baseModifiers: nullable(array(integer)), 562 | body: nullable(ref('Block')), 563 | documentation, 564 | overrides, 565 | parameters, 566 | virtual: boolean, 567 | visibility, 568 | }, 569 | 570 | ModifierInvocation: { 571 | arguments: nullable(array(ref('Expression'))), 572 | kind: optional(literal( 573 | 'modifierInvocation', 574 | 'baseConstructorSpecifier', 575 | )), 576 | modifierName: anyOf( 577 | ref('Identifier'), 578 | ref('IdentifierPath'), 579 | ), 580 | }, 581 | 582 | NewExpression: { 583 | ...baseExpression, 584 | typeName: ref('TypeName'), 585 | }, 586 | 587 | OverrideSpecifier: { 588 | overrides: anyOf( 589 | array(ref('UserDefinedTypeName')), 590 | array(ref('IdentifierPath')), 591 | ), 592 | }, 593 | 594 | ParameterList: { 595 | parameters: array(ref('VariableDeclaration')), 596 | }, 597 | 598 | PlaceholderStatement: { 599 | ...baseStatement, 600 | }, 601 | 602 | PragmaDirective: { 603 | literals: array(string), 604 | }, 605 | 606 | Return: { 607 | ...baseStatement, 608 | expression: nullable(ref('Expression')), 609 | functionReturnParameters: integer, 610 | }, 611 | 612 | RevertStatement: { 613 | ...baseStatement, 614 | errorCall: ref('FunctionCall'), 615 | }, 616 | 617 | StructDefinition: { 618 | ...baseDefinition, 619 | canonicalName: string, 620 | members: array(ref('VariableDeclaration')), 621 | scope: integer, 622 | visibility, 623 | documentation, // nullable but in practice just optional 624 | }, 625 | 626 | StructuredDocumentation: { 627 | text: string, 628 | }, 629 | 630 | TryCatchClause: { 631 | block: ref('Block'), 632 | errorName: string, 633 | parameters: nullable(ref('ParameterList')), 634 | }, 635 | 636 | TryStatement: { 637 | ...baseStatement, 638 | clauses: array(ref('TryCatchClause')), 639 | externalCall: ref('FunctionCall'), 640 | }, 641 | 642 | TupleExpression: { 643 | ...baseExpressionL, 644 | components: array(nullable(ref('Expression'))), 645 | isInlineArray: boolean, 646 | }, 647 | 648 | UnaryOperation: { 649 | ...baseExpressionL, 650 | operator: literal( 651 | '++', 652 | '--', 653 | '-', 654 | '!', 655 | 'delete', 656 | '~' 657 | ), 658 | prefix: boolean, 659 | subExpression: ref('Expression'), 660 | function: optional(integer), 661 | }, 662 | 663 | UncheckedBlock: { 664 | ...baseStatement, 665 | statements: array(ref('Statement')), 666 | }, 667 | 668 | UserDefinedTypeName: { 669 | ...baseTypeName, 670 | contractScope: nullable(), 671 | name: optional(string), 672 | pathNode: optional(ref('IdentifierPath')), 673 | referencedDeclaration: integer, 674 | }, 675 | 676 | UserDefinedValueTypeDefinition: { 677 | ...baseDefinition, 678 | canonicalName: optional(string), 679 | underlyingType: ref('TypeName'), 680 | }, 681 | 682 | UsingForDirective: { 683 | functionList: optional(array(anyOf( 684 | object({ 685 | function: ref('IdentifierPath'), 686 | }), 687 | object({ 688 | operator: literal( 689 | '&', 690 | '|', 691 | '^', 692 | '~', 693 | '+', 694 | '-', 695 | '*', 696 | '/', 697 | '%', 698 | '==', 699 | '!=', 700 | '<', 701 | '<=', 702 | '>', 703 | '>=', 704 | ), 705 | definition: ref('IdentifierPath'), 706 | }), 707 | ))), 708 | global: optional(boolean), 709 | libraryName: optional(anyOf( 710 | ref('UserDefinedTypeName'), 711 | ref('IdentifierPath'), 712 | )), 713 | typeName: nullable(ref('TypeName')), 714 | }, 715 | 716 | VariableDeclaration: { 717 | ...baseDefinition, 718 | baseFunctions: nullable(array(integer)), 719 | constant: boolean, 720 | documentation, 721 | functionSelector: optional(string), 722 | indexed: optional(boolean), 723 | mutability: ref('Mutability'), 724 | overrides, 725 | scope: integer, 726 | stateVariable: boolean, 727 | storageLocation: ref('StorageLocation'), 728 | typeDescriptions, 729 | typeName: nullable(ref('TypeName')), 730 | value: nullable(ref('Expression')), 731 | visibility, 732 | }, 733 | 734 | VariableDeclarationStatement: { 735 | ...baseStatement, 736 | assignments: array(nullable(integer)), 737 | declarations: array(nullable(ref('VariableDeclaration'))), 738 | initialValue: nullable(ref('Expression')), 739 | }, 740 | 741 | WhileStatement: { 742 | ...baseStatement, 743 | body: anyOf( 744 | ref('Block'), 745 | ref('Statement'), 746 | ), 747 | condition: ref('Expression'), 748 | }, 749 | }), 750 | 751 | YulStatement: anyOf( 752 | ref('YulAssignment'), 753 | ref('YulBlock'), 754 | ref('YulBreak'), 755 | ref('YulContinue'), 756 | ref('YulExpressionStatement'), 757 | ref('YulLeave'), 758 | ref('YulForLoop'), 759 | ref('YulFunctionDefinition'), 760 | ref('YulIf'), 761 | ref('YulSwitch'), 762 | ref('YulVariableDeclaration'), 763 | ), 764 | 765 | YulExpression: anyOf( 766 | ref('YulFunctionCall'), 767 | ref('YulIdentifier'), 768 | ref('YulLiteral'), 769 | ), 770 | 771 | YulLiteral: anyOf( 772 | ref('YulLiteralValue'), 773 | ref('YulLiteralHexValue'), 774 | ), 775 | 776 | YulLiteralValue: yulNode('YulLiteral', { 777 | value: string, 778 | kind: literal('number', 'string', 'bool'), 779 | type: string, 780 | }), 781 | 782 | YulLiteralHexValue: yulNode('YulLiteral', { 783 | hexValue: string, 784 | kind: literal('number', 'string', 'bool'), 785 | type: string, 786 | value: optional(string), 787 | }), 788 | 789 | ...yulNodes({ 790 | YulAssignment: { 791 | value: ref('YulExpression'), 792 | variableNames: array(ref('YulIdentifier')), 793 | }, 794 | 795 | YulBlock: { 796 | statements: array(ref('YulStatement')), 797 | }, 798 | 799 | YulBreak: {}, 800 | 801 | YulCase: { 802 | body: ref('YulBlock'), 803 | value: anyOf( 804 | literal('default'), 805 | ref('YulLiteral'), 806 | ), 807 | }, 808 | 809 | YulContinue: {}, 810 | 811 | YulExpressionStatement: { 812 | expression: ref('YulExpression'), 813 | }, 814 | 815 | YulFunctionCall: { 816 | arguments: array(ref('YulExpression')), 817 | functionName: ref('YulIdentifier'), 818 | }, 819 | 820 | YulForLoop: { 821 | body: ref('YulBlock'), 822 | condition: ref('YulExpression'), 823 | post: ref('YulBlock'), 824 | pre: ref('YulBlock'), 825 | }, 826 | 827 | YulFunctionDefinition: { 828 | body: ref('YulBlock'), 829 | name: string, 830 | parameters: optional(array(ref('YulTypedName'))), 831 | returnVariables: optional(array(ref('YulTypedName'))), 832 | }, 833 | 834 | YulIdentifier: { 835 | name: string, 836 | }, 837 | 838 | YulIf: { 839 | body: ref('YulBlock'), 840 | condition: ref('YulExpression'), 841 | }, 842 | 843 | YulLeave: { 844 | }, 845 | 846 | YulSwitch: { 847 | cases: array(ref('YulCase')), 848 | expression: ref('YulExpression'), 849 | }, 850 | 851 | YulTypedName: { 852 | name: string, 853 | type: string, 854 | }, 855 | 856 | YulVariableDeclaration: { 857 | value: nullable(ref('YulExpression')), 858 | variables: array(ref('YulTypedName')), 859 | }, 860 | }), 861 | }, 862 | }; 863 | 864 | if (require.main === module) { 865 | const fs = require('fs'); 866 | fs.writeFileSync('schema.json', JSON.stringify(schema, null, 2)); 867 | } 868 | 869 | module.exports = schema; 870 | -------------------------------------------------------------------------------- /scripts/build-types.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const json2ts = require('json-schema-to-typescript'); 4 | const fs = require('fs'); 5 | 6 | const schema = require('../schema.json'); 7 | 8 | json2ts.compile(schema, schema.title, { 9 | strictIndexSignatures: true, 10 | bannerComment: '/* tslint:disable */', 11 | }).then(result => { 12 | fs.writeFileSync('types.d.ts', result); 13 | }).catch(err => { 14 | console.error(err.stack); 15 | process.exit(1); 16 | }); 17 | -------------------------------------------------------------------------------- /scripts/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm run build:schema 4 | npm run build:types 5 | npm run build:node-type 6 | npm run build:finder 7 | tsc -b 8 | -------------------------------------------------------------------------------- /solc.d.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from './types'; 2 | 3 | export interface SolcOutput { 4 | sources: { 5 | [file in string]: { 6 | ast: SourceUnit; 7 | id: number; 8 | }; 9 | }; 10 | } 11 | 12 | export interface SolcInput { 13 | sources: { 14 | [file in string]: { 15 | keccak256?: string; 16 | content?: string; 17 | urls?: string[]; 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/ast-dereferencer.ts: -------------------------------------------------------------------------------- 1 | import { isNodeType, ExtendedNodeType, ExtendedNodeTypeMap } from '../utils/is-node-type'; 2 | import { findAll } from '../utils/find-all'; 3 | import type { ASTDereferencer, NodeWithSourceUnit } from '../utils'; 4 | import type { Node, NodeType } from '../node'; 5 | import type { SolcOutput } from '../solc'; 6 | 7 | // An ASTDereferencer is a function that looks up an AST node given its id, in all of the source files involved in a 8 | // solc run. It will generally be used together with the AST property `referencedDeclaration` (found in Identifier, 9 | // UserDefinedTypeName, etc.) to look up a variable definition or type definition. 10 | 11 | export function astDereferencer(solcOutput: SolcOutput): ASTDereferencer { 12 | const cache = new Map(); 13 | 14 | const asts = Array.from( 15 | Object.values(solcOutput.sources), 16 | s => s.ast, 17 | ).sort((a, b) => a.id - b.id); 18 | 19 | function deref(nodeType: T | readonly T[], id: number): NodeWithSourceUnit; 20 | function deref(nodeType: ExtendedNodeType | readonly ExtendedNodeType[], id: number): NodeWithSourceUnit { 21 | const cached = cache.get(id); 22 | 23 | if (cached) { 24 | if (isNodeType(nodeType, cached.node)) { 25 | return cached; 26 | } 27 | } else if (id >= 0) { 28 | // Node ids appear to be assigned in postorder. This means that a node's own id is always 29 | // larger than that of the nodes under it. We descend through the AST guided by this 30 | // assumption. Among a set of sibling nodes we choose the one with the smallest id that 31 | // is larger than the id we're looking for. 32 | 33 | let ast = asts.find(ast => (id <= ast.id)); 34 | let searchRoot: Node | Node[] | undefined = ast; 35 | 36 | while (searchRoot) { 37 | if (Array.isArray(searchRoot)) { 38 | searchRoot = searchRoot.find(child => child && (id <= child.id)); 39 | } else if (searchRoot.id === id) { 40 | break; 41 | } else { 42 | let nextRoot, nextRootId; 43 | for (const child of Object.values(searchRoot)) { 44 | if (typeof child !== "object") continue; 45 | const childId = Array.isArray(child) ? child.findLast(n => n)?.id : child?.id; 46 | if (childId === undefined) continue; 47 | if (id <= childId && (nextRootId === undefined || childId <= nextRootId)) { 48 | nextRoot = child; 49 | nextRootId = childId; 50 | } 51 | } 52 | searchRoot = nextRoot; 53 | } 54 | } 55 | 56 | let found: Node | undefined = searchRoot; 57 | 58 | // As a fallback mechanism in case the postorder assumption breaks, if the node is not found 59 | // we proceed to check all nodes in all ASTs. 60 | 61 | if (found === undefined) { 62 | outer: for (ast of asts) { 63 | for (const node of findAll(nodeType, ast)) { 64 | if (node.id === id) { 65 | found = node; 66 | break outer; 67 | } 68 | } 69 | } 70 | } 71 | 72 | if (found !== undefined) { 73 | const nodeWithSourceUnit = { node: found, sourceUnit: ast! }; 74 | cache.set(id, nodeWithSourceUnit); 75 | if (isNodeType(nodeType, found)) { 76 | return nodeWithSourceUnit; 77 | } 78 | } 79 | } 80 | 81 | nodeType = Array.isArray(nodeType) ? nodeType : [nodeType]; 82 | throw new ASTDereferencerError(id, nodeType); 83 | } 84 | 85 | function derefNode(nodeType: NodeType | readonly NodeType[], id: number) { 86 | return deref(nodeType, id).node; 87 | } 88 | 89 | return Object.assign( 90 | curry2(derefNode), 91 | { withSourceUnit: deref } 92 | ); 93 | } 94 | 95 | export interface Curried { 96 | (a: A): (b: B) => T; 97 | (a: A, b: B): T; 98 | } 99 | 100 | export function curry2(fn: (a: A, b: B) => T): Curried { 101 | function curried(a: A): (b: B) => T; 102 | function curried(a: A, b: B): T; 103 | function curried(a: A, ...b: [] | [B]): T | ((b: B) => T) { 104 | if (b.length === 0) { 105 | return b => fn(a, b); 106 | } else { 107 | return fn(a, ...b); 108 | } 109 | } 110 | return curried; 111 | } 112 | 113 | export class ASTDereferencerError extends Error { 114 | constructor(readonly id: number, readonly nodeType: readonly ExtendedNodeType[]) { 115 | super(`No node with id ${id} of type ${nodeType}`); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/src-decoder.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import type { SolcOutput, SolcInput } from '../solc'; 3 | import type { SrcDecoder } from '../utils'; 4 | 5 | interface Source { 6 | name: string; 7 | content: Buffer; 8 | } 9 | 10 | export function srcDecoder(solcInput: SolcInput, solcOutput: SolcOutput, basePath = '.'): SrcDecoder { 11 | const sources: Record = {}; 12 | 13 | function getSource(sourceId: number): Source { 14 | if (sourceId in sources) { 15 | return sources[sourceId]; 16 | } else { 17 | const sourcePath = Object.entries(solcOutput.sources).find(([, { id }]) => sourceId === id)?.[0]; 18 | if (sourcePath === undefined) { 19 | throw new Error(`Source file not available`); 20 | } 21 | const content = solcInput.sources[sourcePath]?.content; 22 | const name = path.relative(basePath, sourcePath); 23 | if (content === undefined) { 24 | throw new Error(`Content for '${name}' not available`); 25 | } 26 | return (sources[sourceId] = { name, content: Buffer.from(content, 'utf8') }); 27 | } 28 | } 29 | 30 | return ({ src }) => { 31 | const [begin, , sourceId] = src.split(':').map(Number); 32 | const { name, content } = getSource(sourceId); 33 | const line = 1 + countInBuffer(content.subarray(0, begin), '\n'); 34 | return name + ':' + line; 35 | }; 36 | } 37 | 38 | function countInBuffer(buf: Buffer, str: string): number { 39 | let count = 0; 40 | let from = 0; 41 | while (true) { 42 | let i = buf.indexOf(str, from); 43 | if (i === -1) break; 44 | count += 1; 45 | from = i + str.length; 46 | } 47 | return count; 48 | } 49 | -------------------------------------------------------------------------------- /test/helpers/assert-valid.js: -------------------------------------------------------------------------------- 1 | const Ajv = require('ajv'); 2 | const lodash = require('lodash'); 3 | const chalk = require('chalk'); 4 | const util = require('util'); 5 | 6 | const ajv = new Ajv({ verbose: true }); 7 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); 8 | const validate = ajv.compile(require('../../scripts/build-schema')); 9 | 10 | function assertValid(ast, file) { 11 | if (!validate(ast)) { 12 | const longest = lodash.maxBy(validate.errors, e => e.instancePath.split('/').length); 13 | throw new Error(formatError(longest, ast, file)); 14 | } 15 | } 16 | 17 | function formatError(error, doc, file) { 18 | const pathComponents = error.instancePath.split('/'); 19 | const nodeTree = pathComponents.map((c, i) => { 20 | const subPath = pathComponents.slice(1, i + 1).concat('nodeType').join('.'); 21 | const nodeType = lodash.get(doc, subPath) || ''; 22 | const indent = i === 0 ? '' : ' '.repeat(i - 1) + '└─'; 23 | if (nodeType === 'SourceUnit') c = file; 24 | return lodash.compact([indent, nodeType, c && chalk.dim(c)]).join(' '); 25 | }).join('\n'); 26 | 27 | const params = Object.values(error.params).join(', '); 28 | 29 | const dataString = util.inspect(error.data, { compact: false, depth: 1 }); 30 | 31 | return `${error.message} (${params})\n\n${nodeTree}\n\n${dataString}`; 32 | } 33 | 34 | module.exports = { assertValid }; 35 | -------------------------------------------------------------------------------- /test/helpers/solc-compile-helper.js: -------------------------------------------------------------------------------- 1 | if (process.send === undefined) { 2 | throw new Error('Must run via child_process.fork'); 3 | } 4 | 5 | const events = require('events'); 6 | 7 | process.once('message', async ({ version, sources }) => { 8 | const solc = require(`solc-${version}`); 9 | 10 | // version() returns something like '0.8.13+commit.abaa5c0e.Emscripten.clang' 11 | const [trueVersion] = solc.version().split('+'); 12 | 13 | let output; 14 | 15 | if (trueVersion !== version) { 16 | output = { 17 | errors: [ 18 | { formattedMessage: `Package solc-${version} is actually solc@${trueVersion}` }, 19 | ], 20 | }; 21 | } else { 22 | output = JSON.parse( 23 | solc.compile( 24 | JSON.stringify({ 25 | sources, 26 | language: 'Solidity', 27 | settings: { 28 | outputSelection: { '*': { '': ['ast'] } }, 29 | }, 30 | }) 31 | ) 32 | ); 33 | } 34 | 35 | process.send(output); 36 | }); 37 | -------------------------------------------------------------------------------- /test/helpers/solc-compile.js: -------------------------------------------------------------------------------- 1 | const proc = require('child_process'); 2 | const events = require('events'); 3 | const lodash = require('lodash'); 4 | 5 | async function compile(version, sources) { 6 | const child = proc.fork(require.resolve('./solc-compile-helper')); 7 | child.send({ version, sources }); 8 | const [output] = await events.once(child, 'message'); 9 | 10 | // allows tests to exit as soon as they finish 11 | // otherwise solc module delays exit 12 | child.disconnect(); 13 | child.unref(); 14 | 15 | if (output.errors && output.errors.some(e => e.severity !== 'warning')) { 16 | throw new Error(lodash.map(output.errors, 'formattedMessage').join('\n')); 17 | } 18 | return output; 19 | } 20 | 21 | module.exports = { compile }; 22 | -------------------------------------------------------------------------------- /test/helpers/solc-versions.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | 3 | const versions = Object.keys(require('../../package.json').devDependencies) 4 | .filter(s => s.startsWith('solc-')) 5 | .map(s => s.replace('solc-', '')) 6 | .sort(semver.compare); 7 | 8 | const latest = versions[versions.length - 1]; 9 | 10 | module.exports = { versions, latest }; 11 | -------------------------------------------------------------------------------- /test/openzeppelin-contracts.js: -------------------------------------------------------------------------------- 1 | const { assertValid } = require('./helpers/assert-valid'); 2 | 3 | const { sources } = require('./openzeppelin-contracts.json'); 4 | 5 | describe('openzeppelin contracts', function () { 6 | for (const [ path, { ast } ] of Object.entries(sources)) { 7 | it(path, function () { 8 | assertValid(ast); 9 | }); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /test/solc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs/promises'); 2 | const path = require('path'); 3 | const semver = require('semver'); 4 | const lodash = require('lodash'); 5 | 6 | const { assertValid } = require('./helpers/assert-valid'); 7 | const { versions } = require('./helpers/solc-versions'); 8 | const { compile } = require('./helpers/solc-compile'); 9 | 10 | describe('solc', function () { 11 | this.timeout(10 * 60 * 1000); 12 | 13 | before('reading solidity sources', async function () { 14 | const files = await fs.readdir(path.join(__dirname, 'sources')); 15 | this.sources = {}; 16 | this.sourceVersions = {}; 17 | this.experimental = {}; 18 | for (const file of files) { 19 | const content = await fs.readFile( 20 | path.join(__dirname, 'sources', file), 21 | 'utf8', 22 | ); 23 | this.sources[file] = { content }; 24 | this.sourceVersions[file] = content.match(/pragma solidity (.*);/)[1]; 25 | this.experimental[file] = /pragma experimental solidity;/.test(content); 26 | } 27 | }); 28 | 29 | for (const version of versions) { 30 | it(version, async function () { 31 | const sources = lodash.pickBy(this.sources, (_, f) => 32 | semver.satisfies(version, this.sourceVersions[f]) && !this.experimental[f], 33 | ); 34 | const output = await compile(version, sources); 35 | for (const source of Object.keys(sources)) { 36 | assertValid(output.sources[source].ast, source); 37 | } 38 | 39 | const experimentalSources = lodash.pickBy(this.sources, (_, f) => 40 | semver.satisfies(version, this.sourceVersions[f]) && this.experimental[f], 41 | ); 42 | if (experimentalSources.length > 0) { 43 | const experimentalOutput = await compile(version, experimentalSources); 44 | for (const source of Object.keys(sources)) { 45 | assertValid(output.sources[source].ast, source); 46 | } 47 | } 48 | }); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /test/solidity-submodule.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const { assertValid } = require('./helpers/assert-valid'); 5 | 6 | const dir = path.join(__dirname, 'solidity/test/libsolidity/ASTJSON'); 7 | 8 | describe('solidity submodule', function () { 9 | // we read all jsons except those marked legacy or parseOnly 10 | const inputs = fs.readdirSync(dir) 11 | .filter(e => /^.*(? fs.promises.readFile(path.resolve(dir, f), 'utf8'))); 15 | this.inputContents = {}; 16 | for (const [i, content] of contents.entries()) { 17 | this.inputContents[inputs[i]] = content; 18 | } 19 | }); 20 | 21 | for (const f of inputs) { 22 | it(f, function () { 23 | const text = this.inputContents[f]; 24 | if (text.length === 0) return; 25 | const doc = JSON.parse(text.replace(/%EVMVERSION%/g, JSON.stringify('berlin'))); 26 | // Some of these files are arrays so we use concat to treat them uniformly. 27 | const asts = [].concat(doc); 28 | for (const ast of asts) { 29 | assertValid(ast); 30 | } 31 | }); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /test/sources/asm-0.6.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Asm { 4 | uint s; 5 | 6 | function foo() external { 7 | assembly { 8 | let x := s_slot 9 | let y := s_offset 10 | 11 | function fail() { 12 | revert(0, 0) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/sources/asm-0.7.5.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.7.5; 2 | 3 | contract Asm { 4 | function foo(bytes calldata b) external { 5 | assembly { 6 | let z := b.length 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/sources/asm-0.7.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.7; 2 | 3 | contract Asm { 4 | uint s; 5 | 6 | function foo() external { 7 | assembly { 8 | let x := s.slot 9 | let y := s.offset 10 | 11 | function fail() { 12 | revert(0, 0) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/sources/ast-deref-2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity *; 4 | 5 | contract D { 6 | function bar() external { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/sources/ast-deref.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity *; 4 | 5 | contract C1 { 6 | function foo() external { 7 | } 8 | } 9 | 10 | contract C2 is C1 {} 11 | contract C3 is C1, C2 {} 12 | -------------------------------------------------------------------------------- /test/sources/basic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // PragmaDirective 4 | pragma solidity *; 5 | 6 | // ImportDirective 7 | import { Import } from 'import.sol'; 8 | 9 | // ImportDirective.symbolAliases[].local 10 | import { Import as Renamed } from 'import.sol'; 11 | 12 | // ImportDirective.unitAlias 13 | import 'import.sol' as imp; 14 | 15 | // ContractDefinition 16 | contract Foo { 17 | 18 | // UsingForDirective 19 | using Lib for Struct; 20 | using Lib for *; 21 | 22 | // EventDefinition 23 | event Ev(uint); 24 | 25 | // VariableDeclaration 26 | // BinaryOperation (.operator = >>) 27 | uint x1 = 2 >> 1; 28 | 29 | // ArrayTypeName (.length = Literal) 30 | uint[4] xs1; 31 | 32 | // ArrayTypeName (.length = Expression) 33 | uint[4+2] xs2; 34 | 35 | // FunctionTypeName 36 | function (uint, uint) internal returns (uint, uint) f; 37 | 38 | // Mapping 39 | mapping (uint => string) mp; 40 | 41 | // StructDefinition 42 | struct Struct { uint m1; uint m2; } 43 | 44 | // EnumDefinition 45 | enum Enum { E1, E2 } 46 | 47 | uint constant x2 = 4; 48 | uint immutable x3 = 4; 49 | 50 | // Literal (.kind = hexString) 51 | bytes public ff = hex"ff"; 52 | 53 | // FunctionDefinition 54 | function foo1() public returns (uint) { 55 | 56 | // EmitStatement 57 | emit Ev(4); 58 | 59 | // VariableDeclarationStatement 60 | uint v = 4; 61 | uint u; u; 62 | 63 | // ExpressionStatement 64 | // Assignment 65 | // Conditional 66 | v = true ? 4 : 2; 67 | 68 | // ElementaryTypeNameExpression 69 | v = uint(4+2); 70 | 71 | // FunctionCall 72 | foo2(); 73 | 74 | // FunctionCallOptions 75 | // MemberAccess 76 | this.foo4{ value: 1 }(); 77 | 78 | // IndexAccess 79 | v = xs1[0]; 80 | uint[] memory xs = abi.decode(msg.data, (uint[])); 81 | 82 | // NewExpression 83 | new X(); 84 | 85 | // TupleExpression 86 | (int y, uint z) = (1, 3); 87 | (y,) = (1, 3); 88 | y; z; 89 | 90 | // UnaryOperation 91 | y = -3; 92 | y = ~int8(8); 93 | 94 | // ForStatement 95 | for (uint i = 0; i < 10; i++) {} 96 | for (uint i = 0; i < 10; i++) z = i; 97 | for (;;) {} 98 | for (y = 0; ; ) {} 99 | 100 | // IfStatement 101 | if (false) v; else v; 102 | if (false) {} else {} 103 | if (false) {} 104 | 105 | // WhileStatement 106 | while (true) { 107 | continue; 108 | break; 109 | } 110 | 111 | do { } while (true); 112 | 113 | Struct memory s1 = Struct(1, 2); 114 | Struct memory s2 = Struct({ m1: 1, m2: 2 }); 115 | s1; s2; 116 | 117 | try this.foo1() { } catch Error(string memory) { } catch (bytes memory) { } 118 | 119 | { 120 | y = 5; 121 | y += 1; 122 | y -= 2; 123 | y *= 1; 124 | y /= 2; 125 | y %= 2; 126 | y |= 2; 127 | y &= 2; 128 | y ^= 2; 129 | y >>= 2; 130 | y <<= 3; 131 | } 132 | 133 | msg.data[:]; 134 | 135 | // Return 136 | return v; 137 | } 138 | 139 | // FunctionDefinition (.stateMutability = pure) 140 | function foo2() public pure {} 141 | 142 | // FunctionDefinition (.stateMutability = view, .visibility = internal) 143 | function foo3() internal view {} 144 | 145 | // FunctionDefinition (.stateMutability = payable, .visibility = external) 146 | function foo4() external payable {} 147 | 148 | // ModifierDefinition 149 | modifier mod(uint u) { 150 | _; 151 | } 152 | 153 | // ModifierInvocation 154 | function foo5() public pure mod(2) {} 155 | 156 | // .storageLocation = calldata 157 | function foo6(string calldata) external {} 158 | 159 | // .storageLocation = storage 160 | function foo7(string storage) internal {} 161 | } 162 | 163 | // InheritanceSpecifier 164 | contract X {} 165 | contract Y is X {} 166 | contract Z is X, Y {} 167 | 168 | // ContractDefinition (.abstract = true) 169 | abstract contract Abs { 170 | 171 | // FunctionDefinition (.implemented = false, .virtual = true) 172 | function abs1() public virtual; 173 | 174 | function x() view external virtual returns (uint) { return 0; } 175 | 176 | modifier foo() virtual { _; } 177 | } 178 | 179 | contract Con is Abs { 180 | function abs1() public override(Abs) {} 181 | 182 | uint public override x; 183 | 184 | modifier foo() override(Abs) { _; } 185 | } 186 | 187 | // ContractDefinition (.contractKind = interface) 188 | interface If {} 189 | 190 | // ContractDefinition (.contractKind = interface) 191 | library Lib {} 192 | 193 | // StructDefinition (under SourceUnit) 194 | struct S { uint m1; } 195 | 196 | // EnumDefinition (under SourceUnit) 197 | enum E { E1 } 198 | 199 | // StructuredDocumentation 200 | /// @dev doc 201 | contract Doc { 202 | /// @dev fun 203 | uint x; 204 | /// @dev fun 205 | function fun() public pure {} 206 | /// @dev Ev 207 | event Ev(); 208 | /// @dev mod 209 | modifier mod() { _; } 210 | } 211 | 212 | contract Asm { 213 | function fun() public pure { 214 | assembly { 215 | let x0 216 | let x1 := 1 217 | let x2 := "abc" 218 | let x3 := add(1, x0) 219 | let x4 := x3 220 | x4 := x1 221 | {} 222 | 223 | function f(x) -> r { 224 | r := 0 225 | leave 226 | } 227 | 228 | if false { 229 | revert(0, 0) 230 | } 231 | 232 | switch calldataload(4) 233 | case 0 { 234 | x0 := true 235 | } 236 | default { 237 | x0 := false 238 | } 239 | 240 | for { } false { } { 241 | break 242 | continue 243 | } 244 | } 245 | } 246 | } 247 | 248 | contract Subdenomination { 249 | // Literal.subdenomination 250 | uint constant internal subden_time = 1 seconds + 1 minutes + 1 hours + 1 days + 1 weeks; 251 | uint constant internal subden_ether = 1 wei + 1 ether; 252 | } 253 | -------------------------------------------------------------------------------- /test/sources/catch-panic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.1; 4 | 5 | contract CatchPanic { 6 | function foo() public { 7 | try this.foo() { 8 | } catch Panic (uint code) { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/sources/constructor-0.6.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // PragmaDirective 4 | pragma solidity <0.7.0; 5 | 6 | // InheritanceSpecifier 7 | contract X {} 8 | contract Y is X { 9 | constructor(uint) public {} 10 | } 11 | contract Z is X, Y(3) {} 12 | -------------------------------------------------------------------------------- /test/sources/constructor-0.7.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // PragmaDirective 4 | pragma solidity >=0.7.0; 5 | 6 | // InheritanceSpecifier 7 | contract X {} 8 | contract Y is X { 9 | constructor(uint) {} 10 | } 11 | contract Z is X, Y(3) {} 12 | 13 | contract A {} 14 | contract B is A { 15 | constructor(uint) {} 16 | } 17 | contract C is A, B { 18 | constructor(uint) A() B(3) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/sources/errors-require.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.26; 4 | 5 | /// @dev docs 6 | error InsufficientBalance(uint available, uint required); 7 | 8 | contract Foo { 9 | function bar(uint x) external { 10 | require(x == 0, InsufficientBalance({ available: 0, required: 0 })); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/sources/errors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.4; 4 | 5 | /// @dev docs 6 | error InsufficientBalance(uint available, uint required); 7 | 8 | contract Foo { 9 | error OhNo(); 10 | 11 | function bar(uint x) external { 12 | if (x > 0) { 13 | revert InsufficientBalance({ 14 | available: 0, 15 | required: 0 16 | }); 17 | } else { 18 | revert OhNo(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/sources/file-level-constant.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.7.4; 4 | 5 | uint constant EXPONENT = 10; 6 | uint constant MULTIPLIER = 2**EXPONENT; 7 | -------------------------------------------------------------------------------- /test/sources/find-all.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity *; 4 | 5 | import {Import} from "./import.sol"; 6 | 7 | struct S1 { 8 | uint x; 9 | } 10 | 11 | contract C1 { 12 | struct S2 { 13 | uint x; 14 | } 15 | 16 | function foo() external { 17 | assembly { 18 | let x := 0 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/sources/finney-szabo.sol: -------------------------------------------------------------------------------- 1 | pragma solidity <0.7.0; 2 | 3 | contract FinneySzabo { 4 | uint constant f = 1 finney; 5 | uint constant s = 1 szabo; 6 | } 7 | -------------------------------------------------------------------------------- /test/sources/free-function.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // PragmaDirective 4 | pragma solidity >=0.7.1; 5 | 6 | function min(uint8 x, uint8 y) pure returns (uint) { 7 | return x < y ? x : y; 8 | } 9 | 10 | function min(uint x, uint y) pure returns (uint) { 11 | return x < y ? x : y; 12 | } 13 | 14 | function sum(uint[] storage items) view returns (uint s) { 15 | for (uint i = 0; i < items.length; i++) 16 | s += items[i]; 17 | } 18 | 19 | function bar(uint x, uint y) { 20 | // Identifier.overloadedDeclarations 21 | min(x, y); 22 | } 23 | -------------------------------------------------------------------------------- /test/sources/gwei.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.11; 2 | 3 | contract Gwei { 4 | uint constant subden_gwei = 1 gwei; 5 | } 6 | -------------------------------------------------------------------------------- /test/sources/identifier-path.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | import * as M1 from './basic.sol'; 6 | 7 | contract C is M1.Abs { 8 | function abs1() override(M1.Abs) public { 9 | } 10 | } 11 | 12 | import * as M2 from './constructor-0.7.sol'; 13 | 14 | contract W1 is M2.Y { 15 | constructor() M2.Y(4) {} 16 | } 17 | 18 | contract W2 is M2.Y(4) { 19 | constructor() {} 20 | } 21 | -------------------------------------------------------------------------------- /test/sources/import.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity *; 4 | 5 | contract Import { 6 | } 7 | -------------------------------------------------------------------------------- /test/sources/modifier-no-body.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.7; 2 | 3 | abstract contract A { 4 | modifier foo() virtual; 5 | } 6 | -------------------------------------------------------------------------------- /test/sources/new-0.8.13.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.13; 2 | 3 | library Lib { 4 | } 5 | 6 | using Lib for uint; 7 | 8 | contract C { 9 | function foo() external { 10 | assembly ("memory-safe") { 11 | } 12 | 13 | /// @solidity memory-safe-assembly 14 | assembly { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/sources/new-0.8.14.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.14; 2 | 3 | contract C { 4 | event E(); 5 | 6 | bytes32 sel = E.selector; 7 | } 8 | -------------------------------------------------------------------------------- /test/sources/new-0.8.18.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.18; 2 | 3 | contract C { 4 | mapping (address a => uint b) m; 5 | } 6 | -------------------------------------------------------------------------------- /test/sources/new-0.8.19.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.19; 2 | 3 | type Int is int; 4 | 5 | using {add as +, negate as -} for Int global; 6 | 7 | function add(Int a, Int b) pure returns (Int) { 8 | return Int.wrap(Int.unwrap(a) + Int.unwrap(b)); 9 | } 10 | 11 | function negate(Int a) pure returns (Int) { 12 | return Int.wrap(-Int.unwrap(a)); 13 | } 14 | 15 | function test(Int a, Int b) pure returns (Int) { 16 | return a + (-b); 17 | } 18 | -------------------------------------------------------------------------------- /test/sources/new-0.8.20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.20; 2 | 3 | contract T { 4 | event E(); 5 | 6 | function test() external { 7 | // ContractDefinition.usedEvents 8 | emit E(); 9 | 10 | // ContractDefinition.internalFunctionIDs 11 | (foo)(); 12 | } 13 | 14 | function foo() internal { 15 | } 16 | 17 | 18 | // StructDefinition.documentation 19 | /// docs 20 | struct A { 21 | uint x; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/sources/new-0.8.21.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.21; 2 | 3 | pragma experimental solidity; 4 | -------------------------------------------------------------------------------- /test/sources/new-0.8.8.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.8; 2 | 3 | // Enum min and max 4 | contract T { 5 | enum E { A, B } 6 | function minmax(bool b) external returns (E) { 7 | return b ? type(E).min : type(E).max; 8 | } 9 | } 10 | 11 | 12 | // User defined value types 13 | type Price is uint128; 14 | contract UDVT { 15 | type Quantity is uint128; 16 | } 17 | -------------------------------------------------------------------------------- /test/sources/only-0.8.13.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.13; 2 | 3 | library Lib { 4 | } 5 | 6 | using Lib for uint global; 7 | -------------------------------------------------------------------------------- /test/sources/src-decoder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity *; 4 | 5 | contract Line5 {} 6 | 7 | // よろしくお願いします 8 | contract Line8 {} 9 | -------------------------------------------------------------------------------- /test/sources/stmt-docs-0.8.4.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | contract C { 5 | error E(); 6 | function foo() public { 7 | /// abc 8 | revert E(); 9 | /// abc 10 | uint x = 3; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/sources/stmt-docs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.2; 3 | 4 | contract C { 5 | event E(); 6 | 7 | modifier DoesNothingModifier() { 8 | /// abc 9 | _; 10 | } 11 | 12 | function incr() external DoesNothingModifier() { 13 | /// abc 14 | {} 15 | /// abc 16 | emit E(); 17 | /// abc 18 | _beforeIncr(); 19 | /// abc 20 | for (;;) {} 21 | /// abc 22 | if (true) {} 23 | /// abc 24 | assembly {} 25 | /// abc 26 | try this.incr() {} catch {} 27 | /// abc 28 | unchecked {} 29 | /// abc 30 | while (true) {} 31 | /// abc 32 | return; 33 | } 34 | 35 | function _beforeIncr() internal virtual {} 36 | } 37 | -------------------------------------------------------------------------------- /test/sources/transient.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.27; 2 | 3 | contract Foo { 4 | uint256 transient x; 5 | } 6 | -------------------------------------------------------------------------------- /test/sources/unchecked.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | contract Foo { 6 | function foo() external returns (uint256 x) { 7 | unchecked { 8 | x += 1; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/sources/unicode-string.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.7.0; 4 | 5 | contract U { 6 | // Literal (.kind = unicodeString) 7 | string public un = unicode"イーサリアム"; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs/promises'); 2 | const path = require('path'); 3 | const assert = require('assert'); 4 | const fc = require('fast-check'); 5 | 6 | const { latest } = require('./helpers/solc-versions'); 7 | const { compile } = require('./helpers/solc-compile'); 8 | 9 | const { isNodeType, findAll, astDereferencer, srcDecoder } = require('../utils'); 10 | 11 | describe('isNodeType', function () { 12 | it('single', function () { 13 | assert(isNodeType('SourceUnit', { nodeType: 'SourceUnit' })); 14 | assert(!isNodeType('SourceUnit', { nodeType: 'ContractDefinition' })); 15 | }); 16 | 17 | it('multiple', function () { 18 | assert(isNodeType(['SourceUnit', 'ContractDefinition'], { nodeType: 'SourceUnit' })); 19 | assert(isNodeType(['SourceUnit', 'ContractDefinition'], { nodeType: 'ContractDefinition' })); 20 | assert(!isNodeType(['SourceUnit', 'ContractDefinition'], { nodeType: 'ImportDirective' })); 21 | }); 22 | 23 | it('single curried', function () { 24 | assert(isNodeType('SourceUnit')({ nodeType: 'SourceUnit' })); 25 | }); 26 | 27 | it('multiple curried', function () { 28 | const curried = isNodeType(['SourceUnit', 'ContractDefinition']); 29 | assert(curried({ nodeType: 'SourceUnit' })); 30 | assert(curried({ nodeType: 'ContractDefinition' })); 31 | }); 32 | }); 33 | 34 | describe('findAll', function () { 35 | const counts = { 36 | Block: 1, 37 | ContractDefinition: 1, 38 | ElementaryTypeName: 2, 39 | FunctionDefinition: 1, 40 | Identifier: 1, 41 | ImportDirective: 1, 42 | InlineAssembly: 1, 43 | ParameterList: 2, 44 | PragmaDirective: 1, 45 | SourceUnit: 1, 46 | StructDefinition: 2, 47 | VariableDeclaration: 2, 48 | YulBlock: 1, 49 | YulLiteral: 1, 50 | YulTypedName: 1, 51 | YulVariableDeclaration: 1, 52 | }; 53 | 54 | const starCount = Object.values(counts).reduce((a, b) => a + b, 0); 55 | 56 | before('reading and compiling source file', async function () { 57 | this.timeout(10 * 60 * 1000); 58 | const output = await compile(latest, { 59 | ['find-all.sol']: { content: await fs.readFile(path.join(__dirname, 'sources/find-all.sol'), 'utf8') }, 60 | ['import.sol']: { content: await fs.readFile(path.join(__dirname, 'sources/import.sol'), 'utf8') }, 61 | }); 62 | this.ast = output.sources['find-all.sol'].ast; 63 | }); 64 | 65 | it('basic', function () { 66 | for (const nodeType in counts) { 67 | const nodes = [...findAll(nodeType, this.ast)]; 68 | assert.strictEqual(nodes.length, counts[nodeType]); 69 | } 70 | }); 71 | 72 | it('curried', function () { 73 | const nodeType = 'StructDefinition'; 74 | const nodes = [...findAll(nodeType)(this.ast)]; 75 | assert.strictEqual(nodes.length, counts[nodeType]); 76 | }); 77 | 78 | it('multiple', function () { 79 | const nodeTypes = Object.keys(counts); 80 | fc.assert( 81 | fc.property(fc.shuffledSubarray(nodeTypes), nodeTypes => { 82 | const count = nodeTypes.map(t => counts[t]).reduce((a, b) => a + b, 0); 83 | const nodes = [...findAll(nodeTypes, this.ast)]; 84 | assert.strictEqual(nodes.length, count); 85 | }) 86 | ); 87 | }); 88 | 89 | it('star', function () { 90 | const nodes = [...findAll('*', this.ast)]; 91 | assert.strictEqual(nodes.length, starCount); 92 | }); 93 | }); 94 | 95 | describe('ast dereferencer', function () { 96 | const source = path.join(__dirname, 'sources/ast-deref.sol'); 97 | 98 | before('reading and compiling source file', async function () { 99 | this.timeout(10 * 60 * 1000); 100 | const content = await fs.readFile(source, 'utf8'); 101 | this.output = await compile(latest, { 0: { content } }); 102 | this.ast = this.output.sources[0].ast; 103 | }); 104 | 105 | it('finds contracts', function () { 106 | const deref = astDereferencer(this.output); 107 | for (const c of findAll('ContractDefinition', this.ast)) { 108 | assert.strictEqual(c, deref('ContractDefinition', c.id)); 109 | } 110 | }); 111 | 112 | it('finds functions', function () { 113 | const deref = astDereferencer(this.output); 114 | for (const c of findAll('FunctionDefinition', this.ast)) { 115 | assert.strictEqual(c, deref('FunctionDefinition', c.id)); 116 | } 117 | }); 118 | 119 | it('cache works', function () { 120 | const deref = astDereferencer(this.output); 121 | const [c1] = findAll('ContractDefinition', this.ast); 122 | assert.strictEqual(c1, deref('ContractDefinition', c1.id)); 123 | assert.strictEqual(c1, deref('ContractDefinition', c1.id)); 124 | }); 125 | 126 | it('errors on wrong type', function () { 127 | const deref = astDereferencer(this.output); 128 | const [c1] = findAll('ContractDefinition', this.ast); 129 | assert.throws( 130 | () => deref('FunctionDefinition', c1.id), 131 | { message: /^No node with id \d+ of type FunctionDefinition$/ }, 132 | ); 133 | }); 134 | 135 | it('errors on unknown id', function () { 136 | const deref = astDereferencer(this.output); 137 | assert.throws( 138 | () => deref('FunctionDefinition', 1e10), 139 | { message: /^No node with id \d+ of type FunctionDefinition$/ }, 140 | ); 141 | }); 142 | 143 | it('multiple node types', function () { 144 | const deref = astDereferencer(this.output); 145 | const [c1] = findAll('ContractDefinition', this.ast); 146 | const [f1] = findAll('FunctionDefinition', this.ast); 147 | assert.strictEqual(c1, deref(['ContractDefinition', 'FunctionDefinition'], c1.id)); 148 | assert.strictEqual(f1, deref(['ContractDefinition', 'FunctionDefinition'], f1.id)); 149 | }); 150 | 151 | it('curried', function () { 152 | const deref = astDereferencer(this.output); 153 | const c3 = [...findAll('ContractDefinition', this.ast)].find(c => c.name === 'C3'); 154 | const baseContracts = c3.linearizedBaseContracts.map(deref('ContractDefinition')); 155 | assert.deepEqual(baseContracts.map(c => c.name), ['C3', 'C2', 'C1']); 156 | }); 157 | }); 158 | 159 | describe('ast dereferencer with source unit', function () { 160 | const source0 = path.join(__dirname, 'sources/ast-deref.sol'); 161 | const source1 = path.join(__dirname, 'sources/ast-deref-2.sol'); 162 | 163 | before('reading and compiling source file', async function () { 164 | this.timeout(10 * 60 * 1000); 165 | const content0 = await fs.readFile(source0, 'utf8'); 166 | const content1 = await fs.readFile(source1, 'utf8'); 167 | this.output = await compile(latest, { 0: { content: content0 }, 1: { content: content1 } }); 168 | this.ast = this.output.sources[0].ast; 169 | }); 170 | 171 | 172 | it('finds contracts', function () { 173 | const deref = astDereferencer(this.output); 174 | for (const { ast } of Object.values(this.output.sources)) { 175 | for (const c of findAll('ContractDefinition', ast)) { 176 | const { node, sourceUnit } = deref.withSourceUnit('ContractDefinition', c.id); 177 | assert.strictEqual(c, node); 178 | assert.strictEqual(sourceUnit, ast); 179 | } 180 | } 181 | }); 182 | }); 183 | 184 | describe('src decoder', function () { 185 | const source = path.join(__dirname, 'sources/src-decoder.sol'); 186 | 187 | before('reading and compiling source file', async function () { 188 | this.timeout(10 * 60 * 1000); 189 | const content = await fs.readFile(source, 'utf8'); 190 | this.input = { sources: { 'file.sol': { content } } }; 191 | this.output = await compile(latest, this.input.sources); 192 | this.ast = this.output.sources['file.sol'].ast; 193 | }); 194 | 195 | it('ascii', function () { 196 | const decodeSrc = srcDecoder(this.input, this.output); 197 | const line5 = [...findAll('ContractDefinition', this.ast)].find(c => c.name === 'Line5'); 198 | assert.strictEqual(decodeSrc(line5), 'file.sol:5'); 199 | }); 200 | 201 | it('multi-byte utf8', function () { 202 | const decodeSrc = srcDecoder(this.input, this.output); 203 | const line8 = [...findAll('ContractDefinition', this.ast)].find(c => c.name === 'Line8'); 204 | assert.strictEqual(decodeSrc(line8), 'file.sol:8'); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "moduleResolution": "node", 5 | "target": "es2022", 6 | "lib": ["es2022", "es2023.array"], 7 | "downlevelIteration": true, 8 | "strict": true, 9 | "module": "commonjs", 10 | "esModuleInterop": true, 11 | }, 12 | "ts-node": { 13 | "transpileOnly": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "strict": true 5 | }, 6 | "files": [ 7 | "types.d.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.emit.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationMap": true 10 | }, 11 | "include": [ 12 | "src/**/*" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "allowJs": true, 6 | "noImplicitAny": false 7 | }, 8 | "include": [ 9 | "scripts/*.js", 10 | "*.d.ts" 11 | ], 12 | "references": [ 13 | { "path": "./tsconfig.docs.json" }, 14 | { "path": "./tsconfig.emit.json" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /typedoc.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-color-background: rgb(248 250 252); 3 | --light-color-background-secondary: rgb(241 245 249); 4 | --light-color-accent: rgb(226 232 240); 5 | } 6 | 7 | * { 8 | scrollbar-width: unset; 9 | scrollbar-color: unset; 10 | } 11 | 12 | .tsd-page-toolbar { 13 | background: var(--color-background); 14 | } 15 | 16 | #tsd-search.has-focus { 17 | background: var(--color-background-secondary); 18 | } 19 | 20 | .tsd-filter-visibility, .tsd-hierarchy { 21 | display: none; 22 | } 23 | 24 | .tsd-signature { 25 | padding: 0; 26 | border: none; 27 | } 28 | 29 | .tsd-navigation a.current, .tsd-page-navigation a.current { 30 | border-radius: calc(6px + 0.25rem); 31 | padding-right: 0.5rem; 32 | } 33 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": [ 3 | "./types.d.ts" 4 | ], 5 | "out": "docs", 6 | "name": "Solidity AST", 7 | "customCss": "./typedoc.css", 8 | "disableSources": true, 9 | "excludeExternals": true 10 | } 11 | -------------------------------------------------------------------------------- /utils.d.ts: -------------------------------------------------------------------------------- 1 | import { SolcInput, SolcOutput } from './solc'; 2 | import { Node, NodeType, NodeTypeMap } from './node'; 3 | import { SourceUnit } from './types'; 4 | 5 | import { isNodeType, ExtendedNodeType, ExtendedNodeTypeMap } from './utils/is-node-type'; 6 | export { isNodeType, ExtendedNodeType, ExtendedNodeTypeMap }; 7 | 8 | export { findAll } from './utils/find-all'; 9 | 10 | export interface NodeWithSourceUnit { 11 | node: N; 12 | sourceUnit: SourceUnit; 13 | } 14 | 15 | export interface ASTDereferencer { 16 | (nodeType: T | readonly T[]): (id: number) => ExtendedNodeTypeMap[T]; 17 | (nodeType: T | readonly T[], id: number): ExtendedNodeTypeMap[T]; 18 | withSourceUnit(nodeType: T | readonly T[], id: number): NodeWithSourceUnit; 19 | } 20 | 21 | export function astDereferencer(solcOutput: SolcOutput): ASTDereferencer; 22 | 23 | export class ASTDereferencerError extends Error { 24 | readonly id: number; 25 | readonly nodeType: readonly ExtendedNodeType[]; 26 | } 27 | 28 | export type SrcDecoder = (node: { src: string }) => string; 29 | 30 | export function srcDecoder(solcInput: SolcInput, solcOutput: SolcOutput): SrcDecoder; 31 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | function curry2(fn) { 2 | return function (nodeType, ...args) { 3 | if (args.length === 0) { 4 | return node => fn(nodeType, node); 5 | } else { 6 | return fn(nodeType, ...args); 7 | } 8 | }; 9 | } 10 | 11 | module.exports.isNodeType = curry2(require('./utils/is-node-type').isNodeType); 12 | module.exports.findAll = curry2(require('./utils/find-all').findAll); 13 | 14 | const { astDereferencer, ASTDereferencerError } = require('./dist/ast-dereferencer'); 15 | module.exports.astDereferencer = astDereferencer; 16 | module.exports.ASTDereferencerError = ASTDereferencerError; 17 | 18 | const { srcDecoder } = require('./dist/src-decoder'); 19 | module.exports.srcDecoder = srcDecoder; 20 | -------------------------------------------------------------------------------- /utils/find-all.d.ts: -------------------------------------------------------------------------------- 1 | import { Node, YulNode, YulNodeType, YulNodeTypeMap } from '../node'; 2 | import { ExtendedNodeType, ExtendedNodeTypeMap } from './is-node-type'; 3 | 4 | export function findAll(nodeType: T | readonly T[]): (node: Node) => Generator; 5 | export function findAll(nodeType: T | readonly T[], node: Node, prune?: (node: Node) => boolean): Generator; 6 | 7 | export function findAll(nodeType: T | readonly T[]): (node: Node | YulNode) => Generator<(ExtendedNodeTypeMap & YulNodeTypeMap)[T]>; 8 | export function findAll(nodeType: T | readonly T[], node: Node | YulNode, prune?: (node: Node | YulNode) => boolean): Generator<(ExtendedNodeTypeMap & YulNodeTypeMap)[T]>; 9 | 10 | -------------------------------------------------------------------------------- /utils/find-all.js: -------------------------------------------------------------------------------- 1 | const { isNodeType } = require('./is-node-type'); 2 | const finder = require('../finder.json'); 3 | 4 | const nextPropsCache = new Map(); 5 | 6 | function* findAll(nodeType, node, prune) { 7 | let cache; 8 | 9 | if (Array.isArray(nodeType)) { 10 | const cacheKey = JSON.stringify(nodeType); 11 | cache = nextPropsCache.get(cacheKey); 12 | if (!cache) { 13 | cache = {}; 14 | nextPropsCache.set(cacheKey, cache); 15 | } 16 | } 17 | 18 | const queue = []; 19 | const push = node => queue.push({ node, props: getNextProps(nodeType, node.nodeType ?? '$other', cache) }); 20 | 21 | push(node); 22 | 23 | for (let i = 0; i < queue.length; i++) { 24 | const { node, props } = queue[i]; 25 | 26 | if (typeof node !== 'object' || (prune && prune(node))) { 27 | continue; 28 | } 29 | 30 | if (isNodeType(nodeType, node)) { 31 | yield node; 32 | } 33 | 34 | for (let j = 0; j < props.length; j++) { 35 | const member = node[props[j]]; 36 | if (Array.isArray(member)) { 37 | for (const sub2 of member) { 38 | if (sub2) { 39 | push(sub2); 40 | } 41 | } 42 | } else if (member) { 43 | push(member); 44 | } 45 | } 46 | } 47 | } 48 | 49 | function getNextProps(wantedNodeTypes, currentNodeType, cache) { 50 | if (typeof wantedNodeTypes === 'string') { 51 | return finder[wantedNodeTypes]?.[currentNodeType] ?? []; 52 | } 53 | if (currentNodeType in cache) { 54 | return cache[currentNodeType]; 55 | } 56 | const next = []; 57 | for (const wantedNodeType of wantedNodeTypes) { 58 | const wantedFinder = finder[wantedNodeType]; 59 | if (wantedFinder && currentNodeType in wantedFinder) { 60 | for (const nextNodeType of wantedFinder[currentNodeType]) { 61 | if (!next.includes(nextNodeType)) { 62 | next.push(nextNodeType); 63 | } 64 | } 65 | } 66 | } 67 | cache[currentNodeType] = next; 68 | return next; 69 | } 70 | 71 | module.exports = { 72 | findAll, 73 | }; 74 | -------------------------------------------------------------------------------- /utils/is-node-type.d.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeType, NodeTypeMap } from '../node'; 2 | 3 | export type ExtendedNodeType = '*' | NodeType; 4 | 5 | export interface ExtendedNodeTypeMap extends NodeTypeMap { 6 | '*': Node; 7 | } 8 | 9 | export function isNodeType(nodeType: T | readonly T[]): (node: N) => node is N & ExtendedNodeTypeMap[T]; 10 | export function isNodeType(nodeType: T | readonly T[], node: N): node is N & ExtendedNodeTypeMap[T]; 11 | 12 | -------------------------------------------------------------------------------- /utils/is-node-type.js: -------------------------------------------------------------------------------- 1 | function isNodeType(nodeType, node) { 2 | return nodeType === node.nodeType || 3 | (nodeType === "*" && node.nodeType != undefined) || 4 | (Array.isArray(nodeType) && nodeType.includes(node.nodeType)); 5 | } 6 | 7 | module.exports = { 8 | isNodeType, 9 | }; 10 | --------------------------------------------------------------------------------