├── .config └── nextest.toml ├── .env.example ├── .envrc ├── .github ├── ISSUE_TEMPLATE │ ├── ✨-feature-request.md │ └── 🐛-bug-report.md ├── dependabot.yml └── workflows │ ├── benches.yml │ ├── main.yml │ ├── release-plz.yml │ └── release.yml ├── .gitignore ├── .lintspec.toml ├── .nsignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── action.yml ├── benches ├── lint.rs └── parser.rs ├── cliff.toml ├── deny.toml ├── dist-workspace.toml ├── flake.lock ├── flake.nix ├── release-plz.toml ├── rust-toolchain.toml ├── screenshot.png ├── src ├── config.rs ├── definitions │ ├── constructor.rs │ ├── enumeration.rs │ ├── error.rs │ ├── event.rs │ ├── function.rs │ ├── mod.rs │ ├── modifier.rs │ ├── structure.rs │ └── variable.rs ├── error.rs ├── files.rs ├── lib.rs ├── lint.rs ├── main.rs ├── natspec.rs ├── parser │ ├── mod.rs │ ├── slang.rs │ └── solar.rs └── utils.rs ├── test-data ├── BasicSample.sol ├── Fuzzers.sol ├── InterfaceSample.sol ├── LatestVersion.sol ├── LibrarySample.sol ├── ParserTest.sol └── UnsupportedVersion.sol └── tests ├── common.rs ├── snapshots ├── tests_basic_sample__all.snap ├── tests_basic_sample__all_no_inheritdoc.snap ├── tests_basic_sample__basic.snap ├── tests_basic_sample__constructor.snap ├── tests_basic_sample__enum.snap ├── tests_basic_sample__inheritdoc.snap ├── tests_basic_sample__struct.snap ├── tests_fuzzers__fuzzers.snap ├── tests_interface_sample__interface.snap ├── tests_library_sample__library.snap ├── tests_parser_test__basic.snap ├── tests_parser_test__constructor.snap ├── tests_parser_test__enum.snap ├── tests_parser_test__inheritdoc.snap └── tests_parser_test__struct.snap ├── tests-basic-sample.rs ├── tests-fuzzers.rs ├── tests-interface-sample.rs ├── tests-library-sample.rs ├── tests-parser-test.rs └── tests-solar.rs /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | slow-timeout = { period = "20s", terminate-after = 3 } 3 | fail-fast = false 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | LINTSPEC_PATHS=[path/to/file.sol,path/to/dir] # paths to files and folders to analyze 2 | LINTSPEC_EXCLUDE=[path/to/ignore] # paths to files or folders to exclude, see also `.nsignore` 3 | LINTSPEC_INHERITDOC=true # enforce that all overridden, public and external items have `@inheritdoc` 4 | LINTSPEC_CONSTRUCTOR=false # enforce that constructors have natspec 5 | LINTSPEC_STRUCT_PARAMS=false # enforce that structs have `@param` for each member 6 | LINTSPEC_ENUM_PARAMS=false # enforce that enums have `@param` for each variant 7 | LINTSPEC_ENFORCE=[variable,struct] # enforce NatSpec on items even if they don't have params/returns/members 8 | LINTSPEC_ENFORCE_ALL=true # same as passing all possible values to `enforce`. Remove if using LINTSPEC_ENFORCE. 9 | LINTSPEC_JSON=false # output diagnostics as JSON 10 | LINTSPEC_COMPACT=false # compact output (minified JSON or compact text) 11 | LINTSPEC_SORT=false # sort results by file path 12 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake . --impure 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/✨-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "✨ Feature request" 3 | about: Suggest an idea for lintspec 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🐛-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help us improve lintspec 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Create the following Solidity file: '...' 16 | 1. Run the following lintspec command: '...' 17 | 1. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Version:** 26 | Lintspec version (output of `lintspec --version`): 27 | 28 | **Desktop (please complete the following information):** 29 | - OS: [e.g. Ubuntu] 30 | - Version [e.g. 24.10] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/benches.yml: -------------------------------------------------------------------------------- 1 | name: CodSpeed 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | # `workflow_dispatch` allows CodSpeed to trigger backtest 8 | # performance analysis in order to generate initial data. 9 | workflow_dispatch: 10 | 11 | jobs: 12 | benchmarks: 13 | name: Run benchmarks 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Setup rust toolchain, cache and cargo-codspeed binary 19 | uses: moonrepo/setup-rust@v1 20 | with: 21 | channel: stable 22 | cache-target: release 23 | bins: cargo-codspeed 24 | 25 | - name: Build the benchmark target(s) 26 | run: cargo codspeed build -F solar 27 | 28 | - name: Run the benchmarks 29 | uses: CodSpeedHQ/action@v3 30 | with: 31 | run: cargo codspeed run 32 | token: ${{ secrets.CODSPEED_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main workflow 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build-test: 13 | strategy: 14 | matrix: 15 | platform: [ubuntu-latest, windows-latest, macos-latest] 16 | runs-on: ${{ matrix.platform }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: dtolnay/rust-toolchain@stable 20 | - uses: taiki-e/install-action@nextest 21 | - uses: taiki-e/install-action@cargo-hack 22 | - name: Run tests 23 | run: cargo hack nextest run --feature-powerset --depth 2 24 | 25 | doctests: 26 | runs-on: ubuntu-latest 27 | timeout-minutes: 30 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: dtolnay/rust-toolchain@stable 31 | - uses: Swatinem/rust-cache@v2 32 | with: 33 | cache-on-failure: true 34 | - run: cargo test --doc 35 | 36 | clippy: 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 30 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: dtolnay/rust-toolchain@stable 42 | with: 43 | components: clippy 44 | - uses: Swatinem/rust-cache@v2 45 | with: 46 | cache-on-failure: true 47 | - run: cargo clippy --all-targets --all-features 48 | env: 49 | RUSTFLAGS: -Dwarnings 50 | 51 | docs: 52 | runs-on: ubuntu-latest 53 | timeout-minutes: 30 54 | steps: 55 | - uses: actions/checkout@v4 56 | - uses: dtolnay/rust-toolchain@stable 57 | - uses: Swatinem/rust-cache@v2 58 | with: 59 | cache-on-failure: true 60 | - run: cargo doc --all-features --no-deps --document-private-items 61 | env: 62 | RUSTDOCFLAGS: '--cfg docsrs -D warnings' 63 | 64 | fmt: 65 | runs-on: ubuntu-latest 66 | timeout-minutes: 30 67 | steps: 68 | - uses: actions/checkout@v4 69 | - uses: dtolnay/rust-toolchain@stable 70 | with: 71 | components: rustfmt 72 | - run: cargo fmt --all --check 73 | 74 | feature-checks: 75 | runs-on: ubuntu-latest 76 | timeout-minutes: 30 77 | steps: 78 | - uses: actions/checkout@v4 79 | - uses: dtolnay/rust-toolchain@stable 80 | - uses: taiki-e/install-action@cargo-hack 81 | - uses: Swatinem/rust-cache@v2 82 | with: 83 | cache-on-failure: true 84 | - name: cargo hack 85 | run: cargo hack check --feature-powerset --depth 2 86 | 87 | deny: 88 | runs-on: ubuntu-latest 89 | timeout-minutes: 30 90 | steps: 91 | - uses: actions/checkout@v4 92 | - uses: dtolnay/rust-toolchain@stable 93 | - uses: taiki-e/install-action@cargo-deny 94 | - run: cargo deny check all 95 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | 14 | release-plz-release: 15 | name: Release-plz release 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | steps: 20 | - name: Generate GitHub token 21 | uses: actions/create-github-app-token@v2 22 | id: generate-token 23 | with: 24 | app-id: ${{ secrets.APP_ID }} 25 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | token: ${{ steps.generate-token.outputs.token }} 31 | - name: Install Rust toolchain 32 | uses: dtolnay/rust-toolchain@stable 33 | - name: Run release-plz 34 | uses: release-plz/action@v0.5 35 | with: 36 | command: release 37 | env: 38 | GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} 39 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 40 | 41 | release-plz-pr: 42 | name: Release-plz PR 43 | runs-on: ubuntu-latest 44 | permissions: 45 | contents: write 46 | pull-requests: write 47 | concurrency: 48 | group: release-plz-${{ github.ref }} 49 | cancel-in-progress: false 50 | steps: 51 | - name: Generate GitHub token 52 | uses: actions/create-github-app-token@v2 53 | id: generate-token 54 | with: 55 | app-id: ${{ secrets.APP_ID }} 56 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | with: 60 | fetch-depth: 0 61 | token: ${{ steps.generate-token.outputs.token }} 62 | - name: Install Rust toolchain 63 | uses: dtolnay/rust-toolchain@stable 64 | - name: Run release-plz 65 | uses: release-plz/action@v0.5 66 | with: 67 | command: release-pr 68 | env: 69 | GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} 70 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-22.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | submodules: recursive 62 | - name: Install dist 63 | # we specify bash to get pipefail; it guards against the `curl` command 64 | # failing. otherwise `sh` won't catch that `curl` returned non-0 65 | shell: bash 66 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh" 67 | - name: Cache dist 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: cargo-dist-cache 71 | path: ~/.cargo/bin/dist 72 | # sure would be cool if github gave us proper conditionals... 73 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 74 | # functionality based on whether this is a pull_request, and whether it's from a fork. 75 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 76 | # but also really annoying to build CI around when it needs secrets to work right.) 77 | - id: plan 78 | run: | 79 | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 80 | echo "dist ran successfully" 81 | cat plan-dist-manifest.json 82 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 83 | - name: "Upload dist-manifest.json" 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: artifacts-plan-dist-manifest 87 | path: plan-dist-manifest.json 88 | 89 | # Build and packages all the platform-specific things 90 | build-local-artifacts: 91 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 92 | # Let the initial task tell us to not run (currently very blunt) 93 | needs: 94 | - plan 95 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 96 | strategy: 97 | fail-fast: false 98 | # Target platforms/runners are computed by dist in create-release. 99 | # Each member of the matrix has the following arguments: 100 | # 101 | # - runner: the github runner 102 | # - dist-args: cli flags to pass to dist 103 | # - install-dist: expression to run to install dist on the runner 104 | # 105 | # Typically there will be: 106 | # - 1 "global" task that builds universal installers 107 | # - N "local" tasks that build each platform's binaries and platform-specific installers 108 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 109 | runs-on: ${{ matrix.runner }} 110 | container: ${{ matrix.container && matrix.container.image || null }} 111 | env: 112 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 113 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 114 | steps: 115 | - name: enable windows longpaths 116 | run: | 117 | git config --global core.longpaths true 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: recursive 121 | - name: Install Rust non-interactively if not already installed 122 | if: ${{ matrix.container }} 123 | run: | 124 | if ! command -v cargo > /dev/null 2>&1; then 125 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 126 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 127 | fi 128 | - name: Install dist 129 | run: ${{ matrix.install_dist.run }} 130 | # Get the dist-manifest 131 | - name: Fetch local artifacts 132 | uses: actions/download-artifact@v4 133 | with: 134 | pattern: artifacts-* 135 | path: target/distrib/ 136 | merge-multiple: true 137 | - name: Install dependencies 138 | run: | 139 | ${{ matrix.packages_install }} 140 | - name: Build artifacts 141 | run: | 142 | # Actually do builds and make zips and whatnot 143 | dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 144 | echo "dist ran successfully" 145 | - id: cargo-dist 146 | name: Post-build 147 | # We force bash here just because github makes it really hard to get values up 148 | # to "real" actions without writing to env-vars, and writing to env-vars has 149 | # inconsistent syntax between shell and powershell. 150 | shell: bash 151 | run: | 152 | # Parse out what we just built and upload it to scratch storage 153 | echo "paths<> "$GITHUB_OUTPUT" 154 | dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" 155 | echo "EOF" >> "$GITHUB_OUTPUT" 156 | 157 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 158 | - name: "Upload artifacts" 159 | uses: actions/upload-artifact@v4 160 | with: 161 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 162 | path: | 163 | ${{ steps.cargo-dist.outputs.paths }} 164 | ${{ env.BUILD_MANIFEST_NAME }} 165 | 166 | # Build and package all the platform-agnostic(ish) things 167 | build-global-artifacts: 168 | needs: 169 | - plan 170 | - build-local-artifacts 171 | runs-on: "ubuntu-22.04" 172 | env: 173 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 174 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 175 | steps: 176 | - uses: actions/checkout@v4 177 | with: 178 | submodules: recursive 179 | - name: Install cached dist 180 | uses: actions/download-artifact@v4 181 | with: 182 | name: cargo-dist-cache 183 | path: ~/.cargo/bin/ 184 | - run: chmod +x ~/.cargo/bin/dist 185 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 186 | - name: Fetch local artifacts 187 | uses: actions/download-artifact@v4 188 | with: 189 | pattern: artifacts-* 190 | path: target/distrib/ 191 | merge-multiple: true 192 | - id: cargo-dist 193 | shell: bash 194 | run: | 195 | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 196 | echo "dist ran successfully" 197 | 198 | # Parse out what we just built and upload it to scratch storage 199 | echo "paths<> "$GITHUB_OUTPUT" 200 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 201 | echo "EOF" >> "$GITHUB_OUTPUT" 202 | 203 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 204 | - name: "Upload artifacts" 205 | uses: actions/upload-artifact@v4 206 | with: 207 | name: artifacts-build-global 208 | path: | 209 | ${{ steps.cargo-dist.outputs.paths }} 210 | ${{ env.BUILD_MANIFEST_NAME }} 211 | # Determines if we should publish/announce 212 | host: 213 | needs: 214 | - plan 215 | - build-local-artifacts 216 | - build-global-artifacts 217 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 218 | if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 219 | env: 220 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 221 | runs-on: "ubuntu-22.04" 222 | outputs: 223 | val: ${{ steps.host.outputs.manifest }} 224 | steps: 225 | - uses: actions/checkout@v4 226 | with: 227 | submodules: recursive 228 | - name: Install cached dist 229 | uses: actions/download-artifact@v4 230 | with: 231 | name: cargo-dist-cache 232 | path: ~/.cargo/bin/ 233 | - run: chmod +x ~/.cargo/bin/dist 234 | # Fetch artifacts from scratch-storage 235 | - name: Fetch artifacts 236 | uses: actions/download-artifact@v4 237 | with: 238 | pattern: artifacts-* 239 | path: target/distrib/ 240 | merge-multiple: true 241 | - id: host 242 | shell: bash 243 | run: | 244 | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 245 | echo "artifacts uploaded and released successfully" 246 | cat dist-manifest.json 247 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 248 | - name: "Upload dist-manifest.json" 249 | uses: actions/upload-artifact@v4 250 | with: 251 | # Overwrite the previous copy 252 | name: artifacts-dist-manifest 253 | path: dist-manifest.json 254 | # Create a GitHub Release while uploading all files to it 255 | - name: "Download GitHub Artifacts" 256 | uses: actions/download-artifact@v4 257 | with: 258 | pattern: artifacts-* 259 | path: artifacts 260 | merge-multiple: true 261 | - name: Cleanup 262 | run: | 263 | # Remove the granular manifests 264 | rm -f artifacts/*-dist-manifest.json 265 | - name: Create GitHub Release 266 | env: 267 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 268 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 269 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 270 | RELEASE_COMMIT: "${{ github.sha }}" 271 | run: | 272 | # Write and read notes from a file to avoid quoting breaking things 273 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 274 | 275 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 276 | 277 | announce: 278 | needs: 279 | - plan 280 | - host 281 | # use "always() && ..." to allow us to wait for all publish jobs while 282 | # still allowing individual publish jobs to skip themselves (for prereleases). 283 | # "host" however must run to completion, no skipping allowed! 284 | if: ${{ always() && needs.host.result == 'success' }} 285 | runs-on: "ubuntu-22.04" 286 | env: 287 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 288 | steps: 289 | - uses: actions/checkout@v4 290 | with: 291 | submodules: recursive 292 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /result 3 | 4 | /.direnv 5 | 6 | .env 7 | .env.* 8 | !.env.example 9 | .vscode 10 | .wake 11 | .cargo 12 | -------------------------------------------------------------------------------- /.lintspec.toml: -------------------------------------------------------------------------------- 1 | [lintspec] 2 | paths = [] # paths to files and folders to analyze 3 | exclude = [] # paths to files or folders to exclude, see also `.nsignore` 4 | inheritdoc = true # enforce that all overridden, public and external items have `@inheritdoc` 5 | notice_or_dev = false # do not distinguish between `@notice` and `@dev` when considering "required" validation rules 6 | skip_version_detection = false # skip detection of the Solidity version from pragma statements and use the latest 7 | 8 | [output] 9 | # out = "" # if provided, redirects output to this file 10 | json = false # output diagnostics as JSON 11 | compact = false # compact output (minified JSON or compact text) 12 | sort = false # sort results by file path 13 | 14 | [constructor] 15 | notice = "ignored" # since constructors rarely have another purpose than deployment, `@notice` is optional 16 | dev = "ignored" 17 | param = "required" 18 | 19 | [enum] 20 | notice = "required" 21 | dev = "ignored" 22 | param = "ignored" # `@param` on enums is not in the official spec 23 | 24 | [error] 25 | notice = "required" 26 | dev = "ignored" 27 | param = "required" 28 | 29 | [event] 30 | notice = "required" 31 | dev = "ignored" 32 | param = "required" 33 | 34 | [function.private] 35 | notice = "required" 36 | dev = "ignored" 37 | param = "required" 38 | return = "required" 39 | 40 | [function.internal] 41 | notice = "required" 42 | dev = "ignored" 43 | param = "required" 44 | return = "required" 45 | 46 | [function.public] 47 | notice = "required" 48 | dev = "ignored" 49 | param = "required" 50 | return = "required" 51 | 52 | [function.external] 53 | notice = "required" 54 | dev = "ignored" 55 | param = "required" 56 | return = "required" 57 | 58 | [modifier] 59 | notice = "required" 60 | dev = "ignored" 61 | param = "required" 62 | 63 | [struct] 64 | notice = "required" 65 | dev = "ignored" 66 | param = "ignored" # `@param` on structs is not in the official spec 67 | 68 | [variable.private] 69 | notice = "required" 70 | dev = "ignored" 71 | 72 | [variable.internal] 73 | notice = "required" 74 | dev = "ignored" 75 | 76 | [variable.public] 77 | notice = "required" 78 | dev = "ignored" 79 | return = "required" 80 | -------------------------------------------------------------------------------- /.nsignore: -------------------------------------------------------------------------------- 1 | # Use this file to specifically exclude some files and folders 2 | # This uses the same syntax as .gitignore 3 | /src 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to Contribute to Lintspec 2 | 3 | #### **Did you find a bug?** 4 | 5 | * **Do not open up a GitHub issue if the bug is a security vulnerability 6 | in lintspec**, and instead [contact the maintainer](mailto:hi@beeb.li). 7 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/beeb/lintspec/issues). 8 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/beeb/lintspec/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. 9 | 10 | #### **Did you write a patch that fixes a bug?** 11 | 12 | * Open a new GitHub pull request with the patch. 13 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 14 | * Make sure to add unit tests to avoid regressions. 15 | 16 | #### **Did you fix whitespace, format code, or make a purely cosmetic patch?** 17 | 18 | Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of lintspec will generally not be accepted. 19 | 20 | #### **Do you intend to add a new feature or change an existing one?** 21 | 22 | * Suggest your change in a feature request issue and wait on feedback from the maintainers before writing code. 23 | * Do not open an issue on GitHub until you have collected positive feedback about the change. 24 | 25 | Thanks! :heart: :heart: :heart: 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Valentin Bersier "] 3 | categories = ["development-tools", "command-line-utilities"] 4 | description = "A blazingly fast linter for NatSpec comments in Solidity code" 5 | edition = "2024" 6 | keywords = ["natspec", "solidity", "linter", "checker", "documentation"] 7 | license = "MIT OR Apache-2.0" 8 | name = "lintspec" 9 | readme = "./README.md" 10 | repository = "https://github.com/beeb/lintspec" 11 | rust-version = "1.87.0" 12 | version = "0.6.1" 13 | 14 | [lints.clippy] 15 | module_name_repetitions = "allow" 16 | missing_errors_doc = "allow" 17 | missing_panics_doc = "allow" 18 | pedantic = { level = "warn", priority = -1 } 19 | 20 | [dependencies] 21 | anyhow = "1.0.95" 22 | bon = "3.3.2" 23 | clap = { version = "4.5.29", features = ["derive"] } 24 | derive_more = { version = "2.0.1", features = [ 25 | "add", 26 | "display", 27 | "from", 28 | "is_variant", 29 | "try_into", 30 | ] } 31 | dotenvy = "0.15.7" 32 | dunce = "1.0.5" 33 | figment = { version = "0.10.19", features = ["env", "toml"] } 34 | ignore = "0.4.23" 35 | itertools = "0.14.0" 36 | miette = { version = "7.5.0", features = ["fancy"] } 37 | rayon = "1.10.0" 38 | regex = "1.11.1" 39 | semver = "1.0.25" 40 | serde = { version = "1.0.217", features = ["derive"] } 41 | serde_json = "1.0.138" 42 | serde_with = "3.12.0" 43 | slang_solidity = "0.18.3" 44 | solar-parse = { version = "0.1.4", default-features = false, optional = true } 45 | thiserror = "2.0.11" 46 | toml = "0.8.20" 47 | winnow = "0.7.2" 48 | 49 | [dev-dependencies] 50 | divan = { version = "3.0.2", package = "codspeed-divan-compat" } 51 | insta = "1.42.1" 52 | similar-asserts = "1.6.1" 53 | 54 | [[bench]] 55 | name = "parser" 56 | harness = false 57 | 58 | [[bench]] 59 | name = "lint" 60 | harness = false 61 | 62 | [features] 63 | solar = ["dep:solar-parse"] 64 | 65 | [profile.release] 66 | lto = "thin" 67 | 68 | # The profile that 'dist' will build with 69 | [profile.dist] 70 | inherits = "release" 71 | lto = "thin" 72 | 73 | [profile.dev.package] 74 | insta.opt-level = 3 75 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Valentin Bersier 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 | # 🔎 lintspec 2 | 3 | ![lintspec screenshot](https://raw.githubusercontent.com/beeb/lintspec/refs/heads/main/screenshot.png) 4 | 5 |
6 | github 11 | crates.io 16 | docs.rs 21 | MSRV 26 | CodSpeed 30 |
31 | 32 | Lintspec is a command-line utility (linter) that checks the completeness and validity of 33 | [NatSpec](https://docs.soliditylang.org/en/latest/natspec-format.html) doc-comments in Solidity code. It is focused on 34 | speed and ergonomics. By default, lintspec will respect gitignore rules when looking for Solidity source files. 35 | 36 | Dual-licensed under MIT or Apache 2.0. 37 | 38 | > Note: the `main` branch can contain unreleased changes. To view the README information for the latest stable release, 39 | > visit [crates.io](https://crates.io/crates/lintspec) or select the latest git tag from the branch/tag dropdown. 40 | 41 | ## Installation 42 | 43 | #### Via `cargo` 44 | 45 | ```bash 46 | cargo install lintspec 47 | ``` 48 | 49 | #### Via [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall) 50 | 51 | ```bash 52 | cargo binstall lintspec 53 | ``` 54 | 55 | #### Via `nix` 56 | 57 | ```bash 58 | nix-env -iA nixpkgs.lintspec 59 | # or 60 | nix-shell -p lintspec 61 | # or 62 | nix run nixpkgs#lintspec 63 | ``` 64 | 65 | #### Pre-built binaries and install script 66 | 67 | Head over to the [releases page](https://github.com/beeb/lintspec/releases)! 68 | 69 | ### Experimental `solar` backend 70 | 71 | An experimental (and very fast) parsing backend using [Solar](https://github.com/paradigmxyz/solar) can be tested 72 | by installing with the `solar` feature flag enabled. This is only possible via `cargo install` at the moment. 73 | 74 | ```bash 75 | cargo install lintspec -F solar 76 | ``` 77 | 78 | With this backend, the parsing step is roughly 15x faster than with the default 79 | [`slang`](https://github.com/NomicFoundation/slang) backend. In practice, overall gains of 2-3x can be expected on the 80 | total execution time. 81 | **Note that Solar only supports Solidity >=0.8.0.** 82 | 83 | ## Usage 84 | 85 | ```text 86 | Usage: lintspec [OPTIONS] [PATH]... [COMMAND] 87 | 88 | Commands: 89 | init Create a `.lintspec.toml` config file with default values 90 | help Print this message or the help of the given subcommand(s) 91 | 92 | Arguments: 93 | [PATH]... One or more paths to files and folders to analyze 94 | 95 | Options: 96 | -e, --exclude Path to a file or folder to exclude (can be used more than once) 97 | -o, --out Write output to a file instead of stderr 98 | --inheritdoc Enforce that all public and external items have `@inheritdoc` 99 | --notice-or-dev Do not distinguish between `@notice` and `@dev` when considering "required" validation rules 100 | --skip-version-detection Skip the detection of the Solidity version from pragma statements 101 | --notice-ignored Ignore `@notice` for these items (can be used more than once) 102 | --notice-required Enforce `@notice` for these items (can be used more than once) 103 | --notice-forbidden Forbid `@notice` for these items (can be used more than once) 104 | --dev-ignored Ignore `@dev` for these items (can be used more than once) 105 | --dev-required Enforce `@dev` for these items (can be used more than once) 106 | --dev-forbidden Forbid `@dev` for these items (can be used more than once) 107 | --param-ignored Ignore `@param` for these items (can be used more than once) 108 | --param-required Enforce `@param` for these items (can be used more than once) 109 | --param-forbidden Forbid `@param` for these items (can be used more than once) 110 | --return-ignored Ignore `@return` for these items (can be used more than once) 111 | --return-required Enforce `@return` for these items (can be used more than once) 112 | --return-forbidden Forbid `@return` for these items (can be used more than once) 113 | --json Output diagnostics in JSON format 114 | --compact Compact output 115 | --sort Sort the results by file path 116 | -h, --help Print help (see more with '--help') 117 | -V, --version Print version 118 | ``` 119 | 120 | ## Configuration 121 | 122 | ### Config File 123 | 124 | Create a default configuration with the following command: 125 | 126 | ```bash 127 | lintspec init 128 | ``` 129 | 130 | This will create a `.lintspec.toml` file with the default configuration in the current directory. 131 | 132 | ### Environment Variables 133 | 134 | Environment variables (in capitals, with the `LS_` prefix) can also be used and take precedence over the 135 | configuration file. They use the same names as in the TOML config file and use the `_` character as delimiter for 136 | nested items. 137 | 138 | Examples: 139 | 140 | - `LS_LINTSPEC_PATHS=[src,test]` 141 | - `LS_LINTSPEC_INHERITDOC=false` 142 | - `LS_LINTSPEC_NOTICE_OR_DEV=true`: if the setting name contains `_`, it is not considered a delimiter 143 | - `LS_OUTPUT_JSON=true` 144 | - `LS_CONSTRUCTOR_NOTICE=required` 145 | 146 | ### CLI Arguments 147 | 148 | Finally, the tool can be customized with command-line arguments, which take precedence over the other two methods. 149 | To see the CLI usage information, run: 150 | 151 | ```bash 152 | lintspec help 153 | ``` 154 | 155 | ## Usage in GitHub Actions 156 | 157 | You can check your code in CI with the lintspec GitHub Action. Any `.lintspec.toml` or `.nsignore` file in the 158 | repository's root will be used to configure the execution. 159 | 160 | The action generates 161 | [annotations](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message) 162 | that are displayed in the source files when viewed (e.g. in a PR's "Files" tab). 163 | 164 | ### Options 165 | 166 | The following options are available for the action (all are optional if a config file is present): 167 | 168 | | Input | Default Value | Description | Example | 169 | |---|---|---|---| 170 | | `working-directory` | `"./"` | Working directory path | `"./src"` | 171 | | `paths` | `"[]"` | Paths to scan, relative to the working directory, in square brackets and separated by commas. Required unless a `.lintspec.toml` file is present in the working directory. | `"[path/to/file.sol,test/test.sol]"` | 172 | | `exclude` | `"[]"` | Paths to exclude, relative to the working directory, in square brackets and separated by commas | `"[path/to/exclude,other/path.sol]"` | 173 | | `extra-args` | | Extra arguments passed to the `lintspec` command | `"--inheritdoc=false"` | 174 | | `version` | `"latest"` | Version of lintspec to use. For enhanced security, you can pin this to a fixed version | `"0.4.1"` | 175 | | `fail-on-problem` | `"true"` | Whether the action should fail when `NatSpec` problems have been found. Disabling this only creates annotations for found problems, but succeeds | `"false"` | 176 | 177 | ### Example Workflow 178 | 179 | ```yaml 180 | name: Lintspec 181 | 182 | on: 183 | pull_request: 184 | 185 | jobs: 186 | lintspec: 187 | runs-on: ubuntu-latest 188 | steps: 189 | - uses: actions/checkout@v2 190 | - uses: beeb/lintspec@v0.4.1 191 | # all the lines below are optional 192 | with: 193 | working-directory: "./" 194 | paths: "[]" 195 | exclude: "[]" 196 | extra-args: "" 197 | version: "latest" 198 | fail-on-problem: "true" 199 | ``` 200 | 201 | ## Credits 202 | 203 | This tool walks in the footsteps of [natspec-smells](https://github.com/defi-wonderland/natspec-smells), thanks to 204 | them for inspiring this project! 205 | 206 | ## Comparison with natspec-smells 207 | 208 | ### Benchmark 209 | 210 | On an AMD Ryzen 9 7950X processor with 64GB of RAM, linting the 211 | [Uniswap/v4-core](https://github.com/Uniswap/v4-core) `src` folder on WSL2 (Ubuntu), lintspec v0.6 is about 300x 212 | faster, or 0.33% of the execution time: 213 | 214 | ```text 215 | Benchmark 1: npx @defi-wonderland/natspec-smells --include 'src/**/*.sol' --enforceInheritdoc --constructorNatspec 216 | Time (mean ± σ): 14.493 s ± 0.492 s [User: 13.851 s, System: 1.046 s] 217 | Range (min … max): 14.129 s … 15.826 s 10 runs 218 | 219 | Benchmark 2: lintspec src --compact --param-required struct 220 | Time (mean ± σ): 44.9 ms ± 1.6 ms [User: 312.8 ms, System: 51.2 ms] 221 | Range (min … max): 41.9 ms … 48.9 ms 67 runs 222 | 223 | Summary 224 | lintspec src --compact --param-required struct ran 225 | 322.47 ± 16.01 times faster than npx @defi-wonderland/natspec-smells --include 'src/**/*.sol' --enforceInheritdoc --constructorNatspec 226 | ``` 227 | 228 | Using the experimental [Solar](https://github.com/paradigmxyz/solar) backend improves that by a further factor of 2-3x: 229 | 230 | ```text 231 | Benchmark 1: lintspec src --compact --skip-version-detection 232 | Time (mean ± σ): 44.8 ms ± 1.9 ms [User: 308.8 ms, System: 49.8 ms] 233 | Range (min … max): 41.3 ms … 49.9 ms 69 runs 234 | 235 | Benchmark 2: lintspec-solar src --compact 236 | Time (mean ± σ): 19.7 ms ± 0.7 ms [User: 15.0 ms, System: 30.7 ms] 237 | Range (min … max): 18.5 ms … 21.9 ms 143 runs 238 | 239 | Summary 240 | lintspec-solar src --compact ran 241 | 2.27 ± 0.13 times faster than lintspec src --compact --skip-version-detection 242 | ``` 243 | 244 | ### Features 245 | 246 | | Feature | `lintspec` | `natspec-smells` | 247 | |---------------------------------|------------|------------------| 248 | | Identify missing NatSpec | ✅ | ✅ | 249 | | Identify duplicate NatSpec | ✅ | ✅ | 250 | | Include files/folders | ✅ | ✅ | 251 | | Exclude files/folders | ✅ | ✅ | 252 | | Enforce usage of `@inheritdoc` | ✅ | ✅ | 253 | | Enforce NatSpec on constructors | ✅ | ✅ | 254 | | Configure via config file | ✅ | ✅ | 255 | | Configure via env variables | ✅ | ❌ | 256 | | Respects gitignore files | ✅ | ❌ | 257 | | Granular validation rules | ✅ | ❌ | 258 | | Pretty output with code excerpt | ✅ | ❌ | 259 | | JSON output | ✅ | ❌ | 260 | | Output to file | ✅ | ❌ | 261 | | Multithreaded | ✅ | ❌ | 262 | | Built-in CI action | ✅ | ❌ | 263 | | No pre-requisites (node/npm) | ✅ | ❌ | 264 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'lintspec' 2 | description: 'Solidity NatSpec linter' 3 | author: Valentin Bersier 4 | 5 | branding: 6 | icon: "search" 7 | color: "orange" 8 | 9 | inputs: 10 | working-directory: 11 | description: Working directory path. Optional. Defaults to "./". 12 | required: false 13 | default: "./" 14 | paths: 15 | description: "Paths to scan, relative to the working directory, in square brackets and separated by commas. Optional. Example: '[src/to/file.sol,test]'" 16 | required: false 17 | default: "[]" 18 | exclude: 19 | description: "Paths to exclude, relative to the working directory, in square brackets and separated by commas. Optional. Example: '[src/to/file.sol,test]'" 20 | required: false 21 | default: "[]" 22 | extra-args: 23 | description: Extra args to be passed to the lintspec command. Optional. 24 | required: false 25 | version: 26 | description: Version of lintspec to use. Optional. Defaults to "latest". Minimum supported version is `0.1.3`. 27 | required: false 28 | default: "latest" 29 | fail-on-problem: 30 | description: Whether the action should fail when NatSpec problems have been found. Optional. Defaults to "true". 31 | required: false 32 | default: "true" 33 | 34 | outputs: 35 | total-diags: 36 | description: The total number of diagnostics found by lintspec 37 | value: ${{ steps.command-run.outputs.total-diags }} 38 | total-files: 39 | description: Total number of files where a diagnostic was found by lintspec 40 | value: ${{ steps.command-run.outputs.total-files }} 41 | 42 | runs: 43 | using: "composite" 44 | steps: 45 | - id: command-run 46 | shell: bash {0} # default github config uses `-e` flag which fails the step on exit code != 0 47 | working-directory: ${{ inputs.working-directory }} 48 | env: 49 | LS_LINTSPEC_PATHS: ${{ inputs.paths }} 50 | LS_LINTSPEC_EXCLUDE: ${{ inputs.exclude }} 51 | LINTSPEC_PATHS: ${{ inputs.paths }} # for older version compatibility 52 | LINTSPEC_EXCLUDE: ${{ inputs.exclude }} # for older version compatibility 53 | VERSION: ${{ inputs.version }} 54 | EXTRA_ARGS: ${{ inputs.extra-args }} 55 | run: | 56 | if [[ "$VERSION" == "latest" ]]; then 57 | installer_url=$(curl -s "https://api.github.com/repos/beeb/lintspec/releases/latest" | jq -r '.assets[] | select(.name == "lintspec-installer.sh") | .browser_download_url') 58 | else 59 | installer_url="https://github.com/beeb/lintspec/releases/download/v$VERSION/lintspec-installer.sh" 60 | fi 61 | # install lintspec 62 | curl --proto '=https' --tlsv1.2 -LsSf "$installer_url" | sh 63 | 64 | # run lintspec 65 | command_output=$(/home/runner/.cargo/bin/lintspec --json=true --compact=true $EXTRA_ARGS 2>&1) # output can be stderr in case of diags 66 | # run command again with text output for debugging 67 | /home/runner/.cargo/bin/lintspec --compact=true $EXTRA_ARGS 2>&1 68 | total_diags=$(echo "$command_output" | jq '[.[].items[].diags | length] | add // 0') 69 | echo "total-diags=$(echo $total_diags)" >> $GITHUB_OUTPUT 70 | total_files=$(echo "$command_output" | jq 'length') 71 | echo "total-files=$(echo $total_files)" >> $GITHUB_OUTPUT 72 | 73 | # create annotations 74 | if [[ $total_diags != "0" ]]; then 75 | echo "$command_output" | jq '.[] | .path as $path | .items[] | .name as $name | [.diags] | flatten[] | "::warning file=\( $path ),col=\( .span.start.column + 1 ),endColumn=\( .span.end.column + 1 ),line=\( .span.start.line + 1 )\( if .span.start.line == .span.end.line then "" else ",endLine=" + "\( .span.end.line + 1 )" end )::\( $name ): \( .message )"' | xargs -n1 echo; 76 | fi 77 | 78 | - name: fail on non-null diags count 79 | if: inputs.fail-on-problem == 'true' 80 | shell: bash 81 | run: if [[ "${{ steps.command-run.outputs.total-diags }}" != "0" ]]; then exit 1; else exit 0; fi 82 | -------------------------------------------------------------------------------- /benches/lint.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use divan::{Bencher, black_box}; 4 | use lintspec::{ 5 | lint::{Validate as _, ValidationOptions, lint}, 6 | parser::{Parse as _, ParsedDocument, slang::SlangParser}, 7 | }; 8 | 9 | const FILES: &[&str] = &[ 10 | "test-data/BasicSample.sol", 11 | "test-data/ParserTest.sol", 12 | "test-data/InterfaceSample.sol", 13 | "test-data/LibrarySample.sol", 14 | "test-data/Fuzzers.sol", 15 | ]; 16 | 17 | fn main() { 18 | divan::main(); 19 | } 20 | 21 | fn parse_file(path: &str) -> ParsedDocument { 22 | let file = File::open(path).unwrap(); 23 | SlangParser::builder() 24 | .skip_version_detection(true) 25 | .build() 26 | .parse_document(file, Some(path), false) 27 | .unwrap() 28 | } 29 | 30 | #[divan::bench(args = FILES)] 31 | fn lint_only(bencher: Bencher, path: &str) { 32 | let doc = parse_file(path); 33 | let options = ValidationOptions::default(); 34 | bencher.bench_local(move || { 35 | black_box( 36 | doc.definitions 37 | .iter() 38 | .map(|item| item.validate(&options)) 39 | .collect::>(), 40 | ); 41 | }); 42 | } 43 | 44 | #[divan::bench(args = FILES)] 45 | fn lint_e2e(bencher: Bencher, path: &str) { 46 | let parser = SlangParser::builder().skip_version_detection(true).build(); 47 | let options = ValidationOptions::default(); 48 | bencher.bench_local(move || { 49 | black_box(lint(parser.clone(), path, &options, false).ok()); 50 | }); 51 | } 52 | 53 | #[cfg(feature = "solar")] 54 | #[divan::bench(args = FILES)] 55 | fn lint_e2e_solar(bencher: Bencher, path: &str) { 56 | let parser = lintspec::parser::solar::SolarParser {}; 57 | let options = ValidationOptions::default(); 58 | bencher.bench_local(move || { 59 | black_box(lint(parser.clone(), path, &options, false).ok()); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /benches/parser.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use divan::{Bencher, black_box}; 4 | use lintspec::parser::{Parse, ParsedDocument, slang::SlangParser}; 5 | 6 | const FILES: &[&str] = &[ 7 | "test-data/BasicSample.sol", 8 | "test-data/ParserTest.sol", 9 | "test-data/InterfaceSample.sol", 10 | "test-data/LibrarySample.sol", 11 | "test-data/Fuzzers.sol", 12 | ]; 13 | 14 | fn main() { 15 | divan::main(); 16 | } 17 | 18 | fn parse_file(mut parser: impl Parse, path: &str) -> ParsedDocument { 19 | let file = File::open(path).unwrap(); 20 | parser.parse_document(file, Some(path), false).unwrap() 21 | } 22 | 23 | #[divan::bench(args = FILES)] 24 | fn parse_slang(bencher: Bencher, path: &str) { 25 | let parser = SlangParser::builder().skip_version_detection(true).build(); 26 | bencher.bench_local(move || { 27 | black_box(parse_file(parser.clone(), path)); 28 | }); 29 | } 30 | 31 | #[cfg(feature = "solar")] 32 | #[divan::bench(args = FILES)] 33 | fn parse_solar(bencher: Bencher, path: &str) { 34 | let parser = lintspec::parser::solar::SolarParser {}; 35 | bencher.bench_local(move || black_box(parse_file(parser.clone(), path))); 36 | } 37 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | [changelog] 2 | header = """ 3 | # Changelog\n 4 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.\n 5 | """ 6 | 7 | body = """ 8 | {% if version %}\ 9 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 10 | {% endif %}\ 11 | {% for group, commits in commits | group_by(attribute="group") %} 12 | ### {{ group | upper_first }} 13 | {% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %} 14 | - **({{commit.scope}})**{% if commit.breaking %} [**breaking**]{% endif %} \ 15 | {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) 16 | {% endfor %} 17 | {% raw %}\n{% endraw %}\ 18 | {%- for commit in commits %} 19 | {%- if commit.scope -%} 20 | {% else -%} 21 | - {% if commit.breaking %} [**breaking**]{% endif %} \ 22 | {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) 23 | {% endif -%} 24 | {% endfor -%} 25 | {% endfor %}\n 26 | {% if version %} 27 | {% if previous.version %} 28 | **Full Changelog**: [{{ previous.version }}...{{ version }}]($REPO/compare/v{{ previous.version }}...v{{ version }}) 29 | {% endif %} 30 | {% else -%} 31 | {% raw %}\n{% endraw %} 32 | {% endif %} 33 | """ 34 | 35 | # remove the leading and trailing whitespace from the template 36 | trim = true 37 | 38 | postprocessors = [ 39 | { pattern = '\$REPO', replace = "https://github.com/beeb/lintspec" }, 40 | ] 41 | 42 | [git] 43 | # parse the commits based on https://www.conventionalcommits.org 44 | conventional_commits = true 45 | # filter out the commits that are not conventional 46 | filter_unconventional = true 47 | # process each line of a commit as an individual commit 48 | split_commits = false 49 | 50 | commit_parsers = [ 51 | { body = ".*security", group = "Security" }, 52 | { message = "^chore: release", skip = true }, 53 | { message = "^doc", group = "Documentation" }, 54 | { message = "^fix", group = "Fixed" }, 55 | { message = "^test", group = "Tests" }, 56 | { message = "^feat", group = "Added" }, 57 | { message = "^.*: add", group = "Added" }, 58 | { message = "^.*: support", group = "Added" }, 59 | { message = "^.*: remove", group = "Removed" }, 60 | { message = "^.*: delete", group = "Removed" }, 61 | { message = "^.*: fix", group = "Fixed" }, 62 | { message = "^.*", group = "Changed" }, 63 | ] # regex for parsing and grouping commits 64 | 65 | # protect breaking changes from being skipped due to matching a skipping commit_parser 66 | protect_breaking_commits = true 67 | # filter out the commits that are not matched by commit parsers 68 | filter_commits = false 69 | # glob pattern for matching git tags 70 | tag_pattern = "v[0-9]*" 71 | # sort the tags topologically 72 | topo_order = false 73 | # sort the commits inside sections by oldest/newest order 74 | sort_commits = "oldest" 75 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 3 | allow = ["MIT", "Apache-2.0", "Unicode-3.0", "ISC"] 4 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.28.0" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = ["shell", "powershell"] 12 | # Target platforms to build apps for (Rust target-triple syntax) 13 | targets = [ 14 | "aarch64-apple-darwin", 15 | "aarch64-unknown-linux-gnu", 16 | "x86_64-apple-darwin", 17 | "x86_64-unknown-linux-gnu", 18 | "x86_64-pc-windows-msvc", 19 | ] 20 | # Path that installers should place binaries in 21 | install-path = "CARGO_HOME" 22 | # Whether to install an updater program 23 | install-updater = false 24 | 25 | [dist.github-custom-runners] 26 | global = "ubuntu-22.04" 27 | 28 | [dist.github-custom-runners.x86_64-unknown-linux-gnu] 29 | runner = "ubuntu-22.04" 30 | 31 | [dist.github-custom-runners.aarch64-unknown-linux-gnu] 32 | runner = "ubuntu-22.04" 33 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "rust-analyzer-src": "rust-analyzer-src" 9 | }, 10 | "locked": { 11 | "lastModified": 1751006353, 12 | "narHash": "sha256-icKFXb83uv2ezRCfuq5G8QSwCuaoLywLljSL+UGmPPI=", 13 | "owner": "nix-community", 14 | "repo": "fenix", 15 | "rev": "b37f026b49ecb295a448c96bcbb0c174c14fc91b", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "nix-community", 20 | "repo": "fenix", 21 | "type": "github" 22 | } 23 | }, 24 | "nixpkgs": { 25 | "locked": { 26 | "lastModified": 1750898778, 27 | "narHash": "sha256-DXI7+SKDlTyA+C4zp0LoIywQ+BfdH5m4nkuxbWgV4UU=", 28 | "owner": "NixOS", 29 | "repo": "nixpkgs", 30 | "rev": "322d8a3c6940039f7cff179a8b09c5d7ca06359d", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "NixOS", 35 | "ref": "nixpkgs-unstable", 36 | "repo": "nixpkgs", 37 | "type": "github" 38 | } 39 | }, 40 | "root": { 41 | "inputs": { 42 | "fenix": "fenix", 43 | "nixpkgs": "nixpkgs" 44 | } 45 | }, 46 | "rust-analyzer-src": { 47 | "flake": false, 48 | "locked": { 49 | "lastModified": 1750871759, 50 | "narHash": "sha256-hMNZXMtlhfjQdu1F4Fa/UFiMoXdZag4cider2R9a648=", 51 | "owner": "rust-lang", 52 | "repo": "rust-analyzer", 53 | "rev": "317542c1e4a3ec3467d21d1c25f6a43b80d83e7d", 54 | "type": "github" 55 | }, 56 | "original": { 57 | "owner": "rust-lang", 58 | "ref": "nightly", 59 | "repo": "rust-analyzer", 60 | "type": "github" 61 | } 62 | } 63 | }, 64 | "root": "root", 65 | "version": 7 66 | } 67 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | fenix = { 5 | url = "github:nix-community/fenix"; 6 | inputs.nixpkgs.follows = "nixpkgs"; 7 | }; 8 | }; 9 | 10 | outputs = { self, nixpkgs, fenix }: 11 | let 12 | forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed; 13 | in 14 | { 15 | devShells = forAllSystems (system: 16 | let 17 | pkgs = import nixpkgs { 18 | inherit system; 19 | overlays = [ fenix.overlays.default ]; 20 | }; 21 | toolchain = fenix.packages.${system}.stable.withComponents [ 22 | "rustc" 23 | "cargo" 24 | "rust-std" 25 | "rustfmt-preview" 26 | "clippy-preview" 27 | "rust-analyzer-preview" 28 | "rust-src" 29 | ]; 30 | in 31 | { 32 | default = pkgs.mkShell { 33 | buildInputs = with pkgs; [ 34 | cargo-dist 35 | cargo-insta 36 | cargo-nextest 37 | toolchain 38 | ]; 39 | 40 | RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library"; 41 | }; 42 | } 43 | ); 44 | packages = forAllSystems (system: 45 | let 46 | pkgs = import nixpkgs { inherit system; }; 47 | lib = pkgs.lib; 48 | in 49 | { 50 | default = pkgs.rustPlatform.buildRustPackage { 51 | pname = "lintspec"; 52 | inherit ((lib.importTOML ./Cargo.toml).package) version; 53 | 54 | src = lib.cleanSource ./.; 55 | 56 | cargoLock = { 57 | lockFile = ./Cargo.lock; 58 | allowBuiltinFetchGit = true; 59 | }; 60 | 61 | doCheck = false; 62 | }; 63 | } 64 | ); 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | changelog_config = "cliff.toml" 3 | dependencies_update = true 4 | git_release_enable = false # will use cargo-dist for that 5 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rustfmt", "clippy", "rust-src"] 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeb/lintspec/f2a3d9c1f60ca185d4bd8712886f1cba37366f59/screenshot.png -------------------------------------------------------------------------------- /src/definitions/constructor.rs: -------------------------------------------------------------------------------- 1 | //! Parsing and validation of constructors. 2 | use crate::{ 3 | lint::{ItemDiagnostics, check_notice_and_dev, check_params}, 4 | natspec::NatSpec, 5 | }; 6 | 7 | use super::{Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions}; 8 | 9 | /// A constructor definition 10 | #[derive(Debug, Clone, bon::Builder)] 11 | #[non_exhaustive] 12 | pub struct ConstructorDefinition { 13 | /// The parent contract (should always be a [`Parent::Contract`]) 14 | pub parent: Option, 15 | 16 | /// The span corresponding to the constructor definition, excluding the body 17 | pub span: TextRange, 18 | 19 | /// The name and span of the constructor's parameters 20 | pub params: Vec, 21 | 22 | /// The [`NatSpec`] associated with the constructor, if any 23 | pub natspec: Option, 24 | } 25 | 26 | impl SourceItem for ConstructorDefinition { 27 | fn item_type(&self) -> ItemType { 28 | ItemType::Constructor 29 | } 30 | 31 | fn parent(&self) -> Option { 32 | self.parent.clone() 33 | } 34 | 35 | fn name(&self) -> String { 36 | "constructor".to_string() 37 | } 38 | 39 | fn span(&self) -> TextRange { 40 | self.span.clone() 41 | } 42 | } 43 | 44 | impl Validate for ConstructorDefinition { 45 | fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics { 46 | let opts = &options.constructors; 47 | let mut out = ItemDiagnostics { 48 | parent: self.parent(), 49 | item_type: self.item_type(), 50 | name: self.name(), 51 | span: self.span(), 52 | diags: vec![], 53 | }; 54 | out.diags.extend(check_notice_and_dev( 55 | &self.natspec, 56 | opts.notice, 57 | opts.dev, 58 | options.notice_or_dev, 59 | self.span(), 60 | )); 61 | out.diags.extend(check_params( 62 | &self.natspec, 63 | opts.param, 64 | &self.params, 65 | self.span(), 66 | )); 67 | out 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use std::sync::LazyLock; 74 | 75 | use similar_asserts::assert_eq; 76 | 77 | use crate::{ 78 | config::{Req, WithParamsRules}, 79 | definitions::Definition, 80 | parser::{Parse as _, slang::SlangParser}, 81 | }; 82 | 83 | use super::*; 84 | 85 | static OPTIONS: LazyLock = 86 | LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build()); 87 | 88 | fn parse_file(contents: &str) -> ConstructorDefinition { 89 | let mut parser = SlangParser::builder().skip_version_detection(true).build(); 90 | let doc = parser 91 | .parse_document(contents.as_bytes(), None::, false) 92 | .unwrap(); 93 | doc.definitions 94 | .into_iter() 95 | .find_map(Definition::to_constructor) 96 | .unwrap() 97 | } 98 | 99 | #[test] 100 | fn test_constructor() { 101 | let contents = "contract Test { 102 | /// @param param1 Test 103 | /// @param param2 Test2 104 | constructor(uint256 param1, bytes calldata param2) { } 105 | }"; 106 | let res = parse_file(contents).validate(&OPTIONS); 107 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 108 | } 109 | 110 | #[test] 111 | fn test_constructor_no_natspec() { 112 | let contents = "contract Test { 113 | constructor(uint256 param1, bytes calldata param2) { } 114 | }"; 115 | let res = parse_file(contents).validate(&OPTIONS); 116 | assert_eq!(res.diags.len(), 2); 117 | assert_eq!(res.diags[0].message, "@param param1 is missing"); 118 | assert_eq!(res.diags[1].message, "@param param2 is missing"); 119 | } 120 | 121 | #[test] 122 | fn test_constructor_only_notice() { 123 | let contents = "contract Test { 124 | /// @notice The constructor 125 | constructor(uint256 param1, bytes calldata param2) { } 126 | }"; 127 | let res = parse_file(contents).validate(&OPTIONS); 128 | assert_eq!(res.diags.len(), 2); 129 | assert_eq!(res.diags[0].message, "@param param1 is missing"); 130 | assert_eq!(res.diags[1].message, "@param param2 is missing"); 131 | } 132 | 133 | #[test] 134 | fn test_constructor_one_missing() { 135 | let contents = "contract Test { 136 | /// @param param1 The first 137 | constructor(uint256 param1, bytes calldata param2) { } 138 | }"; 139 | let res = parse_file(contents).validate(&OPTIONS); 140 | assert_eq!(res.diags.len(), 1); 141 | assert_eq!(res.diags[0].message, "@param param2 is missing"); 142 | } 143 | 144 | #[test] 145 | fn test_constructor_multiline() { 146 | let contents = "contract Test { 147 | /** 148 | * @param param1 Test 149 | * @param param2 Test2 150 | */ 151 | constructor(uint256 param1, bytes calldata param2) { } 152 | }"; 153 | let res = parse_file(contents).validate(&OPTIONS); 154 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 155 | } 156 | 157 | #[test] 158 | fn test_constructor_duplicate() { 159 | let contents = "contract Test { 160 | /// @param param1 The first 161 | /// @param param1 The first again 162 | constructor(uint256 param1) { } 163 | }"; 164 | let res = parse_file(contents).validate(&OPTIONS); 165 | assert_eq!(res.diags.len(), 1); 166 | assert_eq!( 167 | res.diags[0].message, 168 | "@param param1 is present more than once" 169 | ); 170 | } 171 | 172 | #[test] 173 | fn test_constructor_no_params() { 174 | let contents = "contract Test { 175 | constructor() { } 176 | }"; 177 | let res = parse_file(contents).validate(&OPTIONS); 178 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 179 | } 180 | 181 | #[test] 182 | fn test_constructor_notice() { 183 | // inheritdoc should be ignored since it's a constructor 184 | let contents = "contract Test { 185 | /// @inheritdoc ITest 186 | constructor(uint256 param1) { } 187 | }"; 188 | let res = parse_file(contents).validate( 189 | &ValidationOptions::builder() 190 | .inheritdoc(true) 191 | .constructors(WithParamsRules::required()) 192 | .build(), 193 | ); 194 | assert_eq!(res.diags.len(), 2); 195 | assert_eq!(res.diags[0].message, "@notice is missing"); 196 | assert_eq!(res.diags[1].message, "@param param1 is missing"); 197 | } 198 | 199 | #[test] 200 | fn test_constructor_enforce() { 201 | let opts = ValidationOptions::builder() 202 | .constructors(WithParamsRules { 203 | notice: Req::Required, 204 | dev: Req::default(), 205 | param: Req::default(), 206 | }) 207 | .build(); 208 | let contents = "contract Test { 209 | constructor() { } 210 | }"; 211 | let res = parse_file(contents).validate(&opts); 212 | assert_eq!(res.diags.len(), 1); 213 | assert_eq!(res.diags[0].message, "@notice is missing"); 214 | 215 | let contents = "contract Test { 216 | /// @notice Some notice 217 | constructor() { } 218 | }"; 219 | let res = parse_file(contents).validate(&opts); 220 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/definitions/enumeration.rs: -------------------------------------------------------------------------------- 1 | //! Parsing and validation of enum definitions. 2 | use crate::{ 3 | lint::{ItemDiagnostics, check_notice_and_dev, check_params}, 4 | natspec::NatSpec, 5 | }; 6 | 7 | use super::{Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions}; 8 | 9 | /// An enum definition 10 | #[derive(Debug, Clone, bon::Builder)] 11 | #[non_exhaustive] 12 | #[builder(on(String, into))] 13 | pub struct EnumDefinition { 14 | /// The parent for the enum definition, if any 15 | pub parent: Option, 16 | 17 | /// The name of the enum 18 | pub name: String, 19 | 20 | /// The span of the enum definition 21 | pub span: TextRange, 22 | 23 | /// The name and span of the enum variants 24 | pub members: Vec, 25 | 26 | /// The [`NatSpec`] associated with the enum definition, if any 27 | pub natspec: Option, 28 | } 29 | 30 | impl SourceItem for EnumDefinition { 31 | fn item_type(&self) -> ItemType { 32 | ItemType::Enum 33 | } 34 | 35 | fn parent(&self) -> Option { 36 | self.parent.clone() 37 | } 38 | 39 | fn name(&self) -> String { 40 | self.name.clone() 41 | } 42 | 43 | fn span(&self) -> TextRange { 44 | self.span.clone() 45 | } 46 | } 47 | 48 | impl Validate for EnumDefinition { 49 | fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics { 50 | let opts = &options.enums; 51 | let mut out = ItemDiagnostics { 52 | parent: self.parent(), 53 | item_type: self.item_type(), 54 | name: self.name(), 55 | span: self.span(), 56 | diags: vec![], 57 | }; 58 | out.diags.extend(check_notice_and_dev( 59 | &self.natspec, 60 | opts.notice, 61 | opts.dev, 62 | options.notice_or_dev, 63 | self.span(), 64 | )); 65 | out.diags.extend(check_params( 66 | &self.natspec, 67 | opts.param, 68 | &self.members, 69 | self.span(), 70 | )); 71 | out 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use std::sync::LazyLock; 78 | 79 | use similar_asserts::assert_eq; 80 | 81 | use crate::{ 82 | config::{Req, WithParamsRules}, 83 | definitions::Definition, 84 | parser::{Parse as _, slang::SlangParser}, 85 | }; 86 | 87 | use super::*; 88 | 89 | static OPTIONS: LazyLock = LazyLock::new(|| { 90 | ValidationOptions::builder() 91 | .enums(WithParamsRules::required()) 92 | .inheritdoc(false) 93 | .build() 94 | }); 95 | 96 | fn parse_file(contents: &str) -> EnumDefinition { 97 | let mut parser = SlangParser::builder().skip_version_detection(true).build(); 98 | let doc = parser 99 | .parse_document(contents.as_bytes(), None::, false) 100 | .unwrap(); 101 | doc.definitions 102 | .into_iter() 103 | .find_map(Definition::to_enum) 104 | .unwrap() 105 | } 106 | 107 | #[test] 108 | fn test_enum() { 109 | let contents = "contract Test { 110 | /// @notice The enum 111 | enum Foobar { 112 | First, 113 | Second 114 | } 115 | }"; 116 | let res = 117 | parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(false).build()); 118 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 119 | } 120 | 121 | #[test] 122 | fn test_enum_missing() { 123 | let contents = "contract Test { 124 | enum Foobar { 125 | First, 126 | Second 127 | } 128 | }"; 129 | let res = parse_file(contents).validate(&OPTIONS); 130 | assert_eq!(res.diags.len(), 3); 131 | assert_eq!(res.diags[0].message, "@notice is missing"); 132 | assert_eq!(res.diags[1].message, "@param First is missing"); 133 | assert_eq!(res.diags[2].message, "@param Second is missing"); 134 | } 135 | 136 | #[test] 137 | fn test_enum_params() { 138 | let contents = "contract Test { 139 | /// @notice The notice 140 | /// @param First The first 141 | /// @param Second The second 142 | enum Foobar { 143 | First, 144 | Second 145 | } 146 | }"; 147 | let res = parse_file(contents).validate(&OPTIONS); 148 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 149 | } 150 | 151 | #[test] 152 | fn test_enum_only_notice() { 153 | let contents = "contract Test { 154 | /// @notice An enum 155 | enum Foobar { 156 | First, 157 | Second 158 | } 159 | }"; 160 | let res = parse_file(contents).validate(&OPTIONS); 161 | assert_eq!(res.diags.len(), 2); 162 | assert_eq!(res.diags[0].message, "@param First is missing"); 163 | assert_eq!(res.diags[1].message, "@param Second is missing"); 164 | } 165 | 166 | #[test] 167 | fn test_enum_one_missing() { 168 | let contents = "contract Test { 169 | /// @notice An enum 170 | /// @param First The first 171 | enum Foobar { 172 | First, 173 | Second 174 | } 175 | }"; 176 | let res = parse_file(contents).validate(&OPTIONS); 177 | assert_eq!(res.diags.len(), 1); 178 | assert_eq!(res.diags[0].message, "@param Second is missing"); 179 | } 180 | 181 | #[test] 182 | fn test_enum_multiline() { 183 | let contents = "contract Test { 184 | /** 185 | * @notice The enum 186 | * @param First The first 187 | * @param Second The second 188 | */ 189 | enum Foobar { 190 | First, 191 | Second 192 | } 193 | }"; 194 | let res = parse_file(contents).validate(&OPTIONS); 195 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 196 | } 197 | 198 | #[test] 199 | fn test_enum_duplicate() { 200 | let contents = "contract Test { 201 | /// @notice The enum 202 | /// @param First The first 203 | /// @param First The first twice 204 | enum Foobar { 205 | First 206 | } 207 | }"; 208 | let res = parse_file(contents).validate(&OPTIONS); 209 | assert_eq!(res.diags.len(), 1); 210 | assert_eq!( 211 | res.diags[0].message, 212 | "@param First is present more than once" 213 | ); 214 | } 215 | 216 | #[test] 217 | fn test_enum_inheritdoc() { 218 | // inheritdoc should be ignored as it doesn't apply to enums 219 | let contents = "contract Test { 220 | /// @inheritdoc Something 221 | enum Foobar { 222 | First 223 | } 224 | }"; 225 | let res = 226 | parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(true).build()); 227 | assert_eq!(res.diags.len(), 1); 228 | assert_eq!(res.diags[0].message, "@notice is missing"); 229 | } 230 | 231 | #[test] 232 | fn test_enum_enforce() { 233 | let opts = ValidationOptions::builder() 234 | .enums(WithParamsRules { 235 | notice: Req::Required, 236 | dev: Req::default(), 237 | param: Req::default(), 238 | }) 239 | .build(); 240 | let contents = "contract Test { 241 | enum Foobar { 242 | First 243 | } 244 | }"; 245 | let res = parse_file(contents).validate(&opts); 246 | assert_eq!(res.diags.len(), 1); 247 | assert_eq!(res.diags[0].message, "@notice is missing"); 248 | 249 | let contents = "contract Test { 250 | /// @notice Some notice 251 | enum Foobar { 252 | First 253 | } 254 | }"; 255 | let res = parse_file(contents).validate(&opts); 256 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 257 | } 258 | 259 | #[test] 260 | fn test_enum_no_contract() { 261 | let contents = " 262 | /// @notice An enum 263 | /// @param First The first 264 | /// @param Second The second 265 | enum Foobar { 266 | First, 267 | Second 268 | }"; 269 | let res = parse_file(contents).validate(&OPTIONS); 270 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 271 | } 272 | 273 | #[test] 274 | fn test_enum_no_contract_missing() { 275 | let contents = "enum Foobar { 276 | First, 277 | Second 278 | }"; 279 | let res = parse_file(contents).validate(&OPTIONS); 280 | assert_eq!(res.diags.len(), 3); 281 | assert_eq!(res.diags[0].message, "@notice is missing"); 282 | assert_eq!(res.diags[1].message, "@param First is missing"); 283 | assert_eq!(res.diags[2].message, "@param Second is missing"); 284 | } 285 | 286 | #[test] 287 | fn test_enum_no_contract_one_missing() { 288 | let contents = " 289 | /// @notice The enum 290 | /// @param First The first 291 | enum Foobar { 292 | First, 293 | Second 294 | }"; 295 | let res = parse_file(contents).validate(&OPTIONS); 296 | assert_eq!(res.diags.len(), 1); 297 | assert_eq!(res.diags[0].message, "@param Second is missing"); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/definitions/error.rs: -------------------------------------------------------------------------------- 1 | //! Parsing and validation of error definitions. 2 | use crate::{ 3 | lint::{ItemDiagnostics, check_notice_and_dev, check_params}, 4 | natspec::NatSpec, 5 | }; 6 | 7 | use super::{Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions}; 8 | 9 | /// An error definition 10 | #[derive(Debug, Clone, bon::Builder)] 11 | #[non_exhaustive] 12 | #[builder(on(String, into))] 13 | pub struct ErrorDefinition { 14 | /// The parent for the error definition, if any 15 | pub parent: Option, 16 | 17 | /// The name of the error 18 | pub name: String, 19 | 20 | /// The span of the error definition 21 | pub span: TextRange, 22 | 23 | /// The name and span of the error's parameters 24 | pub params: Vec, 25 | 26 | /// The [`NatSpec`] associated with the error definition, if any 27 | pub natspec: Option, 28 | } 29 | 30 | impl SourceItem for ErrorDefinition { 31 | fn item_type(&self) -> ItemType { 32 | ItemType::Error 33 | } 34 | 35 | fn parent(&self) -> Option { 36 | self.parent.clone() 37 | } 38 | 39 | fn name(&self) -> String { 40 | self.name.clone() 41 | } 42 | 43 | fn span(&self) -> TextRange { 44 | self.span.clone() 45 | } 46 | } 47 | 48 | impl Validate for ErrorDefinition { 49 | fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics { 50 | let opts = &options.errors; 51 | let mut out = ItemDiagnostics { 52 | parent: self.parent(), 53 | item_type: self.item_type(), 54 | name: self.name(), 55 | span: self.span(), 56 | diags: vec![], 57 | }; 58 | out.diags.extend(check_notice_and_dev( 59 | &self.natspec, 60 | opts.notice, 61 | opts.dev, 62 | options.notice_or_dev, 63 | self.span(), 64 | )); 65 | out.diags.extend(check_params( 66 | &self.natspec, 67 | opts.param, 68 | &self.params, 69 | self.span(), 70 | )); 71 | out 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use std::sync::LazyLock; 78 | 79 | use similar_asserts::assert_eq; 80 | 81 | use crate::{ 82 | definitions::Definition, 83 | parser::{Parse as _, slang::SlangParser}, 84 | }; 85 | 86 | use super::*; 87 | 88 | static OPTIONS: LazyLock = 89 | LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build()); 90 | 91 | fn parse_file(contents: &str) -> ErrorDefinition { 92 | let mut parser = SlangParser::builder().skip_version_detection(true).build(); 93 | let doc = parser 94 | .parse_document(contents.as_bytes(), None::, false) 95 | .unwrap(); 96 | doc.definitions 97 | .into_iter() 98 | .find_map(Definition::to_error) 99 | .unwrap() 100 | } 101 | 102 | #[test] 103 | fn test_error() { 104 | let contents = "contract Test { 105 | /// @notice An error 106 | /// @param a The first 107 | /// @param b The second 108 | error Foobar(uint256 a, uint256 b); 109 | }"; 110 | let res = parse_file(contents).validate(&OPTIONS); 111 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 112 | } 113 | 114 | #[test] 115 | fn test_error_no_natspec() { 116 | let contents = "contract Test { 117 | error Foobar(uint256 a, uint256 b); 118 | }"; 119 | let res = parse_file(contents).validate(&OPTIONS); 120 | assert_eq!(res.diags.len(), 3); 121 | assert_eq!(res.diags[0].message, "@notice is missing"); 122 | assert_eq!(res.diags[1].message, "@param a is missing"); 123 | assert_eq!(res.diags[2].message, "@param b is missing"); 124 | } 125 | 126 | #[test] 127 | fn test_error_only_notice() { 128 | let contents = "contract Test { 129 | /// @notice An error 130 | error Foobar(uint256 a, uint256 b); 131 | }"; 132 | let res = parse_file(contents).validate(&OPTIONS); 133 | assert_eq!(res.diags.len(), 2); 134 | assert_eq!(res.diags[0].message, "@param a is missing"); 135 | assert_eq!(res.diags[1].message, "@param b is missing"); 136 | } 137 | 138 | #[test] 139 | fn test_error_one_missing() { 140 | let contents = "contract Test { 141 | /// @notice An error 142 | /// @param a The first 143 | error Foobar(uint256 a, uint256 b); 144 | }"; 145 | let res = parse_file(contents).validate(&OPTIONS); 146 | assert_eq!(res.diags.len(), 1); 147 | assert_eq!(res.diags[0].message, "@param b is missing"); 148 | } 149 | 150 | #[test] 151 | fn test_error_multiline() { 152 | let contents = "contract Test { 153 | /** 154 | * @notice An error 155 | * @param a The first 156 | * @param b The second 157 | */ 158 | error Foobar(uint256 a, uint256 b); 159 | }"; 160 | let res = parse_file(contents).validate(&OPTIONS); 161 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 162 | } 163 | 164 | #[test] 165 | fn test_error_duplicate() { 166 | let contents = "contract Test { 167 | /// @notice An error 168 | /// @param a The first 169 | /// @param a The first again 170 | error Foobar(uint256 a); 171 | }"; 172 | let res = parse_file(contents).validate(&OPTIONS); 173 | assert_eq!(res.diags.len(), 1); 174 | assert_eq!(res.diags[0].message, "@param a is present more than once"); 175 | } 176 | 177 | #[test] 178 | fn test_error_no_params() { 179 | let contents = "contract Test { 180 | error Foobar(); 181 | }"; 182 | let res = parse_file(contents).validate(&OPTIONS); 183 | assert_eq!(res.diags.len(), 1); 184 | assert_eq!(res.diags[0].message, "@notice is missing"); 185 | } 186 | 187 | #[test] 188 | fn test_error_inheritdoc() { 189 | // inheritdoc should be ignored as it doesn't apply to errors 190 | let contents = "contract Test { 191 | /// @inheritdoc ITest 192 | error Foobar(uint256 a); 193 | }"; 194 | let res = parse_file(contents).validate(&ValidationOptions::default()); 195 | assert_eq!(res.diags.len(), 2); 196 | assert_eq!(res.diags[0].message, "@notice is missing"); 197 | assert_eq!(res.diags[1].message, "@param a is missing"); 198 | } 199 | 200 | #[test] 201 | fn test_error_no_contract() { 202 | let contents = " 203 | /// @notice An error 204 | /// @param a The first 205 | /// @param b The second 206 | error Foobar(uint256 a, uint256 b); 207 | "; 208 | let res = parse_file(contents).validate(&OPTIONS); 209 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 210 | } 211 | 212 | #[test] 213 | fn test_error_no_contract_missing() { 214 | let contents = "error Foobar(uint256 a, uint256 b);"; 215 | let res = parse_file(contents).validate(&OPTIONS); 216 | assert_eq!(res.diags.len(), 3); 217 | assert_eq!(res.diags[0].message, "@notice is missing"); 218 | assert_eq!(res.diags[1].message, "@param a is missing"); 219 | assert_eq!(res.diags[2].message, "@param b is missing"); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/definitions/event.rs: -------------------------------------------------------------------------------- 1 | //! Parsing and validation of event definitions. 2 | use crate::{ 3 | lint::{ItemDiagnostics, check_notice_and_dev, check_params}, 4 | natspec::NatSpec, 5 | }; 6 | 7 | use super::{Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions}; 8 | 9 | /// An event definition 10 | #[derive(Debug, Clone, bon::Builder)] 11 | #[non_exhaustive] 12 | #[builder(on(String, into))] 13 | pub struct EventDefinition { 14 | /// The parent for the event definition, if any 15 | pub parent: Option, 16 | 17 | /// The name of the event 18 | pub name: String, 19 | 20 | /// The span of the event definition 21 | pub span: TextRange, 22 | 23 | /// The name and span of the event's parameters 24 | pub params: Vec, 25 | 26 | /// The [`NatSpec`] associated with the event definition, if any 27 | pub natspec: Option, 28 | } 29 | 30 | impl SourceItem for EventDefinition { 31 | fn item_type(&self) -> ItemType { 32 | ItemType::Event 33 | } 34 | 35 | fn parent(&self) -> Option { 36 | self.parent.clone() 37 | } 38 | 39 | fn name(&self) -> String { 40 | self.name.clone() 41 | } 42 | 43 | fn span(&self) -> TextRange { 44 | self.span.clone() 45 | } 46 | } 47 | 48 | impl Validate for EventDefinition { 49 | fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics { 50 | let opts = &options.events; 51 | let mut out = ItemDiagnostics { 52 | parent: self.parent(), 53 | item_type: self.item_type(), 54 | name: self.name(), 55 | span: self.span(), 56 | diags: vec![], 57 | }; 58 | out.diags.extend(check_notice_and_dev( 59 | &self.natspec, 60 | opts.notice, 61 | opts.dev, 62 | options.notice_or_dev, 63 | self.span(), 64 | )); 65 | out.diags.extend(check_params( 66 | &self.natspec, 67 | opts.param, 68 | &self.params, 69 | self.span(), 70 | )); 71 | out 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use std::sync::LazyLock; 78 | 79 | use similar_asserts::assert_eq; 80 | 81 | use crate::{ 82 | definitions::Definition, 83 | parser::{Parse as _, slang::SlangParser}, 84 | }; 85 | 86 | use super::*; 87 | 88 | static OPTIONS: LazyLock = 89 | LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build()); 90 | 91 | fn parse_file(contents: &str) -> EventDefinition { 92 | let mut parser = SlangParser::builder().skip_version_detection(true).build(); 93 | let doc = parser 94 | .parse_document(contents.as_bytes(), None::, false) 95 | .unwrap(); 96 | doc.definitions 97 | .into_iter() 98 | .find_map(Definition::to_event) 99 | .unwrap() 100 | } 101 | 102 | #[test] 103 | fn test_event() { 104 | let contents = "contract Test { 105 | /// @notice An event 106 | /// @param a The first 107 | /// @param b The second 108 | event Foobar(uint256 a, uint256 b); 109 | }"; 110 | let res = parse_file(contents).validate(&OPTIONS); 111 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 112 | } 113 | 114 | #[test] 115 | fn test_event_no_natspec() { 116 | let contents = "contract Test { 117 | event Foobar(uint256 a, uint256 b); 118 | }"; 119 | let res = parse_file(contents).validate(&OPTIONS); 120 | assert_eq!(res.diags.len(), 3); 121 | assert_eq!(res.diags[0].message, "@notice is missing"); 122 | assert_eq!(res.diags[1].message, "@param a is missing"); 123 | assert_eq!(res.diags[2].message, "@param b is missing"); 124 | } 125 | 126 | #[test] 127 | fn test_event_only_notice() { 128 | let contents = "contract Test { 129 | /// @notice An event 130 | event Foobar(uint256 a, uint256 b); 131 | }"; 132 | let res = parse_file(contents).validate(&OPTIONS); 133 | assert_eq!(res.diags.len(), 2); 134 | assert_eq!(res.diags[0].message, "@param a is missing"); 135 | assert_eq!(res.diags[1].message, "@param b is missing"); 136 | } 137 | 138 | #[test] 139 | fn test_event_multiline() { 140 | let contents = "contract Test { 141 | /** 142 | * @notice An event 143 | * @param a The first 144 | * @param b The second 145 | */ 146 | event Foobar(uint256 a, uint256 b); 147 | }"; 148 | let res = parse_file(contents).validate(&OPTIONS); 149 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 150 | } 151 | 152 | #[test] 153 | fn test_event_duplicate() { 154 | let contents = "contract Test { 155 | /// @notice An event 156 | /// @param a The first 157 | /// @param a The first again 158 | event Foobar(uint256 a); 159 | }"; 160 | let res = parse_file(contents).validate(&OPTIONS); 161 | assert_eq!(res.diags.len(), 1); 162 | assert_eq!(res.diags[0].message, "@param a is present more than once"); 163 | } 164 | 165 | #[test] 166 | fn test_event_no_params() { 167 | let contents = "contract Test { 168 | event Foobar(); 169 | }"; 170 | let res = parse_file(contents).validate(&OPTIONS); 171 | assert_eq!(res.diags.len(), 1); 172 | assert_eq!(res.diags[0].message, "@notice is missing"); 173 | } 174 | 175 | #[test] 176 | fn test_event_inheritdoc() { 177 | // inheritdoc should be ignored as it doesn't apply to events 178 | let contents = "contract Test { 179 | /// @inheritdoc ITest 180 | event Foobar(uint256 a); 181 | }"; 182 | let res = parse_file(contents).validate(&ValidationOptions::default()); 183 | assert_eq!(res.diags.len(), 2); 184 | assert_eq!(res.diags[0].message, "@notice is missing"); 185 | assert_eq!(res.diags[1].message, "@param a is missing"); 186 | } 187 | 188 | #[test] 189 | fn test_event_no_contract() { 190 | let contents = " 191 | /// @notice An event 192 | /// @param a The first 193 | /// @param b The second 194 | event Foobar(uint256 a, uint256 b); 195 | "; 196 | let res = parse_file(contents).validate(&OPTIONS); 197 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 198 | } 199 | 200 | #[test] 201 | fn test_event_no_contract_missing() { 202 | let contents = "event Foobar(uint256 a, uint256 b);"; 203 | let res = parse_file(contents).validate(&OPTIONS); 204 | assert_eq!(res.diags.len(), 3); 205 | assert_eq!(res.diags[0].message, "@notice is missing"); 206 | assert_eq!(res.diags[1].message, "@param a is missing"); 207 | assert_eq!(res.diags[2].message, "@param b is missing"); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/definitions/function.rs: -------------------------------------------------------------------------------- 1 | //! Parsing and validation of function definitions. 2 | use crate::{ 3 | lint::{Diagnostic, ItemDiagnostics, check_notice_and_dev, check_params, check_returns}, 4 | natspec::{NatSpec, NatSpecKind}, 5 | }; 6 | 7 | use super::{ 8 | Attributes, Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions, 9 | Visibility, 10 | }; 11 | 12 | /// A function definition 13 | #[derive(Debug, Clone, bon::Builder)] 14 | #[non_exhaustive] 15 | #[builder(on(String, into))] 16 | pub struct FunctionDefinition { 17 | /// The parent for the function definition (should always be `Some`) 18 | pub parent: Option, 19 | 20 | /// The name of the function 21 | pub name: String, 22 | 23 | /// The span of the function definition, exluding the body 24 | pub span: TextRange, 25 | 26 | /// The name and span of the function's parameters 27 | pub params: Vec, 28 | 29 | /// The name and span of the function's returns 30 | pub returns: Vec, 31 | 32 | /// The [`NatSpec`] associated with the function definition, if any 33 | pub natspec: Option, 34 | 35 | /// The attributes of the function (visibility and override) 36 | pub attributes: Attributes, 37 | } 38 | 39 | impl FunctionDefinition { 40 | /// Check whether this function requires inheritdoc when we enforce it 41 | /// 42 | /// External and public functions, as well as overridden internal functions must have inheritdoc. 43 | fn requires_inheritdoc(&self) -> bool { 44 | let parent_is_contract = matches!(self.parent, Some(Parent::Contract(_))); 45 | let internal_override = 46 | self.attributes.visibility == Visibility::Internal && self.attributes.r#override; 47 | let public_external = matches!( 48 | self.attributes.visibility, 49 | Visibility::External | Visibility::Public 50 | ); 51 | parent_is_contract && (internal_override || public_external) 52 | } 53 | } 54 | 55 | impl SourceItem for FunctionDefinition { 56 | fn item_type(&self) -> ItemType { 57 | match self.attributes.visibility { 58 | Visibility::External => ItemType::ExternalFunction, 59 | Visibility::Internal => ItemType::InternalFunction, 60 | Visibility::Private => ItemType::PrivateFunction, 61 | Visibility::Public => ItemType::PublicFunction, 62 | } 63 | } 64 | 65 | fn parent(&self) -> Option { 66 | self.parent.clone() 67 | } 68 | 69 | fn name(&self) -> String { 70 | self.name.clone() 71 | } 72 | 73 | fn span(&self) -> TextRange { 74 | self.span.clone() 75 | } 76 | } 77 | 78 | impl Validate for FunctionDefinition { 79 | fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics { 80 | let mut out = ItemDiagnostics { 81 | parent: self.parent(), 82 | item_type: self.item_type(), 83 | name: self.name(), 84 | span: self.span(), 85 | diags: vec![], 86 | }; 87 | // fallback and receive do not require NatSpec 88 | if self.name == "receive" || self.name == "fallback" { 89 | return out; 90 | } 91 | let opts = match self.attributes.visibility { 92 | Visibility::External => options.functions.external, 93 | Visibility::Internal => options.functions.internal, 94 | Visibility::Private => options.functions.private, 95 | Visibility::Public => options.functions.public, 96 | }; 97 | if let Some(natspec) = &self.natspec { 98 | // if there is `inheritdoc`, no further validation is required 99 | if natspec 100 | .items 101 | .iter() 102 | .any(|n| matches!(n.kind, NatSpecKind::Inheritdoc { .. })) 103 | { 104 | return out; 105 | } else if options.inheritdoc && self.requires_inheritdoc() { 106 | out.diags.push(Diagnostic { 107 | span: self.span(), 108 | message: "@inheritdoc is missing".to_string(), 109 | }); 110 | return out; 111 | } 112 | } else if options.inheritdoc && self.requires_inheritdoc() { 113 | out.diags.push(Diagnostic { 114 | span: self.span(), 115 | message: "@inheritdoc is missing".to_string(), 116 | }); 117 | return out; 118 | } 119 | out.diags.extend(check_notice_and_dev( 120 | &self.natspec, 121 | opts.notice, 122 | opts.dev, 123 | options.notice_or_dev, 124 | self.span(), 125 | )); 126 | out.diags.extend(check_params( 127 | &self.natspec, 128 | opts.param, 129 | &self.params, 130 | self.span(), 131 | )); 132 | out.diags.extend(check_returns( 133 | &self.natspec, 134 | opts.returns, 135 | &self.returns, 136 | self.span(), 137 | false, 138 | )); 139 | out 140 | } 141 | } 142 | 143 | #[cfg(test)] 144 | mod tests { 145 | use std::sync::LazyLock; 146 | 147 | use similar_asserts::assert_eq; 148 | 149 | use crate::{ 150 | definitions::Definition, 151 | parser::{Parse as _, slang::SlangParser}, 152 | }; 153 | 154 | use super::*; 155 | 156 | static OPTIONS: LazyLock = 157 | LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build()); 158 | 159 | fn parse_file(contents: &str) -> FunctionDefinition { 160 | let mut parser = SlangParser::builder().skip_version_detection(true).build(); 161 | let doc = parser 162 | .parse_document(contents.as_bytes(), None::, false) 163 | .unwrap(); 164 | doc.definitions 165 | .into_iter() 166 | .find_map(Definition::to_function) 167 | .unwrap() 168 | } 169 | 170 | #[test] 171 | fn test_function() { 172 | let contents = "contract Test { 173 | /// @notice A function 174 | /// @param param1 Test 175 | /// @param param2 Test2 176 | /// @return First output 177 | /// @return out Second output 178 | function foo(uint256 param1, bytes calldata param2) public returns (uint256, uint256 out) { } 179 | }"; 180 | let res = parse_file(contents).validate(&OPTIONS); 181 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 182 | } 183 | 184 | #[test] 185 | fn test_function_no_natspec() { 186 | let contents = "contract Test { 187 | function foo(uint256 param1, bytes calldata param2) public returns (uint256, uint256 out) { } 188 | }"; 189 | let res = parse_file(contents).validate(&OPTIONS); 190 | assert_eq!(res.diags.len(), 5); 191 | assert_eq!(res.diags[0].message, "@notice is missing"); 192 | assert_eq!(res.diags[1].message, "@param param1 is missing"); 193 | assert_eq!(res.diags[2].message, "@param param2 is missing"); 194 | assert_eq!( 195 | res.diags[3].message, 196 | "@return missing for unnamed return #1" 197 | ); 198 | assert_eq!(res.diags[4].message, "@return out is missing"); 199 | } 200 | 201 | #[test] 202 | fn test_function_only_notice() { 203 | let contents = "contract Test { 204 | /// @notice The function 205 | function foo(uint256 param1, bytes calldata param2) public returns (uint256, uint256 out) { } 206 | }"; 207 | let res = parse_file(contents).validate(&OPTIONS); 208 | assert_eq!(res.diags.len(), 4); 209 | assert_eq!(res.diags[0].message, "@param param1 is missing"); 210 | assert_eq!(res.diags[1].message, "@param param2 is missing"); 211 | assert_eq!( 212 | res.diags[2].message, 213 | "@return missing for unnamed return #1" 214 | ); 215 | assert_eq!(res.diags[3].message, "@return out is missing"); 216 | } 217 | 218 | #[test] 219 | fn test_function_one_missing() { 220 | let contents = "contract Test { 221 | /// @notice A function 222 | /// @param param1 The first 223 | function foo(uint256 param1, bytes calldata param2) public { } 224 | }"; 225 | let res = parse_file(contents).validate(&OPTIONS); 226 | assert_eq!(res.diags.len(), 1); 227 | assert_eq!(res.diags[0].message, "@param param2 is missing"); 228 | } 229 | 230 | #[test] 231 | fn test_function_multiline() { 232 | let contents = "contract Test { 233 | /** 234 | * @notice A function 235 | * @param param1 Test 236 | * @param param2 Test2 237 | */ 238 | function foo(uint256 param1, bytes calldata param2) public { } 239 | }"; 240 | let res = parse_file(contents).validate(&OPTIONS); 241 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 242 | } 243 | 244 | #[test] 245 | fn test_function_duplicate() { 246 | let contents = "contract Test { 247 | /// @notice A function 248 | /// @param param1 The first 249 | /// @param param1 The first again 250 | function foo(uint256 param1) public { } 251 | }"; 252 | let res = parse_file(contents).validate(&OPTIONS); 253 | assert_eq!(res.diags.len(), 1); 254 | assert_eq!( 255 | res.diags[0].message, 256 | "@param param1 is present more than once" 257 | ); 258 | } 259 | 260 | #[test] 261 | fn test_function_duplicate_return() { 262 | let contents = "contract Test { 263 | /// @notice A function 264 | /// @return out The output 265 | /// @return out The output again 266 | function foo() public returns (uint256 out) { } 267 | }"; 268 | let res = parse_file(contents).validate(&OPTIONS); 269 | assert_eq!(res.diags.len(), 1); 270 | assert_eq!( 271 | res.diags[0].message, 272 | "@return out is present more than once" 273 | ); 274 | } 275 | 276 | #[test] 277 | fn test_function_duplicate_unnamed_return() { 278 | let contents = "contract Test { 279 | /// @notice A function 280 | /// @return The output 281 | /// @return The output again 282 | function foo() public returns (uint256) { } 283 | }"; 284 | let res = parse_file(contents).validate(&OPTIONS); 285 | assert_eq!(res.diags.len(), 1); 286 | assert_eq!(res.diags[0].message, "too many unnamed returns"); 287 | } 288 | 289 | #[test] 290 | fn test_function_no_params() { 291 | let contents = "contract Test { 292 | function foo() public { } 293 | }"; 294 | let res = parse_file(contents).validate(&OPTIONS); 295 | assert_eq!(res.diags.len(), 1); 296 | assert_eq!(res.diags[0].message, "@notice is missing"); 297 | } 298 | 299 | #[test] 300 | fn test_requires_inheritdoc() { 301 | let contents = "contract Test is ITest { 302 | function a() internal returns (uint256) { } 303 | }"; 304 | let res = parse_file(contents); 305 | assert!(!res.requires_inheritdoc()); 306 | 307 | let contents = "contract Test is ITest { 308 | function b() private returns (uint256) { } 309 | }"; 310 | let res = parse_file(contents); 311 | assert!(!res.requires_inheritdoc()); 312 | 313 | let contents = "contract Test is ITest { 314 | function c() external returns (uint256) { } 315 | }"; 316 | let res = parse_file(contents); 317 | assert!(res.requires_inheritdoc()); 318 | 319 | let contents = "contract Test is ITest { 320 | function d() public returns (uint256) { } 321 | }"; 322 | let res = parse_file(contents); 323 | assert!(res.requires_inheritdoc()); 324 | 325 | let contents = "contract Test is ITest { 326 | function e() internal override (ITest) returns (uint256) { } 327 | }"; 328 | let res = parse_file(contents); 329 | assert!(res.requires_inheritdoc()); 330 | } 331 | 332 | #[test] 333 | fn test_function_inheritdoc() { 334 | let contents = "contract Test is ITest { 335 | /// @inheritdoc ITest 336 | function foo() external { } 337 | }"; 338 | let res = parse_file(contents).validate(&ValidationOptions::default()); 339 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 340 | } 341 | 342 | #[test] 343 | fn test_function_inheritdoc_missing() { 344 | let contents = "contract Test is ITest { 345 | /// @notice Test 346 | function foo() external { } 347 | }"; 348 | let res = parse_file(contents).validate(&ValidationOptions::default()); 349 | assert_eq!(res.diags.len(), 1); 350 | assert_eq!(res.diags[0].message, "@inheritdoc is missing"); 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/definitions/modifier.rs: -------------------------------------------------------------------------------- 1 | //! Parsing and validation of modifier definitions. 2 | use crate::{ 3 | lint::{Diagnostic, ItemDiagnostics, check_notice_and_dev, check_params}, 4 | natspec::{NatSpec, NatSpecKind}, 5 | }; 6 | 7 | use super::{ 8 | Attributes, Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions, 9 | }; 10 | 11 | /// A modifier definition 12 | #[derive(Debug, Clone, bon::Builder)] 13 | #[non_exhaustive] 14 | #[builder(on(String, into))] 15 | pub struct ModifierDefinition { 16 | /// The parent for the modifier definition (should always be `Some`) 17 | pub parent: Option, 18 | 19 | /// The name of the modifier 20 | pub name: String, 21 | 22 | /// The span of the modifier definition, exluding the body 23 | pub span: TextRange, 24 | 25 | /// The name and span of the modifier's parameters 26 | pub params: Vec, 27 | 28 | /// The [`NatSpec`] associated with the modifier definition, if any 29 | pub natspec: Option, 30 | 31 | /// The attributes of the modifier (override) 32 | pub attributes: Attributes, 33 | } 34 | 35 | impl ModifierDefinition { 36 | /// Check whether this modifier requires inheritdoc when we enforce it 37 | /// 38 | /// Overridden modifiers must have inheritdoc. 39 | fn requires_inheritdoc(&self) -> bool { 40 | let parent_is_contract = matches!(self.parent, Some(Parent::Contract(_))); 41 | parent_is_contract && self.attributes.r#override 42 | } 43 | } 44 | 45 | impl SourceItem for ModifierDefinition { 46 | fn item_type(&self) -> ItemType { 47 | ItemType::Modifier 48 | } 49 | 50 | fn parent(&self) -> Option { 51 | self.parent.clone() 52 | } 53 | 54 | fn name(&self) -> String { 55 | self.name.clone() 56 | } 57 | 58 | fn span(&self) -> TextRange { 59 | self.span.clone() 60 | } 61 | } 62 | 63 | impl Validate for ModifierDefinition { 64 | fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics { 65 | let opts = &options.modifiers; 66 | let mut out = ItemDiagnostics { 67 | parent: self.parent(), 68 | item_type: self.item_type(), 69 | name: self.name(), 70 | span: self.span(), 71 | diags: vec![], 72 | }; 73 | if let Some(natspec) = &self.natspec { 74 | // if there is `inheritdoc`, no further validation is required 75 | if natspec 76 | .items 77 | .iter() 78 | .any(|n| matches!(n.kind, NatSpecKind::Inheritdoc { .. })) 79 | { 80 | return out; 81 | } else if options.inheritdoc && self.requires_inheritdoc() { 82 | out.diags.push(Diagnostic { 83 | span: self.span(), 84 | message: "@inheritdoc is missing".to_string(), 85 | }); 86 | return out; 87 | } 88 | } else if options.inheritdoc && self.requires_inheritdoc() { 89 | out.diags.push(Diagnostic { 90 | span: self.span(), 91 | message: "@inheritdoc is missing".to_string(), 92 | }); 93 | return out; 94 | } 95 | out.diags.extend(check_notice_and_dev( 96 | &self.natspec, 97 | opts.notice, 98 | opts.dev, 99 | options.notice_or_dev, 100 | self.span(), 101 | )); 102 | out.diags.extend(check_params( 103 | &self.natspec, 104 | opts.param, 105 | &self.params, 106 | self.span(), 107 | )); 108 | out 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use std::sync::LazyLock; 115 | 116 | use similar_asserts::assert_eq; 117 | 118 | use crate::{ 119 | definitions::Definition, 120 | parser::{Parse as _, slang::SlangParser}, 121 | }; 122 | 123 | use super::*; 124 | 125 | static OPTIONS: LazyLock = 126 | LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build()); 127 | 128 | fn parse_file(contents: &str) -> ModifierDefinition { 129 | let mut parser = SlangParser::builder().skip_version_detection(true).build(); 130 | let doc = parser 131 | .parse_document(contents.as_bytes(), None::, false) 132 | .unwrap(); 133 | doc.definitions 134 | .into_iter() 135 | .find_map(Definition::to_modifier) 136 | .unwrap() 137 | } 138 | 139 | #[test] 140 | fn test_modifier() { 141 | let contents = "contract Test { 142 | /// @notice A modifier 143 | /// @param param1 Test 144 | /// @param param2 Test2 145 | modifier foo(uint256 param1, bytes calldata param2) { _; } 146 | }"; 147 | let res = parse_file(contents).validate(&OPTIONS); 148 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 149 | } 150 | 151 | #[test] 152 | fn test_modifier_no_natspec() { 153 | let contents = "contract Test { 154 | modifier foo(uint256 param1, bytes calldata param2) { _; } 155 | }"; 156 | let res = parse_file(contents).validate(&OPTIONS); 157 | assert_eq!(res.diags.len(), 3); 158 | assert_eq!(res.diags[0].message, "@notice is missing"); 159 | assert_eq!(res.diags[1].message, "@param param1 is missing"); 160 | assert_eq!(res.diags[2].message, "@param param2 is missing"); 161 | } 162 | 163 | #[test] 164 | fn test_modifier_only_notice() { 165 | let contents = "contract Test { 166 | /// @notice The modifier 167 | modifier foo(uint256 param1, bytes calldata param2) { _; } 168 | }"; 169 | let res = parse_file(contents).validate(&OPTIONS); 170 | assert_eq!(res.diags.len(), 2); 171 | assert_eq!(res.diags[0].message, "@param param1 is missing"); 172 | assert_eq!(res.diags[1].message, "@param param2 is missing"); 173 | } 174 | 175 | #[test] 176 | fn test_modifier_one_missing() { 177 | let contents = "contract Test { 178 | /// @notice A modifier 179 | /// @param param1 The first 180 | modifier foo(uint256 param1, bytes calldata param2) { _; } 181 | }"; 182 | let res = parse_file(contents).validate(&OPTIONS); 183 | assert_eq!(res.diags.len(), 1); 184 | assert_eq!(res.diags[0].message, "@param param2 is missing"); 185 | } 186 | 187 | #[test] 188 | fn test_modifier_multiline() { 189 | let contents = "contract Test { 190 | /** 191 | * @notice A modifier 192 | * @param param1 Test 193 | * @param param2 Test2 194 | */ 195 | modifier foo(uint256 param1, bytes calldata param2) { _; } 196 | }"; 197 | let res = parse_file(contents).validate(&OPTIONS); 198 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 199 | } 200 | 201 | #[test] 202 | fn test_modifier_duplicate() { 203 | let contents = "contract Test { 204 | /// @notice A modifier 205 | /// @param param1 The first 206 | /// @param param1 The first again 207 | modifier foo(uint256 param1) { _; } 208 | }"; 209 | let res = parse_file(contents).validate(&OPTIONS); 210 | assert_eq!(res.diags.len(), 1); 211 | assert_eq!( 212 | res.diags[0].message, 213 | "@param param1 is present more than once" 214 | ); 215 | } 216 | 217 | #[test] 218 | fn test_modifier_no_params() { 219 | let contents = "contract Test { 220 | /// @notice A modifier 221 | modifier foo() { _; } 222 | }"; 223 | let res = parse_file(contents).validate(&OPTIONS); 224 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 225 | } 226 | 227 | #[test] 228 | fn test_modifier_no_params_no_paren() { 229 | let contents = "contract Test { 230 | /// @notice A modifier 231 | modifier foo { _; } 232 | }"; 233 | let res = parse_file(contents).validate(&OPTIONS); 234 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 235 | } 236 | 237 | #[test] 238 | fn test_requires_inheritdoc() { 239 | let contents = "contract Test is ITest { 240 | modifier a() { _; } 241 | }"; 242 | let res = parse_file(contents); 243 | assert!(!res.requires_inheritdoc()); 244 | 245 | let contents = "contract Test is ITest { 246 | modifier e() override (ITest) { _; } 247 | }"; 248 | let res = parse_file(contents); 249 | assert!(res.requires_inheritdoc()); 250 | } 251 | 252 | #[test] 253 | fn test_modifier_inheritdoc() { 254 | let contents = "contract Test is ITest { 255 | /// @inheritdoc ITest 256 | modifier foo() override (ITest) { _; } 257 | }"; 258 | let res = parse_file(contents).validate(&ValidationOptions::default()); 259 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 260 | } 261 | 262 | #[test] 263 | fn test_modifier_inheritdoc_missing() { 264 | let contents = "contract Test is ITest { 265 | /// @notice Test 266 | modifier foo() override (ITest) { _; } 267 | }"; 268 | let res = parse_file(contents).validate(&ValidationOptions::default()); 269 | assert_eq!(res.diags.len(), 1); 270 | assert_eq!(res.diags[0].message, "@inheritdoc is missing"); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/definitions/structure.rs: -------------------------------------------------------------------------------- 1 | //! Parsing and validation of struct definitions. 2 | use crate::{ 3 | lint::{ItemDiagnostics, check_notice_and_dev, check_params}, 4 | natspec::NatSpec, 5 | }; 6 | 7 | use super::{Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions}; 8 | 9 | /// A struct definition 10 | #[derive(Debug, Clone, bon::Builder)] 11 | #[non_exhaustive] 12 | #[builder(on(String, into))] 13 | pub struct StructDefinition { 14 | /// The parent for the struct definition, if any 15 | pub parent: Option, 16 | 17 | /// The name of the struct 18 | pub name: String, 19 | 20 | /// The span of the struct definition 21 | pub span: TextRange, 22 | 23 | /// The name and span of the struct members 24 | pub members: Vec, 25 | 26 | /// The [`NatSpec`] associated with the struct definition, if any 27 | pub natspec: Option, 28 | } 29 | 30 | impl SourceItem for StructDefinition { 31 | fn item_type(&self) -> ItemType { 32 | ItemType::Struct 33 | } 34 | 35 | fn parent(&self) -> Option { 36 | self.parent.clone() 37 | } 38 | 39 | fn name(&self) -> String { 40 | self.name.clone() 41 | } 42 | 43 | fn span(&self) -> TextRange { 44 | self.span.clone() 45 | } 46 | } 47 | 48 | impl Validate for StructDefinition { 49 | fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics { 50 | let opts = &options.structs; 51 | let mut out = ItemDiagnostics { 52 | parent: self.parent(), 53 | item_type: self.item_type(), 54 | name: self.name(), 55 | span: self.span(), 56 | diags: vec![], 57 | }; 58 | out.diags.extend(check_notice_and_dev( 59 | &self.natspec, 60 | opts.notice, 61 | opts.dev, 62 | options.notice_or_dev, 63 | self.span(), 64 | )); 65 | out.diags.extend(check_params( 66 | &self.natspec, 67 | opts.param, 68 | &self.members, 69 | self.span(), 70 | )); 71 | out 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use std::sync::LazyLock; 78 | 79 | use similar_asserts::assert_eq; 80 | 81 | use crate::{ 82 | config::WithParamsRules, 83 | definitions::Definition, 84 | parser::{Parse as _, slang::SlangParser}, 85 | }; 86 | 87 | use super::*; 88 | 89 | static OPTIONS: LazyLock = LazyLock::new(|| { 90 | ValidationOptions::builder() 91 | .inheritdoc(false) 92 | .structs(WithParamsRules::required()) 93 | .build() 94 | }); 95 | 96 | fn parse_file(contents: &str) -> StructDefinition { 97 | let mut parser = SlangParser::builder().skip_version_detection(true).build(); 98 | let doc = parser 99 | .parse_document(contents.as_bytes(), None::, false) 100 | .unwrap(); 101 | doc.definitions 102 | .into_iter() 103 | .find_map(Definition::to_struct) 104 | .unwrap() 105 | } 106 | 107 | #[test] 108 | fn test_struct() { 109 | let contents = "contract Test { 110 | /// @notice A struct 111 | struct Foobar { 112 | uint256 a; 113 | bool b; 114 | } 115 | }"; 116 | let res = 117 | parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(false).build()); 118 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 119 | } 120 | 121 | #[test] 122 | fn test_struct_missing() { 123 | let contents = "contract Test { 124 | struct Foobar { 125 | uint256 a; 126 | bool b; 127 | } 128 | }"; 129 | let res = parse_file(contents).validate(&OPTIONS); 130 | assert_eq!(res.diags.len(), 3); 131 | assert_eq!(res.diags[0].message, "@notice is missing"); 132 | assert_eq!(res.diags[1].message, "@param a is missing"); 133 | assert_eq!(res.diags[2].message, "@param b is missing"); 134 | } 135 | 136 | #[test] 137 | fn test_struct_params() { 138 | let contents = "contract Test { 139 | /// @notice A struct 140 | /// @param a The first 141 | /// @param b The second 142 | struct Foobar { 143 | uint256 a; 144 | bool b; 145 | } 146 | }"; 147 | let res = parse_file(contents).validate(&OPTIONS); 148 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 149 | } 150 | 151 | #[test] 152 | fn test_struct_only_notice() { 153 | let contents = "contract Test { 154 | /// @notice A struct 155 | struct Foobar { 156 | uint256 a; 157 | bool b; 158 | } 159 | }"; 160 | let res = parse_file(contents).validate(&OPTIONS); 161 | assert_eq!(res.diags.len(), 2); 162 | assert_eq!(res.diags[0].message, "@param a is missing"); 163 | assert_eq!(res.diags[1].message, "@param b is missing"); 164 | } 165 | 166 | #[test] 167 | fn test_struct_one_missing() { 168 | let contents = "contract Test { 169 | /// @notice A struct 170 | /// @param a The first 171 | struct Foobar { 172 | uint256 a; 173 | bool b; 174 | } 175 | }"; 176 | let res = parse_file(contents).validate(&OPTIONS); 177 | assert_eq!(res.diags.len(), 1); 178 | assert_eq!(res.diags[0].message, "@param b is missing"); 179 | } 180 | 181 | #[test] 182 | fn test_struct_multiline() { 183 | let contents = "contract Test { 184 | /** 185 | * @notice A struct 186 | * @param a The first 187 | * @param b The second 188 | */ 189 | struct Foobar { 190 | uint256 a; 191 | bool b; 192 | } 193 | }"; 194 | let res = parse_file(contents).validate(&OPTIONS); 195 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 196 | } 197 | 198 | #[test] 199 | fn test_struct_duplicate() { 200 | let contents = "contract Test { 201 | /// @notice A struct 202 | /// @param a The first 203 | /// @param a The first twice 204 | struct Foobar { 205 | uint256 a; 206 | } 207 | }"; 208 | let res = parse_file(contents).validate(&OPTIONS); 209 | assert_eq!(res.diags.len(), 1); 210 | assert_eq!(res.diags[0].message, "@param a is present more than once"); 211 | } 212 | 213 | #[test] 214 | fn test_struct_inheritdoc() { 215 | // inheritdoc should be ignored as it doesn't apply to structs 216 | let contents = "contract Test { 217 | /// @inheritdoc ISomething 218 | struct Foobar { 219 | uint256 a; 220 | } 221 | }"; 222 | let res = parse_file(contents).validate( 223 | &ValidationOptions::builder() 224 | .inheritdoc(true) 225 | .structs(WithParamsRules::required()) 226 | .build(), 227 | ); 228 | assert_eq!(res.diags.len(), 2); 229 | assert_eq!(res.diags[0].message, "@notice is missing"); 230 | assert_eq!(res.diags[1].message, "@param a is missing"); 231 | } 232 | 233 | #[test] 234 | fn test_struct_no_contract() { 235 | let contents = " 236 | /// @notice A struct 237 | /// @param a The first 238 | /// @param b The second 239 | struct Foobar { 240 | uint256 a; 241 | bool b; 242 | }"; 243 | let res = parse_file(contents).validate(&OPTIONS); 244 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 245 | } 246 | 247 | #[test] 248 | fn test_struct_no_contract_missing() { 249 | let contents = "struct Foobar { 250 | uint256 a; 251 | bool b; 252 | }"; 253 | let res = parse_file(contents).validate(&OPTIONS); 254 | assert_eq!(res.diags.len(), 3); 255 | assert_eq!(res.diags[0].message, "@notice is missing"); 256 | assert_eq!(res.diags[1].message, "@param a is missing"); 257 | assert_eq!(res.diags[2].message, "@param b is missing"); 258 | } 259 | 260 | #[test] 261 | fn test_struct_no_contract_one_missing() { 262 | let contents = " 263 | /// @notice A struct 264 | /// @param a The first 265 | struct Foobar { 266 | uint256 a; 267 | bool b; 268 | }"; 269 | let res = parse_file(contents).validate(&OPTIONS); 270 | assert_eq!(res.diags.len(), 1); 271 | assert_eq!(res.diags[0].message, "@param b is missing"); 272 | } 273 | 274 | #[test] 275 | fn test_struct_missing_space() { 276 | let contents = " 277 | /// @notice A struct 278 | /// @param fooThe param 279 | struct Test { 280 | uint256 foo; 281 | }"; 282 | let res = parse_file(contents).validate(&OPTIONS); 283 | assert_eq!(res.diags.len(), 2); 284 | assert_eq!(res.diags[0].message, "extra @param fooThe"); 285 | assert_eq!(res.diags[1].message, "@param foo is missing"); 286 | } 287 | 288 | #[test] 289 | fn test_struct_extra_param() { 290 | let contents = " 291 | /// @notice A struct 292 | /// @param foo The param 293 | /// @param bar Some other param 294 | struct Test { 295 | uint256 foo; 296 | }"; 297 | let res = parse_file(contents).validate(&OPTIONS); 298 | assert_eq!(res.diags.len(), 1); 299 | assert_eq!(res.diags[0].message, "extra @param bar"); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/definitions/variable.rs: -------------------------------------------------------------------------------- 1 | //! Parsing and validation of state variable declarations. 2 | use crate::{ 3 | lint::{Diagnostic, ItemDiagnostics, check_notice_and_dev, check_returns}, 4 | natspec::{NatSpec, NatSpecKind}, 5 | }; 6 | 7 | use super::{ 8 | Attributes, Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions, 9 | Visibility, 10 | }; 11 | 12 | /// A state variable declaration 13 | #[derive(Debug, Clone, bon::Builder)] 14 | #[non_exhaustive] 15 | #[builder(on(String, into))] 16 | pub struct VariableDeclaration { 17 | /// The parent for the state variable declaration (should always be `Some`) 18 | pub parent: Option, 19 | 20 | /// The name of the state variable 21 | pub name: String, 22 | 23 | /// The span of the state variable declaration 24 | pub span: TextRange, 25 | 26 | /// The [`NatSpec`] associated with the state variable declaration, if any 27 | pub natspec: Option, 28 | 29 | /// The attributes of the state variable (visibility) 30 | pub attributes: Attributes, 31 | } 32 | 33 | impl VariableDeclaration { 34 | /// Check whether this variable requires inheritdoc when we enforce it 35 | /// 36 | /// Public state variables must have inheritdoc. 37 | fn requires_inheritdoc(&self) -> bool { 38 | let parent_is_contract = matches!(self.parent, Some(Parent::Contract(_))); 39 | let public = self.attributes.visibility == Visibility::Public; 40 | parent_is_contract && public 41 | } 42 | } 43 | 44 | impl SourceItem for VariableDeclaration { 45 | fn item_type(&self) -> ItemType { 46 | match self.attributes.visibility { 47 | Visibility::External => unreachable!("variables cannot be external"), 48 | Visibility::Internal => ItemType::InternalVariable, 49 | Visibility::Private => ItemType::PrivateVariable, 50 | Visibility::Public => ItemType::PublicVariable, 51 | } 52 | } 53 | 54 | fn parent(&self) -> Option { 55 | self.parent.clone() 56 | } 57 | 58 | fn name(&self) -> String { 59 | self.name.clone() 60 | } 61 | 62 | fn span(&self) -> TextRange { 63 | self.span.clone() 64 | } 65 | } 66 | 67 | impl Validate for VariableDeclaration { 68 | fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics { 69 | let (notice, dev, returns) = match self.attributes.visibility { 70 | Visibility::External => unreachable!("variables cannot be external"), 71 | Visibility::Internal => ( 72 | options.variables.internal.notice, 73 | options.variables.internal.dev, 74 | None, 75 | ), 76 | Visibility::Private => ( 77 | options.variables.private.notice, 78 | options.variables.private.dev, 79 | None, 80 | ), 81 | Visibility::Public => ( 82 | options.variables.public.notice, 83 | options.variables.public.dev, 84 | Some(options.variables.public.returns), 85 | ), 86 | }; 87 | let mut out = ItemDiagnostics { 88 | parent: self.parent(), 89 | item_type: self.item_type(), 90 | name: self.name(), 91 | span: self.span(), 92 | diags: vec![], 93 | }; 94 | if let Some(natspec) = &self.natspec { 95 | // if there is `inheritdoc`, no further validation is required 96 | if natspec 97 | .items 98 | .iter() 99 | .any(|n| matches!(n.kind, NatSpecKind::Inheritdoc { .. })) 100 | { 101 | return out; 102 | } else if options.inheritdoc && self.requires_inheritdoc() { 103 | out.diags.push(Diagnostic { 104 | span: self.span(), 105 | message: "@inheritdoc is missing".to_string(), 106 | }); 107 | return out; 108 | } 109 | } else if options.inheritdoc && self.requires_inheritdoc() { 110 | out.diags.push(Diagnostic { 111 | span: self.span(), 112 | message: "@inheritdoc is missing".to_string(), 113 | }); 114 | return out; 115 | } 116 | out.diags.extend(check_notice_and_dev( 117 | &self.natspec, 118 | notice, 119 | dev, 120 | options.notice_or_dev, 121 | self.span(), 122 | )); 123 | if let Some(returns) = returns { 124 | out.diags.extend(check_returns( 125 | &self.natspec, 126 | returns, 127 | &[Identifier { 128 | name: None, 129 | span: self.span(), 130 | }], 131 | self.span(), 132 | true, 133 | )); 134 | } 135 | out 136 | } 137 | } 138 | 139 | #[cfg(test)] 140 | mod tests { 141 | use std::sync::LazyLock; 142 | 143 | use similar_asserts::assert_eq; 144 | 145 | use crate::{ 146 | definitions::Definition, 147 | parser::{Parse as _, slang::SlangParser}, 148 | }; 149 | 150 | use super::*; 151 | 152 | static OPTIONS: LazyLock = LazyLock::new(Default::default); 153 | 154 | fn parse_file(contents: &str) -> VariableDeclaration { 155 | let mut parser = SlangParser::builder().skip_version_detection(true).build(); 156 | let doc = parser 157 | .parse_document(contents.as_bytes(), None::, false) 158 | .unwrap(); 159 | doc.definitions 160 | .into_iter() 161 | .find_map(Definition::to_variable) 162 | .unwrap() 163 | } 164 | 165 | #[test] 166 | fn test_variable() { 167 | let contents = "contract Test is ITest { 168 | /// @inheritdoc ITest 169 | uint256 public a; 170 | }"; 171 | let res = parse_file(contents).validate(&OPTIONS); 172 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 173 | } 174 | 175 | #[test] 176 | fn test_variable_no_natspec() { 177 | let contents = "contract Test { 178 | uint256 public a; 179 | }"; 180 | let res = 181 | parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(false).build()); 182 | assert_eq!(res.diags.len(), 2); 183 | assert_eq!(res.diags[0].message, "@notice is missing"); 184 | assert_eq!(res.diags[1].message, "@return is missing"); 185 | } 186 | 187 | #[test] 188 | fn test_variable_no_natspec_inheritdoc() { 189 | let contents = "contract Test { 190 | uint256 public a; 191 | }"; 192 | let res = parse_file(contents).validate(&OPTIONS); 193 | assert_eq!(res.diags.len(), 1); 194 | assert_eq!(res.diags[0].message, "@inheritdoc is missing"); 195 | } 196 | 197 | #[test] 198 | fn test_variable_only_notice() { 199 | let contents = "contract Test { 200 | /// @notice The variable 201 | uint256 public a; 202 | }"; 203 | let res = parse_file(contents).validate(&OPTIONS); 204 | assert_eq!(res.diags.len(), 1); 205 | assert_eq!(res.diags[0].message, "@inheritdoc is missing"); 206 | } 207 | 208 | #[test] 209 | fn test_variable_multiline() { 210 | let contents = "contract Test is ITest { 211 | /** 212 | * @inheritdoc ITest 213 | */ 214 | uint256 public a; 215 | }"; 216 | let res = parse_file(contents).validate(&OPTIONS); 217 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 218 | } 219 | 220 | #[test] 221 | fn test_requires_inheritdoc() { 222 | let contents = "contract Test is ITest { 223 | uint256 public a; 224 | }"; 225 | let res = parse_file(contents); 226 | assert!(res.requires_inheritdoc()); 227 | 228 | let contents = "contract Test is ITest { 229 | uint256 internal a; 230 | }"; 231 | let res = parse_file(contents); 232 | assert!(!res.requires_inheritdoc()); 233 | } 234 | 235 | #[test] 236 | fn test_variable_no_inheritdoc() { 237 | let contents = "contract Test { 238 | /// @notice A variable 239 | uint256 internal a; 240 | }"; 241 | let res = 242 | parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(false).build()); 243 | assert!(res.diags.is_empty(), "{:#?}", res.diags); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! The error and result types for lintspec 2 | use std::path::PathBuf; 3 | 4 | use crate::definitions::{Parent, TextIndex, TextRange}; 5 | 6 | /// The result of a lintspec operation 7 | pub type Result = std::result::Result; 8 | 9 | /// A lintspec error 10 | #[derive(thiserror::Error, Debug)] 11 | #[non_exhaustive] 12 | pub enum Error { 13 | /// Solidity version is not supported 14 | #[error("the provided Solidity version is not supported: `{0}`")] 15 | SolidityUnsupportedVersion(String), 16 | 17 | #[error("there was an error while parsing {path}:{loc}:\n{message}")] 18 | ParsingError { 19 | path: PathBuf, 20 | loc: TextIndex, 21 | message: String, 22 | }, 23 | 24 | /// Error during parsing of a version specifier string 25 | #[error("error parsing a semver string: {0}")] 26 | SemverParsingError(#[from] semver::Error), 27 | 28 | /// Error during parsing of a `NatSpec` comment 29 | #[error("error parsing a natspec comment: {message}")] 30 | NatspecParsingError { 31 | parent: Option, 32 | span: TextRange, 33 | message: String, 34 | }, 35 | 36 | /// IO error 37 | #[error("IO error for {path:?}: {err}")] 38 | IOError { path: PathBuf, err: std::io::Error }, 39 | 40 | /// An unspecified error happening during parsing 41 | #[error("unknown error while parsing Solidity")] 42 | UnknownError, 43 | } 44 | -------------------------------------------------------------------------------- /src/files.rs: -------------------------------------------------------------------------------- 1 | //! Find Solidity files to analyze 2 | use std::{ 3 | path::{Path, PathBuf}, 4 | sync::{Arc, mpsc}, 5 | }; 6 | 7 | use ignore::{WalkBuilder, WalkState, types::TypesBuilder}; 8 | 9 | use crate::error::{Error, Result}; 10 | 11 | /// Find paths to Solidity files in the provided parent paths, in parallel. 12 | /// 13 | /// An optional list of excluded paths (files or folders) can be provided too. 14 | /// `.ignore`, `.gitignore` and `.nsignore` files are honored when filtering the files. 15 | /// Global git ignore configurations as well as parent folder gitignores are not taken into account. 16 | /// Hidden files are included. 17 | /// Returned paths are canonicalized. 18 | pub fn find_sol_files>( 19 | paths: &[T], 20 | exclude: &[T], 21 | sort: bool, 22 | ) -> Result> { 23 | // canonicalize exclude paths 24 | let exclude = exclude 25 | .iter() 26 | .map(|p| { 27 | dunce::canonicalize(p.as_ref()).map_err(|err| Error::IOError { 28 | path: p.as_ref().to_path_buf(), 29 | err, 30 | }) 31 | }) 32 | .collect::>>()?; 33 | let exclude = Arc::new(exclude.iter().map(PathBuf::as_path).collect::>()); 34 | 35 | // types filter to only consider Solidity files 36 | let types = TypesBuilder::new() 37 | .add_defaults() 38 | .negate("all") 39 | .select("solidity") 40 | .build() 41 | .expect("types builder should build"); 42 | 43 | // build the walker 44 | let mut walker: Option = None; 45 | for path in paths { 46 | let path = dunce::canonicalize(path.as_ref()).map_err(|err| Error::IOError { 47 | path: path.as_ref().to_path_buf(), 48 | err, 49 | })?; 50 | if let Some(ext) = path.extension() { 51 | // if users submit paths to non-solidity files, we ignore them 52 | if ext != "sol" { 53 | continue; 54 | } 55 | } 56 | if let Some(ref mut w) = walker { 57 | w.add(path); 58 | } else { 59 | walker = Some(WalkBuilder::new(path)); 60 | } 61 | } 62 | let Some(mut walker) = walker else { 63 | // no path was provided 64 | return Ok(Vec::new()); 65 | }; 66 | walker 67 | .hidden(false) 68 | .git_global(false) 69 | .git_exclude(false) 70 | .add_custom_ignore_filename(".nsignore") 71 | .types(types); 72 | let walker = walker.build_parallel(); 73 | 74 | let (tx, rx) = mpsc::channel::(); 75 | walker.run(|| { 76 | let tx = tx.clone(); 77 | let exclude = Arc::clone(&exclude); 78 | // function executed for each DirEntry 79 | Box::new(move |result| { 80 | let Ok(entry) = result else { 81 | return WalkState::Continue; 82 | }; 83 | let path = entry.path(); 84 | // skip path if excluded (don't descend into directories and skip files) 85 | if exclude.contains(&path) { 86 | return WalkState::Skip; 87 | } 88 | // descend into other directories 89 | if path.is_dir() { 90 | return WalkState::Continue; 91 | } 92 | // we found a suitable file 93 | tx.send(path.to_path_buf()) 94 | .expect("channel receiver should never be dropped before end of function scope"); 95 | WalkState::Continue 96 | }) 97 | }); 98 | 99 | drop(tx); 100 | // this cannot happen before tx is dropped safely 101 | let mut files = Vec::new(); 102 | while let Ok(path) = rx.recv() { 103 | files.push(path); 104 | } 105 | if sort { 106 | files.sort_unstable(); 107 | } 108 | Ok(files) 109 | } 110 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::doc_markdown)] 2 | #![doc = include_str!("../README.md")] 3 | use std::{io, path::Path, sync::Arc}; 4 | 5 | use lint::{FileDiagnostics, ItemDiagnostics}; 6 | use miette::{LabeledSpan, MietteDiagnostic, NamedSource}; 7 | 8 | pub mod config; 9 | pub mod definitions; 10 | pub mod error; 11 | pub mod files; 12 | pub mod lint; 13 | pub mod natspec; 14 | pub mod parser; 15 | pub mod utils; 16 | 17 | /// Print the reports for a given file, either as pretty or compact text output 18 | /// 19 | /// The root path is the current working directory used to compute relative paths if possible. If the file path is 20 | /// not a child of the root path, then the full canonical path of the file is used instead. 21 | /// The writer can be anything that implement [`io::Write`]. 22 | pub fn print_reports( 23 | f: &mut impl io::Write, 24 | root_path: impl AsRef, 25 | file_diags: FileDiagnostics, 26 | compact: bool, 27 | ) -> std::result::Result<(), io::Error> { 28 | if compact { 29 | for item_diags in file_diags.items { 30 | item_diags.print_compact(f, &file_diags.path, &root_path)?; 31 | } 32 | } else { 33 | let source_name = match file_diags.path.strip_prefix(root_path.as_ref()) { 34 | Ok(relative_path) => relative_path.to_string_lossy(), 35 | Err(_) => file_diags.path.to_string_lossy(), 36 | }; 37 | let source = Arc::new(NamedSource::new( 38 | source_name, 39 | file_diags.contents.unwrap_or_default(), 40 | )); 41 | for item_diags in file_diags.items { 42 | print_report(f, Arc::clone(&source), item_diags)?; 43 | } 44 | } 45 | Ok(()) 46 | } 47 | 48 | /// Print a single report related to one source item with [`miette`]. 49 | /// 50 | /// The writer can be anything that implement [`io::Write`]. 51 | fn print_report( 52 | f: &mut impl io::Write, 53 | source: Arc>, 54 | item: ItemDiagnostics, 55 | ) -> std::result::Result<(), io::Error> { 56 | let msg = if let Some(parent) = &item.parent { 57 | format!("{} {}.{}", item.item_type, parent, item.name) 58 | } else { 59 | format!("{} {}", item.item_type, item.name) 60 | }; 61 | let labels: Vec<_> = item 62 | .diags 63 | .into_iter() 64 | .map(|d| { 65 | LabeledSpan::new( 66 | Some(d.message), 67 | d.span.start.utf8, 68 | d.span.end.utf8 - d.span.start.utf8, 69 | ) 70 | }) 71 | .collect(); 72 | let report: miette::Report = MietteDiagnostic::new(msg).with_labels(labels).into(); 73 | write!(f, "{:?}", report.with_source_code(source)) 74 | } 75 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs::File}; 2 | 3 | use anyhow::{Result, bail}; 4 | use clap::Parser as _; 5 | use lintspec::{ 6 | config::{Args, Commands, read_config, write_default_config}, 7 | error::Error, 8 | files::find_sol_files, 9 | lint::{ValidationOptions, lint}, 10 | print_reports, 11 | }; 12 | 13 | #[cfg(not(feature = "solar"))] 14 | use lintspec::parser::slang::SlangParser; 15 | 16 | #[cfg(feature = "solar")] 17 | use lintspec::parser::solar::SolarParser; 18 | 19 | use rayon::iter::{IntoParallelRefIterator as _, ParallelIterator}; 20 | 21 | fn main() -> Result<()> { 22 | dotenvy::dotenv().ok(); // load .env file if present 23 | 24 | // parse config from CLI args, environment variables and the `.lintspec.toml` file. 25 | let args = Args::parse(); 26 | if let Some(Commands::Init) = args.command { 27 | let path = write_default_config()?; 28 | println!("Default config was written to {}", path.display()); 29 | println!("Exiting"); 30 | return Ok(()); 31 | } 32 | 33 | let config = read_config(args)?; 34 | 35 | // identify Solidity files to parse 36 | let paths = find_sol_files( 37 | &config.lintspec.paths, 38 | &config.lintspec.exclude, 39 | config.output.sort, 40 | )?; 41 | if paths.is_empty() { 42 | bail!("no Solidity file found, nothing to analyze"); 43 | } 44 | 45 | // lint all the requested Solidity files 46 | let options: ValidationOptions = (&config).into(); 47 | 48 | #[cfg(feature = "solar")] 49 | let parser = SolarParser {}; 50 | 51 | #[cfg(not(feature = "solar"))] 52 | let parser = SlangParser::builder() 53 | .skip_version_detection(config.lintspec.skip_version_detection) 54 | .build(); 55 | 56 | let diagnostics = paths 57 | .par_iter() 58 | .filter_map(|p| { 59 | lint( 60 | parser.clone(), 61 | p, 62 | &options, 63 | !config.output.compact && !config.output.json, 64 | ) 65 | .map_err(Into::into) 66 | .transpose() 67 | }) 68 | .collect::>>()?; 69 | 70 | // check if we should output to file or to stderr/stdout 71 | let mut output_file: Box = match config.output.out { 72 | Some(path) => { 73 | let _ = miette::set_hook(Box::new(|_| { 74 | Box::new( 75 | miette::MietteHandlerOpts::new() 76 | .terminal_links(false) 77 | .unicode(false) 78 | .color(false) 79 | .build(), 80 | ) 81 | })); 82 | Box::new( 83 | File::options() 84 | .truncate(true) 85 | .create(true) 86 | .write(true) 87 | .open(&path) 88 | .map_err(|err| Error::IOError { 89 | path: path.clone(), 90 | err, 91 | })?, 92 | ) 93 | } 94 | None => { 95 | if diagnostics.is_empty() { 96 | Box::new(std::io::stdout()) 97 | } else { 98 | Box::new(std::io::stderr()) 99 | } 100 | } 101 | }; 102 | 103 | // no issue was found 104 | if diagnostics.is_empty() { 105 | if config.output.json { 106 | writeln!(&mut output_file, "[]")?; 107 | } else { 108 | writeln!(&mut output_file, "No issue found")?; 109 | } 110 | return Ok(()); 111 | } 112 | 113 | // some issues were found, output according to the desired format (json/text, pretty/compact) 114 | if config.output.json { 115 | if config.output.compact { 116 | writeln!(&mut output_file, "{}", serde_json::to_string(&diagnostics)?)?; 117 | } else { 118 | writeln!( 119 | &mut output_file, 120 | "{}", 121 | serde_json::to_string_pretty(&diagnostics)? 122 | )?; 123 | } 124 | } else { 125 | let cwd = dunce::canonicalize(env::current_dir()?)?; 126 | for file_diags in diagnostics { 127 | print_reports(&mut output_file, &cwd, file_diags, config.output.compact)?; 128 | } 129 | } 130 | std::process::exit(1); // indicate that there were diagnostics (errors) 131 | } 132 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | //! Solidity parser interface 2 | use std::{io, path::Path}; 3 | 4 | use crate::{definitions::Definition, error::Result}; 5 | 6 | pub mod slang; 7 | 8 | #[cfg(feature = "solar")] 9 | pub mod solar; 10 | 11 | /// The result of parsing and identifying source items in a document 12 | #[derive(Debug)] 13 | pub struct ParsedDocument { 14 | /// The list of definitions found in the document 15 | pub definitions: Vec, 16 | 17 | /// The contents of the file, if requested 18 | pub contents: Option, 19 | } 20 | 21 | /// The trait implemented by all parsers 22 | pub trait Parse { 23 | /// Parse a document from a reader and identify the relevant source items 24 | /// 25 | /// If a path is provided, then this can be used to enrich diagnostics. 26 | /// The fact that this takes in a mutable reference to the parser allows for stateful parsers. 27 | fn parse_document( 28 | &mut self, 29 | input: impl io::Read, 30 | path: Option>, 31 | keep_contents: bool, 32 | ) -> Result; 33 | } 34 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utils for parsing Solidity source code. 2 | use std::{fmt::Write as _, path::Path, sync::LazyLock}; 3 | 4 | use regex::Regex; 5 | pub use semver; 6 | use semver::{Version, VersionReq}; 7 | use slang_solidity::{ 8 | cst::{NonterminalKind, Query, TextIndex}, 9 | parser::Parser, 10 | }; 11 | 12 | use crate::error::{Error, Result}; 13 | 14 | /// A regex to identify version pragma statements so that the whole file does not need to be parsed. 15 | static REGEX: LazyLock = LazyLock::new(|| { 16 | Regex::new(r"pragma\s+solidity[^;]+;").expect("the version pragma regex should compile") 17 | }); 18 | 19 | /// Search for `pragma solidity` statements in the source and return the highest matching Solidity version. 20 | /// 21 | /// If no pragma directive is found, the version defaults to `0.8.0`. Only the first pragma directive is considered, 22 | /// other ones in the file are ignored. Multiple version specifiers separated by a space are taken as meaning "and", 23 | /// specifiers separated by `||` are taken as meaning "or". Spaces take precedence over double-pipes. 24 | /// 25 | /// Example: `0.6.0 || >=0.7.0 <0.8.0` means "either 0.6.0 or 0.7.x". 26 | /// 27 | /// Within the specifiers' constraints, the highest version that is supported by [`slang_solidity`] is returned. In 28 | /// the above example, version `0.7.6` would be used. 29 | /// 30 | /// # Errors 31 | /// This function errors if the found version string cannot be parsed to a [`VersionReq`] or if the version is not 32 | /// supported by [`slang_solidity`]. 33 | /// 34 | /// # Panics 35 | /// This function panics if the [`Parser::SUPPORTED_VERSIONS`] list is empty. 36 | /// 37 | /// # Examples 38 | /// 39 | /// ``` 40 | /// # use std::path::PathBuf; 41 | /// # use lintspec::utils::{detect_solidity_version, semver::Version}; 42 | /// assert_eq!( 43 | /// detect_solidity_version("pragma solidity >=0.8.4 <0.8.26;", PathBuf::from("./file.sol")).unwrap(), 44 | /// Version::new(0, 8, 25) 45 | /// ); 46 | /// assert_eq!( 47 | /// detect_solidity_version("pragma solidity ^0.4.0 || 0.6.x;", PathBuf::from("./file.sol")).unwrap(), 48 | /// Version::new(0, 6, 12) 49 | /// ); 50 | /// assert_eq!( 51 | /// detect_solidity_version("contract Foo {}", PathBuf::from("./file.sol")).unwrap(), 52 | /// Version::new(0, 8, 0) 53 | /// ); 54 | /// // this version of Solidity does not exist 55 | /// assert!(detect_solidity_version("pragma solidity 0.7.7;", PathBuf::from("./file.sol")).is_err()); 56 | /// ``` 57 | pub fn detect_solidity_version(src: &str, path: impl AsRef) -> Result { 58 | let Some(pragma) = REGEX.find(src) else { 59 | return Ok(Version::new(0, 8, 0)); 60 | }; 61 | 62 | let parser = Parser::create(get_latest_supported_version()) 63 | .expect("the Parser should be initialized correctly with a supported solidity version"); 64 | 65 | let parse_result = parser.parse(NonterminalKind::PragmaDirective, pragma.as_str()); 66 | if !parse_result.is_valid() { 67 | let Some(error) = parse_result.errors().first() else { 68 | return Err(Error::UnknownError); 69 | }; 70 | return Err(Error::ParsingError { 71 | path: path.as_ref().to_path_buf(), 72 | loc: error.text_range().start.into(), 73 | message: error.message(), 74 | }); 75 | } 76 | 77 | let cursor = parse_result.create_tree_cursor(); 78 | let query_set = Query::parse("@version_set [VersionExpressionSet]") 79 | .expect("version set query should compile"); 80 | let query_expr = Query::parse("@version_expr [VersionExpression]") 81 | .expect("version expr query should compile"); 82 | 83 | let mut version_reqs = Vec::new(); 84 | for m in cursor.query(vec![query_set]) { 85 | let Some((_, mut it)) = m.capture("version_set") else { 86 | continue; 87 | }; 88 | let Some(set) = it.next() else { 89 | continue; 90 | }; 91 | version_reqs.push(String::new()); 92 | let cursor = set.node().cursor_with_offset(TextIndex::default()); 93 | for m in cursor.query(vec![query_expr.clone()]) { 94 | let Some((_, mut it)) = m.capture("version_expr") else { 95 | continue; 96 | }; 97 | let Some(expr) = it.next() else { 98 | continue; 99 | }; 100 | let text = expr.node().unparse(); 101 | let text = text.trim(); 102 | // check if we are dealing with a version range with hyphen format 103 | if text.contains('-') { 104 | let (start, end) = text 105 | .split_once('-') 106 | .expect("version range should have a minus character"); 107 | let v = version_reqs 108 | .last_mut() 109 | .expect("version expression should be inside an expression set"); 110 | let _ = write!(v, ",>={},<={}", start.trim(), end.trim()); 111 | } else { 112 | let v = version_reqs 113 | .last_mut() 114 | .expect("version expression should be inside an expression set"); 115 | // for `semver`, the different specifiers should be combined with a comma if they must all match 116 | if let Some(true) = text.chars().next().map(|c| c.is_ascii_digit()) { 117 | // for `semver`, no comparator is the same as the caret comparator, but for solidity is means `=` 118 | let _ = write!(v, ",={text}"); 119 | } else { 120 | let _ = write!(v, ",{text}"); 121 | } 122 | } 123 | } 124 | } 125 | let reqs = version_reqs 126 | .into_iter() 127 | .map(|r| VersionReq::parse(r.trim_start_matches(',')).map_err(Into::into)) 128 | .collect::>>()?; 129 | reqs.iter() 130 | .filter_map(|r| { 131 | Parser::SUPPORTED_VERSIONS 132 | .iter() 133 | .rev() 134 | .find(|v| r.matches(v)) 135 | }) 136 | .max() 137 | .cloned() 138 | .ok_or_else(|| Error::SolidityUnsupportedVersion(pragma.as_str().to_string())) 139 | } 140 | 141 | /// Get the latest Solidity version supported by the [`slang_solidity`] parser 142 | #[must_use] 143 | pub fn get_latest_supported_version() -> Version { 144 | Parser::SUPPORTED_VERSIONS 145 | .last() 146 | .expect("the SUPPORTED_VERSIONS list should not be empty") 147 | .to_owned() 148 | } 149 | -------------------------------------------------------------------------------- /test-data/BasicSample.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.19; 3 | 4 | abstract contract AbstractBasic { 5 | function overriddenFunction() internal pure virtual returns (uint256 _returned); 6 | } 7 | 8 | contract BasicSample is AbstractBasic { 9 | /** 10 | * @notice Some notice of the struct 11 | */ 12 | struct TestStruct { 13 | address someAddress; 14 | uint256 someNumber; 15 | } 16 | 17 | /// @notice An enum 18 | enum TestEnum { 19 | First, 20 | Second 21 | } 22 | 23 | /** 24 | * @notice Some error missing parameter natspec 25 | */ 26 | error BasicSample_SomeError(uint256 _param1); 27 | 28 | /** 29 | * @notice An event missing parameter natspec 30 | */ 31 | event BasicSample_BasicEvent(uint256 _param1); 32 | 33 | /** 34 | * @notice Empty string for revert checks 35 | * @dev result of doing keccak256(bytes('')) 36 | */ 37 | bytes32 internal constant _EMPTY_STRING = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; 38 | 39 | /** 40 | * @notice A public state variable 41 | */ 42 | uint256 public somePublicNumber; 43 | 44 | constructor(bool _randomFlag) {} 45 | 46 | /** 47 | * @notice External function that returns a bool 48 | * @dev A dev comment 49 | * @param _magicNumber A parameter description 50 | * @param _name Another parameter description 51 | * @param _name Another parameter description 52 | * @return _isMagic Some return data 53 | */ 54 | function externalSimple(uint256 _magicNumber, string memory _name) external pure returns (bool _isMagic) { 55 | return true; 56 | } 57 | 58 | /** 59 | * @notice Private test function 60 | * @param _magicNumber A parameter description 61 | */ 62 | function privateSimple(uint256 _magicNumber) private pure {} 63 | 64 | /** 65 | * @notice Private test function 66 | * with multiple 67 | * lines 68 | */ 69 | function multiline() external pure {} 70 | 71 | /** 72 | * @notice Private test function 73 | * @notice Another notice 74 | */ 75 | function multitag() external pure {} 76 | 77 | /** 78 | * @notice External function that returns a bool 79 | * @dev A dev comment 80 | * @param _magicNumber A parameter description 81 | * @param _name Another parameter description 82 | * @return _isMagic Some return data 83 | * @return Test test 84 | */ 85 | function externalSimpleMultipleReturn(uint256 _magicNumber, string memory _name) 86 | external 87 | pure 88 | returns (bool _isMagic, uint256) 89 | { 90 | return (true, 111); 91 | } 92 | 93 | /** 94 | * @notice External function that returns a bool 95 | * @dev A dev comment 96 | * @return Some return data 97 | */ 98 | function externalSimpleMultipleUnnamedReturn() external pure returns (bool, uint256) { 99 | return (true, 111); 100 | } 101 | 102 | /** 103 | * @notice This function should have an inheritdoc tag 104 | */ 105 | function overriddenFunction() internal pure override returns (uint256 _returned) { 106 | return 1; 107 | } 108 | 109 | function virtualFunction() public pure virtual returns (uint256 _returned) {} 110 | 111 | /** 112 | * @notice Modifier notice 113 | */ 114 | modifier transferFee(uint256 _receiver) { 115 | _; 116 | } 117 | 118 | /** 119 | * @dev This func must be ignored 120 | */ 121 | receive() external payable {} 122 | 123 | /** 124 | * @dev This func must be ignored 125 | */ 126 | fallback() external {} 127 | } 128 | -------------------------------------------------------------------------------- /test-data/Fuzzers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | import {StdUtils} from "forge-std/StdUtils.sol"; 6 | 7 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 8 | import {PoolKey} from "../types/PoolKey.sol"; 9 | import {BalanceDelta} from "../types/BalanceDelta.sol"; 10 | import {TickMath} from "../libraries/TickMath.sol"; 11 | import {Pool} from "../libraries/Pool.sol"; 12 | import {PoolModifyLiquidityTest} from "./PoolModifyLiquidityTest.sol"; 13 | import {LiquidityAmounts} from "../../test/utils/LiquidityAmounts.sol"; 14 | import {SafeCast} from "../../src/libraries/SafeCast.sol"; 15 | 16 | /// @notice Taken from the uniswap v4 repository 17 | contract Fuzzers is StdUtils { 18 | using SafeCast for uint256; 19 | 20 | Vm internal constant _vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); 21 | 22 | /// @dev test with a comment 23 | Vm internal constant _vm2 = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); 24 | 25 | function boundLiquidityDelta(PoolKey memory key, int256 liquidityDeltaUnbounded, int256 liquidityMaxByAmount) 26 | internal 27 | pure 28 | returns (int256) 29 | { 30 | int256 liquidityMaxPerTick = int256(uint256(Pool.tickSpacingToMaxLiquidityPerTick(key.tickSpacing))); 31 | 32 | // Finally bound the seeded liquidity by either the max per tick, or by the amount allowed in the position range. 33 | int256 liquidityMax = liquidityMaxByAmount > liquidityMaxPerTick ? liquidityMaxPerTick : liquidityMaxByAmount; 34 | _vm.assume(liquidityMax != 0); 35 | return bound(liquidityDeltaUnbounded, 1, liquidityMax); 36 | } 37 | 38 | // Uses tickSpacingToMaxLiquidityPerTick/2 as one of the possible bounds. 39 | // Potentially adjust this value to be more strict for positions that touch the same tick. 40 | function boundLiquidityDeltaTightly( 41 | PoolKey memory key, 42 | int256 liquidityDeltaUnbounded, 43 | int256 liquidityMaxByAmount, 44 | uint256 maxPositions 45 | ) internal pure returns (int256) { 46 | // Divide by half to bound liquidity more. TODO: Probably a better way to do this. 47 | int256 liquidityMaxTightBound = 48 | int256(uint256(Pool.tickSpacingToMaxLiquidityPerTick(key.tickSpacing)) / maxPositions); 49 | 50 | // Finally bound the seeded liquidity by either the max per tick, or by the amount allowed in the position range. 51 | int256 liquidityMax = 52 | liquidityMaxByAmount > liquidityMaxTightBound ? liquidityMaxTightBound : liquidityMaxByAmount; 53 | _vm.assume(liquidityMax != 0); 54 | return bound(liquidityDeltaUnbounded, 1, liquidityMax); 55 | } 56 | 57 | function getLiquidityDeltaFromAmounts(int24 tickLower, int24 tickUpper, uint160 sqrtPriceX96) 58 | internal 59 | pure 60 | returns (int256) 61 | { 62 | // First get the maximum amount0 and maximum amount1 that can be deposited at this range. 63 | (uint256 maxAmount0, uint256 maxAmount1) = LiquidityAmounts.getAmountsForLiquidity( 64 | sqrtPriceX96, 65 | TickMath.getSqrtPriceAtTick(tickLower), 66 | TickMath.getSqrtPriceAtTick(tickUpper), 67 | uint128(type(int128).max) 68 | ); 69 | 70 | // Compare the max amounts (defined by the range of the position) to the max amount constrained by the type container. 71 | // The true maximum should be the minimum of the two. 72 | // (ie If the position range allows a deposit of more then int128.max in any token, then here we cap it at int128.max.) 73 | 74 | uint256 amount0 = uint256(type(uint128).max / 2); 75 | uint256 amount1 = uint256(type(uint128).max / 2); 76 | 77 | maxAmount0 = maxAmount0 > amount0 ? amount0 : maxAmount0; 78 | maxAmount1 = maxAmount1 > amount1 ? amount1 : maxAmount1; 79 | 80 | int256 liquidityMaxByAmount = uint256( 81 | LiquidityAmounts.getLiquidityForAmounts( 82 | sqrtPriceX96, 83 | TickMath.getSqrtPriceAtTick(tickLower), 84 | TickMath.getSqrtPriceAtTick(tickUpper), 85 | maxAmount0, 86 | maxAmount1 87 | ) 88 | ).toInt256(); 89 | 90 | return liquidityMaxByAmount; 91 | } 92 | 93 | function boundTicks(int24 tickLower, int24 tickUpper, int24 tickSpacing) internal pure returns (int24, int24) { 94 | tickLower = int24( 95 | bound( 96 | int256(tickLower), 97 | int256(TickMath.minUsableTick(tickSpacing)), 98 | int256(TickMath.maxUsableTick(tickSpacing)) 99 | ) 100 | ); 101 | tickUpper = int24( 102 | bound( 103 | int256(tickUpper), 104 | int256(TickMath.minUsableTick(tickSpacing)), 105 | int256(TickMath.maxUsableTick(tickSpacing)) 106 | ) 107 | ); 108 | 109 | // round down ticks 110 | tickLower = (tickLower / tickSpacing) * tickSpacing; 111 | tickUpper = (tickUpper / tickSpacing) * tickSpacing; 112 | 113 | (tickLower, tickUpper) = tickLower < tickUpper ? (tickLower, tickUpper) : (tickUpper, tickLower); 114 | 115 | if (tickLower == tickUpper) { 116 | if (tickLower != TickMath.minUsableTick(tickSpacing)) tickLower = tickLower - tickSpacing; 117 | else tickUpper = tickUpper + tickSpacing; 118 | } 119 | 120 | return (tickLower, tickUpper); 121 | } 122 | 123 | function boundTicks(PoolKey memory key, int24 tickLower, int24 tickUpper) internal pure returns (int24, int24) { 124 | return boundTicks(tickLower, tickUpper, key.tickSpacing); 125 | } 126 | 127 | function createRandomSqrtPriceX96(int24 tickSpacing, int256 seed) internal pure returns (uint160) { 128 | int256 min = int256(TickMath.minUsableTick(tickSpacing)); 129 | int256 max = int256(TickMath.maxUsableTick(tickSpacing)); 130 | int256 randomTick = bound(seed, min + 1, max - 1); 131 | return TickMath.getSqrtPriceAtTick(int24(randomTick)); 132 | } 133 | 134 | /// @dev Obtain fuzzed and bounded parameters for creating liquidity 135 | /// @param key The pool key 136 | /// @param params IPoolManager.ModifyLiquidityParams Note that these parameters are unbounded 137 | /// @param sqrtPriceX96 The current sqrt price 138 | function createFuzzyLiquidityParams( 139 | PoolKey memory key, 140 | IPoolManager.ModifyLiquidityParams memory params, 141 | uint160 sqrtPriceX96 142 | ) internal pure returns (IPoolManager.ModifyLiquidityParams memory result) { 143 | (result.tickLower, result.tickUpper) = boundTicks(key, params.tickLower, params.tickUpper); 144 | int256 liquidityDeltaFromAmounts = 145 | getLiquidityDeltaFromAmounts(result.tickLower, result.tickUpper, sqrtPriceX96); 146 | result.liquidityDelta = boundLiquidityDelta(key, params.liquidityDelta, liquidityDeltaFromAmounts); 147 | } 148 | 149 | // Creates liquidity parameters with a stricter bound. Should be used if multiple positions being initialized on the pool, with potential for tick overlap. 150 | function createFuzzyLiquidityParamsWithTightBound( 151 | PoolKey memory key, 152 | IPoolManager.ModifyLiquidityParams memory params, 153 | uint160 sqrtPriceX96, 154 | uint256 maxPositions 155 | ) internal pure returns (IPoolManager.ModifyLiquidityParams memory result) { 156 | (result.tickLower, result.tickUpper) = boundTicks(key, params.tickLower, params.tickUpper); 157 | int256 liquidityDeltaFromAmounts = 158 | getLiquidityDeltaFromAmounts(result.tickLower, result.tickUpper, sqrtPriceX96); 159 | 160 | result.liquidityDelta = 161 | boundLiquidityDeltaTightly(key, params.liquidityDelta, liquidityDeltaFromAmounts, maxPositions); 162 | } 163 | 164 | function createFuzzyLiquidity( 165 | PoolModifyLiquidityTest modifyLiquidityRouter, 166 | PoolKey memory key, 167 | IPoolManager.ModifyLiquidityParams memory params, 168 | uint160 sqrtPriceX96, 169 | bytes memory hookData 170 | ) internal returns (IPoolManager.ModifyLiquidityParams memory result, BalanceDelta delta) { 171 | result = createFuzzyLiquidityParams(key, params, sqrtPriceX96); 172 | delta = modifyLiquidityRouter.modifyLiquidity(key, result, hookData); 173 | } 174 | 175 | // There exists possible positions in the pool, so we tighten the boundaries of liquidity. 176 | function createFuzzyLiquidityWithTightBound( 177 | PoolModifyLiquidityTest modifyLiquidityRouter, 178 | PoolKey memory key, 179 | IPoolManager.ModifyLiquidityParams memory params, 180 | uint160 sqrtPriceX96, 181 | bytes memory hookData, 182 | uint256 maxPositions 183 | ) internal returns (IPoolManager.ModifyLiquidityParams memory result, BalanceDelta delta) { 184 | result = createFuzzyLiquidityParamsWithTightBound(key, params, sqrtPriceX96, maxPositions); 185 | delta = modifyLiquidityRouter.modifyLiquidity(key, result, hookData); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /test-data/InterfaceSample.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.19; 3 | 4 | interface IInterfacedSample { 5 | /** 6 | * @notice Greets the caller 7 | * 8 | * @return _balance Current token balance of the caller 9 | */ 10 | function greet() external view returns (string memory _greeting, uint256 _balance); 11 | } 12 | 13 | contract InterfacedSample is IInterfacedSample { 14 | /// @dev some dev thingy 15 | function greet() external view returns (string memory _greeting, uint256 _balance) {} 16 | } 17 | -------------------------------------------------------------------------------- /test-data/LatestVersion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.28; 3 | 4 | contract LatestVersion {} 5 | -------------------------------------------------------------------------------- /test-data/LibrarySample.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.19; 3 | 4 | library StringUtils { 5 | function nothing(string memory input) public pure returns (string memory) { 6 | return input; 7 | } 8 | } 9 | 10 | contract BasicSample { 11 | using StringUtils for string; 12 | } 13 | -------------------------------------------------------------------------------- /test-data/ParserTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.19; 3 | 4 | // forgefmt: disable-start 5 | // This file is used for testing the parser 6 | 7 | interface IParserTest { 8 | /// @notice Thrown whenever something goes wrong 9 | error SimpleError(uint256 _param1, uint256 _param2); 10 | 11 | /// @notice Emitted whenever something happens 12 | event SimpleEvent(uint256 _param1, uint256 _param2); 13 | 14 | /// @notice The enum description 15 | enum SimpleEnum { 16 | A, 17 | B, 18 | C 19 | } 20 | 21 | /// @notice View function with no parameters 22 | /// @dev Natspec for the return value is missing 23 | /// @return The returned value 24 | function viewFunctionNoParams() external view returns (uint256); 25 | 26 | /** 27 | * @notice A function with different style of natspec 28 | * @param _param1 The first parameter 29 | * @param _param2 The second parameter 30 | * @return The returned value 31 | */ 32 | function viewFunctionWithParams(uint256 _param1, uint256 _param2) external view returns (uint256); 33 | 34 | /// @notice A state variable 35 | /// @return Some value 36 | function someVariable() external view returns (uint256); 37 | 38 | /// @notice A struct holding 2 variables of type uint256 39 | /// @param a The first variable 40 | /// @param b The second variable 41 | /// @dev This is definitely a struct 42 | struct SimplestStruct { 43 | uint256 a; 44 | uint256 b; 45 | } 46 | 47 | /// @notice A constant of type uint256 48 | function SOME_CONSTANT() external view returns (uint256 _returned); 49 | } 50 | 51 | /// @notice A contract with correct natspec 52 | contract ParserTest is IParserTest { 53 | /// @inheritdoc IParserTest 54 | uint256 public someVariable; 55 | 56 | /// @inheritdoc IParserTest 57 | uint256 public constant SOME_CONSTANT = 123; 58 | 59 | /// @notice The constructor 60 | /// @param _struct The struct parameter 61 | constructor(SimplestStruct memory _struct) { 62 | someVariable = _struct.a + _struct.b; 63 | } 64 | 65 | /// @notice The description of the modifier 66 | /// @param _param1 The only parameter 67 | modifier someModifier(bool _param1) { 68 | _; 69 | } 70 | 71 | /// @notice The description of the modifier 72 | modifier modifierWithoutParam { 73 | _; 74 | } 75 | 76 | fallback() external {} 77 | receive () external payable {} 78 | 79 | /// @inheritdoc IParserTest 80 | /// @dev Dev comment for the function 81 | function viewFunctionNoParams() external pure returns (uint256){ 82 | return 1; 83 | } 84 | 85 | /// @inheritdoc IParserTest 86 | function viewFunctionWithParams(uint256 _param1, uint256 _param2) external pure returns (uint256) { 87 | return _param1 + _param2; 88 | } 89 | 90 | /// @notice Some private stuff 91 | /// @dev Dev comment for the private function 92 | /// @param _paramName The parameter name 93 | /// @return _returned The returned value 94 | function _viewPrivate(uint256 _paramName) private pure returns (uint256 _returned) { 95 | return 1; 96 | } 97 | 98 | /// @notice Some internal stuff 99 | /// @dev Dev comment for the internal function 100 | /// @param _paramName The parameter name 101 | /// @return _returned The returned value 102 | function _viewInternal(uint256 _paramName) internal pure returns (uint256 _returned) { 103 | return 1; 104 | } 105 | 106 | /// @notice Some internal stuff 107 | /// Separate line 108 | /// Third one 109 | function _viewMultiline() internal pure { 110 | } 111 | 112 | /// @notice Some internal stuff 113 | /// @notice Separate line 114 | function _viewDuplicateTag() internal pure { 115 | } 116 | } 117 | 118 | // This is a contract with invalid / missing natspec 119 | contract ParserTestFunny is IParserTest { 120 | // no natspec, just a comment 121 | struct SimpleStruct { 122 | /// @notice The first variable 123 | uint256 a; 124 | /// @notice The first variable 125 | uint256 b; 126 | } 127 | 128 | modifier someModifier() { 129 | _; 130 | } 131 | 132 | /// @inheritdoc IParserTest 133 | /// @dev Providing context 134 | uint256 public someVariable; 135 | 136 | // @inheritdoc IParserTest 137 | uint256 public constant SOME_CONSTANT = 123; 138 | 139 | /// @inheritdoc IParserTest 140 | function viewFunctionNoParams() external view returns (uint256){ 141 | return 1; 142 | } 143 | 144 | // Forgot there is @inheritdoc and @notice 145 | function viewFunctionWithParams(uint256 _param1, uint256 _param2) external view returns (uint256) { 146 | return _param1 + _param2; 147 | } 148 | 149 | // @notice Some internal stuff 150 | function _viewInternal() internal view returns (uint256) { 151 | return 1; 152 | } 153 | 154 | /** 155 | * 156 | * 157 | * 158 | * This should be ignored 159 | */ 160 | 161 | /** 162 | * @notice Some private stuff 163 | * @param _paramName The parameter name 164 | * @return _returned The returned value 165 | */ 166 | function _viewPrivateMulti(uint256 _paramName) private pure returns (uint256 _returned) { 167 | return 1; 168 | } 169 | 170 | /// @dev This should be ignored 171 | /** 172 | * @dev this too 173 | */ 174 | /// @notice Some private stuff 175 | /// @param _paramName The parameter name 176 | /// @return _returned The returned value 177 | function _viewPrivateSingle(uint256 _paramName) private pure returns (uint256 _returned) { 178 | return 1; 179 | } 180 | 181 | // @notice Forgot one slash and it's not natspec anymore 182 | //// @dev Too many slashes is also not a valid comment 183 | function _viewInternal(uint256 _paramName) internal pure returns (uint256 _returned) { 184 | return 1; 185 | } 186 | 187 | /*** 188 | @notice Too many stars is not a valid comment 189 | */ 190 | function _viewBlockLinterFail() internal pure { 191 | } 192 | 193 | /// @notice Linter fail 194 | /// @dev What have I done 195 | function _viewLinterFail() internal pure { 196 | 197 | } 198 | 199 | /////////// 200 | // hello // 201 | /////////// 202 | /// fun fact: there are extra spaces after the 1st return 203 | /// @return 204 | /// @return 205 | function functionUnnamedEmptyReturn() external view returns (uint256, bool){ 206 | return (1, true); 207 | } 208 | } 209 | // forgefmt: disable-end 210 | -------------------------------------------------------------------------------- /test-data/UnsupportedVersion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.1234; 3 | 4 | contract UnsupportedVersion {} 5 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | use lintspec::{ 2 | lint::{FileDiagnostics, ValidationOptions, lint}, 3 | parser::slang::SlangParser, 4 | print_reports, 5 | }; 6 | use std::path::PathBuf; 7 | 8 | #[cfg(feature = "solar")] 9 | use lintspec::parser::solar::SolarParser; 10 | #[cfg(feature = "solar")] 11 | use similar_asserts::assert_eq; 12 | 13 | #[must_use] 14 | pub fn snapshot_content(path: &str, options: &ValidationOptions, keep_contents: bool) -> String { 15 | let diags_slang = lint(SlangParser::builder().build(), path, options, keep_contents).unwrap(); 16 | let output = generate_output(diags_slang); 17 | 18 | #[cfg(feature = "solar")] 19 | { 20 | let diags_solar = lint(SolarParser {}, path, options, keep_contents).unwrap(); 21 | assert_eq!(output, generate_output(diags_solar)); 22 | } 23 | 24 | output 25 | } 26 | 27 | fn generate_output(diags: Option) -> String { 28 | let Some(diags) = diags else { 29 | return String::new(); 30 | }; 31 | let mut buf = Vec::new(); 32 | print_reports(&mut buf, PathBuf::new(), diags, true).unwrap(); 33 | String::from_utf8(buf).unwrap() 34 | } 35 | -------------------------------------------------------------------------------- /tests/snapshots/tests_basic_sample__all.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-basic-sample.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/BasicSample.sol:5:5 6 | function AbstractBasic.overriddenFunction 7 | @notice is missing 8 | @return _returned is missing 9 | 10 | ./test-data/BasicSample.sol:9:5 11 | struct BasicSample.TestStruct 12 | @param someAddress is missing 13 | @param someNumber is missing 14 | 15 | ./test-data/BasicSample.sol:17:5 16 | enum BasicSample.TestEnum 17 | @param First is missing 18 | @param Second is missing 19 | 20 | ./test-data/BasicSample.sol:23:5 21 | error BasicSample.BasicSample_SomeError 22 | @param _param1 is missing 23 | 24 | ./test-data/BasicSample.sol:28:5 25 | event BasicSample.BasicSample_BasicEvent 26 | @param _param1 is missing 27 | 28 | ./test-data/BasicSample.sol:39:5 29 | variable BasicSample.somePublicNumber 30 | @inheritdoc is missing 31 | 32 | ./test-data/BasicSample.sol:44:5 33 | constructor BasicSample.constructor 34 | @notice is missing 35 | @param _randomFlag is missing 36 | 37 | ./test-data/BasicSample.sol:46:5 38 | function BasicSample.externalSimple 39 | @inheritdoc is missing 40 | 41 | ./test-data/BasicSample.sol:64:5 42 | function BasicSample.multiline 43 | @inheritdoc is missing 44 | 45 | ./test-data/BasicSample.sol:71:5 46 | function BasicSample.multitag 47 | @inheritdoc is missing 48 | 49 | ./test-data/BasicSample.sol:77:5 50 | function BasicSample.externalSimpleMultipleReturn 51 | @inheritdoc is missing 52 | 53 | ./test-data/BasicSample.sol:93:5 54 | function BasicSample.externalSimpleMultipleUnnamedReturn 55 | @inheritdoc is missing 56 | 57 | ./test-data/BasicSample.sol:102:5 58 | function BasicSample.overriddenFunction 59 | @inheritdoc is missing 60 | 61 | ./test-data/BasicSample.sol:109:5 62 | function BasicSample.virtualFunction 63 | @inheritdoc is missing 64 | 65 | ./test-data/BasicSample.sol:111:5 66 | modifier BasicSample.transferFee 67 | @param _receiver is missing 68 | -------------------------------------------------------------------------------- /tests/snapshots/tests_basic_sample__all_no_inheritdoc.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-basic-sample.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/BasicSample.sol:5:5 6 | function AbstractBasic.overriddenFunction 7 | @notice is missing 8 | @return _returned is missing 9 | 10 | ./test-data/BasicSample.sol:9:5 11 | struct BasicSample.TestStruct 12 | @param someAddress is missing 13 | @param someNumber is missing 14 | 15 | ./test-data/BasicSample.sol:17:5 16 | enum BasicSample.TestEnum 17 | @param First is missing 18 | @param Second is missing 19 | 20 | ./test-data/BasicSample.sol:23:5 21 | error BasicSample.BasicSample_SomeError 22 | @param _param1 is missing 23 | 24 | ./test-data/BasicSample.sol:28:5 25 | event BasicSample.BasicSample_BasicEvent 26 | @param _param1 is missing 27 | 28 | ./test-data/BasicSample.sol:39:5 29 | variable BasicSample.somePublicNumber 30 | @return is missing 31 | 32 | ./test-data/BasicSample.sol:44:5 33 | constructor BasicSample.constructor 34 | @notice is missing 35 | @param _randomFlag is missing 36 | 37 | ./test-data/BasicSample.sol:46:5 38 | function BasicSample.externalSimple 39 | @param _name is present more than once 40 | 41 | ./test-data/BasicSample.sol:93:5 42 | function BasicSample.externalSimpleMultipleUnnamedReturn 43 | @return missing for unnamed return #2 44 | 45 | ./test-data/BasicSample.sol:102:5 46 | function BasicSample.overriddenFunction 47 | @return _returned is missing 48 | 49 | ./test-data/BasicSample.sol:109:5 50 | function BasicSample.virtualFunction 51 | @notice is missing 52 | @return _returned is missing 53 | 54 | ./test-data/BasicSample.sol:111:5 55 | modifier BasicSample.transferFee 56 | @param _receiver is missing 57 | -------------------------------------------------------------------------------- /tests/snapshots/tests_basic_sample__basic.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-basic-sample.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/BasicSample.sol:5:5 6 | function AbstractBasic.overriddenFunction 7 | @notice is missing 8 | @return _returned is missing 9 | 10 | ./test-data/BasicSample.sol:23:5 11 | error BasicSample.BasicSample_SomeError 12 | @param _param1 is missing 13 | 14 | ./test-data/BasicSample.sol:28:5 15 | event BasicSample.BasicSample_BasicEvent 16 | @param _param1 is missing 17 | 18 | ./test-data/BasicSample.sol:39:5 19 | variable BasicSample.somePublicNumber 20 | @return is missing 21 | 22 | ./test-data/BasicSample.sol:44:5 23 | constructor BasicSample.constructor 24 | @param _randomFlag is missing 25 | 26 | ./test-data/BasicSample.sol:46:5 27 | function BasicSample.externalSimple 28 | @param _name is present more than once 29 | 30 | ./test-data/BasicSample.sol:93:5 31 | function BasicSample.externalSimpleMultipleUnnamedReturn 32 | @return missing for unnamed return #2 33 | 34 | ./test-data/BasicSample.sol:102:5 35 | function BasicSample.overriddenFunction 36 | @return _returned is missing 37 | 38 | ./test-data/BasicSample.sol:109:5 39 | function BasicSample.virtualFunction 40 | @notice is missing 41 | @return _returned is missing 42 | 43 | ./test-data/BasicSample.sol:111:5 44 | modifier BasicSample.transferFee 45 | @param _receiver is missing 46 | -------------------------------------------------------------------------------- /tests/snapshots/tests_basic_sample__constructor.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-basic-sample.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/BasicSample.sol:5:5 6 | function AbstractBasic.overriddenFunction 7 | @notice is missing 8 | @return _returned is missing 9 | 10 | ./test-data/BasicSample.sol:23:5 11 | error BasicSample.BasicSample_SomeError 12 | @param _param1 is missing 13 | 14 | ./test-data/BasicSample.sol:28:5 15 | event BasicSample.BasicSample_BasicEvent 16 | @param _param1 is missing 17 | 18 | ./test-data/BasicSample.sol:39:5 19 | variable BasicSample.somePublicNumber 20 | @return is missing 21 | 22 | ./test-data/BasicSample.sol:44:5 23 | constructor BasicSample.constructor 24 | @notice is missing 25 | @param _randomFlag is missing 26 | 27 | ./test-data/BasicSample.sol:46:5 28 | function BasicSample.externalSimple 29 | @param _name is present more than once 30 | 31 | ./test-data/BasicSample.sol:93:5 32 | function BasicSample.externalSimpleMultipleUnnamedReturn 33 | @return missing for unnamed return #2 34 | 35 | ./test-data/BasicSample.sol:102:5 36 | function BasicSample.overriddenFunction 37 | @return _returned is missing 38 | 39 | ./test-data/BasicSample.sol:109:5 40 | function BasicSample.virtualFunction 41 | @notice is missing 42 | @return _returned is missing 43 | 44 | ./test-data/BasicSample.sol:111:5 45 | modifier BasicSample.transferFee 46 | @param _receiver is missing 47 | -------------------------------------------------------------------------------- /tests/snapshots/tests_basic_sample__enum.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-basic-sample.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/BasicSample.sol:5:5 6 | function AbstractBasic.overriddenFunction 7 | @notice is missing 8 | @return _returned is missing 9 | 10 | ./test-data/BasicSample.sol:17:5 11 | enum BasicSample.TestEnum 12 | @param First is missing 13 | @param Second is missing 14 | 15 | ./test-data/BasicSample.sol:23:5 16 | error BasicSample.BasicSample_SomeError 17 | @param _param1 is missing 18 | 19 | ./test-data/BasicSample.sol:28:5 20 | event BasicSample.BasicSample_BasicEvent 21 | @param _param1 is missing 22 | 23 | ./test-data/BasicSample.sol:39:5 24 | variable BasicSample.somePublicNumber 25 | @return is missing 26 | 27 | ./test-data/BasicSample.sol:44:5 28 | constructor BasicSample.constructor 29 | @param _randomFlag is missing 30 | 31 | ./test-data/BasicSample.sol:46:5 32 | function BasicSample.externalSimple 33 | @param _name is present more than once 34 | 35 | ./test-data/BasicSample.sol:93:5 36 | function BasicSample.externalSimpleMultipleUnnamedReturn 37 | @return missing for unnamed return #2 38 | 39 | ./test-data/BasicSample.sol:102:5 40 | function BasicSample.overriddenFunction 41 | @return _returned is missing 42 | 43 | ./test-data/BasicSample.sol:109:5 44 | function BasicSample.virtualFunction 45 | @notice is missing 46 | @return _returned is missing 47 | 48 | ./test-data/BasicSample.sol:111:5 49 | modifier BasicSample.transferFee 50 | @param _receiver is missing 51 | -------------------------------------------------------------------------------- /tests/snapshots/tests_basic_sample__inheritdoc.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-basic-sample.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/BasicSample.sol:5:5 6 | function AbstractBasic.overriddenFunction 7 | @notice is missing 8 | @return _returned is missing 9 | 10 | ./test-data/BasicSample.sol:23:5 11 | error BasicSample.BasicSample_SomeError 12 | @param _param1 is missing 13 | 14 | ./test-data/BasicSample.sol:28:5 15 | event BasicSample.BasicSample_BasicEvent 16 | @param _param1 is missing 17 | 18 | ./test-data/BasicSample.sol:39:5 19 | variable BasicSample.somePublicNumber 20 | @inheritdoc is missing 21 | 22 | ./test-data/BasicSample.sol:44:5 23 | constructor BasicSample.constructor 24 | @param _randomFlag is missing 25 | 26 | ./test-data/BasicSample.sol:46:5 27 | function BasicSample.externalSimple 28 | @inheritdoc is missing 29 | 30 | ./test-data/BasicSample.sol:64:5 31 | function BasicSample.multiline 32 | @inheritdoc is missing 33 | 34 | ./test-data/BasicSample.sol:71:5 35 | function BasicSample.multitag 36 | @inheritdoc is missing 37 | 38 | ./test-data/BasicSample.sol:77:5 39 | function BasicSample.externalSimpleMultipleReturn 40 | @inheritdoc is missing 41 | 42 | ./test-data/BasicSample.sol:93:5 43 | function BasicSample.externalSimpleMultipleUnnamedReturn 44 | @inheritdoc is missing 45 | 46 | ./test-data/BasicSample.sol:102:5 47 | function BasicSample.overriddenFunction 48 | @inheritdoc is missing 49 | 50 | ./test-data/BasicSample.sol:109:5 51 | function BasicSample.virtualFunction 52 | @inheritdoc is missing 53 | 54 | ./test-data/BasicSample.sol:111:5 55 | modifier BasicSample.transferFee 56 | @param _receiver is missing 57 | -------------------------------------------------------------------------------- /tests/snapshots/tests_basic_sample__struct.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-basic-sample.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/BasicSample.sol:5:5 6 | function AbstractBasic.overriddenFunction 7 | @notice is missing 8 | @return _returned is missing 9 | 10 | ./test-data/BasicSample.sol:9:5 11 | struct BasicSample.TestStruct 12 | @param someAddress is missing 13 | @param someNumber is missing 14 | 15 | ./test-data/BasicSample.sol:23:5 16 | error BasicSample.BasicSample_SomeError 17 | @param _param1 is missing 18 | 19 | ./test-data/BasicSample.sol:28:5 20 | event BasicSample.BasicSample_BasicEvent 21 | @param _param1 is missing 22 | 23 | ./test-data/BasicSample.sol:39:5 24 | variable BasicSample.somePublicNumber 25 | @return is missing 26 | 27 | ./test-data/BasicSample.sol:44:5 28 | constructor BasicSample.constructor 29 | @param _randomFlag is missing 30 | 31 | ./test-data/BasicSample.sol:46:5 32 | function BasicSample.externalSimple 33 | @param _name is present more than once 34 | 35 | ./test-data/BasicSample.sol:93:5 36 | function BasicSample.externalSimpleMultipleUnnamedReturn 37 | @return missing for unnamed return #2 38 | 39 | ./test-data/BasicSample.sol:102:5 40 | function BasicSample.overriddenFunction 41 | @return _returned is missing 42 | 43 | ./test-data/BasicSample.sol:109:5 44 | function BasicSample.virtualFunction 45 | @notice is missing 46 | @return _returned is missing 47 | 48 | ./test-data/BasicSample.sol:111:5 49 | modifier BasicSample.transferFee 50 | @param _receiver is missing 51 | -------------------------------------------------------------------------------- /tests/snapshots/tests_fuzzers__fuzzers.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-fuzzers.rs 3 | expression: "snapshot_content(\"./test-data/Fuzzers.sol\",\n&ValidationOptions::builder().inheritdoc(false).build(), true,)" 4 | --- 5 | ./test-data/Fuzzers.sol:20:5 6 | variable Fuzzers._vm 7 | @notice is missing 8 | 9 | ./test-data/Fuzzers.sol:22:5 10 | variable Fuzzers._vm2 11 | @notice is missing 12 | 13 | ./test-data/Fuzzers.sol:25:5 14 | function Fuzzers.boundLiquidityDelta 15 | @notice is missing 16 | @param key is missing 17 | @param liquidityDeltaUnbounded is missing 18 | @param liquidityMaxByAmount is missing 19 | @return missing for unnamed return #1 20 | 21 | ./test-data/Fuzzers.sol:40:5 22 | function Fuzzers.boundLiquidityDeltaTightly 23 | @notice is missing 24 | @param key is missing 25 | @param liquidityDeltaUnbounded is missing 26 | @param liquidityMaxByAmount is missing 27 | @param maxPositions is missing 28 | @return missing for unnamed return #1 29 | 30 | ./test-data/Fuzzers.sol:57:5 31 | function Fuzzers.getLiquidityDeltaFromAmounts 32 | @notice is missing 33 | @param tickLower is missing 34 | @param tickUpper is missing 35 | @param sqrtPriceX96 is missing 36 | @return missing for unnamed return #1 37 | 38 | ./test-data/Fuzzers.sol:93:5 39 | function Fuzzers.boundTicks 40 | @notice is missing 41 | @param tickLower is missing 42 | @param tickUpper is missing 43 | @param tickSpacing is missing 44 | @return missing for unnamed return #1 45 | @return missing for unnamed return #2 46 | 47 | ./test-data/Fuzzers.sol:123:5 48 | function Fuzzers.boundTicks 49 | @notice is missing 50 | @param key is missing 51 | @param tickLower is missing 52 | @param tickUpper is missing 53 | @return missing for unnamed return #1 54 | @return missing for unnamed return #2 55 | 56 | ./test-data/Fuzzers.sol:127:5 57 | function Fuzzers.createRandomSqrtPriceX96 58 | @notice is missing 59 | @param tickSpacing is missing 60 | @param seed is missing 61 | @return missing for unnamed return #1 62 | 63 | ./test-data/Fuzzers.sol:134:5 64 | function Fuzzers.createFuzzyLiquidityParams 65 | @notice is missing 66 | @return result is missing 67 | 68 | ./test-data/Fuzzers.sol:150:5 69 | function Fuzzers.createFuzzyLiquidityParamsWithTightBound 70 | @notice is missing 71 | @param key is missing 72 | @param params is missing 73 | @param sqrtPriceX96 is missing 74 | @param maxPositions is missing 75 | @return result is missing 76 | 77 | ./test-data/Fuzzers.sol:164:5 78 | function Fuzzers.createFuzzyLiquidity 79 | @notice is missing 80 | @param modifyLiquidityRouter is missing 81 | @param key is missing 82 | @param params is missing 83 | @param sqrtPriceX96 is missing 84 | @param hookData is missing 85 | @return result is missing 86 | @return delta is missing 87 | 88 | ./test-data/Fuzzers.sol:176:5 89 | function Fuzzers.createFuzzyLiquidityWithTightBound 90 | @notice is missing 91 | @param modifyLiquidityRouter is missing 92 | @param key is missing 93 | @param params is missing 94 | @param sqrtPriceX96 is missing 95 | @param hookData is missing 96 | @param maxPositions is missing 97 | @return result is missing 98 | @return delta is missing 99 | -------------------------------------------------------------------------------- /tests/snapshots/tests_interface_sample__interface.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-interface-sample.rs 3 | expression: "snapshot_content(\"./test-data/InterfaceSample.sol\",\n&ValidationOptions::builder().inheritdoc(false).build(), true,)" 4 | --- 5 | ./test-data/InterfaceSample.sol:5:5 6 | function IInterfacedSample.greet 7 | @return _greeting is missing 8 | 9 | ./test-data/InterfaceSample.sol:14:5 10 | function InterfacedSample.greet 11 | @notice is missing 12 | @return _greeting is missing 13 | @return _balance is missing 14 | -------------------------------------------------------------------------------- /tests/snapshots/tests_library_sample__library.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-library-sample.rs 3 | expression: "snapshot_content(\"./test-data/LibrarySample.sol\",\n&ValidationOptions::builder().inheritdoc(false).build(), true,)" 4 | --- 5 | ./test-data/LibrarySample.sol:5:5 6 | function StringUtils.nothing 7 | @notice is missing 8 | @param input is missing 9 | @return missing for unnamed return #1 10 | -------------------------------------------------------------------------------- /tests/snapshots/tests_parser_test__basic.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-parser-test.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/ParserTest.sol:8:3 6 | error IParserTest.SimpleError 7 | @param _param1 is missing 8 | @param _param2 is missing 9 | 10 | ./test-data/ParserTest.sol:11:3 11 | event IParserTest.SimpleEvent 12 | @param _param1 is missing 13 | @param _param2 is missing 14 | 15 | ./test-data/ParserTest.sol:47:3 16 | function IParserTest.SOME_CONSTANT 17 | @return _returned is missing 18 | 19 | ./test-data/ParserTest.sol:121:3 20 | struct ParserTestFunny.SimpleStruct 21 | @notice is missing 22 | 23 | ./test-data/ParserTest.sol:128:3 24 | modifier ParserTestFunny.someModifier 25 | @notice is missing 26 | 27 | ./test-data/ParserTest.sol:137:3 28 | variable ParserTestFunny.SOME_CONSTANT 29 | @notice is missing 30 | @return is missing 31 | 32 | ./test-data/ParserTest.sol:145:3 33 | function ParserTestFunny.viewFunctionWithParams 34 | @notice is missing 35 | @param _param1 is missing 36 | @param _param2 is missing 37 | @return missing for unnamed return #1 38 | 39 | ./test-data/ParserTest.sol:150:3 40 | function ParserTestFunny._viewInternal 41 | @notice is missing 42 | @return missing for unnamed return #1 43 | 44 | ./test-data/ParserTest.sol:183:3 45 | function ParserTestFunny._viewInternal 46 | @notice is missing 47 | @param _paramName is missing 48 | @return _returned is missing 49 | 50 | ./test-data/ParserTest.sol:190:3 51 | function ParserTestFunny._viewBlockLinterFail 52 | @notice is missing 53 | -------------------------------------------------------------------------------- /tests/snapshots/tests_parser_test__constructor.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-parser-test.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/ParserTest.sol:8:3 6 | error IParserTest.SimpleError 7 | @param _param1 is missing 8 | @param _param2 is missing 9 | 10 | ./test-data/ParserTest.sol:11:3 11 | event IParserTest.SimpleEvent 12 | @param _param1 is missing 13 | @param _param2 is missing 14 | 15 | ./test-data/ParserTest.sol:47:3 16 | function IParserTest.SOME_CONSTANT 17 | @return _returned is missing 18 | 19 | ./test-data/ParserTest.sol:121:3 20 | struct ParserTestFunny.SimpleStruct 21 | @notice is missing 22 | 23 | ./test-data/ParserTest.sol:128:3 24 | modifier ParserTestFunny.someModifier 25 | @notice is missing 26 | 27 | ./test-data/ParserTest.sol:137:3 28 | variable ParserTestFunny.SOME_CONSTANT 29 | @notice is missing 30 | @return is missing 31 | 32 | ./test-data/ParserTest.sol:145:3 33 | function ParserTestFunny.viewFunctionWithParams 34 | @notice is missing 35 | @param _param1 is missing 36 | @param _param2 is missing 37 | @return missing for unnamed return #1 38 | 39 | ./test-data/ParserTest.sol:150:3 40 | function ParserTestFunny._viewInternal 41 | @notice is missing 42 | @return missing for unnamed return #1 43 | 44 | ./test-data/ParserTest.sol:183:3 45 | function ParserTestFunny._viewInternal 46 | @notice is missing 47 | @param _paramName is missing 48 | @return _returned is missing 49 | 50 | ./test-data/ParserTest.sol:190:3 51 | function ParserTestFunny._viewBlockLinterFail 52 | @notice is missing 53 | -------------------------------------------------------------------------------- /tests/snapshots/tests_parser_test__enum.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-parser-test.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/ParserTest.sol:8:3 6 | error IParserTest.SimpleError 7 | @param _param1 is missing 8 | @param _param2 is missing 9 | 10 | ./test-data/ParserTest.sol:11:3 11 | event IParserTest.SimpleEvent 12 | @param _param1 is missing 13 | @param _param2 is missing 14 | 15 | ./test-data/ParserTest.sol:14:3 16 | enum IParserTest.SimpleEnum 17 | @param A is missing 18 | @param B is missing 19 | @param C is missing 20 | 21 | ./test-data/ParserTest.sol:47:3 22 | function IParserTest.SOME_CONSTANT 23 | @return _returned is missing 24 | 25 | ./test-data/ParserTest.sol:121:3 26 | struct ParserTestFunny.SimpleStruct 27 | @notice is missing 28 | 29 | ./test-data/ParserTest.sol:128:3 30 | modifier ParserTestFunny.someModifier 31 | @notice is missing 32 | 33 | ./test-data/ParserTest.sol:137:3 34 | variable ParserTestFunny.SOME_CONSTANT 35 | @notice is missing 36 | @return is missing 37 | 38 | ./test-data/ParserTest.sol:145:3 39 | function ParserTestFunny.viewFunctionWithParams 40 | @notice is missing 41 | @param _param1 is missing 42 | @param _param2 is missing 43 | @return missing for unnamed return #1 44 | 45 | ./test-data/ParserTest.sol:150:3 46 | function ParserTestFunny._viewInternal 47 | @notice is missing 48 | @return missing for unnamed return #1 49 | 50 | ./test-data/ParserTest.sol:183:3 51 | function ParserTestFunny._viewInternal 52 | @notice is missing 53 | @param _paramName is missing 54 | @return _returned is missing 55 | 56 | ./test-data/ParserTest.sol:190:3 57 | function ParserTestFunny._viewBlockLinterFail 58 | @notice is missing 59 | -------------------------------------------------------------------------------- /tests/snapshots/tests_parser_test__inheritdoc.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-parser-test.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/ParserTest.sol:8:3 6 | error IParserTest.SimpleError 7 | @param _param1 is missing 8 | @param _param2 is missing 9 | 10 | ./test-data/ParserTest.sol:11:3 11 | event IParserTest.SimpleEvent 12 | @param _param1 is missing 13 | @param _param2 is missing 14 | 15 | ./test-data/ParserTest.sol:47:3 16 | function IParserTest.SOME_CONSTANT 17 | @return _returned is missing 18 | 19 | ./test-data/ParserTest.sol:121:3 20 | struct ParserTestFunny.SimpleStruct 21 | @notice is missing 22 | 23 | ./test-data/ParserTest.sol:128:3 24 | modifier ParserTestFunny.someModifier 25 | @notice is missing 26 | 27 | ./test-data/ParserTest.sol:137:3 28 | variable ParserTestFunny.SOME_CONSTANT 29 | @inheritdoc is missing 30 | 31 | ./test-data/ParserTest.sol:145:3 32 | function ParserTestFunny.viewFunctionWithParams 33 | @inheritdoc is missing 34 | 35 | ./test-data/ParserTest.sol:150:3 36 | function ParserTestFunny._viewInternal 37 | @notice is missing 38 | @return missing for unnamed return #1 39 | 40 | ./test-data/ParserTest.sol:183:3 41 | function ParserTestFunny._viewInternal 42 | @notice is missing 43 | @param _paramName is missing 44 | @return _returned is missing 45 | 46 | ./test-data/ParserTest.sol:190:3 47 | function ParserTestFunny._viewBlockLinterFail 48 | @notice is missing 49 | 50 | ./test-data/ParserTest.sol:202:3 51 | function ParserTestFunny.functionUnnamedEmptyReturn 52 | @inheritdoc is missing 53 | -------------------------------------------------------------------------------- /tests/snapshots/tests_parser_test__struct.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/tests-parser-test.rs 3 | expression: generate_output(diags) 4 | --- 5 | ./test-data/ParserTest.sol:8:3 6 | error IParserTest.SimpleError 7 | @param _param1 is missing 8 | @param _param2 is missing 9 | 10 | ./test-data/ParserTest.sol:11:3 11 | event IParserTest.SimpleEvent 12 | @param _param1 is missing 13 | @param _param2 is missing 14 | 15 | ./test-data/ParserTest.sol:47:3 16 | function IParserTest.SOME_CONSTANT 17 | @return _returned is missing 18 | 19 | ./test-data/ParserTest.sol:121:3 20 | struct ParserTestFunny.SimpleStruct 21 | @notice is missing 22 | @param a is missing 23 | @param b is missing 24 | 25 | ./test-data/ParserTest.sol:128:3 26 | modifier ParserTestFunny.someModifier 27 | @notice is missing 28 | 29 | ./test-data/ParserTest.sol:137:3 30 | variable ParserTestFunny.SOME_CONSTANT 31 | @notice is missing 32 | @return is missing 33 | 34 | ./test-data/ParserTest.sol:145:3 35 | function ParserTestFunny.viewFunctionWithParams 36 | @notice is missing 37 | @param _param1 is missing 38 | @param _param2 is missing 39 | @return missing for unnamed return #1 40 | 41 | ./test-data/ParserTest.sol:150:3 42 | function ParserTestFunny._viewInternal 43 | @notice is missing 44 | @return missing for unnamed return #1 45 | 46 | ./test-data/ParserTest.sol:183:3 47 | function ParserTestFunny._viewInternal 48 | @notice is missing 49 | @param _paramName is missing 50 | @return _returned is missing 51 | 52 | ./test-data/ParserTest.sol:190:3 53 | function ParserTestFunny._viewBlockLinterFail 54 | @notice is missing 55 | -------------------------------------------------------------------------------- /tests/tests-basic-sample.rs: -------------------------------------------------------------------------------- 1 | use lintspec::{ 2 | config::{NoticeDevRules, Req, VariableConfig, WithParamsRules}, 3 | lint::ValidationOptions, 4 | }; 5 | 6 | mod common; 7 | use common::*; 8 | 9 | #[test] 10 | fn test_basic() { 11 | insta::assert_snapshot!(snapshot_content( 12 | "./test-data/BasicSample.sol", 13 | &ValidationOptions::builder().inheritdoc(false).build(), 14 | true, 15 | )); 16 | } 17 | 18 | #[test] 19 | fn test_inheritdoc() { 20 | insta::assert_snapshot!(snapshot_content( 21 | "./test-data/BasicSample.sol", 22 | &ValidationOptions::default(), 23 | true, 24 | )); 25 | } 26 | 27 | #[test] 28 | fn test_constructor() { 29 | insta::assert_snapshot!(snapshot_content( 30 | "./test-data/BasicSample.sol", 31 | &ValidationOptions::builder() 32 | .inheritdoc(false) 33 | .constructors(WithParamsRules::required()) 34 | .build(), 35 | true, 36 | )); 37 | } 38 | 39 | #[test] 40 | fn test_struct() { 41 | insta::assert_snapshot!(snapshot_content( 42 | "./test-data/BasicSample.sol", 43 | &ValidationOptions::builder() 44 | .inheritdoc(false) 45 | .structs(WithParamsRules::required()) 46 | .build(), 47 | true, 48 | )); 49 | } 50 | 51 | #[test] 52 | fn test_enum() { 53 | insta::assert_snapshot!(snapshot_content( 54 | "./test-data/BasicSample.sol", 55 | &ValidationOptions::builder() 56 | .inheritdoc(false) 57 | .enums(WithParamsRules::required()) 58 | .build(), 59 | true, 60 | )); 61 | } 62 | 63 | #[test] 64 | fn test_all() { 65 | insta::assert_snapshot!(snapshot_content( 66 | "./test-data/BasicSample.sol", 67 | &ValidationOptions::builder() 68 | .constructors(WithParamsRules::required()) 69 | .enums(WithParamsRules::required()) 70 | .modifiers(WithParamsRules::required()) 71 | .structs(WithParamsRules::required()) 72 | .variables( 73 | VariableConfig::builder() 74 | .private( 75 | NoticeDevRules::builder() 76 | .notice(Req::Required) 77 | .dev(Req::Required) 78 | .build(), 79 | ) 80 | .internal( 81 | NoticeDevRules::builder() 82 | .notice(Req::Required) 83 | .dev(Req::Required) 84 | .build(), 85 | ) 86 | .build(), 87 | ) 88 | .build(), 89 | true, 90 | )); 91 | } 92 | 93 | #[test] 94 | fn test_all_no_inheritdoc() { 95 | insta::assert_snapshot!(snapshot_content( 96 | "./test-data/BasicSample.sol", 97 | &ValidationOptions::builder() 98 | .inheritdoc(false) 99 | .constructors(WithParamsRules::required()) 100 | .enums(WithParamsRules::required()) 101 | .modifiers(WithParamsRules::required()) 102 | .structs(WithParamsRules::required()) 103 | .variables( 104 | VariableConfig::builder() 105 | .private( 106 | NoticeDevRules::builder() 107 | .notice(Req::Required) 108 | .dev(Req::Required) 109 | .build(), 110 | ) 111 | .internal( 112 | NoticeDevRules::builder() 113 | .notice(Req::Required) 114 | .dev(Req::Required) 115 | .build(), 116 | ) 117 | .build(), 118 | ) 119 | .build(), 120 | true, 121 | )); 122 | } 123 | -------------------------------------------------------------------------------- /tests/tests-fuzzers.rs: -------------------------------------------------------------------------------- 1 | use lintspec::lint::ValidationOptions; 2 | 3 | mod common; 4 | use common::*; 5 | 6 | #[test] 7 | fn test_fuzzers() { 8 | insta::assert_snapshot!(snapshot_content( 9 | "./test-data/Fuzzers.sol", 10 | &ValidationOptions::builder().inheritdoc(false).build(), 11 | true, 12 | )); 13 | } 14 | -------------------------------------------------------------------------------- /tests/tests-interface-sample.rs: -------------------------------------------------------------------------------- 1 | use lintspec::lint::ValidationOptions; 2 | 3 | mod common; 4 | use common::*; 5 | 6 | #[test] 7 | fn test_interface() { 8 | insta::assert_snapshot!(snapshot_content( 9 | "./test-data/InterfaceSample.sol", 10 | &ValidationOptions::builder().inheritdoc(false).build(), 11 | true, 12 | )); 13 | } 14 | -------------------------------------------------------------------------------- /tests/tests-library-sample.rs: -------------------------------------------------------------------------------- 1 | use lintspec::lint::ValidationOptions; 2 | 3 | mod common; 4 | use common::*; 5 | 6 | #[test] 7 | fn test_library() { 8 | insta::assert_snapshot!(snapshot_content( 9 | "./test-data/LibrarySample.sol", 10 | &ValidationOptions::builder().inheritdoc(false).build(), 11 | true, 12 | )); 13 | } 14 | -------------------------------------------------------------------------------- /tests/tests-parser-test.rs: -------------------------------------------------------------------------------- 1 | use lintspec::{config::WithParamsRules, lint::ValidationOptions}; 2 | 3 | mod common; 4 | use common::*; 5 | 6 | #[test] 7 | fn test_basic() { 8 | insta::assert_snapshot!(snapshot_content( 9 | "./test-data/ParserTest.sol", 10 | &ValidationOptions::builder().inheritdoc(false).build(), 11 | true, 12 | )); 13 | } 14 | 15 | #[test] 16 | fn test_inheritdoc() { 17 | insta::assert_snapshot!(snapshot_content( 18 | "./test-data/ParserTest.sol", 19 | &ValidationOptions::default(), 20 | true, 21 | )); 22 | } 23 | 24 | #[test] 25 | fn test_constructor() { 26 | insta::assert_snapshot!(snapshot_content( 27 | "./test-data/ParserTest.sol", 28 | &ValidationOptions::builder() 29 | .inheritdoc(false) 30 | .constructors(WithParamsRules::required()) 31 | .build(), 32 | true, 33 | )); 34 | } 35 | 36 | #[test] 37 | fn test_struct() { 38 | insta::assert_snapshot!(snapshot_content( 39 | "./test-data/ParserTest.sol", 40 | &ValidationOptions::builder() 41 | .inheritdoc(false) 42 | .structs(WithParamsRules::required()) 43 | .build(), 44 | true, 45 | )); 46 | } 47 | 48 | #[test] 49 | fn test_enum() { 50 | insta::assert_snapshot!(snapshot_content( 51 | "./test-data/ParserTest.sol", 52 | &ValidationOptions::builder() 53 | .inheritdoc(false) 54 | .enums(WithParamsRules::required()) 55 | .build(), 56 | true, 57 | )); 58 | } 59 | -------------------------------------------------------------------------------- /tests/tests-solar.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "solar")] 2 | use lintspec::{ 3 | lint::{ValidationOptions, lint}, 4 | parser::{slang::SlangParser, solar::SolarParser}, 5 | }; 6 | use similar_asserts::assert_eq; 7 | 8 | #[test] 9 | fn test_basic() { 10 | let diags_slang = lint( 11 | SlangParser::builder().build(), 12 | "./test-data/BasicSample.sol", 13 | &ValidationOptions::default(), 14 | false, 15 | ) 16 | .unwrap() 17 | .unwrap(); 18 | let diags_solar = lint( 19 | SolarParser {}, 20 | "./test-data/BasicSample.sol", 21 | &ValidationOptions::default(), 22 | false, 23 | ) 24 | .unwrap() 25 | .unwrap(); 26 | assert_eq!(slang: format!("{diags_slang:#?}"), solar: format!("{diags_solar:#?}")); 27 | } 28 | 29 | #[test] 30 | fn test_fuzzers() { 31 | let diags_slang = lint( 32 | SlangParser::builder().build(), 33 | "./test-data/Fuzzers.sol", 34 | &ValidationOptions::default(), 35 | false, 36 | ) 37 | .unwrap() 38 | .unwrap(); 39 | let diags_solar = lint( 40 | SolarParser {}, 41 | "./test-data/Fuzzers.sol", 42 | &ValidationOptions::default(), 43 | false, 44 | ) 45 | .unwrap() 46 | .unwrap(); 47 | assert_eq!(slang: format!("{diags_slang:#?}"), solar: format!("{diags_solar:#?}")); 48 | } 49 | 50 | #[test] 51 | fn test_interface() { 52 | let diags_slang = lint( 53 | SlangParser::builder().build(), 54 | "./test-data/InterfaceSample.sol", 55 | &ValidationOptions::default(), 56 | false, 57 | ) 58 | .unwrap() 59 | .unwrap(); 60 | let diags_solar = lint( 61 | SolarParser {}, 62 | "./test-data/InterfaceSample.sol", 63 | &ValidationOptions::default(), 64 | false, 65 | ) 66 | .unwrap() 67 | .unwrap(); 68 | assert_eq!(slang: format!("{diags_slang:#?}"), solar: format!("{diags_solar:#?}")); 69 | } 70 | 71 | #[test] 72 | fn test_library() { 73 | let diags_slang = lint( 74 | SlangParser::builder().build(), 75 | "./test-data/LibrarySample.sol", 76 | &ValidationOptions::default(), 77 | false, 78 | ) 79 | .unwrap() 80 | .unwrap(); 81 | let diags_solar = lint( 82 | SolarParser {}, 83 | "./test-data/LibrarySample.sol", 84 | &ValidationOptions::default(), 85 | false, 86 | ) 87 | .unwrap() 88 | .unwrap(); 89 | assert_eq!(slang: format!("{diags_slang:#?}"), solar: format!("{diags_solar:#?}")); 90 | } 91 | 92 | #[test] 93 | fn test_parsertest() { 94 | let diags_slang = lint( 95 | SlangParser::builder().build(), 96 | "./test-data/ParserTest.sol", 97 | &ValidationOptions::default(), 98 | false, 99 | ) 100 | .unwrap() 101 | .unwrap(); 102 | let diags_solar = lint( 103 | SolarParser {}, 104 | "./test-data/ParserTest.sol", 105 | &ValidationOptions::default(), 106 | false, 107 | ) 108 | .unwrap() 109 | .unwrap(); 110 | assert_eq!(slang: format!("{diags_slang:#?}"), solar: format!("{diags_solar:#?}")); 111 | } 112 | --------------------------------------------------------------------------------