├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── rule_suggestion.md ├── labeler.yml └── workflows │ ├── autofix.yaml │ ├── ci.yaml │ ├── codecov.yaml │ ├── js.yaml │ ├── pr.yaml │ └── release.yaml ├── .gitignore ├── .prettierignore ├── .typos.toml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── Justfile ├── LICENSE ├── README.md ├── apps └── vscode-extension │ ├── .gitignore │ ├── .vscodeignore │ ├── LICENSE │ ├── README.md │ ├── build.ts │ ├── bun.lock │ ├── package.json │ ├── src │ ├── BinaryService.ts │ ├── ConfigService.ts │ ├── DiagnosticsService.ts │ ├── extension.ts │ └── util.ts │ └── tsconfig.json ├── build.zig ├── build.zig.zon ├── docs ├── assets │ └── diagnostic-example.jpg └── rules │ ├── avoid-as.md │ ├── case-convention.md │ ├── empty-file.md │ ├── homeless-try.md │ ├── line-length.md │ ├── must-return-ref.md │ ├── no-catch-return.md │ ├── no-return-try.md │ ├── no-unresolved.md │ ├── suppressed-errors.md │ ├── unsafe-undefined.md │ ├── unused-decls.md │ └── useless-error-return.md ├── entitlements.dev.plist ├── src ├── Error.zig ├── cli │ ├── Options.zig │ ├── lint_command.zig │ ├── lint_config.zig │ ├── print_command.zig │ └── test │ │ └── print_ast_test.zig ├── json.zig ├── lint.zig ├── linter │ ├── Config.zig │ ├── LintService.zig │ ├── RuleSet.zig │ ├── ast_utils.zig │ ├── config │ │ ├── rule_config.zig │ │ ├── rules_config.methods.zig │ │ └── rules_config.zig │ ├── disable_directives.zig │ ├── disable_directives │ │ ├── Comment.zig │ │ ├── Parser.zig │ │ └── Parser_test.zig │ ├── fix.zig │ ├── lint_context.zig │ ├── linter.zig │ ├── rule.zig │ ├── rules.zig │ ├── rules │ │ ├── avoid_as.zig │ │ ├── case_convention.zig │ │ ├── empty_file.zig │ │ ├── homeless_try.zig │ │ ├── line_length.zig │ │ ├── must_return_ref.zig │ │ ├── no_catch_return.zig │ │ ├── no_return_try.zig │ │ ├── no_unresolved.zig │ │ ├── snapshots │ │ │ ├── avoid-as.snap │ │ │ ├── case-convention.snap │ │ │ ├── empty-file.snap │ │ │ ├── homeless-try.snap │ │ │ ├── line-length.snap │ │ │ ├── must-return-ref.snap │ │ │ ├── no-catch-return.snap │ │ │ ├── no-return-try.snap │ │ │ ├── no-unresolved.snap │ │ │ ├── suppressed-errors.snap │ │ │ ├── unsafe-undefined.snap │ │ │ ├── unused-decls.snap │ │ │ └── useless-error-return.snap │ │ ├── suppressed_errors.zig │ │ ├── unsafe_undefined.zig │ │ ├── unused_decls.zig │ │ └── useless_error_return.zig │ ├── test │ │ ├── disabling_rules_test.zig │ │ ├── fix_test.zig │ │ └── lint_context_test.zig │ └── tester.zig ├── main.zig ├── printer │ ├── AstPrinter.zig │ ├── Printer.zig │ └── SemanticPrinter.zig ├── reporter.zig ├── reporter │ ├── Reporter.zig │ ├── StringWriter.zig │ ├── formatter.zig │ └── formatters │ │ ├── GithubFormatter.zig │ │ ├── GraphicalFormatter.zig │ │ ├── GraphicalTheme.zig │ │ └── JSONFormatter.zig ├── root.zig ├── semantic.zig ├── semantic │ ├── ModuleRecord.zig │ ├── NodeLinks.zig │ ├── Parse.zig │ ├── Reference.zig │ ├── Scope.zig │ ├── Semantic.zig │ ├── SemanticBuilder.zig │ ├── Symbol.zig │ ├── ast.zig │ ├── builtins.zig │ ├── test │ │ ├── members_and_exports_test.zig │ │ ├── modules_test.zig │ │ ├── scope_flags_test.zig │ │ ├── symbol_decl_test.zig │ │ ├── symbol_ref_test.zig │ │ └── util.zig │ └── tokenizer.zig ├── source.zig ├── span.zig ├── util.zig ├── util │ ├── bitflags.zig │ ├── bitflags_test.zig │ ├── cow.zig │ ├── debug_only.zig │ ├── env.zig │ ├── feature_flags.zig │ └── id.zig ├── visit │ ├── walk.zig │ └── walk_test.zig └── walk │ ├── Walker.zig │ ├── glob.zig │ └── glob_test.zig ├── tasks ├── confgen.zig ├── create-ast-json.sh ├── docgen.zig ├── gen_utils.zig ├── init.sh ├── install.sh ├── lldb │ ├── .lldbinit │ └── lldb_pretty_printers.py ├── new-rule.ts └── submodules.sh ├── test ├── README.md ├── fixtures │ ├── config │ │ └── zlint.json │ └── simple │ │ ├── fail │ │ └── syntax_missing_semi.zig │ │ └── pass │ │ ├── block.zig │ │ ├── block_comptime.zig │ │ ├── cond_if.zig │ │ ├── container_error.zig │ │ ├── enum_members.zig │ │ ├── fibonacci.zig │ │ ├── fn_comptime.zig │ │ ├── fn_in_fn.zig │ │ ├── foo.zig │ │ ├── loops_for.zig │ │ ├── loops_while.zig │ │ ├── stmt_test.zig │ │ ├── struct_members.zig │ │ ├── struct_tuple.zig │ │ ├── top_level_struct.zig │ │ ├── unresolved_import.zig │ │ └── writer_interface.zig ├── harness.zig ├── harness │ ├── TestSuite.zig │ └── runner.zig ├── repos.json ├── semantic │ ├── ecosystem_coverage.zig │ └── snapshot_coverage.zig ├── snapshots │ ├── semantic-coverage │ │ ├── bun.snap │ │ ├── ghostty.snap │ │ └── zig.snap │ └── snapshot-coverage │ │ └── simple │ │ ├── fail.snap │ │ ├── fail │ │ └── syntax_missing_semi.zig.snap │ │ ├── pass.snap │ │ └── pass │ │ ├── block.zig.snap │ │ ├── block_comptime.zig.snap │ │ ├── cond_if.zig.snap │ │ ├── container_error.zig.snap │ │ ├── enum_members.zig.snap │ │ ├── fibonacci.zig.snap │ │ ├── fn_comptime.zig.snap │ │ ├── fn_in_fn.zig.snap │ │ ├── foo.zig.snap │ │ ├── loops_for.zig.snap │ │ ├── loops_while.zig.snap │ │ ├── stmt_test.zig.snap │ │ ├── struct_members.zig.snap │ │ ├── struct_tuple.zig.snap │ │ ├── top_level_struct.zig.snap │ │ ├── unresolved_import.zig.snap │ │ └── writer_interface.zig.snap ├── test_e2e.zig └── utils.zig ├── zlint.schema.json └── zls.json /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:jammy 2 | 3 | USER root 4 | RUN apt-get update && \ 5 | apt-get install -y \ 6 | valgrind \ 7 | kcov 8 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu 3 | { 4 | "name": "Ubuntu on M1 Mac", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "build": { 7 | "dockerfile": "Dockerfile", 8 | "context": "." 9 | }, 10 | // Features to add to the dev container. More info: https://containers.dev/features. 11 | "features": { 12 | // homebrew intentionally excluded. Not available on aarch64. 13 | "ghcr.io/devcontainers-extra/features/curl-apt-get:1": {}, 14 | "ghcr.io/devcontainers-extra/features/zig:1": {}, 15 | "ghcr.io/guiyomh/features/just:0": {} 16 | }, 17 | 18 | // Use 'postCreateCommand' to run commands after the container is created. 19 | // "postCreateCommand": "uname -a", 20 | 21 | // Configure tool-specific properties. 22 | "customizations": { 23 | "vscode": { 24 | "extensions": [ 25 | "ziglang.vscode-zig", 26 | "vadimcn.vscode-lldb" 27 | ] 28 | } 29 | } 30 | 31 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 32 | // "remoteUser": "root" 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: C-bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Version** 11 | Please provide the semver version or git commit hash of the version of zlint you're using 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 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 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: C-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/rule_suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rule Suggestion 3 | about: Suggest a new lint rule 4 | title: 'rule: `some-rule-name`' 5 | labels: A-linter, C-rule 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | Please describe what syntax or coding patterns this rule prohibits. Make sure you describe _why_ its bad. 12 | 13 | ## Examples 14 | 15 | Examples of **incorrect** code for this rule 16 | ```zig 17 | // put examples of code that should produce any violations 18 | ``` 19 | 20 | Examples of **correct** code for this rule 21 | ```zig 22 | // Put examples of code that should _not_ produce any violations 23 | ``` 24 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | A-cli: 2 | - changed-files: 3 | - any-glob-to-any-file: ["src/cli/**"] 4 | 5 | A-linter: 6 | - changed-files: 7 | - any-glob-to-any-file: ["src/linter/**", "src/linter.zig"] 8 | 9 | A-semantic: 10 | - changed-files: 11 | - any-glob-to-any-file: ["src/semantic/**", "src/semantic.zig"] 12 | 13 | A-reporter: 14 | - changed-files: 15 | - any-glob-to-any-file: ["src/reporter/**", "src/reporter.zig"] 16 | 17 | C-ci: 18 | - changed-files: 19 | - any-glob-to-any-file: [".github/workflows/**"] 20 | 21 | C-test: 22 | - changed-files: 23 | - any-glob-to-any-file: ["test/**"] 24 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yaml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["main"] 7 | 8 | permissions: 9 | contents: read 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | 14 | env: 15 | ZIG_VERSION: 0.14.0 16 | 17 | jobs: 18 | autofix: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: mlugg/setup-zig@v1 23 | with: 24 | version: ${{ env.ZIG_VERSION }} 25 | 26 | - name: Fix typos 27 | uses: crate-ci/typos@v1.26.8 28 | with: 29 | write_changes: true 30 | - run: zig fmt src test/harness tasks build.zig build.zig.zon 31 | 32 | - run: rm -f ./typos 33 | 34 | - uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: [opened, synchronize] 8 | paths-ignore: 9 | - "*.md" 10 | - ".vscode/*" 11 | - zls.json 12 | - ".github/workflows/*" 13 | - "!.github/workflows/ci.yaml" 14 | push: 15 | branches: 16 | - main 17 | paths-ignore: 18 | - "*.md" 19 | - ".vscode/*" 20 | - zls.json 21 | - ".github/workflows/*" 22 | - "!.github/workflows/ci.yaml" 23 | 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.ref }} 26 | cancel-in-progress: true 27 | 28 | env: 29 | ZIG_VERSION: 0.14.0 30 | 31 | jobs: 32 | check: 33 | name: Check 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | # Install tooling 38 | - uses: extractions/setup-just@v2 39 | with: 40 | just-version: ${{ env.JUST_VERSION }} 41 | - uses: mlugg/setup-zig@v1 42 | with: 43 | version: ${{ env.ZIG_VERSION }} 44 | # Run checks 45 | - run: just check 46 | 47 | test: 48 | name: Test 49 | strategy: 50 | matrix: 51 | os: [ubuntu-latest, macos-latest, windows-latest] 52 | runs-on: ${{ matrix.os }} 53 | steps: 54 | - uses: actions/checkout@v4 55 | # Install tooling 56 | - uses: extractions/setup-just@v2 57 | - uses: mlugg/setup-zig@v1 58 | with: 59 | version: ${{ env.ZIG_VERSION }} 60 | # Run tests 61 | - run: zig build --fetch 62 | - run: zig build test --summary all -freference-trace --color on 63 | - name: Check for changes 64 | # windows adds crlf line endings 65 | if: ${{ matrix.os != 'windows-latest' }} 66 | run: git diff --exit-code 67 | 68 | # https://imgflip.com/i/9eygz0 69 | lint: 70 | name: Lint Changed 71 | if: ${{ github.event_name == 'pull_request' }} 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v4 75 | # Install tooling 76 | - uses: extractions/setup-just@v2 77 | - uses: mlugg/setup-zig@v1 78 | with: 79 | version: ${{ env.ZIG_VERSION }} 80 | - run: zig build --fetch 81 | - run: zig build 82 | - uses: DonIsaac/zlint-action@v0 83 | with: 84 | binary: ./zig-out/bin/zlint 85 | 86 | e2e: 87 | name: Test E2E 88 | runs-on: ubuntu-latest 89 | strategy: 90 | matrix: 91 | optimize: ["Debug", "ReleaseSafe", "ReleaseFast"] 92 | steps: 93 | - uses: actions/checkout@v4 94 | # Install tooling 95 | - uses: extractions/setup-just@v2 96 | - uses: mlugg/setup-zig@v1 97 | with: 98 | version: ${{ env.ZIG_VERSION }} 99 | - run: rm -rf ~/.cache/zig 100 | - run: zig build --fetch 101 | - run: just submodules 102 | - run: just e2e -Doptimize=${{ matrix.optimize }} 103 | - name: Check for changes 104 | run: git diff --exit-code 105 | 106 | docs: 107 | name: Docs + JSON Schema 108 | runs-on: ubuntu-latest 109 | steps: 110 | - uses: actions/checkout@v4 111 | # Install tooling 112 | - uses: oven-sh/setup-bun@v2 113 | with: 114 | bun-version: latest 115 | - uses: extractions/setup-just@v2 116 | - uses: mlugg/setup-zig@v1 117 | with: 118 | version: ${{ env.ZIG_VERSION }} 119 | - run: zig build docs codegen 120 | - name: Check for changes 121 | run: git diff --exit-code 122 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yaml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | env: 11 | ZIG_VERSION: 0.14.0 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | codecov: 19 | name: Collect and upload test coverage 20 | runs-on: "ubuntu-22.04" 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Install kcov 24 | run: | 25 | sudo apt-get update 26 | sudo apt-get install -y kcov 27 | - uses: extractions/setup-just@v2 28 | - uses: mlugg/setup-zig@v1 29 | with: 30 | version: ${{ env.ZIG_VERSION }} 31 | 32 | - run: just submodules 33 | - run: zig build 34 | - run: just coverage 35 | - uses: codecov/codecov-action@v4 36 | with: 37 | verbose: true 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/js.yaml: -------------------------------------------------------------------------------- 1 | name: JavaScript 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: [opened, synchronize] 8 | paths-ignore: 9 | - "*.zig" 10 | - "*.zon" 11 | - "*.snap" 12 | - "*.md" 13 | - "*.sh" 14 | - ".vscode/*" 15 | - ".github/*" 16 | - "!.github/workflows/js.yaml" 17 | push: 18 | branches: 19 | - main 20 | paths-ignore: 21 | - "*.zig" 22 | - "*.zon" 23 | - "*.snap" 24 | - "*.md" 25 | - "*.sh" 26 | - ".vscode/*" 27 | - ".github/*" 28 | - "!.github/workflows/js.yaml" 29 | 30 | concurrency: 31 | group: ${{ github.workflow }}-${{ github.ref }} 32 | 33 | env: 34 | BUN_VERSION: latest 35 | 36 | jobs: 37 | lint: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: oven-sh/setup-bun@v2 42 | with: 43 | bun-version: ${{ env.BUN_VERSION }} 44 | - name: Oxlint 45 | run: bunx oxlint@latest --format github -D correctness -D suspicious -D perf 46 | 47 | check-vscode: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: oven-sh/setup-bun@v2 52 | with: 53 | bun-version: ${{ env.BUN_VERSION }} 54 | - name: Install Dependencies 55 | working-directory: apps/vscode-extension 56 | run: bun install --frozen-lockfile 57 | - name: Typecheck 58 | working-directory: apps/vscode-extension 59 | run: bun tsc --noEmit 60 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: Label PR 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | pr: 12 | if: github.repository == 'DonIsaac/zlint' 13 | name: Label PR 14 | permissions: 15 | contents: read 16 | pull-requests: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 20 | - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | env: 9 | ZIG_VERSION: 0.14.0 10 | 11 | permissions: 12 | id-token: write 13 | contents: write 14 | attestations: write 15 | 16 | jobs: 17 | build: 18 | strategy: 19 | # keep building others in case it's os-specific 20 | fail-fast: false 21 | matrix: 22 | os: [linux, windows, macos] 23 | arch: [aarch64, x86_64] 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: mlugg/setup-zig@v1 28 | with: 29 | version: ${{ env.ZIG_VERSION }} 30 | 31 | - name: Get binary name 32 | id: binary_name 33 | run: | 34 | binary_name=zlint-${{ matrix.os }}-${{ matrix.arch }} 35 | if [[ "${{ matrix.os }}" == "windows" ]] ; then 36 | binary_name="${binary_name}.exe" 37 | fi 38 | echo "binary_name=$binary_name" >> "$GITHUB_OUTPUT" 39 | 40 | - name: zig build 41 | env: 42 | BINARY_NAME: ${{ steps.binary_name.outputs.binary_name }} 43 | run: | 44 | binary_name=zlint 45 | if [[ "${{ matrix.os }}" == "windows" ]] ; then 46 | binary_name="${binary_name}.exe" 47 | fi 48 | zig build --summary all --color on \ 49 | --release=safe \ 50 | -Dversion=${{ github.ref_name }} \ 51 | -Dtarget=${{ matrix.arch }}-${{ matrix.os }} 52 | mv zig-out/bin/${binary_name} zig-out/bin/${{ env.BINARY_NAME }} 53 | 54 | - name: Generate artifact attestation 55 | uses: actions/attest-build-provenance@v1 56 | env: 57 | BINARY_NAME: ${{ steps.binary_name.outputs.binary_name }} 58 | with: 59 | subject-path: zig-out/bin/${{ env.BINARY_NAME }} 60 | 61 | - name: Upload zlint binary 62 | uses: actions/upload-artifact@v4 63 | env: 64 | BINARY_NAME: ${{ steps.binary_name.outputs.binary_name }} 65 | with: 66 | path: zig-out/bin/${{ env.BINARY_NAME}} 67 | name: ${{ env.BINARY_NAME }} 68 | retention-days: 1 69 | 70 | release: 71 | # Not used. Leaving here in case we want to create canary builds 72 | if: startsWith(github.ref, 'refs/tags/') 73 | runs-on: ubuntu-latest 74 | needs: 75 | - build 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v4 79 | - name: Download release artifacts 80 | uses: actions/download-artifact@v4 81 | with: 82 | pattern: zlint-* 83 | path: zig-out/dist 84 | merge-multiple: true 85 | - name: Release 86 | uses: softprops/action-gh-release@v2 87 | with: 88 | generate_release_notes: true 89 | files: zig-out/dist/* 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | zig-out 3 | .zig-cache 4 | 5 | # test artifacts 6 | .coverage 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # os 12 | .DS_Store 13 | 14 | # editor 15 | *.swp 16 | *.swo 17 | .idea 18 | 19 | # temp & backup files 20 | tmp/** 21 | .tmp/ 22 | temp/ 23 | *.tmp 24 | 25 | vendor 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DonIsaac/zlint/d399cbc2a60ed927b43e66242733e565097ef409/.prettierignore -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-identifiers] 2 | # used to store multi-array slice to Node.Data. 3 | datas = "datas" 4 | 5 | [files] 6 | extend-exclude = [ 7 | "src/walk/glob_test.zig", 8 | ] 9 | 10 | [type.zig] 11 | extend-glob = ["*.zig", "*.zog"] 12 | check-file = true 13 | 14 | [type.zig.extend-words] 15 | "hlep" = "hlep" # "--hlep" is checked for in options parsing, used as --help alias. 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "zlint [multi-thread]", 9 | "type": "lldb", 10 | "request": "launch", 11 | "program": "./zig-out/bin/zlint", 12 | "cwd": "${workspaceRoot}", 13 | "preLaunchTask": "build", 14 | "postRunCommands": ["command source '${workspaceFolder}/tasks/lldb/.lldbinit'"] 15 | }, 16 | { 17 | "name": "zlint", 18 | "type": "lldb", 19 | "request": "launch", 20 | "program": "./zig-out/bin/zlint", 21 | "cwd": "${workspaceRoot}", 22 | "preLaunchTask": "build -Dsingle-threaded", 23 | "postRunCommands": ["command source '${workspaceFolder}/tasks/lldb/.lldbinit'"] 24 | }, 25 | { 26 | "name": "zlint [bun]", 27 | "type": "lldb", 28 | "request": "launch", 29 | "program": "${workspaceRoot}/zig-out/bin/zlint", 30 | "cwd": "${workspaceRoot}/zig-out/repos/bun", 31 | "preLaunchTask": "build -Dsingle-threaded", 32 | "postRunCommands": ["command source '${workspaceFolder}/tasks/lldb/.lldbinit'"] 33 | }, 34 | { 35 | "name": "zlint --print-ast ${file}", 36 | "type": "lldb", 37 | "request": "launch", 38 | "args": ["--print-ast", "${relativeFile}"], 39 | "program": "${workspaceRoot}/zig-out/bin/zlint", 40 | "cwd": "${workspaceRoot}", 41 | "stdio": "${workspaceRoot}/tmp/ast.json", 42 | "preLaunchTask": "pre print-ast", 43 | "postRunCommands": ["command source '${workspaceFolder}/tasks/lldb/.lldbinit'"] 44 | }, 45 | { 46 | "name": "test-e2e", 47 | "type": "lldb", 48 | "request": "launch", 49 | "program": "./zig-out/bin/test-e2e", 50 | "cwd": "${workspaceRoot}", 51 | "preLaunchTask": "build -Dsingle-threaded", 52 | "postRunCommands": ["command source '${workspaceFolder}/tasks/lldb/.lldbinit'"] 53 | }, 54 | { 55 | "name": "test", 56 | "type": "lldb", 57 | "request": "launch", 58 | "program": "./zig-out/bin/test", 59 | "cwd": "${workspaceRoot}", 60 | "preLaunchTask": "build -Dsingle-threaded", 61 | "postRunCommands": ["command source '${workspaceFolder}/tasks/lldb/.lldbinit'"] 62 | }, 63 | { 64 | "name": "test-utils", 65 | "type": "lldb", 66 | "request": "launch", 67 | "program": "./zig-out/bin/test-utils", 68 | "cwd": "${workspaceRoot}", 69 | "preLaunchTask": "build -Dsingle-threaded", 70 | "postRunCommands": ["command source '${workspaceFolder}/tasks/lldb/.lldbinit'"] 71 | }, 72 | // { 73 | // "name": "zig test [file]", 74 | // "type": "lldb", 75 | // "request": "launch", 76 | // "program": "./zig-out/bin/${fileBasenameNoExtension}-test", 77 | // "cwd": "${workspaceRoot}", 78 | // "preLaunchTask": "build zig test [file]" 79 | // } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "zig.zls.enableBuildOnSave": true, 3 | "zig.zls.buildOnSaveStep": "check", 4 | "zls.zigExePath": "./vendor/zig14/zig", 5 | "files.exclude": { 6 | ".coverage": true, 7 | ".zig-cache": true, 8 | "zig-out": true, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "zig", 7 | "args": ["build"], 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "problemMatcher": ["$zig"], 13 | "presentation": { 14 | "echo": true, 15 | "reveal": "silent", 16 | "focus": false, 17 | "panel": "shared", 18 | "showReuseMessage": false, 19 | "clear": true 20 | } 21 | }, 22 | 23 | { 24 | "label": "build -Dsingle-threaded", 25 | "command": "zig", 26 | "args": ["build", "-Dsingle-threaded"], 27 | "group": { 28 | "kind": "build", 29 | "isDefault": false 30 | }, 31 | "problemMatcher": ["$zig"], 32 | "presentation": { 33 | "echo": true, 34 | "reveal": "silent", 35 | "focus": false, 36 | "panel": "shared", 37 | "showReuseMessage": false, 38 | "clear": true 39 | } 40 | }, 41 | { 42 | "label": "test", 43 | "command": "zig", 44 | "args": ["build", "test"], 45 | "group": { 46 | "kind": "test", 47 | "isDefault": false 48 | }, 49 | "problemMatcher": ["$zig"], 50 | "presentation": { 51 | "echo": true, 52 | "reveal": "silent", 53 | "focus": false, 54 | "panel": "shared", 55 | "showReuseMessage": false, 56 | "clear": true 57 | } 58 | }, 59 | { 60 | "label": "create tmp/ast.json", 61 | "type": "shell", 62 | "command": "${workspaceFolder}/tasks/create-ast-json.sh", 63 | "presentation": { 64 | "echo": true, 65 | "reveal": "never", 66 | "focus": false, 67 | "panel": "shared", 68 | "showReuseMessage": false, 69 | "clear": true 70 | } 71 | }, 72 | { 73 | "label": "pre print-ast", 74 | "dependsOn": [ 75 | "create tmp/ast.json", 76 | "build -Dsingle-threaded" 77 | ] 78 | } 79 | ], 80 | } 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to ZLint! This document outlines how to 4 | set up your development environment and contribution guidelines. 5 | 6 | ## Setup 7 | 8 | You'll obviously need to have [Zig](https://ziglang.org/) installed. Right now 9 | we are using version `0.13.0`. 10 | 11 | ### Tl;Dr 12 | 13 | We use the following tools: 14 | 15 | - [just](https://github.com/casey/just) for running tasks 16 | - [entr](http://eradman.com/entrproject/) for an ad-hoc watch mode 17 | - [typos](https://github.com/crate-ci/typos) for spell checking 18 | - [bun](https://bun.sh/) for generating boilerplate for new rules and other tasks 19 | 20 | ### Details 21 | 22 | `just` and `typos` are both cargo crates. If you're familiar with Rust, you can 23 | install them with `cargo install` (or even better, with [`cargo 24 | binstall`](https://github.com/cargo-bins/cargo-binstall). 25 | 26 | > NOTE: both crates are also available on Homebrew under the same name. 27 | 28 | ```sh 29 | cargo binstall just typos-cli 30 | ``` 31 | 32 | Otherwise, you can 33 | follow their respective installation instructions. 34 | 35 | - [just installation 36 | guide](https://github.com/casey/just?tab=readme-ov-file#installation) 37 | - [typos installation 38 | guide](https://github.com/crate-ci/typos?tab=readme-ov-file#install) 39 | 40 | You'll also want `entr`. We use it for checking code on save. You only need it 41 | to run `just watch`, but you'll definitely want to have this. Install it using 42 | your package manager of choice. 43 | 44 | ```sh 45 | apt install entr 46 | # or 47 | brew install entr 48 | ``` 49 | 50 | ## Building, Testing, etc. 51 | 52 | Run `just` (with no arguments) to see a full list of available tasks. 53 | 54 | When debugging E2E tests or the `zlint` binary, using a single-threaded build is 55 | often helpful. 56 | 57 | ```sh 58 | just run -Dsingle-threaded 59 | just e2e -Dsingle-threaded 60 | ``` 61 | 62 | ## Contributing New Rules 63 | Check out the [Creating 64 | Rules](https://github.com/DonIsaac/zlint/wiki/Creating-Rules) guide for how to 65 | write new lint rules. A list of rules we want to implement can be found on the 66 | [Rule Ideas board](https://github.com/DonIsaac/zlint/issues/3). 67 | 68 | ## Conventions 69 | Please follow these conventions when contributing to ZLint. 70 | 71 | ### Constructors and Destructors 72 | 73 | 1. Constructors that allocate memory are named `init`. 74 | 2. Constructors that do not allocate memory are named `new`. 75 | 3. Destructors are named `deinit`. 76 | 77 | ### File Naming and Structure 78 | 79 | There are two kinds of files: "object" files and "namespace" files. Object files 80 | use the entire file as a single `struct`, storing their members in the top 81 | level. Namespace files do not do this, and instead declare or re-export various 82 | data types. 83 | 84 | #### Object File Conventions 85 | 86 | Object files use `PascalCase` for the file name. Their layout follows this order: 87 | 88 | 1. field properties 89 | 2. Self-defined constants 90 | 3. Methods (static and instance) 91 | a. constructors and destructors (`init`, `deinit`) come first 92 | b. other methods come after 93 | 4. Nested data structures (e.g. structs, enums, unions) 94 | 5. Imports 95 | 6. Tests 96 | 97 | #### Namespace File Conventions 98 | 99 | Namespace files use `snake_case` for the file name. Avoid declaring functions in 100 | the top scope of these files. This is not a hard rule, as it makes sense in some 101 | cases, but try to group them by domain (where the domain is a `struct`). 102 | 103 | Their layout follows this order: 104 | 105 | 1. Imports 106 | 2. Public data types 107 | 3. Public functions 108 | 4. Private data types & private methods (grouped logically) 109 | 5. Tests 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Don Isaac 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 | # ⚡️ ZLint 2 | 3 | [![codecov](https://codecov.io/gh/DonIsaac/zlint/graph/badge.svg?token=5bDT3yGZt8)](https://codecov.io/gh/DonIsaac/zlint) 4 | [![CI](https://github.com/DonIsaac/zlint/actions/workflows/ci.yaml/badge.svg)](https://github.com/DonIsaac/zlint/actions/workflows/ci.yaml) 5 | [![Discord](https://img.shields.io/static/v1?logo=discord&label=discord&message=Join&color=blue)](https://discord.gg/UcB7HjJxcG) 6 | 7 | An opinionated linter for the Zig programming language. 8 | 9 | ## ✨ Features 10 | 11 | - 🔍 **Custom Analysis**. ZLint has its own semantic analyzer, heavily inspired 12 | by [the Oxc project](https://github.com/oxc-project/oxc), that is completely 13 | separate from the Zig compiler. This means that ZLint still checks and 14 | understands code that may otherwise be ignored by Zig due to dead code 15 | elimination. 16 | - ⚡️ **Fast**. Designed from the ground-up to be highly performant, ZLint 17 | typically takes a few hundred milliseconds to lint large projects. 18 | - 💡 **Understandable**. Error messages are pretty, detailed, and easy to understand. 19 | Most rules come with explanations on how to fix them and what _exactly_ is wrong. 20 | ![diagnostic example](./docs/assets/diagnostic-example.jpg) 21 | 22 | ## 📦 Installation 23 | Pre-built binaries for Windows, MacOS, and Linux on x64 and aarch64 are 24 | available [for each release](https://github.com/DonIsaac/zlint/releases/latest). 25 | 26 | ```sh 27 | curl -fsSL https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/tasks/install.sh | bash 28 | ``` 29 | 30 | > [!WARNING] 31 | > This installation script does not work on Windows. Please download Windows 32 | > binaries directly from the releases page. 33 | 34 | ### 🔨 Building from Source 35 | 36 | Clone this repo and compile the project with Zig. 37 | 38 | ```sh 39 | zig build --release=safe 40 | ``` 41 | 42 | ## ⚡️ Lint Rules 43 | 44 | All lints and what they do can be found [here](docs/rules/). 45 | 46 | ## ⚙️ Configuration 47 | 48 | Create a `zlint.json` file in the same directory as `build.zig`. This disables 49 | all default rules, only enabling the ones you choose. 50 | 51 | ```json 52 | { 53 | "rules": { 54 | "unsafe-undefined": "error", 55 | "homeless-try": "warn" 56 | } 57 | } 58 | ``` 59 | 60 | ZLint also supports [ESLint-like disable directives](https://eslint.org/docs/latest/use/configure/rules#comment-descriptions) to turn off some or all rules for a specific file. 61 | 62 | ```zig 63 | // zlint-disable unsafe-undefined -- We need to come back and fix this later 64 | const x: i32 = undefined; 65 | ``` 66 | 67 | ## 🙋‍♂️ Contributing 68 | 69 | If you have any rule ideas, please add them to the [rule ideas 70 | board](https://github.com/DonIsaac/zlint/issues/3). 71 | 72 | Interested in contributing code? Check out the [contributing 73 | guide](CONTRIBUTING.md). 74 | -------------------------------------------------------------------------------- /apps/vscode-extension/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | *.vsix 9 | 10 | # code coverage 11 | coverage 12 | *.lcov 13 | 14 | # logs 15 | logs 16 | _.log 17 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 18 | 19 | # dotenv environment variable files 20 | .env 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | .env.local 25 | 26 | # caches 27 | .eslintcache 28 | .cache 29 | *.tsbuildinfo 30 | 31 | # IntelliJ based IDEs 32 | .idea 33 | 34 | # Finder (MacOS) folder config 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /apps/vscode-extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | build.ts 4 | bun.lock 5 | tsconfig.json 6 | -------------------------------------------------------------------------------- /apps/vscode-extension/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Don Isaac 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 | -------------------------------------------------------------------------------- /apps/vscode-extension/README.md: -------------------------------------------------------------------------------- 1 | # ZLint VSCode Extension 2 | 3 | A Visual Studio Code extension that integrates the [ZLint](https://github.com/DonIsaac/zlint) linter for Zig code. 4 | 5 | ## ✨ Features 6 | 7 | - 🔍 **Real-time Linting**: Automatically detects and highlights issues in your Zig code 8 | - ⚡️ **Fast Performance**: Quick analysis without slowing down your editor 9 | - 💡 **Detailed Diagnostics**: Clear error messages with explanations and suggestions 10 | - 🛠️ **Configurable**: Customize linting behavior through VSCode settings 11 | 12 | ## 📦 Installation 13 | 14 | 1. Install the ZLint binary: 15 | ```sh 16 | curl -fsSL https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/tasks/install.sh | bash 17 | ``` 18 | 19 | 2. Install the extension from the VSCode marketplace or build it locally: 20 | ```sh 21 | bun install 22 | ``` 23 | 24 | ## ⚙️ Configuration 25 | 26 | The extension can be configured through VSCode settings: 27 | 28 | ```json 29 | { 30 | "zig.zlint": { 31 | "enabled": true, 32 | "path": "/path/to/zlint" // Optional: specify custom path to zlint binary 33 | } 34 | } 35 | ``` 36 | 37 | ### Settings 38 | 39 | - `zig.zlint.enabled`: Enable/disable the extension (default: `true`) 40 | - `zig.zlint.path`: Custom path to the zlint binary (optional) 41 | 42 | ## 🔨 Building from Source 43 | 44 | 1. Clone the repository 45 | 2. Install dependencies: 46 | ```sh 47 | bun install 48 | ``` 49 | 3. Build the extension: 50 | ```sh 51 | bun run index.ts 52 | ``` 53 | 54 | ## 📝 License 55 | 56 | This extension is licensed under the same terms as the ZLint project. 57 | -------------------------------------------------------------------------------- /apps/vscode-extension/build.ts: -------------------------------------------------------------------------------- 1 | var isProd = process.env.NODE_ENV === "production"; 2 | for (const arg of process.argv) { 3 | switch (arg) { 4 | case "--production": 5 | case "-p": 6 | isProd = true; 7 | break; 8 | } 9 | } 10 | 11 | const res = await Bun.build({ 12 | entrypoints: ["src/extension.ts"], 13 | outdir: "dist", 14 | external: ["vscode"], 15 | target: "node", 16 | format: "cjs", 17 | sourcemap: "linked", 18 | minify: isProd && { 19 | whitespace: false, 20 | syntax: true, 21 | identifiers: true 22 | } 23 | }); 24 | for (const log of res.logs) { 25 | console.log(`[${log.level}] ${log.message}`); 26 | } 27 | if (!res.success) process.exit(1); 28 | -------------------------------------------------------------------------------- /apps/vscode-extension/bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "vscode-extension", 6 | "devDependencies": { 7 | "@types/bun": "latest", 8 | "@types/vscode": "^1.78.0", 9 | }, 10 | "peerDependencies": { 11 | "typescript": "^5", 12 | }, 13 | }, 14 | }, 15 | "packages": { 16 | "@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="], 17 | 18 | "@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="], 19 | 20 | "@types/vscode": ["@types/vscode@1.99.0", "", {}, "sha512-30sjmas1hQ0gVbX68LAWlm/YYlEqUErunPJJKLpEl+xhK0mKn+jyzlCOpsdTwfkZfPy4U6CDkmygBLC3AB8W9Q=="], 21 | 22 | "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 23 | 24 | "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], 25 | 26 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 27 | 28 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/vscode-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zlint-vscode", 3 | "displayName": "ZLint", 4 | "description": "A linter for the Zig programming language", 5 | "version": "0.0.0", 6 | "type": "commonjs", 7 | "main": "dist/extension.js", 8 | "private": true, 9 | "license": "MIT", 10 | "categories": [ 11 | "Linters" 12 | ], 13 | "publisher": "disaac", 14 | "author": { 15 | "name": "Don Isaac", 16 | "url": "https://github.com/DonIsaac" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/DonIsaac/zlint", 21 | "directory": "apps/vscode-extension" 22 | }, 23 | "engines": { 24 | "vscode": "^1.78.0" 25 | }, 26 | "scripts": { 27 | "package": "vsce package --no-dependencies -o zlint.vsix", 28 | "build:prod": "bun build.ts --production", 29 | "install-extension": "code --install-extension zlint.vsix --force", 30 | "all": "bun run build && bun package && bun install-extension" 31 | }, 32 | "devDependencies": { 33 | "@types/bun": "latest", 34 | "@types/vscode": "^1.78.0" 35 | }, 36 | "peerDependencies": { 37 | "typescript": "^5" 38 | }, 39 | "activationEvents": [ 40 | "onLanguage:zig" 41 | ], 42 | "contributes": { 43 | "commands": [ 44 | { 45 | "command": "zig.zlint.lint", 46 | "title": "Run ZLint", 47 | "category": "ZLint" 48 | } 49 | ], 50 | "configuration": { 51 | "title": "ZLint", 52 | "properties": { 53 | "zig.zlint.enabled": { 54 | "type": "boolean", 55 | "default": true, 56 | "description": "Enable ZLint" 57 | }, 58 | "zig.zlint.path": { 59 | "type": "string", 60 | "description": "Path to `zlint` binary. By default, it will use the one in your PATH." 61 | } 62 | } 63 | }, 64 | "jsonValidation": [ 65 | { 66 | "fileMatch": "zlint.json", 67 | "url": "https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/zlint.schema.json" 68 | } 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/ConfigService.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConfigurationTarget, 3 | Disposable, 4 | EventEmitter, 5 | workspace, 6 | type ConfigurationChangeEvent, 7 | type OutputChannel, 8 | type WorkspaceConfiguration, 9 | } from 'vscode' 10 | 11 | type EnabledEvent = { type: 'enabled' } 12 | type DisabledEvent = { type: 'disabled' } 13 | type ConfigChangeEvent = { type: 'change' } 14 | export type Event = EnabledEvent | DisabledEvent | ConfigChangeEvent 15 | 16 | export class ConfigService extends EventEmitter implements Disposable { 17 | public static configSection = 'zig.zlint' 18 | #subscriptions: Disposable[] = [] 19 | #config: Config 20 | 21 | constructor(private log: OutputChannel) { 22 | super() 23 | this.#config = ConfigService.load() 24 | this.onConfigChange = this.onConfigChange.bind(this) 25 | this.emit = this.emit.bind(this) 26 | this.#subscriptions.push( 27 | workspace.onDidChangeConfiguration(this.onConfigChange, this), 28 | ) 29 | } 30 | 31 | public set( 32 | key: K, 33 | value: Config[K], 34 | target?: ConfigurationTarget | boolean | null 35 | ): void { 36 | const config = workspace.getConfiguration(Config.scope) 37 | config.update(key, value, target) 38 | } 39 | 40 | private onConfigChange(event: ConfigurationChangeEvent): void { 41 | if (!(this instanceof ConfigService)) 42 | throw new TypeError("bad 'this' type; expected ConfigService") 43 | const relevant = event.affectsConfiguration(ConfigService.configSection) 44 | 45 | this.log.appendLine('config changed. Do we care? ' + relevant) 46 | if (!relevant) return 47 | 48 | const { enabled, path } = ConfigService.load() 49 | 50 | if (this.#config.enabled !== enabled) { 51 | this.#config.enabled = enabled 52 | this.emit({ type: enabled ? 'enabled' : 'disabled' } as 53 | | EnabledEvent 54 | | DisabledEvent) 55 | return 56 | } 57 | if (!this.#config.enabled) return 58 | 59 | let didChange = false 60 | if (this.#config.path !== path) { 61 | this.#config.path = path 62 | didChange = true 63 | } 64 | 65 | if (didChange) { 66 | this.emit({ type: 'change' } as ConfigChangeEvent) 67 | } 68 | } 69 | 70 | public get config(): Readonly { 71 | return this.#config 72 | } 73 | 74 | private static load(): Config { 75 | return Config.loadFromWorkspace( 76 | workspace.getConfiguration(this.configSection), 77 | ) 78 | } 79 | 80 | private emit(event: Event): void { 81 | this.log.appendLine('emitting event: ' + JSON.stringify(event)) 82 | this.fire(event) 83 | } 84 | 85 | public dispose() { 86 | super.dispose() 87 | this.#subscriptions.forEach((s) => s.dispose()) 88 | } 89 | } 90 | 91 | export interface Config { 92 | /** 93 | * Is the extension enabled? 94 | * 95 | * path: `.enabled` 96 | * 97 | * @default true 98 | */ 99 | enabled: boolean 100 | /** 101 | * Absolute path to the `zlint` binary. 102 | * 103 | * path: `.path` 104 | */ 105 | path: string | undefined 106 | } 107 | namespace Config { 108 | export const scope = 'zig.zlint' 109 | 110 | /** 111 | * Subscribe to configuration changes 112 | */ 113 | export function subscribe( 114 | callback: (event: ConfigurationChangeEvent) => void, 115 | thisArg?: any, 116 | ): Disposable { 117 | return workspace.onDidChangeConfiguration(function (event) { 118 | if (event.affectsConfiguration(scope)) { 119 | callback.call(thisArg, event) 120 | } 121 | }) 122 | } 123 | 124 | // oxlint-disable consistent-function-scoping 125 | export function loadFromWorkspace(config: WorkspaceConfiguration): Config { 126 | return { 127 | enabled: config.get('enabled', true), 128 | path: config.get('path'), 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { ConfigService } from './ConfigService' 3 | import { BinaryService } from './BinaryService' 4 | import { DiagnosticsService } from './DiagnosticsService' 5 | 6 | export function activate(context: vscode.ExtensionContext) { 7 | const logs = vscode.window.createOutputChannel('zlint') 8 | logs.clear() 9 | logs.appendLine('zlint extension activated') 10 | const config = new ConfigService(logs) 11 | logs.appendLine('Found config: ' + JSON.stringify(config.config)) 12 | const bin = new BinaryService(config, logs) 13 | const diagnostics = new DiagnosticsService(config, bin, logs) 14 | 15 | bin 16 | .findZLintBinary() 17 | .catch((e) => logs.appendLine('error finding zlint binary: ' + e)) 18 | 19 | const lintCmd = vscode.commands.registerCommand('zig.zlint.lint', () => { 20 | if (!bin.ready) { 21 | logs.appendLine('zlint binary not ready, not running zlint.lint') 22 | return 23 | } 24 | logs.appendLine('collecting diagnostics') 25 | diagnostics 26 | .collectDiagnostics() 27 | .then(() => logs.appendLine('done')) 28 | .catch((e) => logs.appendLine('error collecting diagnostics: ' + e)) 29 | }) 30 | 31 | // FIXME: this command is not working 32 | // const toggleCommand = vscode.commands.registerCommand( 33 | // 'zig.zlint.toggle', 34 | // () => config.set( 35 | // 'enabled', 36 | // !config.config.enabled, 37 | // vscode.ConfigurationTarget.WorkspaceFolder 38 | // ), 39 | // ) 40 | 41 | 42 | context.subscriptions.push(config, bin, lintCmd) 43 | } 44 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/util.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'node:stream' 2 | import assert from 'node:assert' 3 | 4 | export function readableStreamToString(stream: Readable) { 5 | assert(stream && stream.readable) 6 | const { promise, resolve } = Promise.withResolvers() 7 | 8 | const chunks: Buffer[] = [] 9 | const finalize = () => Buffer.concat(chunks).toString('utf8') 10 | stream 11 | .on('data', (chunk) => chunks.push(chunk)) 12 | .on('end', () => resolve(finalize())) 13 | .on('close', () => resolve(finalize())) 14 | 15 | return promise 16 | } 17 | -------------------------------------------------------------------------------- /apps/vscode-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | // This is the default name used by packages depending on this one. For 3 | // example, when a user runs `zig fetch --save `, this field is used 4 | // as the key in the `dependencies` table. Although the user can choose a 5 | // different name, most users will stick with this provided value. 6 | // 7 | // It is redundant to include "zig" in this name because it is already 8 | // within the Zig package namespace. 9 | .name = .zlint, 10 | 11 | // This is a [Semantic Version](https://semver.org/). 12 | // In a future version of Zig it will be used for package deduplication. 13 | .version = "0.7.7", 14 | 15 | // This field is optional. 16 | // This is currently advisory only; Zig does not yet do anything 17 | // with this value. 18 | .minimum_zig_version = "0.14.0", 19 | 20 | .fingerprint = 0xefa0446b0e2197e9, 21 | 22 | // This field is optional. 23 | // Each dependency must either provide a `url` and `hash`, or a `path`. 24 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 25 | // Once all dependencies are fetched, `zig build` no longer requires 26 | // internet connectivity. 27 | .dependencies = .{ 28 | .smart_pointers = .{ 29 | .url = "https://github.com/DonIsaac/smart-pointers/archive/refs/tags/v0.0.3.tar.gz", 30 | .hash = "smart_pointers-0.0.3-NPos2MOwAABoujUzLcVLofXqRAgYWLc5pG-TKDhyK0cq", 31 | }, 32 | .chameleon = .{ 33 | .url = "https://github.com/DonIsaac/chameleon/archive/7c7477fa76da53c2791f9e1f860481f64140ccbc.zip", 34 | .hash = "chameleon-3.0.0-bqfnCfhtAAAAxXGw5t9odkb4ayCTTqOcPvL-TgSMUacF", 35 | }, 36 | .recover = .{ 37 | .url = "https://github.com/dimdin/zig-recover/archive/36133afaa1b085db7063ffc97c08ae0bddc2de4e.zip", 38 | .hash = "recover-1.1.0-Zd97oqomAADqISI8KEhW_UUjiPSExhw9hzeoNpg1Nveo", 39 | }, 40 | }, 41 | .paths = .{ 42 | "build.zig", 43 | "build.zig.zon", 44 | "src", 45 | "LICENSE", 46 | "README.md", 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /docs/assets/diagnostic-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DonIsaac/zlint/d399cbc2a60ed927b43e66242733e565097ef409/docs/assets/diagnostic-example.jpg -------------------------------------------------------------------------------- /docs/rules/avoid-as.md: -------------------------------------------------------------------------------- 1 | # `avoid-as` 2 | 3 | > Category: pedantic 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows using `@as()` when types can be otherwise inferred. 10 | 11 | Zig has powerful [Result Location Semantics](https://ziglang.org/documentation/master/#Result-Location-Semantics) for inferring what type 12 | something should be. This happens in function parameters, return types, 13 | and type annotations. `@as()` is a last resort when no other contextual 14 | information is available. In any other case, other type inference mechanisms 15 | should be used. 16 | 17 | > [!NOTE] 18 | > Checks for function parameters and return types are not yet implemented. 19 | 20 | ## Examples 21 | 22 | Examples of **incorrect** code for this rule: 23 | 24 | ```zig 25 | const x = @as(u32, 1); 26 | 27 | fn foo(x: u32) u64 { 28 | return @as(u64, x); // type is inferred from return type 29 | } 30 | foo(@as(u32, 1)); // type is inferred from function signature 31 | ``` 32 | 33 | Examples of **correct** code for this rule: 34 | 35 | ```zig 36 | const x: u32 = 1; 37 | 38 | fn foo(x: u32) void { 39 | // ... 40 | } 41 | foo(1); 42 | ``` 43 | 44 | ## Configuration 45 | 46 | This rule has no configuration. 47 | -------------------------------------------------------------------------------- /docs/rules/case-convention.md: -------------------------------------------------------------------------------- 1 | # `case-convention` 2 | 3 | > Category: style 4 | > 5 | > Enabled by default?: No 6 | 7 | ## What This Rule Does 8 | 9 | Checks for function names that are not in camel case. Specially coming from Rust, 10 | some people may be used to use snake_case for their functions, which can lead to 11 | inconsistencies in the code 12 | 13 | ## Examples 14 | 15 | Examples of **incorrect** code for this rule: 16 | 17 | ```zig 18 | fn this_one_is_in_snake_case() void {} 19 | ``` 20 | 21 | Examples of **correct** code for this rule: 22 | 23 | ```zig 24 | fn thisFunctionIsInCamelCase() void {} 25 | ``` 26 | 27 | ## Configuration 28 | 29 | This rule has no configuration. 30 | -------------------------------------------------------------------------------- /docs/rules/empty-file.md: -------------------------------------------------------------------------------- 1 | # `empty-file` 2 | 3 | > Category: style 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | This rule checks for empty .zig files in the project. 10 | A file should be deemed empty if it has no content (zero bytes) or only comments and whitespace characters, 11 | as defined by the standard library in [`std.ascii.whitespace`](https://ziglang.org/documentation/master/std/#std.ascii.whitespace). 12 | 13 | ## Examples 14 | 15 | Examples of **incorrect** code for this rule: 16 | 17 | ```zig 18 | // an "empty" file is actually a file without meaningful code: just comments (doc or normal) or whitespace 19 | ``` 20 | 21 | Examples of **correct** code for this rule: 22 | 23 | ```zig 24 | fn exampleFunction() void { 25 | } 26 | ``` 27 | 28 | ## Configuration 29 | 30 | This rule has no configuration. 31 | -------------------------------------------------------------------------------- /docs/rules/homeless-try.md: -------------------------------------------------------------------------------- 1 | # `homeless-try` 2 | 3 | > Category: compiler 4 | > 5 | > Enabled by default?: Yes (error) 6 | 7 | ## What This Rule Does 8 | 9 | Checks for `try` statements used outside of error-returning functions. 10 | 11 | As a `compiler`-level lint, this rule checks for errors also caught by the 12 | Zig compiler. 13 | 14 | ## Examples 15 | 16 | Examples of **incorrect** code for this rule: 17 | 18 | ```zig 19 | const std = @import("std"); 20 | 21 | var not_in_a_function = try std.heap.page_allocator.alloc(u8, 8); 22 | 23 | fn foo() void { 24 | var my_str = try std.heap.page_allocator.alloc(u8, 8); 25 | } 26 | 27 | fn bar() !void { 28 | const Baz = struct { 29 | property: u32 = try std.heap.page_allocator.alloc(u8, 8), 30 | }; 31 | } 32 | ``` 33 | 34 | Examples of **correct** code for this rule: 35 | 36 | ```zig 37 | fn foo() !void { 38 | var my_str = try std.heap.page_allocator.alloc(u8, 8); 39 | } 40 | ``` 41 | 42 | Zig allows `try` in comptime scopes in or nested within functions. This rule 43 | does not flag these cases. 44 | 45 | ```zig 46 | const std = @import("std"); 47 | fn foo(x: u32) void { 48 | comptime { 49 | // valid 50 | try bar(x); 51 | } 52 | } 53 | fn bar(x: u32) !void { 54 | return if (x == 0) error.Unreachable else void; 55 | } 56 | ``` 57 | 58 | Zig also allows `try` on functions whose error union sets are empty. ZLint 59 | does _not_ respect this case. Please refactor such functions to not return 60 | an error union. 61 | 62 | ```zig 63 | const std = @import("std"); 64 | fn foo() !u32 { 65 | // compiles, but treated as a violation. `bar` should return `u32`. 66 | const x = try bar(); 67 | return x + 1; 68 | } 69 | fn bar() u32 { 70 | return 1; 71 | } 72 | ``` 73 | 74 | ## Configuration 75 | 76 | This rule has no configuration. 77 | -------------------------------------------------------------------------------- /docs/rules/line-length.md: -------------------------------------------------------------------------------- 1 | # `line-length` 2 | 3 | > Category: style 4 | > 5 | > Enabled by default?: No 6 | 7 | ## What This Rule Does 8 | 9 | Checks if any line goes beyond a given number of columns. 10 | 11 | ## Examples 12 | 13 | Examples of **incorrect** code for this rule (with a threshold of 120 columns): 14 | 15 | ```zig 16 | const std = @import("std"); 17 | const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 }; 18 | fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 { 19 | return 123; 20 | } 21 | ``` 22 | 23 | Examples of **correct** code for this rule (with a threshold of 120 columns): 24 | 25 | ```zig 26 | const std = @import("std"); 27 | const longStructInMultipleLines = struct { 28 | max_length: u32 = 120, 29 | a: usize = 123, 30 | b: usize = 12354, 31 | c: usize = 1234352, 32 | }; 33 | fn Get123Constant() u32 { 34 | return 123; 35 | } 36 | ``` 37 | 38 | ## Configuration 39 | 40 | This rule accepts the following options: 41 | 42 | - max_length: int 43 | -------------------------------------------------------------------------------- /docs/rules/must-return-ref.md: -------------------------------------------------------------------------------- 1 | # `must-return-ref` 2 | 3 | > Category: suspicious 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows returning copies of types that store a `capacity`. 10 | 11 | Zig does not have move semantics. Returning a value by value copies it. 12 | Returning a copy of a struct's field that records how much memory it has 13 | allocated can easily lead to memory leaks. 14 | 15 | ```zig 16 | const std = @import("std"); 17 | pub const Foo = struct { 18 | list: std.ArrayList(u32), 19 | pub fn getList(self: *Foo) std.ArrayList(u32) { 20 | return self.list; 21 | } 22 | }; 23 | 24 | pub fn main() !void { 25 | var foo: Foo = .{ 26 | .list = try std.ArrayList(u32).init(std.heap.page_allocator) 27 | }; 28 | defer foo.list.deinit(); 29 | var list = foo.getList(); 30 | try list.append(1); // leaked! 31 | } 32 | ``` 33 | 34 | ## Examples 35 | 36 | Examples of **incorrect** code for this rule: 37 | 38 | ```zig 39 | fn foo(self: *Foo) std.ArrayList(u32) { 40 | return self.list; 41 | } 42 | ``` 43 | 44 | Examples of **correct** code for this rule: 45 | 46 | ```zig 47 | // pass by reference 48 | fn foo(self: *Foo) *std.ArrayList(u32) { 49 | return &self.list; 50 | } 51 | 52 | // new instances are fine 53 | fn foo() ArenaAllocator { 54 | return std.mem.ArenaAllocator.init(std.heap.page_allocator); 55 | } 56 | ``` 57 | 58 | ## Configuration 59 | 60 | This rule has no configuration. 61 | -------------------------------------------------------------------------------- /docs/rules/no-catch-return.md: -------------------------------------------------------------------------------- 1 | # `no-catch-return` 2 | 3 | > Category: pedantic 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows `catch` blocks that immediately return the caught error. 10 | 11 | Catch blocks that do nothing but return their error can and should be 12 | replaced with a `try` statement. This rule allows for `catch`es that 13 | have side effects such as printing the error or switching over it. 14 | 15 | ## Examples 16 | 17 | Examples of **incorrect** code for this rule: 18 | 19 | ```zig 20 | fn foo() !void { 21 | riskyOp() catch |e| return e; 22 | riskyOp() catch |e| { return e; }; 23 | } 24 | ``` 25 | 26 | Examples of **correct** code for this rule: 27 | 28 | ```zig 29 | const std = @import("std"); 30 | 31 | fn foo() !void{ 32 | try riskyOp(); 33 | } 34 | 35 | // re-throwing with side effects is fine 36 | fn bar() !void { 37 | riskyOp() catch |e| { 38 | std.debug.print("Error: {any}\n", .{e}); 39 | return e; 40 | }; 41 | } 42 | 43 | // throwing a new error is fine 44 | fn baz() !void { 45 | riskyOp() catch |e| return error.OutOfMemory; 46 | } 47 | ``` 48 | 49 | ## Configuration 50 | 51 | This rule has no configuration. 52 | -------------------------------------------------------------------------------- /docs/rules/no-return-try.md: -------------------------------------------------------------------------------- 1 | # `no-return-try` 2 | 3 | > Category: pedantic 4 | > 5 | > Enabled by default?: No 6 | 7 | ## What This Rule Does 8 | 9 | Disallows `return`ing a `try` expression. 10 | 11 | Returning an error union directly has the same exact semantics as `try`ing 12 | it and then returning the result. 13 | 14 | ## Examples 15 | 16 | Examples of **incorrect** code for this rule: 17 | 18 | ```zig 19 | const std = @import("std"); 20 | 21 | fn foo() !void { 22 | return error.OutOfMemory; 23 | } 24 | 25 | fn bar() !void { 26 | return try foo(); 27 | } 28 | ``` 29 | 30 | Examples of **correct** code for this rule: 31 | 32 | ```zig 33 | const std = @import("std"); 34 | 35 | fn foo() !void { 36 | return error.OutOfMemory; 37 | } 38 | 39 | fn bar() !void { 40 | errdefer { 41 | std.debug.print("this still gets printed.\n", .{}); 42 | } 43 | 44 | return foo(); 45 | } 46 | ``` 47 | 48 | ## Configuration 49 | 50 | This rule has no configuration. 51 | -------------------------------------------------------------------------------- /docs/rules/no-unresolved.md: -------------------------------------------------------------------------------- 1 | # `no-unresolved` 2 | 3 | > Category: correctness 4 | > 5 | > Enabled by default?: Yes (error) 6 | 7 | ## What This Rule Does 8 | 9 | Checks for imports to files that do not exist. 10 | 11 | This rule only checks for file-based imports. Modules added by `build.zig` 12 | are not checked. More precisely, imports to paths ending in `.zig` will be 13 | resolved. This rule checks that a file exists at the imported path and is 14 | not a directory. Symlinks are allowed but are not followed. 15 | 16 | ## Examples 17 | 18 | Assume the following directory structure: 19 | 20 | ```plaintext 21 | . 22 | ├── foo.zig 23 | ├── mod 24 | │ └── bar.zig 25 | ├── not_a_file.zig 26 | │ └── baz.zig 27 | └── root.zig 28 | ``` 29 | 30 | Examples of **incorrect** code for this rule: 31 | 32 | ```zig 33 | // root.zig 34 | const x = @import("mod/foo.zig"); // foo.zig is in the root directory. 35 | const y = @import("not_a_file.zig"); // directory, not a file 36 | ``` 37 | 38 | Examples of **correct** code for this rule: 39 | 40 | ```zig 41 | // root.zig 42 | const x = @import("foo.zig"); 43 | const y = @import("mod/bar.zig"); 44 | ``` 45 | 46 | ## Configuration 47 | 48 | This rule has no configuration. 49 | -------------------------------------------------------------------------------- /docs/rules/suppressed-errors.md: -------------------------------------------------------------------------------- 1 | # `suppressed-errors` 2 | 3 | > Category: suspicious 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows suppressing or otherwise mishandling caught errors. 10 | 11 | Functions that return error unions could error during "normal" execution. 12 | If they didn't, they would not return an error or would panic instead. 13 | 14 | This rule enforces that errors are 15 | 16 | 1. Propagated up to callers either implicitly or by returning a new error, 17 | ```zig 18 | const a = try foo(); 19 | const b = foo catch |e| { 20 | switch (e) { 21 | FooError.OutOfMemory => error.OutOfMemory, 22 | // ... 23 | } 24 | } 25 | ``` 26 | 2. Are inspected and handled to continue normal execution 27 | ```zig 28 | /// It's fine if users are missing a config file, and open() + err 29 | // handling is faster than stat() then open() 30 | var config?: Config = openConfig() catch null; 31 | ``` 32 | 3. Caught and `panic`ed on to provide better crash diagnostics 33 | ```zig 34 | const str = try allocator.alloc(u8, size) catch @panic("Out of memory"); 35 | ``` 36 | 37 | ## Examples 38 | 39 | Examples of **incorrect** code for this rule: 40 | 41 | ```zig 42 | const x = foo() catch {}; 43 | const y = foo() catch { 44 | // comments within empty catch blocks are still considered violations. 45 | }; 46 | // `unreachable` is for code that will never be reached due to invariants. 47 | const y = foo() catch unreachable 48 | ``` 49 | 50 | Examples of **correct** code for this rule: 51 | 52 | ```zig 53 | const x = foo() catch @panic("foo failed."); 54 | const y = foo() catch { 55 | std.debug.print("Foo failed.\n", .{}); 56 | }; 57 | const z = foo() catch null; 58 | // Writer errors may be safely ignored 59 | writer.print("{}", .{5}) catch {}; 60 | 61 | // suppression is allowed in tests 62 | test foo { 63 | foo() catch {}; 64 | } 65 | ``` 66 | 67 | ## Configuration 68 | 69 | This rule has no configuration. 70 | -------------------------------------------------------------------------------- /docs/rules/unsafe-undefined.md: -------------------------------------------------------------------------------- 1 | # `unsafe-undefined` 2 | 3 | > Category: restriction 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows initializing or assigning variables to `undefined`. 10 | 11 | Reading uninitialized memory is one of the most common sources of undefined 12 | behavior. While debug builds come with runtime safety checks for `undefined` 13 | access, they are otherwise undetectable and will not cause panics in release 14 | builds. 15 | 16 | ### Allowed Scenarios 17 | 18 | There are some cases where using `undefined` makes sense, such as array 19 | initialization. Some cases are implicitly allowed, but others should be 20 | communicated to other programmers via a safety comment. Adding `SAFETY: 21 | ` before the line using `undefined` will not trigger a rule 22 | violation. 23 | 24 | ```zig 25 | // SAFETY: foo is written to by `initializeFoo`, so `undefined` is never 26 | // read. 27 | var foo: u32 = undefined 28 | initializeFoo(&foo); 29 | 30 | // SAFETY: this covers the entire initialization 31 | const bar: Bar = .{ 32 | .a = undefined, 33 | .b = undefined, 34 | }; 35 | ``` 36 | 37 | > [!NOTE] 38 | > Oviously unsafe usages of `undefined`, such `x == undefined`, are not 39 | > allowed even in these exceptions. 40 | 41 | #### Arrays 42 | 43 | Array-typed variable declarations may be initialized to undefined. 44 | Array-typed container fields with `undefined` as a default value will still 45 | trigger a violation. 46 | 47 | ```zig 48 | // arrays may be set to undefined without a safety comment 49 | var arr: [10]u8 = undefined; 50 | @memset(&arr, 0); 51 | 52 | // This is not allowed 53 | const Foo = struct { 54 | foo: [4]u32 = undefined 55 | }; 56 | ``` 57 | 58 | #### Whitelisting Types 59 | 60 | You may whitelist specific types that are allowed to be initialized to `undefined`. 61 | Any variable with this type will not have a violation triggered, as long as 62 | the type is obvious to ZLint's semantic analyzer. By default the whitelist 63 | contains `ThreadPool`/`Thread.Pool` from `std.Thread.Pool` 64 | 65 | ```zig 66 | // "unsafe-undefined": ["error", { "allow_types": ["CustomBuffer"] }] 67 | const CustomBuffer = [4096]u8; 68 | var buf: CustomBuffer = undefined; // ok 69 | ``` 70 | 71 | > [!NOTE] 72 | > ZLint does not have a type checker yet, so implicit struct initializations 73 | > will not be ignored. 74 | 75 | #### Destructors 76 | 77 | Invalidating freed pointers/data by setting it to `undefined` is helpful for 78 | finding use-after-free bugs. Using `undefined` in destructors will not trigger 79 | a violation, unless it is obviously unsafe (e.g. in a comparison). 80 | 81 | ```zig 82 | const std = @import("std"); 83 | const Foo = struct { 84 | data: []u8, 85 | pub fn init(allocator: std.mem.Allocator) !Foo { 86 | const data = try allocator.alloc(u8, 8); 87 | return .{ .data = data }; 88 | } 89 | pub fn deinit(self: *Foo, allocator: std.mem.Allocator) void { 90 | allocator.free(self.data); 91 | self.* = undefined; // safe 92 | } 93 | }; 94 | ``` 95 | 96 | A method is considered a destructor if it is named 97 | 98 | - `deinit` 99 | - `destroy` 100 | - `reset` 101 | 102 | #### `test` blocks 103 | 104 | All usages of `undefined` in `test` blocks are allowed. Code that isn't safe 105 | will be caught by the test runner. 106 | 107 | ## Examples 108 | 109 | Examples of **incorrect** code for this rule: 110 | 111 | ```zig 112 | const x = undefined; 113 | 114 | // Consumers of `Foo` should be forced to initialize `x`. 115 | const Foo = struct { 116 | x: *u32 = undefined, 117 | }; 118 | 119 | var y: *u32 = allocator.create(u32); 120 | y.* = undefined; 121 | ``` 122 | 123 | Examples of **correct** code for this rule: 124 | 125 | ```zig 126 | const Foo = struct { 127 | x: *u32, 128 | 129 | fn init(allocator: *std.mem.Allocator, value: u32) void { 130 | self.x = allocator.create(u32); 131 | self.x.* = value; 132 | } 133 | 134 | // variables may be re-assigned to `undefined` in destructors 135 | fn deinit(self: *Foo, alloc: std.mem.Allocator) void { 136 | alloc.destroy(self.x); 137 | self.x = undefined; 138 | } 139 | }; 140 | 141 | test Foo { 142 | // Allowed. If this is truly unsafe, it will be caught by the test. 143 | var foo: Foo = undefined; 144 | // ... 145 | } 146 | ``` 147 | 148 | ## Configuration 149 | 150 | This rule accepts the following options: 151 | 152 | - allowed_types: array 153 | - allow_arrays: boolean 154 | -------------------------------------------------------------------------------- /docs/rules/unused-decls.md: -------------------------------------------------------------------------------- 1 | # `unused-decls` 2 | 3 | > Category: correctness 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows container-scoped variables that are declared but never used. Note 10 | that top-level declarations are included. 11 | 12 | The Zig compiler checks for unused parameters, payloads bound by `if`, 13 | `catch`, etc, and `const`/`var` declaration within functions. However, 14 | variables and functions declared in container scopes are not given the same 15 | treatment. This rule handles those cases. 16 | 17 | > [!WARNING] 18 | > ZLint's semantic analyzer does not yet record references to variables on 19 | > member access expressions (e.g. `bar` on `foo.bar`). It also does not 20 | > handle method calls correctly. Until these features are added, only 21 | > top-level `const` variable declarations are checked. 22 | 23 | ## Examples 24 | 25 | Examples of **incorrect** code for this rule: 26 | 27 | ```zig 28 | // `std` used, but `Allocator` is not. 29 | const std = @import("std"); 30 | const Allocator = std.mem.Allocator; 31 | 32 | // Variables available to other code, either via `export` or `pub`, are not 33 | // reported. 34 | pub const x = 1; 35 | export fn foo(x: u32) void {} 36 | 37 | // `extern` functions are not reported 38 | extern fn bar(a: i32) void; 39 | ``` 40 | 41 | Examples of **correct** code for this rule: 42 | 43 | ```zig 44 | // Discarded variables are considered "used". 45 | const x = 1; 46 | _ = x; 47 | 48 | // non-container scoped variables are allowed by this rule but banned by the 49 | // compiler. `x`, `y`, and `z` are ignored by this rule. 50 | pub fn foo(x: u32) void { 51 | const y = true; 52 | var z: u32 = 1; 53 | } 54 | ``` 55 | 56 | ## Configuration 57 | 58 | This rule has no configuration. 59 | -------------------------------------------------------------------------------- /docs/rules/useless-error-return.md: -------------------------------------------------------------------------------- 1 | # `useless-error-return` 2 | 3 | > Category: suspicious 4 | > 5 | > Enabled by default?: No 6 | 7 | ## What This Rule Does 8 | 9 | Detects functions that have an error union return type but never actually return an error. 10 | This can happen in two ways: 11 | 12 | 1. The function never returns an error value 13 | 2. The function catches all errors internally and never propagates them to the caller 14 | 15 | Having an error union return type when errors are never returned makes the code less clear 16 | and forces callers to handle errors that will never occur. 17 | 18 | ## Examples 19 | 20 | Examples of **incorrect** code for this rule: 21 | 22 | ```zig 23 | // Function declares error return but only returns void 24 | fn foo() !void { 25 | return; 26 | } 27 | 28 | // Function catches all errors internally 29 | pub fn init(allocator: std.mem.Allocator) !Foo { 30 | const new = allocator.create(Foo) catch @panic("OOM"); 31 | new.* = .{}; 32 | return new; 33 | } 34 | 35 | // Function only returns success value 36 | fn bar() !void { 37 | const e = baz(); 38 | return e; 39 | } 40 | ``` 41 | 42 | Examples of **correct** code for this rule: 43 | 44 | ```zig 45 | // Function properly propagates errors 46 | fn foo() !void { 47 | return error.Oops; 48 | } 49 | 50 | // Function returns result of fallible operation 51 | fn bar() !void { 52 | return baz(); 53 | } 54 | 55 | // Function propagates caught errors 56 | fn qux() !void { 57 | bar() catch |e| return e; 58 | } 59 | 60 | // Function with conditional error return 61 | fn check(x: bool) !void { 62 | return if (x) error.Invalid else {}; 63 | } 64 | 65 | // Empty error set is explicitly allowed 66 | fn noErrors() error{}!void {} 67 | ``` 68 | 69 | ## Configuration 70 | 71 | This rule has no configuration. 72 | -------------------------------------------------------------------------------- /entitlements.dev.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | com.apple.security.get-task-allow 15 | 16 | com.apple.security.cs.debugger 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/cli/print_command.zig: -------------------------------------------------------------------------------- 1 | //! Hacky AST printer for debugging purposes. 2 | //! 3 | //! Resolves AST nodes and prints them as JSON. This can be safely piped into a file, since `std.debug.print` writes to stderr. 4 | //! 5 | //! ## Usage 6 | //! ```sh 7 | //! # note: right now, no target file can be specified. Run 8 | //! zig build run -- --print-ast | prettier --stdin-filepath foo.ast.json > tmp/foo.ast.json 9 | //! ``` 10 | const std = @import("std"); 11 | const Allocator = std.mem.Allocator; 12 | 13 | const Options = @import("../cli/Options.zig"); 14 | const Source = @import("../source.zig").Source; 15 | const Semantic = @import("../semantic.zig").Semantic; 16 | 17 | const Printer = @import("../printer/Printer.zig"); 18 | const AstPrinter = @import("../printer/AstPrinter.zig"); 19 | const SemanticPrinter = @import("../printer/SemanticPrinter.zig"); 20 | 21 | /// Borrows source. 22 | pub fn parseAndPrint(alloc: Allocator, opts: Options, source: Source, writer_: ?std.io.AnyWriter) !void { 23 | var builder = Semantic.Builder.init(alloc); 24 | defer builder.deinit(); 25 | var sema_result = try builder.build(source.text()); 26 | defer sema_result.deinit(); 27 | if (sema_result.hasErrors()) { 28 | for (sema_result.errors.items) |err| { 29 | std.debug.print("{s}\n", .{err.message.str}); 30 | } 31 | return; 32 | } 33 | const sema = &sema_result.value; 34 | const writer = writer_ orelse std.io.getStdOut().writer().any(); 35 | var printer = Printer.init(alloc, writer); 36 | defer printer.deinit(); 37 | var ast_printer = AstPrinter.new(&printer, .{ .verbose = opts.verbose }, source, &sema.parse.ast); 38 | ast_printer.setNodeLinks(&sema.node_links); 39 | var semantic_printer = SemanticPrinter.new(&printer, &sema_result.value); 40 | 41 | try printer.pushObject(); 42 | defer printer.pop(); 43 | try printer.pPropName("ast"); 44 | try ast_printer.printAst(); 45 | try printer.pPropName("symbols"); 46 | try semantic_printer.printSymbolTable(); 47 | try printer.pPropName("scopes"); 48 | try semantic_printer.printScopeTree(); 49 | try printer.pPropName("modules"); 50 | try semantic_printer.printModuleRecord(); 51 | } 52 | 53 | test { 54 | _ = @import("test/print_ast_test.zig"); 55 | } 56 | -------------------------------------------------------------------------------- /src/cli/test/print_ast_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Source = @import("../../source.zig").Source; 3 | 4 | const path = std.fs.path; 5 | const json = std.json; 6 | 7 | // const allocator = std.testing.allocator; 8 | const print_command = @import("../print_command.zig"); 9 | 10 | const source_code: [:0]const u8 = 11 | \\pub const Foo = struct { 12 | \\ a: u32, 13 | \\ b: bool, 14 | \\ c: ?[]const u8, 15 | \\}; 16 | \\const x: Foo = Foo{ .a = 1, .b = true, .c = "hello" }; 17 | ; 18 | 19 | test print_command { 20 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 21 | defer arena.deinit(); 22 | const allocator = arena.allocator(); 23 | var source = try Source.fromString(allocator, @constCast(source_code), "foo.zig"); 24 | var buf = try std.ArrayList(u8).initCapacity(allocator, source.text().len); 25 | const writer = buf.writer(); 26 | 27 | try print_command.parseAndPrint(allocator, .{ .verbose = true }, source, writer.any()); 28 | // fixme: lots of trailing commas 29 | // const parsed = try std.json.parseFromSliceLeaky(json.Value, allocator, buf.items, .{}); 30 | // try std.testing.expect(parsed == .object); 31 | } 32 | -------------------------------------------------------------------------------- /src/lint.zig: -------------------------------------------------------------------------------- 1 | pub const Linter = @import("linter/linter.zig").Linter; 2 | pub const LintService = @import("linter/LintService.zig"); 3 | pub const Config = @import("linter/Config.zig"); 4 | pub const Rule = @import("linter/rule.zig").Rule; 5 | pub const rules = @import("linter/rules.zig"); 6 | pub const Fix = @import("linter/fix.zig").Fix; 7 | 8 | test { 9 | const std = @import("std"); 10 | 11 | // Ensure intellisense. Especially important when authoring a new rule. 12 | std.testing.refAllDecls(@This()); 13 | std.testing.refAllDeclsRecursive(@import("linter/rules.zig")); 14 | 15 | // Test suites 16 | _ = @import("linter/test/disabling_rules_test.zig"); 17 | _ = @import("linter/test/fix_test.zig"); 18 | } 19 | -------------------------------------------------------------------------------- /src/linter/RuleSet.zig: -------------------------------------------------------------------------------- 1 | rules: std.ArrayListUnmanaged(Rule.WithSeverity) = .{}, 2 | 3 | const RuleSet = @This(); 4 | 5 | /// Total number of all lint rules. 6 | pub const RULES_COUNT: usize = @typeInfo(all_rules).@"struct".decls.len; 7 | const ALL_RULE_IMPLS_SIZE: usize = Rule.MAX_SIZE * @typeInfo(all_rules).@"struct".decls.len; 8 | const ALL_RULES_SIZE: usize = @sizeOf(Rule.WithSeverity) * @typeInfo(all_rules).@"struct".decls.len; 9 | 10 | pub fn ensureTotalCapacityForAllRules(self: *RuleSet, arena: Allocator) Allocator.Error!void { 11 | try self.rules.ensureTotalCapacityPrecise(arena.allocator(), ALL_RULE_IMPLS_SIZE); 12 | } 13 | 14 | pub fn loadRulesFromConfig(self: *RuleSet, arena: Allocator, config: *const RulesConfig) !void { 15 | try self.rules.ensureUnusedCapacity(arena, ALL_RULES_SIZE); 16 | const info = @typeInfo(RulesConfig); 17 | inline for (info.@"struct".fields) |field| { 18 | const rule = @field(config, field.name); 19 | if (rule.severity != Severity.off) { 20 | self.rules.appendAssumeCapacity(.{ 21 | .severity = rule.severity, 22 | // FIXME: unsafe const cast 23 | .rule = @constCast(&rule).rule(), 24 | }); 25 | } 26 | } 27 | } 28 | 29 | pub fn deinit(self: *RuleSet, arena: Allocator) void { 30 | self.rules.deinit(arena); 31 | } 32 | 33 | const std = @import("std"); 34 | const Allocator = std.mem.Allocator; 35 | const Rule = @import("rule.zig").Rule; 36 | const RulesConfig = @import("config/rules_config.zig").RulesConfig; 37 | const all_rules = @import("rules.zig"); 38 | const Severity = @import("../Error.zig").Severity; 39 | -------------------------------------------------------------------------------- /src/linter/ast_utils.zig: -------------------------------------------------------------------------------- 1 | const Context = @import("./lint_context.zig"); 2 | const semantic = @import("../semantic.zig"); 3 | const Semantic = semantic.Semantic; 4 | const Ast = semantic.Ast; 5 | const Node = Ast.Node; 6 | const Token = Semantic.Token; 7 | 8 | const NULL_NODE = Semantic.NULL_NODE; 9 | 10 | /// - `foo` -> `foo` 11 | /// - `foo.bar` -> `bar` 12 | /// - `foo()` -> `foo` 13 | /// - `foo.bar()` -> `bar` 14 | pub fn getRightmostIdentifier(ctx: *Context, id: Node.Index) ?[]const u8 { 15 | const nodes = ctx.ast().nodes; 16 | const tag: Node.Tag = nodes.items(.tag)[id]; 17 | 18 | return switch (tag) { 19 | .identifier => ctx.semantic.tokenSlice(nodes.items(.main_token)[id]), 20 | .field_access => ctx.semantic.tokenSlice(nodes.items(.data)[id].rhs), 21 | .call, .call_comma, .call_one, .call_one_comma => getRightmostIdentifier(ctx, nodes.items(.data)[id].lhs), 22 | else => null, 23 | }; 24 | } 25 | 26 | pub fn isInTest(ctx: *const Context, node: Node.Index) bool { 27 | const tags: []const Node.Tag = ctx.ast().nodes.items(.tag); 28 | var parents = ctx.links().iterParentIds(node); 29 | 30 | while (parents.next()) |parent| { 31 | // NOTE: container and fn decls may be nested within a test. 32 | switch (tags[parent]) { 33 | .test_decl => return true, 34 | else => continue, 35 | } 36 | } 37 | return false; 38 | } 39 | 40 | pub fn isBlock(tags: []const Node.Tag, node: Node.Index) bool { 41 | return switch (tags[node]) { 42 | .block, .block_semicolon, .block_two, .block_two_semicolon => true, 43 | else => false, 44 | }; 45 | } 46 | 47 | /// Check if some type node is or has an error union. 48 | /// 49 | /// Examples where this returns true: 50 | /// ```zig 51 | /// !void 52 | /// Allocator.Error!u32 53 | /// if (cond) !void else void 54 | /// ``` 55 | pub fn hasErrorUnion(ast: *const Ast, node: Node.Index) bool { 56 | return getErrorUnion(ast, node) != NULL_NODE; 57 | } 58 | 59 | pub fn getErrorUnion(ast: *const Ast, node: Node.Index) Node.Index { 60 | const tags: []const Node.Tag = ast.nodes.items(.tag); 61 | const tok_tags: []const Token.Tag = ast.tokens.items(.tag); 62 | return switch (tags[node]) { 63 | .root => NULL_NODE, 64 | .error_union, .merge_error_sets, .error_set_decl => node, 65 | .if_simple => getErrorUnion(ast, ast.nodes.items(.data)[node].rhs), 66 | .@"if" => blk: { 67 | const ifnode = ast.ifFull(node); 68 | // there's a bug in fn return types for functions with return types 69 | // like `!if (cond) ...`. `ast.return_type` is `@"if"` instead of 70 | // the error union. 71 | const main = ast.nodes.items(.main_token)[node]; 72 | if (main > 1 and tok_tags[main - 1] == .bang) break :blk node; 73 | break :blk unwrapNode(getErrorUnion(ast, ifnode.ast.then_expr)) orelse getErrorUnion(ast, ifnode.ast.else_expr); 74 | }, 75 | else => blk: { 76 | const prev_tok = ast.firstToken(node) -| 1; 77 | break :blk if (tok_tags[prev_tok] == .bang) node else NULL_NODE; 78 | }, 79 | }; 80 | } 81 | 82 | /// Returns `null` if `node` is the null node. Identity function otherwise. 83 | pub inline fn unwrapNode(node: Node.Index) ?Node.Index { 84 | return if (node == NULL_NODE) null else node; 85 | } 86 | -------------------------------------------------------------------------------- /src/linter/config/rule_config.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const json = std.json; 3 | const Severity = @import("../../Error.zig").Severity; 4 | const Allocator = std.mem.Allocator; 5 | const Rule = @import("../rule.zig").Rule; 6 | const Schema = @import("../../json.zig").Schema; 7 | 8 | const ParseError = json.ParseError(json.Scanner); 9 | 10 | pub fn RuleConfig(RuleImpl: type) type { 11 | return struct { 12 | severity: Severity = .off, 13 | // FIXME: unsafe const cast 14 | rule_impl: *anyopaque = @ptrCast(@constCast(&default)), 15 | 16 | pub const name = RuleImpl.meta.name; 17 | pub const meta: Rule.Meta = RuleImpl.meta; 18 | pub const default: RuleImpl = .{}; 19 | const Self = @This(); 20 | 21 | pub fn jsonParse(allocator: Allocator, source: *json.Scanner, options: json.ParseOptions) ParseError!Self { 22 | switch (try source.peekNextTokenType()) { 23 | .string, .number => { 24 | @branchHint(.likely); 25 | const severity = try Severity.jsonParse(allocator, source, options); 26 | const rule_impl = try allocator.create(RuleImpl); 27 | rule_impl.* = .{}; 28 | return Self{ .severity = severity, .rule_impl = rule_impl }; 29 | }, 30 | .array_begin => { 31 | _ = try source.next(); 32 | const severity = try Severity.jsonParse(allocator, source, options); 33 | const rule_impl = try allocator.create(RuleImpl); 34 | if (try source.peekNextTokenType() == .array_end) { 35 | _ = try source.next(); 36 | rule_impl.* = .{}; 37 | } else { 38 | rule_impl.* = try json.innerParse(RuleImpl, allocator, source, options); 39 | const tok = try source.next(); 40 | if (tok != .array_end) { 41 | @branchHint(.cold); 42 | return ParseError.UnexpectedToken; 43 | } 44 | } 45 | return Self{ .severity = severity, .rule_impl = rule_impl }; 46 | }, 47 | else => return ParseError.UnexpectedToken, 48 | } 49 | } 50 | 51 | pub fn jsonSchema(ctx: *Schema.Context) !Schema { 52 | const severity = try ctx.ref(Severity); 53 | var rule_config = try ctx.ref(RuleImpl); 54 | rule_config.@"$ref".resolve(ctx).?.common().title = try std.fmt.allocPrint(ctx.allocator, "{s} config", .{RuleImpl.meta.name}); 55 | 56 | const schema = if (rule_config == .object and rule_config.object.properties.count() == 0) 57 | severity 58 | else compound: { 59 | var config_schema = try ctx.tuple([_]Schema{ severity, rule_config }); 60 | var common = config_schema.common(); 61 | try common.extra_values.put(ctx.allocator, "items", json.Value{ .bool = false }); 62 | 63 | break :compound try ctx.oneOf(&[_]Schema{ severity, config_schema }); 64 | }; 65 | 66 | return schema; 67 | } 68 | 69 | pub fn rule(self: *Self) Rule { 70 | const rule_impl: *RuleImpl = @ptrCast(@alignCast(@constCast(self.rule_impl))); 71 | return rule_impl.rule(); 72 | } 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/linter/config/rules_config.methods.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const json = std.json; 3 | const meta = std.meta; 4 | const mem = std.mem; 5 | const Schema = @import("../../json.zig").Schema; 6 | 7 | const Allocator = std.mem.Allocator; 8 | 9 | const assert = std.debug.assert; 10 | 11 | const ParseError = json.ParseError(json.Scanner); 12 | 13 | /// RulesConfig methods are separated out so that they can be easily integrated 14 | /// with codegen'd struct definitions. 15 | pub fn RulesConfigMethods(RulesConfig: type) type { 16 | return struct { 17 | /// See: `std.json.parseFromTokenSource()` 18 | pub fn jsonParse( 19 | allocator: Allocator, 20 | source: *json.Scanner, 21 | options: json.ParseOptions, 22 | ) !RulesConfig { 23 | var config = RulesConfig{}; 24 | 25 | // eat '{' 26 | if (try source.next() != .object_begin) return ParseError.UnexpectedToken; 27 | 28 | while (try source.peekNextTokenType() != .object_end) { 29 | const key_tok = try source.next(); 30 | const key = switch (key_tok) { 31 | .string => key_tok.string, 32 | else => return ParseError.UnexpectedToken, 33 | }; 34 | 35 | var found = false; 36 | inline for (meta.fields(RulesConfig)) |field| { 37 | const RuleConfigImpl = @TypeOf(@field(config, field.name)); 38 | if (mem.eql(u8, key, RuleConfigImpl.name)) { 39 | @field(config, field.name) = try RuleConfigImpl.jsonParse(allocator, source, options); 40 | found = true; 41 | break; 42 | } 43 | } 44 | if (!found) return ParseError.UnknownField; 45 | } 46 | 47 | // eat '}' 48 | const end = try source.next(); 49 | assert(end == .object_end); 50 | 51 | return config; 52 | } 53 | 54 | pub fn jsonSchema(ctx: *Schema.Context) !Schema { 55 | const info = @typeInfo(RulesConfig).@"struct"; 56 | var obj = try ctx.object(info.fields.len); 57 | inline for (info.fields) |field| { 58 | const Rule = field.type; 59 | var prop_schema: Schema = try Rule.jsonSchema(ctx); 60 | prop_schema.common().default = .{ .string = Rule.meta.default.asSlice() }; 61 | obj.properties.putAssumeCapacityNoClobber(Rule.name, prop_schema); 62 | } 63 | obj.common.description = "Configure which rules are enabled and how."; 64 | 65 | return .{ .object = obj }; 66 | } 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/linter/config/rules_config.zig: -------------------------------------------------------------------------------- 1 | // Auto-generated by `tasks/confgen.zig`. Do not edit manually. 2 | const RuleConfig = @import("rule_config.zig").RuleConfig; 3 | const rules = @import("../rules.zig"); 4 | 5 | pub const RulesConfig = struct { 6 | pub usingnamespace @import("./rules_config.methods.zig").RulesConfigMethods(@This()); 7 | homeless_try: RuleConfig(rules.HomelessTry) = .{}, 8 | line_length: RuleConfig(rules.LineLength) = .{}, 9 | must_return_ref: RuleConfig(rules.MustReturnRef) = .{}, 10 | no_catch_return: RuleConfig(rules.NoCatchReturn) = .{}, 11 | no_return_try: RuleConfig(rules.NoReturnTry) = .{}, 12 | no_unresolved: RuleConfig(rules.NoUnresolved) = .{}, 13 | suppressed_errors: RuleConfig(rules.SuppressedErrors) = .{}, 14 | unsafe_undefined: RuleConfig(rules.UnsafeUndefined) = .{}, 15 | unused_decls: RuleConfig(rules.UnusedDecls) = .{}, 16 | useless_error_return: RuleConfig(rules.UselessErrorReturn) = .{}, 17 | empty_file: RuleConfig(rules.EmptyFile) = .{}, 18 | avoid_as: RuleConfig(rules.AvoidAs) = .{}, 19 | case_convention: RuleConfig(rules.CaseConvention) = .{}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/linter/disable_directives.zig: -------------------------------------------------------------------------------- 1 | pub const Comment = @import("./disable_directives/Comment.zig"); 2 | pub const Parser = @import("./disable_directives/Parser.zig"); 3 | -------------------------------------------------------------------------------- /src/linter/disable_directives/Comment.zig: -------------------------------------------------------------------------------- 1 | //! A disable directive parsed from a comment. 2 | //! 3 | //! ## Examples 4 | //! ```zig 5 | //! // zlint-disable 6 | //! // zlint-disable no undefined 7 | //! // zlint-disable-next-line 8 | //! // zlint-disable-next-line foo bar baz 9 | //! ``` 10 | 11 | /// An empty set means all rules are disabled. 12 | disabled_rules: []Span = ALL_RULES_DISABLED, 13 | /// I'm not really sure what this should be the span _of_. The entire comment? 14 | /// Just the directive? Which is more useful? 15 | span: Span, 16 | kind: Kind, 17 | 18 | pub const DisableDirectiveComment = @This(); 19 | const ALL_RULES_DISABLED = &[_]Span{}; 20 | 21 | pub const Kind = enum { 22 | /// Disables a set of rules for an entire file. 23 | /// 24 | /// `zlint-disable` 25 | global, 26 | /// Just disable the next line. 27 | /// 28 | /// `zlint-disable-next-line` 29 | line, 30 | }; 31 | 32 | /// Returns `true` if this disable directive applies to an entire file. 33 | pub inline fn isGlobal(self: *const DisableDirectiveComment) bool { 34 | return self.kind == .global; 35 | } 36 | 37 | /// Does this directive disable diagnostics for all rules? 38 | /// 39 | /// _(say that 3 times fast lol)_ 40 | pub inline fn disablesAllRules(self: *const DisableDirectiveComment) bool { 41 | return self.disabled_rules.len == 0; 42 | } 43 | 44 | pub fn eql(self: DisableDirectiveComment, other: DisableDirectiveComment) bool { 45 | if (self.kind != other.kind or !self.span.eql(other.span)) return false; 46 | if (self.disabled_rules == other.disabled_rules) return true; 47 | if (self.disabled_rules.len != other.disabled_rules.len) return false; 48 | 49 | for (self.disabled_rules, 0..) |rule, i| { 50 | if (!rule.eql(other.disabled_rules[i])) return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | pub fn deinit(self: *DisableDirectiveComment, allocator: Allocator) void { 57 | if (self.disabled_rules.len > 0) allocator.free(self.disabled_rules); 58 | self.disabled_rules.ptr = undefined; 59 | self.* = undefined; 60 | } 61 | 62 | const std = @import("std"); 63 | 64 | const Allocator = std.mem.Allocator; 65 | const Span = @import("../../span.zig").Span; 66 | -------------------------------------------------------------------------------- /src/linter/rules.zig: -------------------------------------------------------------------------------- 1 | pub const HomelessTry = @import("./rules/homeless_try.zig"); 2 | pub const LineLength = @import("./rules/line_length.zig"); 3 | pub const MustReturnRef = @import("./rules/must_return_ref.zig"); 4 | pub const NoCatchReturn = @import("./rules/no_catch_return.zig"); 5 | pub const NoReturnTry = @import("./rules/no_return_try.zig"); 6 | pub const NoUnresolved = @import("./rules/no_unresolved.zig"); 7 | pub const SuppressedErrors = @import("./rules/suppressed_errors.zig"); 8 | pub const UnsafeUndefined = @import("./rules/unsafe_undefined.zig"); 9 | pub const UnusedDecls = @import("./rules/unused_decls.zig"); 10 | pub const UselessErrorReturn = @import("./rules/useless_error_return.zig"); 11 | pub const EmptyFile = @import("./rules/empty_file.zig"); 12 | pub const AvoidAs = @import("./rules/avoid_as.zig"); 13 | pub const CaseConvention = @import("./rules/case_convention.zig"); 14 | -------------------------------------------------------------------------------- /src/linter/rules/empty_file.zig: -------------------------------------------------------------------------------- 1 | //! ## What This Rule Does 2 | //! This rule checks for empty .zig files in the project. 3 | //! A file should be deemed empty if it has no content (zero bytes) or only comments and whitespace characters, 4 | //! as defined by the standard library in [`std.ascii.whitespace`](https://ziglang.org/documentation/master/std/#std.ascii.whitespace). 5 | //! 6 | //! ## Examples 7 | //! 8 | //! Examples of **incorrect** code for this rule: 9 | //! 10 | //! 11 | //! ```zig 12 | //! // an "empty" file is actually a file without meaningful code: just comments (doc or normal) or whitespace 13 | //! ``` 14 | //! 15 | //! Examples of **correct** code for this rule: 16 | //! ```zig 17 | //! fn exampleFunction() void { 18 | //! } 19 | //! ``` 20 | 21 | const std = @import("std"); 22 | const _rule = @import("../rule.zig"); 23 | const util = @import("util"); 24 | 25 | const LinterContext = @import("../lint_context.zig"); 26 | const Rule = _rule.Rule; 27 | 28 | const Error = @import("../../Error.zig"); 29 | 30 | // Rule metadata 31 | const EmptyFile = @This(); 32 | pub const meta: Rule.Meta = .{ 33 | .name = "empty-file", 34 | .category = .style, 35 | .default = .warning, 36 | }; 37 | 38 | pub fn fileDiagnosticWithMessage(ctx: *LinterContext, msg: []const u8) Error { 39 | const filename = ctx.source.pathname orelse "anonymous source file"; 40 | return ctx.diagnosticf("{s} {s}", .{ filename, msg }, .{}); 41 | } 42 | 43 | // Runs once per source file. Useful for unique checks 44 | pub fn runOnce(_: *const EmptyFile, ctx: *LinterContext) void { 45 | const source = ctx.source.text(); 46 | var message: ?[]const u8 = null; 47 | if (source.len == 0) { 48 | message = "has zero bytes"; 49 | } else if (std.mem.indexOfNone(u8, source, &std.ascii.whitespace) == null) { 50 | message = "contains only whitespace"; 51 | } else if (ctx.ast().nodes.len == 1) { 52 | message = "only contains comments"; 53 | } 54 | if (message) |msg| { 55 | ctx.report(fileDiagnosticWithMessage(ctx, msg)); 56 | } 57 | } 58 | 59 | // Used by the Linter to register the rule so it can be run. 60 | pub fn rule(self: *EmptyFile) Rule { 61 | return Rule.init(self); 62 | } 63 | 64 | const RuleTester = @import("../tester.zig"); 65 | test EmptyFile { 66 | const t = std.testing; 67 | 68 | var empty_file = EmptyFile{}; 69 | var runner = RuleTester.init(t.allocator, empty_file.rule()); 70 | defer runner.deinit(); 71 | 72 | // Code your rule should pass on 73 | const pass = &[_][:0]const u8{ 74 | \\// non-empty file 75 | \\fn exampleFunction() void { 76 | \\} 77 | , 78 | \\// anything that is not a comment or whitespace will turn 79 | \\// this into a non-empty file 80 | \\var x = 0; 81 | }; 82 | 83 | // Code your rule should fail on 84 | const fail = &[_][:0]const u8{ 85 | // no content 86 | "", 87 | // newlines 88 | \\ 89 | \\ 90 | \\ 91 | , 92 | // space 93 | \\ 94 | , 95 | // tabs 96 | \\ 97 | , 98 | \\// only a comment 99 | , 100 | \\//! only a doc comment 101 | , 102 | \\//! a doc comment 103 | \\// but with a normal comment just below! 104 | , 105 | \\// only a comment with some whitespace 106 | \\ 107 | \\ 108 | }; 109 | 110 | try runner 111 | .withPass(pass) 112 | .withFail(fail) 113 | .run(); 114 | } 115 | -------------------------------------------------------------------------------- /src/linter/rules/line_length.zig: -------------------------------------------------------------------------------- 1 | //! ## What This Rule Does 2 | //! 3 | //! Checks if any line goes beyond a given number of columns. 4 | //! 5 | //! ## Examples 6 | //! 7 | //! Examples of **incorrect** code for this rule (with a threshold of 120 columns): 8 | //! ```zig 9 | //! const std = @import("std"); 10 | //! const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 }; 11 | //! fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 { 12 | //! return 123; 13 | //! } 14 | //! ``` 15 | //! 16 | //! Examples of **correct** code for this rule (with a threshold of 120 columns): 17 | //! ```zig 18 | //! const std = @import("std"); 19 | //! const longStructInMultipleLines = struct { 20 | //! max_length: u32 = 120, 21 | //! a: usize = 123, 22 | //! b: usize = 12354, 23 | //! c: usize = 1234352, 24 | //! }; 25 | //! fn Get123Constant() u32 { 26 | //! return 123; 27 | //! } 28 | //! ``` 29 | 30 | const std = @import("std"); 31 | const _rule = @import("../rule.zig"); 32 | const span = @import("../../span.zig"); 33 | 34 | const LinterContext = @import("../lint_context.zig"); 35 | const Rule = _rule.Rule; 36 | const Error = @import("../../Error.zig"); 37 | const LabeledSpan = span.LabeledSpan; 38 | 39 | max_length: u32 = 120, 40 | 41 | const LineLength = @This(); 42 | pub const meta: Rule.Meta = .{ 43 | .name = "line-length", 44 | .category = .style, 45 | .default = .off, 46 | }; 47 | 48 | pub fn lineLengthDiagnostic(ctx: *LinterContext, line_start: u32, line_length: u32) Error { 49 | return ctx.diagnosticf( 50 | "line length of {} characters is too big.", 51 | .{line_length}, 52 | .{LabeledSpan.unlabeled(line_start, line_start + line_length)}, 53 | ); 54 | } 55 | 56 | pub fn getNewlineOffset(line: []const u8) u32 { 57 | if (line.len > 1 and line[line.len - 2] == '\r') { 58 | return 2; 59 | } 60 | return 1; 61 | } 62 | 63 | pub fn runOnce(self: *const LineLength, ctx: *LinterContext) void { 64 | var line_start_idx: u32 = 0; 65 | var lines = std.mem.splitSequence(u8, ctx.source.text(), "\n"); 66 | const newline_offset = getNewlineOffset(lines.first()); 67 | lines.reset(); 68 | while (lines.next()) |line| { 69 | const line_length = @as(u32, @intCast(line.len)); 70 | if (line.len > self.max_length) { 71 | ctx.report(lineLengthDiagnostic(ctx, line_start_idx, line_length)); 72 | } 73 | line_start_idx += line_length + newline_offset; 74 | } 75 | } 76 | 77 | pub fn rule(self: *LineLength) Rule { 78 | return Rule.init(self); 79 | } 80 | 81 | const RuleTester = @import("../tester.zig"); 82 | test LineLength { 83 | const t = std.testing; 84 | 85 | var line_length = LineLength{}; 86 | var runner = RuleTester.init(t.allocator, line_length.rule()); 87 | defer runner.deinit(); 88 | 89 | const pass = &[_][:0]const u8{ 90 | \\const std = @import("std"); 91 | \\fn foo() std.mem.Allocator.Error!void { 92 | \\ _ = try std.heap.page_allocator.alloc(u8, 8); 93 | \\} 94 | , 95 | \\const std = @import("std"); 96 | \\const longStructInMultipleLines = struct { 97 | \\ max_length: u32 = 120, 98 | \\ a: usize = 123, 99 | \\ b: usize = 12354, 100 | \\ c: usize = 1234352, 101 | \\}; 102 | \\fn Get123Constant() u32 { 103 | \\ return 123; 104 | \\} 105 | }; 106 | 107 | const fail = &[_][:0]const u8{ 108 | \\const std = @import("std"); 109 | \\fn foo() std.mem.Allocator.Error!void { 110 | \\ // ok so this is a super unnecessary line that is artificially being made long through this self-referential comment thats keeps on going until hitting a number of columns that violates the rule 111 | \\ _ = try std.heap.page_allocator.alloc(u8, 8); 112 | \\} 113 | , 114 | \\const std = @import("std"); 115 | \\const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 }; 116 | \\fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 { 117 | \\ return 123; 118 | \\} 119 | }; 120 | 121 | try runner 122 | .withPass(pass) 123 | .withFail(fail) 124 | .run(); 125 | } 126 | -------------------------------------------------------------------------------- /src/linter/rules/no_return_try.zig: -------------------------------------------------------------------------------- 1 | //! ## What This Rule Does 2 | //! 3 | //! Disallows `return`ing a `try` expression. 4 | //! 5 | //! Returning an error union directly has the same exact semantics as `try`ing 6 | //! it and then returning the result. 7 | //! 8 | //! ## Examples 9 | //! 10 | //! Examples of **incorrect** code for this rule: 11 | //! ```zig 12 | //! const std = @import("std"); 13 | //! 14 | //! fn foo() !void { 15 | //! return error.OutOfMemory; 16 | //! } 17 | //! 18 | //! fn bar() !void { 19 | //! return try foo(); 20 | //! } 21 | //! ``` 22 | //! 23 | //! Examples of **correct** code for this rule: 24 | //! ```zig 25 | //! const std = @import("std"); 26 | //! 27 | //! fn foo() !void { 28 | //! return error.OutOfMemory; 29 | //! } 30 | //! 31 | //! fn bar() !void { 32 | //! errdefer { 33 | //! std.debug.print("this still gets printed.\n", .{}); 34 | //! } 35 | //! 36 | //! return foo(); 37 | //! } 38 | //! ``` 39 | 40 | const std = @import("std"); 41 | const util = @import("util"); 42 | const semantic = @import("../../semantic.zig"); 43 | const _rule = @import("../rule.zig"); 44 | const _span = @import("../../span.zig"); 45 | 46 | const Ast = std.zig.Ast; 47 | const Node = Ast.Node; 48 | const LinterContext = @import("../lint_context.zig"); 49 | const LabeledSpan = _span.LabeledSpan; 50 | const Rule = _rule.Rule; 51 | const NodeWrapper = _rule.NodeWrapper; 52 | const Error = @import("../../Error.zig"); 53 | const Cow = util.Cow(false); 54 | 55 | // Rule metadata 56 | const NoReturnTry = @This(); 57 | pub const meta: Rule.Meta = .{ 58 | .name = "no-return-try", 59 | .category = .pedantic, 60 | .default = .off, 61 | }; 62 | 63 | fn returnTryDiagnostic(ctx: *LinterContext, return_start: u32, try_start: u32) Error { 64 | const span = LabeledSpan.unlabeled(return_start, try_start + 3); 65 | var e = ctx.diagnostic("This error union can be directly returned.", .{span}); 66 | e.help = Cow.static("Replace `return try` with `return`"); 67 | return e; 68 | } 69 | 70 | // Runs on each node in the AST. Useful for syntax-based rules. 71 | pub fn runOnNode(_: *const NoReturnTry, wrapper: NodeWrapper, ctx: *LinterContext) void { 72 | const ast = ctx.ast(); 73 | const node = wrapper.node; 74 | const returned_id = node.data.lhs; 75 | if (node.tag != .@"return" or returned_id == semantic.Semantic.NULL_NODE) return; 76 | 77 | const returned: Node.Tag = ast.nodes.items(.tag)[returned_id]; 78 | if (returned != .@"try") return; 79 | 80 | const starts = ast.tokens.items(.start); 81 | const return_start = starts[node.main_token]; 82 | const try_start = starts[ast.nodes.items(.main_token)[returned_id]]; 83 | ctx.report(returnTryDiagnostic(ctx, return_start, try_start)); 84 | } 85 | 86 | pub fn rule(self: *NoReturnTry) Rule { 87 | return Rule.init(self); 88 | } 89 | 90 | const RuleTester = @import("../tester.zig"); 91 | test NoReturnTry { 92 | const t = std.testing; 93 | 94 | var no_return_try = NoReturnTry{}; 95 | var runner = RuleTester.init(t.allocator, no_return_try.rule()); 96 | defer runner.deinit(); 97 | 98 | const pass = &[_][:0]const u8{ 99 | \\fn foo() void { return; } 100 | , 101 | \\fn foo() !void { return; } 102 | \\fn bar() !void { return foo(); } 103 | , 104 | \\fn foo() !void { return; } 105 | \\fn bar() !void { try foo(); } 106 | , 107 | // should probably try to check for this case 108 | \\fn foo() !void { return; } 109 | \\fn bar() !void { return blk: { break :blk try foo(); }; } 110 | }; 111 | 112 | const fail = &[_][:0]const u8{ 113 | \\fn foo() !void { return; } 114 | \\fn bar() !void { return try foo(); } 115 | , 116 | }; 117 | 118 | try runner 119 | .withPass(pass) 120 | .withFail(fail) 121 | .run(); 122 | } 123 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/avoid-as.snap: -------------------------------------------------------------------------------- 1 | 𝙭 avoid-as: Prefer using type annotations over @as(). 2 | ╭─[avoid-as.zig:1:11] 3 | 1 │ const x = @as(u32, 1); 4 | · ─── 5 | ╰──── 6 | help: Use a type annotation instead. 7 | 8 | 𝙭 avoid-as: Unnecessary use of @as(). 9 | ╭─[avoid-as.zig:1:16] 10 | 1 │ const x: u32 = @as(u32, 1); 11 | · ─── 12 | ╰──── 13 | help: Remove the @as() call. 14 | 15 | 𝙭 avoid-as: Unnecessary use of @as(). 16 | ╭─[avoid-as.zig:1:31] 17 | 1 │ const Foo = struct { x: u32 = @as(u32, 1) }; 18 | · ─── 19 | ╰──── 20 | help: Remove the @as() call. 21 | 22 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/case-convention.snap: -------------------------------------------------------------------------------- 1 | 𝙭 case-convention: Function ThisFunctionIsInPascalCase name is in PascalCase. It should be camelCase 2 | ╭─[case-convention.zig:1:4] 3 | 1 │ fn ThisFunctionIsInPascalCase() void {} 4 | · ────────────────────────── 5 | ╰──── 6 | 7 | 𝙭 case-convention: Function @"this-one-is-in-kebab-case" name is in kebab-case. It should be camelCase 8 | ╭─[case-convention.zig:1:4] 9 | 1 │ fn @"this-one-is-in-kebab-case"() void {} 10 | · ──────────────────────────── 11 | ╰──── 12 | 13 | 𝙭 case-convention: Function this_one_is_in_snake_case name is in snake_case. It should be camelCase 14 | ╭─[case-convention.zig:1:4] 15 | 1 │ fn this_one_is_in_snake_case() void {} 16 | · ───────────────────────── 17 | ╰──── 18 | 19 | 𝙭 case-convention: Function @"This-is-both-Pascal-and-Kebab-kinda" name is not in camelCase 20 | ╭─[case-convention.zig:1:4] 21 | 1 │ fn @"This-is-both-Pascal-and-Kebab-kinda"() void {} 22 | · ────────────────────────────────────── 23 | ╰──── 24 | 25 | 𝙭 case-convention: Function This_is_both_snake_case_and_pascal_kinda name is not in camelCase 26 | ╭─[case-convention.zig:1:4] 27 | 1 │ fn This_is_both_snake_case_and_pascal_kinda() void {} 28 | · ──────────────────────────────────────── 29 | ╰──── 30 | 31 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/empty-file.snap: -------------------------------------------------------------------------------- 1 | 𝙭 empty-file: empty-file.zig has zero bytes 2 | 3 | 4 | 𝙭 empty-file: empty-file.zig contains only whitespace 5 | 6 | 7 | 𝙭 empty-file: empty-file.zig contains only whitespace 8 | 9 | 10 | 𝙭 empty-file: empty-file.zig contains only whitespace 11 | 12 | 13 | 𝙭 empty-file: empty-file.zig only contains comments 14 | 15 | 16 | 𝙭 empty-file: empty-file.zig only contains comments 17 | 18 | 19 | 𝙭 empty-file: empty-file.zig only contains comments 20 | 21 | 22 | 𝙭 empty-file: empty-file.zig only contains comments 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/homeless-try.snap: -------------------------------------------------------------------------------- 1 | 𝙭 homeless-try: `try` cannot be used in functions that do not return errors. 2 | ╭─[homeless-try.zig:2:4] 3 | 1 │ const std = @import("std"); 4 | 2 │ fn foo() void { 5 | · ─┬─ 6 | · ╰── function `foo` is declared here. 7 | 3 │ const x = try std.heap.page_allocator.alloc(u8, 8); 8 | · ─┬─ 9 | · ╰── it cannot propagate error unions. 10 | ╰──── 11 | help: Change the return type to `!void`. 12 | 13 | 𝙭 homeless-try: `try` cannot be used outside of a function or test block. 14 | ╭─[homeless-try.zig:2:11] 15 | 1 │ const std = @import("std"); 16 | 2 │ const x = try std.heap.page_allocator.alloc(u8, 8); 17 | · ─┬─ 18 | · ╰── there is nowhere to propagate errors to. 19 | ╰──── 20 | 21 | 𝙭 homeless-try: `try` cannot be used outside of a function or test block. 22 | ╭─[homeless-try.zig:4:17] 23 | 3 │ const Bar = struct { 24 | 4 │ baz: []u8 = try std.heap.page_allocator.alloc(u8, 8), 25 | · ─┬─ 26 | · ╰── there is nowhere to propagate errors to. 27 | 5 │ }; 28 | ╰──── 29 | 30 | 𝙭 homeless-try: `try` cannot be used in functions that do not return errors. 31 | ╭─[homeless-try.zig:2:8] 32 | 1 │ const std = @import("std"); 33 | 2 │ pub fn push(list: std.ArrayList(u32), x: u32, comptime assume_capacity: bool) if(assume_capacity) void else void { 34 | · ──┬── 35 | · ╰── function `push` is declared here. 36 | 3 │ if (comptime assume_capacity) { 37 | 5 │ } else { 38 | 6 │ try list.append(x); 39 | · ─┬─ 40 | · ╰── it cannot propagate error unions. 41 | 7 │ } 42 | ╰──── 43 | help: Change the return type to `!if(assume_capacity) void else void`. 44 | 45 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/line-length.snap: -------------------------------------------------------------------------------- 1 | 𝙭 line-length: line length of 196 characters is too big. 2 | ╭─[line-length.zig:3:1] 3 | 2 │ fn foo() std.mem.Allocator.Error!void { 4 | 3 │ // ok so this is a super unnecessary line that is artificially being made long through this self-referential comment thats keeps on going until hitting a number of columns that violates the rule 5 | · ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 6 | 4 │ _ = try std.heap.page_allocator.alloc(u8, 8); 7 | ╰──── 8 | 9 | 𝙭 line-length: line length of 126 characters is too big. 10 | ╭─[line-length.zig:2:1] 11 | 1 │ const std = @import("std"); 12 | 2 │ const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 }; 13 | · ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 14 | 3 │ fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 { 15 | ╰──── 16 | 17 | 𝙭 line-length: line length of 151 characters is too big. 18 | ╭─[line-length.zig:3:1] 19 | 2 │ const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 }; 20 | 3 │ fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 { 21 | · ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 22 | 4 │ return 123; 23 | ╰──── 24 | 25 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/must-return-ref.snap: -------------------------------------------------------------------------------- 1 | 𝙭 must-return-ref: Members of type `ArenaAllocator` must be passed by reference 2 | ╭─[must-return-ref.zig:7:12] 3 | 6 │ pub fn getArena(self: *Foo) ArenaAllocator { 4 | 7 │ return self.arena; 5 | · ─────┬───── 6 | · ╰── This is a copy, not a move. 7 | 8 │ } 8 | ╰──── 9 | help: This type records its allocation size, so mutating a copy can result in a memory leak. 10 | 11 | 𝙭 must-return-ref: Members of type `ArrayList` must be passed by reference 12 | ╭─[must-return-ref.zig:5:12] 13 | 4 │ } else { 14 | 5 │ return self.list; 15 | · ────┬──── 16 | · ╰── This is a copy, not a move. 17 | 6 │ } 18 | ╰──── 19 | help: This type records its allocation size, so mutating a copy can result in a memory leak. 20 | 21 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/no-catch-return.snap: -------------------------------------------------------------------------------- 1 | 𝙭 no-catch-return: Caught error is immediately returned 2 | ╭─[no-catch-return.zig:3:19] 3 | 2 │ fn foo() !void { 4 | 3 │ bar() catch |e| return e; 5 | · ──────── 6 | 4 │ } 7 | ╰──── 8 | help: Use a `try` statement to return unhandled errors. 9 | 10 | 𝙭 no-catch-return: Caught error is immediately returned 11 | ╭─[no-catch-return.zig:4:5] 12 | 3 │ bar() catch |e| { 13 | 4 │ return e; 14 | · ──────── 15 | 5 │ }; 16 | ╰──── 17 | help: Use a `try` statement to return unhandled errors. 18 | 19 | 𝙭 no-catch-return: Caught error is immediately returned 20 | ╭─[no-catch-return.zig:5:5] 21 | 4 │ // comments won't save you 22 | 5 │ return e; 23 | · ──────── 24 | 6 │ }; 25 | ╰──── 26 | help: Use a `try` statement to return unhandled errors. 27 | 28 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/no-return-try.snap: -------------------------------------------------------------------------------- 1 | 𝙭 no-return-try: This error union can be directly returned. 2 | ╭─[no-return-try.zig:2:18] 3 | 1 │ fn foo() !void { return; } 4 | 2 │ fn bar() !void { return try foo(); } 5 | · ────────── 6 | ╰──── 7 | help: Replace `return try` with `return` 8 | 9 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/no-unresolved.snap: -------------------------------------------------------------------------------- 1 | 𝙭 no-unresolved: Unresolved import to 'does-not-exist.zig' 2 | ╭─[src/no-unresolved.zig:1:19] 3 | 1 │ const x = @import("does-not-exist.zig"); 4 | · ──────────┬────────── 5 | · ╰── file 'does-not-exist.zig' does not exist 6 | ╰──── 7 | 8 | 𝙭 no-unresolved: Unresolved import to directory './walk' 9 | ╭─[src/no-unresolved.zig:1:19] 10 | 1 │ const x = @import("./walk"); 11 | · ────┬──── 12 | · ╰── './walk' is a folder 13 | ╰──── 14 | 15 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/suppressed-errors.snap: -------------------------------------------------------------------------------- 1 | 𝙭 suppressed-errors: `catch` statement suppresses errors 2 | ╭─[suppressed-errors.zig:2:9] 3 | 1 │ fn foo() void { 4 | 2 │ bar() catch {}; 5 | · ──────── 6 | 3 │ } 7 | ╰──── 8 | help: Handle this error or propagate it to the caller with `try`. 9 | 10 | 𝙭 suppressed-errors: `catch` statement suppresses errors 11 | ╭─[suppressed-errors.zig:2:9] 12 | 1 │ fn foo() void { 13 | 2 │ bar() catch |_| {}; 14 | · ──────────── 15 | 3 │ } 16 | ╰──── 17 | help: Handle this error or propagate it to the caller with `try`. 18 | 19 | 𝙭 suppressed-errors: `catch` statement suppresses errors 20 | ╭─[suppressed-errors.zig:2:9] 21 | 1 │ fn foo() void { 22 | 2 │ bar() catch { 23 | · ─────── 24 | 3 │ // ignore 25 | ╰──── 26 | help: Handle this error or propagate it to the caller with `try`. 27 | 28 | 𝙭 suppressed-errors: Caught error is mishandled with `unreachable` 29 | ╭─[suppressed-errors.zig:2:15] 30 | 1 │ fn foo() void { 31 | 2 │ bar() catch unreachable; 32 | · ─────────── 33 | 3 │ } 34 | ╰──── 35 | help: Use `try` to propagate this error. If this branch shouldn't happen, use `@panic` or `std.debug.panic` instead. 36 | 37 | 𝙭 suppressed-errors: Caught error is mishandled with `unreachable` 38 | ╭─[suppressed-errors.zig:2:17] 39 | 1 │ fn foo() void { 40 | 2 │ bar() catch { unreachable; }; 41 | · ─────────── 42 | 3 │ } 43 | ╰──── 44 | help: Use `try` to propagate this error. If this branch shouldn't happen, use `@panic` or `std.debug.panic` instead. 45 | 46 | 𝙭 suppressed-errors: Caught error is mishandled with `unreachable` 47 | ╭─[suppressed-errors.zig:4:11] 48 | 3 │ break :blk w.print("{}", .{5}); 49 | 4 │ } catch unreachable; 50 | · ─────────── 51 | 5 │ } 52 | ╰──── 53 | help: Use `try` to propagate this error. If this branch shouldn't happen, use `@panic` or `std.debug.panic` instead. 54 | 55 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/unused-decls.snap: -------------------------------------------------------------------------------- 1 | 𝙭 unused-decls: variable 'x' is declared but never used. 2 | ╭─[unused-decls.zig:1:7] 3 | 1 │ const x = 1; 4 | · ─ 5 | ╰──── 6 | 7 | 𝙭 unused-decls: variable 'Allocator' is declared but never used. 8 | ╭─[unused-decls.zig:1:35] 9 | 1 │ const std = @import("std"); const Allocator = std.mem.Allocator; 10 | · ───────── 11 | ╰──── 12 | 13 | 𝙭 unused-decls: variable 'x' is declared but never used. 14 | ╭─[unused-decls.zig:1:14] 15 | 1 │ extern const x: usize; 16 | · ─ 17 | ╰──── 18 | 19 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/useless-error-return.snap: -------------------------------------------------------------------------------- 1 | 𝙭 useless-error-return: Function 'foo' has an error union return type but never returns an error. 2 | ╭─[useless-error-return.zig:1:4] 3 | 1 │ fn foo() !void { return; } 4 | · ─┬─ 5 | · ╰── 'foo' is declared here 6 | ╰──── 7 | help: Remove the error union return type. 8 | 9 | 𝙭 useless-error-return: Function 'init' has an error union return type but suppresses all its errors. 10 | ╭─[useless-error-return.zig:3:10] 11 | 2 │ pub const Foo = struct { 12 | 3 │ pub fn init(allocator: std.mem.Allocator) !Foo { 13 | · ──┬── 14 | · ╰── 'init' is declared here 15 | 4 │ const new = allocator.create(Foo) catch @panic("OOM"); 16 | · ──┬── 17 | · ╰── It catches errors here 18 | ╰──── 19 | help: Use `try` to propagate errors to the caller. 20 | 21 | 𝙭 useless-error-return: Function 'foo' has an error union return type but never returns an error. 22 | ╭─[useless-error-return.zig:1:4] 23 | 1 │ fn foo() !void { 24 | · ─┬─ 25 | · ╰── 'foo' is declared here 26 | 2 │ const e = bar(); 27 | ╰──── 28 | help: Remove the error union return type. 29 | 30 | -------------------------------------------------------------------------------- /src/linter/test/lint_context_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const LinterContext = @import("../lint_context.zig"); 3 | const Fix = @import("../fix.zig").Fix; 4 | const Semantic = @import("../../semantic.zig").Semantic; 5 | const _span = @import("../../span.zig"); 6 | const Source = @import("../../source.zig").Source; 7 | 8 | const t = std.testing; 9 | const print = std.debug.print; 10 | 11 | fn createCtx(src: [:0]const u8, sema_out: *Semantic, source_out: *Source) !LinterContext { 12 | const src2 = try t.allocator.dupeZ(u8, src); 13 | var source = try Source.fromString(t.allocator, src2, null); 14 | errdefer source.deinit(); 15 | source_out.* = source; 16 | 17 | var builder = Semantic.Builder.init(t.allocator); 18 | builder.withSource(&source); 19 | defer builder.deinit(); 20 | 21 | var res = try builder.build(src); 22 | defer res.deinitErrors(); 23 | if (res.hasErrors()) { 24 | defer res.value.deinit(); 25 | print("Semantic analysis failed with {d} errors. Source:\n\n{s}\n", .{ res.errors.items.len, src }); 26 | return error.TestFailed; 27 | } 28 | sema_out.* = res.value; 29 | return LinterContext.init(t.allocator, sema_out, source_out); 30 | } 31 | 32 | test "Dangerous fixes do not get saved when only safe fixes are allowed" { 33 | var sema: Semantic = undefined; 34 | var source: Source = undefined; 35 | var ctx = try createCtx("fn foo() void { return 1; }", &sema, &source); 36 | defer { 37 | var diagnostics = ctx.takeDiagnostics(); 38 | for (diagnostics.items) |*diagnostic| { 39 | diagnostic.deinit(t.allocator); 40 | } 41 | diagnostics.deinit(); 42 | ctx.deinit(); 43 | } 44 | defer sema.deinit(); 45 | defer source.deinit(); 46 | 47 | const DangerousFixer = struct { 48 | span: _span.Span, 49 | 50 | fn remove(self: @This(), b: Fix.Builder) anyerror!Fix { 51 | var fix = b.delete(self.span); 52 | fix.meta.dangerous = true; 53 | return fix; 54 | } 55 | }; 56 | 57 | const fix_ctx = DangerousFixer{ .span = _span.Span.EMPTY }; 58 | ctx.reportWithFix( 59 | fix_ctx, 60 | ctx.diagnostic("ahhh", .{_span.LabeledSpan.from(fix_ctx.span)}), 61 | &DangerousFixer.remove, 62 | ); 63 | 64 | try t.expectEqual(1, ctx.diagnostics.items.len); 65 | try t.expectEqual(null, ctx.diagnostics.items[0].fix); 66 | try t.expectEqualStrings("ahhh", ctx.diagnostics.items[0].err.message.borrow()); 67 | } 68 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const util = @import("util"); 3 | const Source = @import("source.zig").Source; 4 | const config = @import("config"); 5 | const Error = @import("./Error.zig"); 6 | 7 | const fs = std.fs; 8 | const print = std.debug.print; 9 | 10 | const Options = @import("./cli/Options.zig"); 11 | const print_cmd = @import("cli/print_command.zig"); 12 | const lint_cmd = @import("cli/lint_command.zig"); 13 | 14 | // in debug builds, include more information for debugging memory leaks, 15 | // double-frees, etc. 16 | const DebugAllocator = std.heap.GeneralPurposeAllocator(.{ 17 | .never_unmap = util.IS_DEBUG, 18 | .retain_metadata = util.IS_DEBUG, 19 | }); 20 | var debug_allocator = DebugAllocator.init; 21 | pub fn main() !u8 { 22 | const alloc = if (comptime util.IS_DEBUG) 23 | debug_allocator.allocator() 24 | else 25 | std.heap.smp_allocator; 26 | 27 | defer if (comptime util.IS_DEBUG) { 28 | _ = debug_allocator.deinit(); 29 | }; 30 | var stack = std.heap.stackFallback(16, alloc); 31 | const stack_alloc = stack.get(); 32 | 33 | var err: Error = undefined; 34 | var opts = Options.parseArgv(stack_alloc, &err) catch { 35 | std.debug.print("{s}\n{s}\n", .{ err.message, Options.usage }); 36 | err.deinit(stack_alloc); 37 | return 1; 38 | }; 39 | defer opts.deinit(stack_alloc); 40 | 41 | if (opts.version) { 42 | const stdout = std.io.getStdOut().writer(); 43 | stdout.print("{s}\n", .{config.version}) catch |e| { 44 | std.debug.panic("Failed to write version: {s}\n", .{@errorName(e)}); 45 | }; 46 | return 0; 47 | } else if (opts.print_ast) { 48 | if (opts.args.items.len == 0) { 49 | print("No files to print\nUsage: zlint --print-ast [filename]", .{}); 50 | std.process.exit(1); 51 | } 52 | 53 | const relative_path = opts.args.items[0]; 54 | print("Printing AST for {s}\n", .{relative_path}); 55 | const file = try fs.cwd().openFile(relative_path, .{}); 56 | errdefer file.close(); 57 | var source = try Source.init(alloc, file, null); 58 | defer source.deinit(); 59 | try print_cmd.parseAndPrint(alloc, opts, source, null); 60 | return 0; 61 | } 62 | 63 | return lint_cmd.lint(alloc, opts); 64 | } 65 | 66 | test { 67 | std.testing.refAllDecls(@This()); 68 | std.testing.refAllDecls(@import("visit/walk.zig")); 69 | std.testing.refAllDecls(@import("json.zig")); 70 | } 71 | -------------------------------------------------------------------------------- /src/reporter.zig: -------------------------------------------------------------------------------- 1 | const reporter = @import("./reporter/Reporter.zig"); 2 | pub const Reporter = reporter.Reporter; 3 | pub const Options = reporter.Options; 4 | 5 | pub const formatter = @import("./reporter/formatter.zig"); 6 | -------------------------------------------------------------------------------- /src/reporter/StringWriter.zig: -------------------------------------------------------------------------------- 1 | /// A `std.io.Writer` that writes data to an internally managed, allocated 2 | /// buffer. 3 | const StringWriter = @This(); 4 | const Writer = io.GenericWriter(*StringWriter, Allocator.Error, write); 5 | 6 | buf: std.ArrayList(u8), 7 | 8 | /// Create a new, empty `StringWriter`. Does not allocate memory. 9 | pub fn init(allocator: Allocator) StringWriter { 10 | return StringWriter{ .buf = .init(allocator) }; 11 | } 12 | /// Free this `StringWriter`'s internal buffer. 13 | pub fn deinit(self: *StringWriter) void { 14 | self.buf.deinit(); 15 | } 16 | 17 | /// Create a new `StringWriter` that pre-allocates enough memory for at least 18 | /// `capacity` bytes. 19 | pub fn initCapacity(capacity: usize, allocator: Allocator) Allocator.Error!StringWriter { 20 | const buf = try std.ArrayList(u8).initCapacity(allocator, capacity); 21 | return StringWriter{ .buf = buf }; 22 | } 23 | 24 | /// Get the bytes written to this `StringWriter`. 25 | pub inline fn slice(self: *const StringWriter) []const u8 { 26 | return self.buf.items; 27 | } 28 | 29 | pub fn writer(self: *StringWriter) Writer { 30 | return Writer{ .context = self }; 31 | } 32 | 33 | /// Write `bytes` to this writer. Returns the number of bytes written. 34 | pub fn write(self: *StringWriter, bytes: []const u8) Allocator.Error!usize { 35 | try self.buf.appendSlice(bytes); 36 | return bytes.len; 37 | } 38 | 39 | /// Write a formatted string to this `StringWriter`. 40 | pub fn print(self: *StringWriter, comptime format: []const u8, args: anytype) Allocator.Error!void { 41 | const size = math.cast(usize, fmt.count(format, args)) orelse return error.OutOfMemory; 42 | try self.buf.ensureUnusedCapacity(size); 43 | const len = self.buf.items.len; 44 | self.buf.items.len += size; 45 | _ = fmt.bufPrint(self.buf.items[len..], format, args) catch |err| switch (err) { 46 | error.NoSpaceLeft => unreachable, // we just counted the size above 47 | }; 48 | } 49 | 50 | const std = @import("std"); 51 | const io = std.io; 52 | const fmt = std.fmt; 53 | const math = std.math; 54 | const Allocator = std.mem.Allocator; 55 | 56 | test print { 57 | const t = std.testing; 58 | var w = StringWriter.init(t.allocator); 59 | defer w.deinit(); 60 | 61 | try w.print("Hello, {s}", .{"world"}); 62 | try t.expectEqualStrings("Hello, world", w.slice()); 63 | } 64 | -------------------------------------------------------------------------------- /src/reporter/formatter.zig: -------------------------------------------------------------------------------- 1 | //! Formatters process diagnostics for a `Reporter`. 2 | 3 | pub const Github = @import("formatters/GithubFormatter.zig"); 4 | pub const Graphical = @import("formatters/GraphicalFormatter.zig"); 5 | pub const JSON = @import("formatters/JSONFormatter.zig"); 6 | 7 | pub const Meta = struct { 8 | report_statistics: bool, 9 | }; 10 | 11 | pub const Kind = enum { 12 | graphical, 13 | github, 14 | json, 15 | 16 | const FormatMap = std.StaticStringMapWithEql( 17 | Kind, 18 | std.static_string_map.eqlAsciiIgnoreCase, 19 | ); 20 | const formats = FormatMap.initComptime(&[_]struct { []const u8, Kind }{ 21 | .{ "github", .github }, 22 | .{ "gh", .github }, 23 | .{ "json", .json }, 24 | .{ "graphical", .graphical }, 25 | .{ "default", .graphical }, 26 | }); 27 | 28 | /// Get a formatter kind by name. Names are case-insensitive. 29 | pub fn fromString(str: []const u8) ?Kind { 30 | return formats.get(str); 31 | } 32 | }; 33 | 34 | pub const FormatError = Writer.Error || Allocator.Error; 35 | 36 | const std = @import("std"); 37 | const Writer = std.io.AnyWriter; 38 | const Allocator = std.mem.Allocator; 39 | -------------------------------------------------------------------------------- /src/reporter/formatters/GithubFormatter.zig: -------------------------------------------------------------------------------- 1 | //! Formats diagnostics in a such a way that they appear as annotations in 2 | //! Github Actions. 3 | //! 4 | //! e.g. 5 | //! ``` 6 | //! ::error file={name},line={line},endLine={endLine},title={title}::{message} 7 | //! ``` 8 | 9 | const GithubFormatter = @This(); 10 | 11 | pub const meta: Meta = .{ 12 | .report_statistics = false, 13 | }; 14 | 15 | pub fn format(_: *GithubFormatter, w: *Writer, e: Error) FormatError!void { 16 | const level: []const u8 = switch (e.severity) { 17 | .err => "error", 18 | .warning => "warning", 19 | .notice => "notice", 20 | .off => @panic("disabled error passed to formatter"), 21 | }; 22 | 23 | const primary: ?LabeledSpan = blk: { 24 | if (e.labels.items.len == 0) break :blk null; 25 | for (e.labels.items) |label| { 26 | if (label.primary) break :blk label; 27 | } 28 | break :blk e.labels.items[0]; 29 | }; 30 | 31 | const line, const col = blk: { 32 | if (primary) |p| { 33 | if (e.source) |source| { 34 | const loc = Location.fromSpan(source.deref().*, p.span); 35 | break :blk .{ loc.line, loc.column }; 36 | } 37 | } 38 | break :blk .{ 1, 1 }; 39 | }; 40 | 41 | // TODO: endLine, endCol 42 | try w.print("::{s} file={s},line={d},col={d},title={s}::{s}\n", .{ 43 | level, 44 | e.source_name orelse "", 45 | line, 46 | col, 47 | e.code, 48 | e.message, 49 | }); 50 | } 51 | 52 | test GithubFormatter { 53 | const Cow = @import("util").Cow(false); 54 | const allocator = std.testing.allocator; 55 | const expectEqualStrings = std.testing.expectEqualStrings; 56 | 57 | var buf = std.ArrayList(u8).init(allocator); 58 | defer buf.deinit(); 59 | var f = GithubFormatter{}; 60 | var w = buf.writer().any(); 61 | 62 | var err = Error.newStatic("Something happened"); 63 | err.help = Cow.static("help pls"); 64 | err.code = "code"; 65 | err.source_name = "some/file.zig"; 66 | 67 | try f.format(&w, err); 68 | try expectEqualStrings("::error file=some/file.zig,line=1,col=1,title=code::Something happened\n", buf.items); 69 | 70 | buf.clearRetainingCapacity(); 71 | err.severity = .warning; 72 | try err.labels.append(allocator, .{ 73 | .label = Cow.static("here it is"), 74 | .span = .{ .start = 5, .end = 10 }, 75 | }); 76 | defer err.labels.deinit(allocator); 77 | 78 | try f.format(&w, err); 79 | try expectEqualStrings("::warning file=some/file.zig,line=1,col=1,title=code::Something happened\n", buf.items); 80 | 81 | buf.clearRetainingCapacity(); 82 | var src = try Error.ArcStr.init( 83 | allocator, 84 | try allocator.dupeZ(u8, 85 | \\ 86 | \\foo bar baz bang 87 | ), 88 | ); 89 | defer src.deinit(); 90 | err.source = src; 91 | 92 | try f.format(&w, err); 93 | try expectEqualStrings("::warning file=some/file.zig,line=2,col=5,title=code::Something happened\n", buf.items); 94 | } 95 | 96 | const std = @import("std"); 97 | const formatter = @import("../formatter.zig"); 98 | const Meta = formatter.Meta; 99 | const FormatError = formatter.FormatError; 100 | const Writer = std.io.AnyWriter; 101 | const Error = @import("../../Error.zig"); 102 | const _span = @import("../../span.zig"); 103 | const LabeledSpan = _span.LabeledSpan; 104 | const Location = _span.Location; 105 | -------------------------------------------------------------------------------- /src/reporter/formatters/JSONFormatter.zig: -------------------------------------------------------------------------------- 1 | //! Formats diagnostics in a such a way that they appear as annotations in 2 | //! Github Actions. 3 | //! 4 | //! e.g. 5 | //! ``` 6 | //! ::error file={name},line={line},endLine={endLine},title={title}::{message} 7 | //! ``` 8 | 9 | const JSONFormatter = @This(); 10 | 11 | pub const meta: Meta = .{ 12 | .report_statistics = false, 13 | }; 14 | 15 | pub fn format(_: *JSONFormatter, w: *Writer, e: Error) FormatError!void { 16 | return std.json.stringify(e, .{}, w.*); 17 | } 18 | 19 | test JSONFormatter { 20 | const Source = @import("../../source.zig").Source; 21 | const json = std.json; 22 | const Value = json.Value; 23 | const expect = std.testing.expect; 24 | const expectEqual = std.testing.expectEqual; 25 | const expectEqualStrings = std.testing.expectEqualStrings; 26 | 27 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 28 | defer arena.deinit(); 29 | const allocator = arena.allocator(); 30 | 31 | const source: [:0]const u8 = "const x: u32 = 1;"; 32 | const src = try Source.fromString(allocator, @constCast(source), "test.zig"); 33 | 34 | var buf = std.ArrayList(u8).init(allocator); 35 | defer buf.deinit(); 36 | 37 | var err = Error.newStatic("oof"); 38 | err.source = src.contents; 39 | err.source_name = src.pathname; 40 | err.help = Cow.static("help pls"); 41 | err.code = "code"; 42 | try err.labels.append(allocator, LabeledSpan{ 43 | .label = Cow.static("some label"), 44 | .span = _span.Span.new(0, 4), 45 | .primary = true, 46 | }); 47 | 48 | var f = JSONFormatter{}; 49 | var w = buf.writer().any(); 50 | try f.format(&w, err); 51 | 52 | var value = try json.parseFromSlice(json.Value, allocator, buf.items, .{}); 53 | defer value.deinit(); 54 | const obj = value.value.object; 55 | 56 | try expectEqualStrings("oof", obj.get("message").?.string); 57 | try expectEqualStrings("code", obj.get("code").?.string); 58 | try expectEqualStrings("help pls", obj.get("help").?.string); 59 | const labels = obj.get("labels") orelse return error.ZigTestFailing; 60 | try expect(labels == .array); 61 | try expectEqual(1, labels.array.items.len); 62 | const label = labels.array.items[0].object; 63 | try expectEqual(Value{ .bool = true }, label.get("primary")); 64 | try expectEqualStrings("some label", label.get("label").?.string); 65 | try expect(label.get("start").? == .object); 66 | try expect(label.get("end").? == .object); 67 | } 68 | 69 | const std = @import("std"); 70 | const Cow = @import("util").Cow(false); 71 | const formatter = @import("../formatter.zig"); 72 | const Meta = formatter.Meta; 73 | const FormatError = formatter.FormatError; 74 | const Writer = std.io.AnyWriter; 75 | const Error = @import("../../Error.zig"); 76 | const _span = @import("../../span.zig"); 77 | const LabeledSpan = _span.LabeledSpan; 78 | const Location = _span.Location; 79 | -------------------------------------------------------------------------------- /src/root.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const semantic = @import("semantic.zig"); 4 | pub const Source = @import("source.zig").Source; 5 | 6 | pub const report = @import("reporter.zig"); 7 | 8 | pub const lint = @import("lint.zig"); 9 | 10 | /// Internal. Exported for codegen. 11 | pub const json = @import("json.zig"); 12 | 13 | pub const printer = struct { 14 | pub const Printer = @import("printer/Printer.zig"); 15 | pub const SemanticPrinter = @import("printer/SemanticPrinter.zig"); 16 | pub const AstPrinter = @import("printer/AstPrinter.zig"); 17 | }; 18 | 19 | pub const walk = @import("visit/walk.zig"); 20 | 21 | test { 22 | std.testing.refAllDecls(@import("util")); 23 | } 24 | -------------------------------------------------------------------------------- /src/semantic.zig: -------------------------------------------------------------------------------- 1 | //! semantic analysis of a zig AST. 2 | //! 3 | //! We are intentionally not using Zig's AIR. That format strips away dead 4 | //! code, which may be in the process of being authored. Instead, we perform 5 | //! our own minimalist semantic analysis of an entire zig program. 6 | //! 7 | //! Throughout this file you'll see mentions of a "program". This does not mean 8 | //! an entire linked binary or library; rather it refers to a single parsed 9 | //! file. 10 | 11 | pub const Semantic = @import("./semantic/Semantic.zig"); 12 | // parts of semantic 13 | pub const Ast = std.zig.Ast; 14 | pub const Symbol = @import("./semantic/Symbol.zig"); 15 | pub const Scope = @import("./semantic/Scope.zig"); 16 | pub const Reference = @import("./semantic/Reference.zig"); 17 | 18 | const std = @import("std"); 19 | -------------------------------------------------------------------------------- /src/semantic/ModuleRecord.zig: -------------------------------------------------------------------------------- 1 | const ModuleRecord = @This(); 2 | 3 | imports: std.ArrayListUnmanaged(ImportEntry) = .{}, 4 | 5 | pub fn deinit(self: *ModuleRecord, allocator: Allocator) void { 6 | self.imports.deinit(allocator); 7 | self.* = undefined; 8 | } 9 | 10 | /// An import to a module or file. 11 | /// 12 | /// Does not include `@cImport`s (for now, this may change). 13 | /// 14 | /// ### Example 15 | ///```zig 16 | /// @import("some_module") // kind: module 17 | /// // ^^^^^^^^^^^ specifier 18 | /// ``` 19 | pub const ImportEntry = struct { 20 | specifier: []const u8, 21 | /// The `@import` node 22 | node: NodeIndex, 23 | kind: Kind, 24 | 25 | pub const Kind = enum { 26 | /// An import to a named module, such as `std`, `builtin`, or some dependency. 27 | /// 28 | /// Non-intrinsic modules are set in `build.zid`. 29 | module, 30 | /// An import to a `.zig` or `.zon` file. 31 | /// 32 | /// Specifier is a relative path to the file. 33 | file, 34 | }; 35 | }; 36 | 37 | const std = @import("std"); 38 | const Allocator = std.mem.Allocator; 39 | const NodeIndex = @import("ast.zig").NodeIndex; 40 | -------------------------------------------------------------------------------- /src/semantic/NodeLinks.zig: -------------------------------------------------------------------------------- 1 | //! Links AST nodes to other semantic data 2 | 3 | const NodeLinks = @This(); 4 | 5 | /// Map of AST nodes to their parents. Index is the child node id. 6 | /// 7 | /// Confusingly, the root node id is also used as the "null" node id, so the 8 | /// root node technically uses itself as its parent (`parents[0] == 0`). Prefer 9 | /// `getParent` if you need to disambiguate between the root node and the null 10 | /// node. 11 | /// 12 | /// Do not insert into this list directly; use `setParent` instead. This method 13 | /// upholds link invariants. 14 | /// 15 | /// ### Invariants: 16 | /// - No node is its own parent 17 | /// - No node is the parent of the root node (0 in this case means `null`). 18 | parents: std.ArrayListUnmanaged(NodeIndex) = .{}, 19 | /// Map AST nodes to the scope they are in. Index is the node id. 20 | /// 21 | /// This is _not_ a mapping for scopes that nodes create. 22 | scopes: std.ArrayListUnmanaged(Scope.Id) = .{}, 23 | /// Maps identifier tokens to the symbols bound to them. 24 | /// 25 | /// These are the same as `symbol.identifier`, but allow for lookups the other 26 | /// way. 27 | symbols: std.AutoHashMapUnmanaged(Ast.TokenIndex, Symbol.Id) = .{}, 28 | /// Maps tokens (usually `.identifier`s) to the references they create. Since 29 | /// references are sparse in an AST, a hashmap is used to avoid wasting memory. 30 | references: std.AutoHashMapUnmanaged(Ast.TokenIndex, Reference.Id) = .{}, 31 | 32 | pub fn init(alloc: Allocator, ast: *const Ast) Allocator.Error!NodeLinks { 33 | var links: NodeLinks = .{}; 34 | 35 | try links.parents.ensureTotalCapacityPrecise(alloc, ast.nodes.len); 36 | links.parents.appendNTimesAssumeCapacity(NULL_NODE, @intCast(ast.nodes.len)); 37 | try links.scopes.ensureTotalCapacityPrecise(alloc, ast.nodes.len); 38 | links.scopes.appendNTimesAssumeCapacity(ROOT_SCOPE_ID, ast.nodes.len); 39 | 40 | try links.references.ensureTotalCapacity(alloc, 16); 41 | 42 | return links; 43 | } 44 | 45 | pub fn deinit(self: *NodeLinks, alloc: Allocator) void { 46 | inline for (.{ "parents", "scopes", "symbols", "references" }) |name| { 47 | @field(self, name).deinit(alloc); 48 | } 49 | } 50 | 51 | pub inline fn setScope(self: *NodeLinks, node_id: NodeIndex, scope_id: Scope.Id) void { 52 | assert( 53 | node_id < self.scopes.items.len, 54 | "Node id out of bounds (id {d} >= {d})", 55 | .{ node_id, self.scopes.items.len }, 56 | ); 57 | 58 | self.scopes.items[node_id] = scope_id; 59 | } 60 | 61 | pub inline fn setParent(self: *NodeLinks, child_id: NodeIndex, parent_id: NodeIndex) void { 62 | assert(child_id != parent_id, "AST nodes cannot be children of themselves", .{}); 63 | assert(child_id != NULL_NODE, "Re-assigning the root node's parent is illegal behavior", .{}); 64 | assert( 65 | parent_id < self.parents.items.len, 66 | "Parent node id out of bounds (id {d} >= {d})", 67 | .{ parent_id, self.parents.items.len }, 68 | ); 69 | 70 | self.parents.items[child_id] = parent_id; 71 | } 72 | 73 | pub inline fn getParent(self: *const NodeLinks, node_id: NodeIndex) ?NodeIndex { 74 | if (node_id == ROOT_NODE_ID) { 75 | return null; 76 | } 77 | return self.parents.items[node_id]; 78 | } 79 | 80 | /// Iterate over a node's parents. The first element is the node itself, and 81 | /// the last will be the root node. 82 | pub fn iterParentIds(self: *const NodeLinks, node_id: NodeIndex) ParentIdsIterator { 83 | return ParentIdsIterator{ .links = self, .curr_id = node_id }; 84 | } 85 | 86 | const ParentIdsIterator = struct { 87 | links: *const NodeLinks, 88 | curr_id: ?NodeIndex, 89 | 90 | pub fn next(self: *ParentIdsIterator) ?NodeIndex { 91 | const curr_id = self.curr_id orelse return null; 92 | // NOTE: using getParent instead of direct _parents access to ensure 93 | // root node is yielded. 94 | defer self.curr_id = self.links.getParent(curr_id); 95 | return self.curr_id; 96 | } 97 | }; 98 | 99 | const std = @import("std"); 100 | const _ast = @import("ast.zig"); 101 | const util = @import("util"); 102 | 103 | const Ast = _ast.Ast; 104 | const NodeIndex = _ast.NodeIndex; 105 | const Semantic = @import("./Semantic.zig"); 106 | const ROOT_NODE_ID = Semantic.ROOT_NODE_ID; 107 | const NULL_NODE = Semantic.NULL_NODE; 108 | const ROOT_SCOPE_ID = Semantic.ROOT_SCOPE_ID; 109 | const Scope = Semantic.Scope; 110 | const Symbol = Semantic.Symbol; 111 | const Reference = Semantic.Reference; 112 | 113 | const Allocator = std.mem.Allocator; 114 | const assert = util.assert; 115 | -------------------------------------------------------------------------------- /src/semantic/Parse.zig: -------------------------------------------------------------------------------- 1 | const Parse = @This(); 2 | 3 | ast: Ast, 4 | // NOTE: We re-tokenize and store our own tokens b/c AST throws away the end 5 | // position of each token. B/c of this, `ast.stokenSlice` re-tokenizes each 6 | // time. So we do it once, eat the memory overhead, and help the linter avoid 7 | // constant re-tokenization. 8 | // NOTE: allocated in _arena 9 | tokens: TokenList.Slice, 10 | comments: CommentList.Slice, 11 | 12 | pub fn build( 13 | allocator: Allocator, 14 | source: [:0]const u8, 15 | ) Allocator.Error!struct { Parse, tokenizer.TokenBundle.Stats } { 16 | var token_bundle = try tokenizer.tokenize( 17 | allocator, 18 | source, 19 | ); 20 | errdefer { 21 | // NOTE: free'd in reverse order they're allocated 22 | token_bundle.comments.deinit(allocator); 23 | token_bundle.tokens.deinit(allocator); 24 | } 25 | 26 | const ast = try Ast.parse(allocator, source, .zig); 27 | return .{ 28 | Parse{ 29 | .ast = ast, 30 | .tokens = token_bundle.tokens, 31 | .comments = token_bundle.comments, 32 | }, 33 | token_bundle.stats, 34 | }; 35 | } 36 | 37 | pub fn deinit(self: *Parse, allocator: Allocator) void { 38 | self.ast.deinit(allocator); 39 | self.tokens.deinit(allocator); 40 | self.comments.deinit(allocator); 41 | } 42 | 43 | const std = @import("std"); 44 | const tokenizer = @import("tokenizer.zig"); 45 | const Allocator = std.mem.Allocator; 46 | const Ast = std.zig.Ast; 47 | const TokenList = tokenizer.TokenList; 48 | const CommentList = tokenizer.CommentList; 49 | -------------------------------------------------------------------------------- /src/semantic/ast.zig: -------------------------------------------------------------------------------- 1 | //! Re-exports of data structures used in Zig's AST. 2 | //! 3 | //! Also includes additional types used in other semantic components. 4 | const std = @import("std"); 5 | const NominalId = @import("util").NominalId; 6 | 7 | pub const Ast = std.zig.Ast; 8 | pub const Node = Ast.Node; 9 | 10 | /// The struct used in AST tokens SOA is not pub so we hack it in here. 11 | pub const RawToken = struct { 12 | tag: std.zig.Token.Tag, 13 | start: Ast.ByteOffset, 14 | pub const Tag = std.zig.Token.Tag; 15 | }; 16 | 17 | pub const TokenIndex = Ast.TokenIndex; 18 | pub const NodeIndex = Node.Index; 19 | pub const MaybeTokenId = NominalId(Ast.TokenIndex).Optional; 20 | -------------------------------------------------------------------------------- /src/semantic/builtins.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const StaticStringSet = std.StaticStringMap(void); 3 | 4 | /// ## References 5 | /// - [Zig Docs - Primitive Types](https://ziglang.org/documentation/0.13.0/#Primitive-Types) 6 | const PRIMITIVE_TYPES = StaticStringSet.initComptime([_]struct { []const u8 }{ 7 | // integers 8 | .{"i8"}, 9 | .{"i16"}, 10 | .{"i32"}, 11 | .{"i64"}, 12 | .{"i128"}, 13 | .{"isize"}, 14 | .{"u8"}, // unsigned 15 | .{"u16"}, 16 | .{"u32"}, 17 | .{"u64"}, 18 | .{"u128"}, 19 | .{"usize"}, 20 | 21 | // floats 22 | .{"f16"}, 23 | .{"f32"}, 24 | .{"f64"}, 25 | .{"f80"}, 26 | .{"f128"}, 27 | 28 | // c types 29 | .{"c_char"}, 30 | .{"c_short"}, 31 | .{"c_int"}, 32 | .{"c_long"}, 33 | .{"c_longlong"}, 34 | .{"c_longdouble"}, 35 | .{"c_ushort"}, // unsigned 36 | .{"c_uint"}, 37 | .{"c_ulong"}, 38 | .{"c_ulonglong"}, 39 | 40 | // etc 41 | .{"bool"}, 42 | .{"void"}, 43 | .{"anyopaque"}, 44 | .{"noreturn"}, 45 | .{"type"}, 46 | .{"anyerror"}, 47 | .{"comptime_int"}, 48 | .{"comptime_float"}, 49 | }); 50 | 51 | /// ## References 52 | /// - [Zig Docs - Primitive Values](https://ziglang.org/documentation/0.13.0/#Primitive-Values) 53 | const PRIMITIVE_VALUES = StaticStringSet.initComptime([_]struct { []const u8 }{ 54 | .{"null"}, 55 | .{"undefined"}, 56 | .{"true"}, 57 | .{"false"}, 58 | }); 59 | 60 | /// Check if a type is built in to Zig itself. 61 | /// 62 | /// ## References 63 | /// - [Zig Docs - Primitive Types](https://ziglang.org/documentation/0.13.0/#Primitive-Types) 64 | pub fn isPrimitiveType(typename: []const u8) bool { 65 | if (PRIMITIVE_TYPES.has(typename)) return true; 66 | 67 | // Zig allows arbitrary-sized integers. 68 | if (typename.len > 2 and (typename[0] == 'u' or typename[0] == 'i')) { 69 | for (1..typename.len) |i| switch (typename[i]) { 70 | '0'...'9' => {}, 71 | else => return false, 72 | }; 73 | 74 | return true; 75 | } 76 | 77 | return false; 78 | } 79 | 80 | /// Check if an identifier refers to a primitive value. 81 | /// 82 | /// ## References 83 | /// - [Zig Docs - Primitive Values](https://ziglang.org/documentation/0.13.0/#Primitive-Values) 84 | pub fn isPrimitiveValue(value: []const u8) bool { 85 | return PRIMITIVE_VALUES.has(value); 86 | } 87 | -------------------------------------------------------------------------------- /src/semantic/test/members_and_exports_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const meta = std.meta; 3 | const test_util = @import("util.zig"); 4 | 5 | const Symbol = @import("../Symbol.zig"); 6 | const Reference = @import("../Reference.zig"); 7 | 8 | const t = std.testing; 9 | const panic = std.debug.panic; 10 | const print = std.debug.print; 11 | const build = test_util.build; 12 | 13 | test "container exports" { 14 | const TestCase = struct { 15 | src: [:0]const u8, 16 | container_id: Symbol.Id, 17 | exports: []const []const u8, 18 | fn init(src: [:0]const u8, container_id: Symbol.Id.Repr, exports: anytype) @This() { 19 | return .{ .src = src, .container_id = Symbol.Id.from(container_id), .exports = exports }; 20 | } 21 | }; 22 | 23 | const test_cases = [_]TestCase{ 24 | .init( 25 | "const foo = 1;", 26 | 0, 27 | &[_][]const u8{"foo"}, 28 | ), 29 | .init( 30 | \\const Foo = struct { 31 | \\ a: u32, 32 | \\ b: u32, 33 | \\ const C = 1; 34 | \\ pub const D = struct {}; 35 | \\ fn e() void {} 36 | \\}; 37 | , 38 | 1, 39 | &[_][]const u8{ "C", "D", "e" }, 40 | ), 41 | }; 42 | 43 | for (test_cases) |tc| { 44 | var semantic = try build(tc.src); 45 | defer semantic.deinit(); 46 | const container = semantic.symbols.get(tc.container_id); 47 | t.expectEqual(tc.exports.len, container.exports.items.len) catch |e| { 48 | print("\nexports: ", .{}); 49 | for (container.exports.items) |symbol_id| { 50 | const symbol = semantic.symbols.get(symbol_id); 51 | print("'{s}', ", .{symbol.name}); 52 | } 53 | print("\n", .{}); 54 | return e; 55 | }; 56 | for (tc.exports) |expected_export| { 57 | var found = false; 58 | for (container.exports.items) |symbol_id| { 59 | const symbol = semantic.symbols.get(symbol_id); 60 | if (std.mem.eql(u8, symbol.name, expected_export)) { 61 | found = true; 62 | break; 63 | } 64 | } 65 | if (!found) { 66 | print("expected export {s} not found\nexports: ", .{expected_export}); 67 | for (container.exports.items) |symbol_id| { 68 | const symbol = semantic.symbols.get(symbol_id); 69 | print("'{s}'', ", .{symbol.name}); 70 | } 71 | print("\n", .{}); 72 | return error.ZigTestFailed; 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/semantic/test/modules_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const test_util = @import("util.zig"); 3 | 4 | const Semantic = @import("../Semantic.zig"); 5 | 6 | const t = std.testing; 7 | 8 | test "@import(\"std\")" { 9 | const src = 10 | \\const std = @import("std"); 11 | ; 12 | var sema = try test_util.build(src); 13 | defer sema.deinit(); 14 | try t.expectEqual(1, sema.modules.imports.items.len); 15 | const import = sema.modules.imports.items[0]; 16 | try t.expectEqualStrings("std", import.specifier); 17 | try t.expectEqual(.module, import.kind); 18 | try t.expect(import.node != Semantic.NULL_NODE); 19 | } 20 | 21 | test "@import(\"foo.zig\")" { 22 | const src = 23 | \\const std = @import("foo.zig"); 24 | ; 25 | var sema = try test_util.build(src); 26 | defer sema.deinit(); 27 | try t.expectEqual(1, sema.modules.imports.items.len); 28 | const import = sema.modules.imports.items[0]; 29 | try t.expectEqualStrings("foo.zig", import.specifier); 30 | try t.expectEqual(.file, import.kind); 31 | try t.expect(import.node != Semantic.NULL_NODE); 32 | } 33 | -------------------------------------------------------------------------------- /src/semantic/test/util.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const _source = @import("../../source.zig"); 4 | const Semantic = @import("../Semantic.zig"); 5 | const report = @import("../../reporter.zig"); 6 | 7 | const printer = @import("../../root.zig").printer; 8 | 9 | const t = std.testing; 10 | const print = std.debug.print; 11 | 12 | pub fn build(src: [:0]const u8) !Semantic { 13 | var r = try report.Reporter.graphical( 14 | std.io.getStdErr().writer().any(), 15 | t.allocator, 16 | report.formatter.Graphical.Theme.unicodeNoColor(), 17 | ); 18 | defer r.deinit(); 19 | var builder = Semantic.Builder.init(t.allocator); 20 | var source = try _source.Source.fromString( 21 | t.allocator, 22 | try t.allocator.dupeZ(u8, src), 23 | try t.allocator.dupe(u8, "test.zig"), 24 | ); 25 | defer source.deinit(); 26 | builder.withSource(&source); 27 | defer builder.deinit(); 28 | 29 | var result = builder.build(src) catch |e| { 30 | print("Analysis failed on source:\n\n{s}\n\n", .{src}); 31 | return e; 32 | }; 33 | errdefer result.value.deinit(); 34 | if (result.hasErrors()) { 35 | print("Analysis failed.\n", .{}); 36 | r.reportErrors(result.errors.toManaged(t.allocator)); 37 | print("\nSource:\n\n{s}\n\n", .{src}); 38 | return error.AnalysisFailed; 39 | } 40 | 41 | return result.value; 42 | } 43 | 44 | pub fn debugSemantic(semantic: *const Semantic) !void { 45 | var p = printer.Printer.init(t.allocator, std.io.getStdErr().writer()); 46 | defer p.deinit(); 47 | var sp = printer.SemanticPrinter.new(&p, semantic); 48 | 49 | print("Symbol table:\n\n", .{}); 50 | try sp.printSymbolTable(); 51 | 52 | print("\n\nUnresolved references:\n\n", .{}); 53 | try sp.printUnresolvedReferences(); 54 | 55 | print("\n\nScopes:\n\n", .{}); 56 | try sp.printScopeTree(); 57 | print("\n\n", .{}); 58 | 59 | print("\n\nModules:\n\n", .{}); 60 | try sp.printModuleRecord(); 61 | print("\n\n", .{}); 62 | } 63 | -------------------------------------------------------------------------------- /src/source.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ptrs = @import("smart-pointers"); 3 | const fs = std.fs; 4 | 5 | const Allocator = std.mem.Allocator; 6 | const Arc = ptrs.Arc; 7 | const assert = std.debug.assert; 8 | 9 | pub const ArcStr = Arc([:0]u8); 10 | 11 | pub const Source = struct { 12 | // contents: Arc([]const u8), 13 | contents: ArcStr, 14 | pathname: ?[]const u8 = null, 15 | gpa: Allocator, 16 | 17 | /// Create a source from an opened file. This file must be opened with at least read permissions. 18 | /// 19 | /// Both `file` and `pathname` are moved into the source. 20 | pub fn init(gpa: Allocator, file: fs.File, pathname: ?[]const u8) !Source { 21 | defer file.close(); 22 | const meta = try file.metadata(); 23 | const contents = try gpa.allocSentinel(u8, meta.size(), 0); 24 | errdefer gpa.free(contents); 25 | const bytes_read = try file.readAll(contents); 26 | assert(bytes_read == meta.size()); 27 | // const contents = try std.zig.readSourceFileToEndAlloc(gpa, file, meta.size()); 28 | return Source{ 29 | .contents = try ArcStr.init(gpa, contents), 30 | .pathname = pathname, 31 | .gpa = gpa, 32 | }; 33 | } 34 | /// Create a source file directly from a string. Takes ownership of both 35 | /// `contents` and `pathname`. 36 | /// 37 | /// Primarily used for testing. 38 | pub fn fromString(gpa: Allocator, contents: [:0]u8, pathname: ?[]const u8) Allocator.Error!Source { 39 | const contents_arc = try ArcStr.init(gpa, contents); 40 | return Source{ 41 | .contents = contents_arc, 42 | .pathname = pathname, 43 | .gpa = gpa, 44 | }; 45 | } 46 | 47 | pub inline fn text(self: *const Source) [:0]const u8 { 48 | return self.contents.deref().*; 49 | } 50 | 51 | pub fn deinit(self: *Source) void { 52 | self.contents.deinit(); 53 | if (self.pathname) |p| self.gpa.free(p); 54 | self.* = undefined; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/util.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | pub const RUNTIME_SAFETY = builtin.mode != .ReleaseFast; 5 | pub const IS_DEBUG = builtin.mode == .Debug; 6 | pub const IS_TEST = builtin.is_test; 7 | pub const IS_WINDOWS = builtin.target.os.tag == .windows; 8 | pub const NEWLINE = if (IS_WINDOWS) "\r\n" else "\n"; 9 | 10 | pub const @"inline": std.builtin.CallingConvention = if (IS_DEBUG) .Inline else .Unspecified; 11 | 12 | pub const env = @import("util/env.zig"); 13 | pub const NominalId = @import("util/id.zig").NominalId; 14 | pub const Cow = @import("util/cow.zig").Cow; 15 | pub const DebugOnly = @import("util/debug_only.zig").DebugOnly; 16 | pub const debugOnly = @import("util/debug_only.zig").debugOnly; 17 | pub const Bitflags = @import("util/bitflags.zig").Bitflags; 18 | pub const FeatureFlags = @import("util/feature_flags.zig"); 19 | 20 | /// remove leading and trailing whitespace characters from a string 21 | pub fn trimWhitespace(s: []const u8) []const u8 { 22 | return std.mem.trim(u8, s, &std.ascii.whitespace); 23 | } 24 | pub fn trimWhitespaceRight(s: []const u8) []const u8 { 25 | return std.mem.trimRight(u8, s, &std.ascii.whitespace); 26 | } 27 | pub fn isWhitespace(c: u8) bool { 28 | return std.mem.indexOfScalar(u8, &std.ascii.whitespace, c) != null; 29 | } 30 | 31 | /// Assert that `condition` is true, panicking if it is not. 32 | /// 33 | /// Behaves identically to `std.debug.assert`, except that assertions will fail 34 | /// with a formatted message in debug builds. `fmt` and `args` follow the same 35 | /// formatting conventions as `std.debug.print` and `std.debug.panic`. 36 | /// 37 | /// Similarly to `std.debug.assert`, undefined behavior is invoked if 38 | /// `condition` is false. In `ReleaseFast` mode, `unreachable` is stripped and 39 | /// assumed to be true by the compiler, which will lead to strange program 40 | /// behavior. 41 | pub inline fn assert(condition: bool, comptime fmt: []const u8, args: anytype) void { 42 | if (comptime IS_DEBUG) { 43 | if (!condition) std.debug.panic(fmt, args); 44 | } else { 45 | if (!condition) unreachable; 46 | } 47 | } 48 | 49 | pub inline fn debugAssert(condition: bool, comptime fmt: []const u8, args: anytype) void { 50 | if (comptime IS_DEBUG) { 51 | if (!condition) std.debug.panic(fmt, args); 52 | } 53 | } 54 | 55 | pub inline fn assertUnsafe(condition: bool) void { 56 | if (comptime IS_DEBUG) { 57 | if (!condition) @panic("assertion failed"); 58 | } else { 59 | @setRuntimeSafety(IS_DEBUG); 60 | if (!condition) unreachable; 61 | } 62 | } 63 | 64 | test { 65 | std.testing.refAllDeclsRecursive(@This()); 66 | } 67 | -------------------------------------------------------------------------------- /src/util/bitflags_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Bitflags = @import("./bitflags.zig").Bitflags; 3 | 4 | const expectEqual = std.testing.expectEqual; 5 | const expectFmt = std.testing.expectFmt; 6 | const expect = std.testing.expect; 7 | 8 | const TestFlags = packed struct { 9 | a: bool = false, 10 | b: bool = false, 11 | c: bool = false, 12 | d: bool = false, 13 | 14 | pub usingnamespace Bitflags(@This()); 15 | }; 16 | 17 | const PaddedFlags = packed struct(u8) { 18 | a: bool = false, 19 | b: bool = false, 20 | c: bool = false, 21 | d: bool = false, 22 | _: u4 = 0, 23 | 24 | pub usingnamespace Bitflags(@This()); 25 | }; 26 | 27 | test "Bitflags.isEmpty" { 28 | try expectEqual(TestFlags{}, TestFlags.empty); 29 | try expect(TestFlags.empty.isEmpty()); 30 | try expect(!(TestFlags{ .a = true }).isEmpty()); 31 | } 32 | 33 | test "Bitflags.all" { 34 | const expected: TestFlags = .{ .a = true, .b = true, .c = true, .d = true }; 35 | try expectEqual(expected, TestFlags.all); 36 | try expect(TestFlags.all.eql(expected)); 37 | } 38 | 39 | test "Bitflags.intersects" { 40 | const ab: TestFlags = .{ .a = true, .b = true }; 41 | const bc: TestFlags = .{ .b = true, .c = true }; 42 | 43 | try expect(ab.intersects(ab)); 44 | try expect(ab.intersects(bc)); 45 | try expect(ab.intersects(.{ .a = true })); 46 | // TODO: should this be true? 47 | // try expect(ab.intersects(TestFlags.empty)); 48 | try expect(!ab.intersects(.{ .c = true })); 49 | } 50 | 51 | test "Bitflags.contains" {} 52 | 53 | test "Bitflags.merge" { 54 | const a = TestFlags{ .a = true }; 55 | const b = TestFlags{ .b = true }; 56 | const empty = TestFlags{}; 57 | 58 | try expectEqual(TestFlags{ .a = true, .b = true }, a.merge(b)); 59 | try expectEqual(a, a.merge(empty)); 60 | try expectEqual(a, a.merge(a)); 61 | try expectEqual(a, a.merge(.{ .a = false })); 62 | 63 | // does not mutate 64 | try expectEqual(TestFlags{ .a = true }, a); 65 | try expectEqual(TestFlags{ .b = true }, b); 66 | try expectEqual(TestFlags{}, empty); 67 | } 68 | 69 | test "Bitflags.set" { 70 | var f = TestFlags{ .a = true, .c = true }; 71 | const initial = f; 72 | 73 | f.set(.{ .b = true }, false); 74 | try expectEqual(initial, f); 75 | 76 | f.set(.{ .a = true }, true); 77 | try expectEqual(initial, f); 78 | 79 | f.set(.{ .a = true, .b = true, .c = false }, false); 80 | try expectEqual(TestFlags{ .c = true }, f); 81 | } 82 | 83 | test "Bitflags.not" { 84 | inline for (.{ TestFlags, PaddedFlags }) |Flags| { 85 | const ab: Flags = .{ .a = true, .b = true }; 86 | const cd = Flags{ .c = true, .d = true }; 87 | try expectEqual(cd, ab.not()); 88 | try expectEqual(ab, cd.not()); 89 | try expectEqual(ab, ab.not().not()); 90 | try expectEqual(TestFlags.all, TestFlags.empty.not()); 91 | try expectEqual(TestFlags.empty, TestFlags.all.not()); 92 | } 93 | } 94 | 95 | test "Bitflags.format" { 96 | const empty = TestFlags{}; 97 | const some = TestFlags{ .a = true, .c = true }; 98 | const all = TestFlags{ .a = true, .b = true, .c = true, .d = true }; 99 | try expectFmt("0", "{d}", .{empty}); 100 | const name = "util.bitflags_test.TestFlags"; 101 | try expectFmt(name ++ "()", "{}", .{empty}); 102 | try expectFmt(name ++ "(a | c)", "{}", .{some}); 103 | try expectFmt(name ++ "(a | b | c | d)", "{}", .{all}); 104 | } 105 | -------------------------------------------------------------------------------- /src/util/debug_only.zig: -------------------------------------------------------------------------------- 1 | const IS_DEBUG = @import("../util.zig").IS_DEBUG; 2 | 3 | /// Wraps a type so that it is eliminated in release builds. Useful for 4 | /// eliminating container members that are used only for debug checks. 5 | /// 6 | /// ## Example 7 | /// ```zig 8 | /// const Foo = struct { 9 | /// debug_check: DebugOnly(u32) = debugOnly(u32, 42), 10 | /// }; 11 | /// ``` 12 | pub fn DebugOnly(comptime T: type) type { 13 | return comptime if (IS_DEBUG) T else void; 14 | } 15 | 16 | /// Eliminates a value in release builds. In debug builds, this is an identity 17 | /// function. 18 | /// 19 | /// ## Example 20 | /// ```zig 21 | /// const Foo = struct { 22 | /// debug_check: DebugOnly(u32) = debugOnly(u32, 42), 23 | /// }; 24 | /// ``` 25 | pub inline fn debugOnly(T: type, value: T) DebugOnly(T) { 26 | return if (IS_DEBUG) value; 27 | } 28 | -------------------------------------------------------------------------------- /src/util/env.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const unicode = std.unicode; 4 | const posix = std.posix; 5 | 6 | const native_os = builtin.os.tag; 7 | 8 | /// Categories for environment values. Used to check flags, not when you actually 9 | /// want the value itself 10 | pub const ValueKind = enum { 11 | /// Environment variable is present 12 | defined, 13 | /// Environment variable has a "truthy" value (`1`, `on`, whatever) 14 | enabled, 15 | }; 16 | 17 | /// Check a flag-like environment variable. Whether the flag is "on" depends on 18 | /// `kind`: 19 | /// - `.defined`: `true` if the env var is present at all 20 | /// - `.enabled`: `true` if it has an affirmative value (`1` or `on`). 21 | /// Case-insensitive. 22 | pub fn checkEnvFlag(comptime key: []const u8, comptime kind: ValueKind) bool { 23 | if (kind == .defined) return std.process.hasEnvVarConstant(key); 24 | if (native_os == .windows) { 25 | const key_w = unicode.utf8ToUtf16LeStringLiteral(key); 26 | const value = std.process.getenvW(key_w) orelse return false; 27 | // true for 1, on 28 | // NOTE: yes? 29 | return switch (value.len) { 30 | 0 => false, 31 | 1 => value[0] == '1', 32 | 2 => (value[0] == 'o' or value[0] == 'O') and (value[1] == 'n' or value[1] == 'N'), 33 | else => false, 34 | }; 35 | } else if (native_os == .wasi and !builtin.link_libc) { 36 | @compileError("ahg we need to support WASI?"); 37 | } else { 38 | const value = posix.getenv(key) orelse return false; 39 | // true for 1, on 40 | // NOTE: yes? 41 | return switch (value.len) { 42 | 0 => false, 43 | 1 => value[0] == '1', 44 | 2 => (value[0] == 'o' or value[0] == 'O') and (value[1] == 'n' or value[1] == 'N'), 45 | else => false, 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/util/feature_flags.zig: -------------------------------------------------------------------------------- 1 | //! Feature flags guard work-in-progress features until they are ready to be 2 | //! shipped. 3 | //! 4 | //! All flags are comptime-known. This may change in the future. 5 | 6 | /// Enable language server features. 7 | /// 8 | /// Not yet implemented. 9 | pub const lsp: bool = false; 10 | 11 | /// Wraps `T` so that it becomes `void` if `feature_flag` is not enabled. 12 | pub fn IfEnabled(feature_flag: anytype, T: type) type { 13 | const flag: bool = @field(@This(), @tagName(feature_flag)); 14 | return if (flag) T else void; 15 | } 16 | 17 | /// Returns `value` if `feature_flag` is enabled, void otherwise. 18 | pub fn ifEnabled(feature_flag: anytype, T: type, value: T) IfEnabled(feature_flag, T) { 19 | const flag: bool = @field(@This(), @tagName(feature_flag)); 20 | if (flag) { 21 | return value; 22 | } else { 23 | return {}; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tasks/confgen.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gen = @import("./gen_utils.zig"); 3 | const zlint = @import("zlint"); 4 | const Allocator = std.mem.Allocator; 5 | const ArenaAllocator = std.heap.ArenaAllocator; 6 | const Schema = zlint.json.Schema; 7 | const Config = zlint.lint.Config; 8 | 9 | const fs = std.fs; 10 | const panic = std.debug.panic; 11 | 12 | const CONFIG_OUT = "src/linter/config/rules_config.zig"; 13 | const SCHEMA_OUT = "zlint.schema.json"; 14 | const RULES_DIR = "src/linter/rules"; 15 | 16 | pub fn main() !void { 17 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 18 | const allocator = gpa.allocator(); 19 | var stack = std.heap.stackFallback(256, allocator); 20 | const stackalloc = stack.get(); 21 | 22 | const out = try fs.cwd().createFile(CONFIG_OUT, .{}); 23 | defer out.close(); 24 | const w = out.writer(); 25 | 26 | try w.writeAll( 27 | \\// Auto-generated by `tasks/confgen.zig`. Do not edit manually. 28 | \\const RuleConfig = @import("rule_config.zig").RuleConfig; 29 | \\const rules = @import("../rules.zig"); 30 | \\ 31 | \\pub const RulesConfig = struct { 32 | \\ pub usingnamespace @import("./rules_config.methods.zig").RulesConfigMethods(@This()); 33 | \\ 34 | ); 35 | defer w.writeAll("};\n") catch @panic("failed to write closing curlies for RulesConfig."); 36 | 37 | for (gen.RuleInfo.all_rules) |rule_info| { 38 | const snake_name = try rule_info.snakeName(stackalloc); 39 | defer stackalloc.free(snake_name); 40 | 41 | // e.g. homeless_try: RuleConfig(rules.HomelessTry) = .{}, 42 | try w.print( 43 | " {s}: RuleConfig(rules.{s}) = .{{}},\n", 44 | .{ snake_name, rule_info.name(.pascale) }, 45 | ); 46 | } 47 | try createJsonSchema(allocator); 48 | } 49 | 50 | fn createJsonSchema(allocator: Allocator) !void { 51 | var arena = ArenaAllocator.init(allocator); 52 | defer arena.deinit(); 53 | var ctx = Schema.Context.init(allocator); 54 | const root = try ctx.genSchema(Config); 55 | const rules_config: *Schema.Object = &ctx.getSchema(Config.RulesConfig).?.object; 56 | 57 | var source_arena = ArenaAllocator.init(allocator); 58 | defer arena.deinit(); 59 | 60 | const root_dir = std.fs.cwd(); 61 | for (gen.RuleInfo.all_rules) |rule| { 62 | const alloc = source_arena.allocator(); 63 | defer { 64 | _ = arena.reset(.retain_capacity); 65 | } 66 | 67 | std.log.info("Rule: {s}", .{rule.path}); 68 | const source = try gen.readSourceFile(alloc, root_dir, rule.path); 69 | const rule_docs = try gen.getModuleDocs(source, alloc) orelse panic( 70 | "Reached EOF on rule '{s}' before finding docs and/or rule impl.", 71 | .{rule.name(.kebab)}, 72 | ); 73 | const rule_schema = rules_config.properties.getPtr(rule.name(.kebab)).?; 74 | const copied = try ctx.allocator.dupe(u8, rule_docs); 75 | var common = rule_schema.common(); 76 | common.description = copied; 77 | try common.extra_values.put(ctx.allocator, "markdownDescription", .{ .string = copied }); 78 | } 79 | 80 | const schema = try ctx.toJson(root); 81 | var out = try fs.cwd().createFile(SCHEMA_OUT, .{}); 82 | defer out.close(); 83 | try std.json.stringify(schema, .{ .whitespace = .indent_4 }, out.writer()); 84 | } 85 | -------------------------------------------------------------------------------- /tasks/create-ast-json.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mkdir -p tmp 3 | touch tmp/ast.json 4 | -------------------------------------------------------------------------------- /tasks/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # Exit on error 4 | 5 | function try_install() { 6 | package_manager=$1 7 | package_name=$2 8 | binary_name=${3:-$2} 9 | 10 | if ! which $binary_name > /dev/null; then 11 | echo "Installing $package_name" 12 | brew install $package_name 13 | else 14 | echo "$package_name is already installed, skipping" 15 | fi 16 | } 17 | 18 | 19 | if which brew > /dev/null; then 20 | try_install brew entr 21 | try_install brew typos-cli typos 22 | try_install brew kcov 23 | try_install brew bun 24 | elif which apt-get > /dev/null; then 25 | try_install apt-get entr 26 | try_install apt-get typos-cli typos 27 | try_install apt-get kcov 28 | if ! which bun > /dev/null; then 29 | echo "Bun is not installed. Please follow steps on https://bun.sh" 30 | fi 31 | else 32 | echo "No supported package manager found. Please install dependencies manually and/or update this script to support your package manager." 33 | exit 1 34 | fi 35 | -------------------------------------------------------------------------------- /tasks/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | platform=$(uname -ms) 5 | 6 | if [[ ${OS:-} = Windows_NT ]]; then 7 | if [[ $platform != MINGW64* ]]; then 8 | # powershell -c "irm zlint.sh/install.ps1|iex" 9 | # exit $? 10 | echo "zlint's install does not support Windows yet. Please download a copy of zlint here: https://github.com/DonIsaac/zlint/releases/latest" 11 | exit 1 12 | fi 13 | fi 14 | 15 | # Reset 16 | Color_Off='' 17 | 18 | # Regular Colors 19 | Red='' 20 | Green='' 21 | Dim='' # White 22 | 23 | # Bold 24 | Bold_White='' 25 | Bold_Green='' 26 | 27 | if [[ -t 1 ]]; then 28 | # Reset 29 | Color_Off='\033[0m' # Text Reset 30 | 31 | # Regular Colors 32 | Red='\033[0;31m' # Red 33 | Green='\033[0;32m' # Green 34 | Dim='\033[0;2m' # White 35 | 36 | # Bold 37 | Bold_Green='\033[1;32m' # Bold Green 38 | Bold_White='\033[1m' # Bold White 39 | fi 40 | 41 | error() { 42 | echo -e "${Red}error${Color_Off}:" "$@" >&2 43 | exit 1 44 | } 45 | 46 | info() { 47 | echo -e "${Dim}$@ ${Color_Off}" 48 | } 49 | 50 | info_bold() { 51 | echo -e "${Bold_White}$@ ${Color_Off}" 52 | } 53 | 54 | success() { 55 | echo -e "${Green}$@ ${Color_Off}" 56 | } 57 | 58 | case $platform in 59 | 'Darwin x86_64') 60 | target=macos-x86_64 61 | ;; 62 | 'Darwin arm64') 63 | target=macos-aarch64 64 | ;; 65 | 'Linux aarch64' | 'Linux arm64') 66 | target=linux-aarch64 67 | ;; 68 | 'MINGW64'*) 69 | target=windows-x86_64 70 | ;; 71 | 'Linux x86_64' | *) 72 | target=linux-x86_64 73 | ;; 74 | esac 75 | 76 | if [[ $target = macos-x86_64 ]]; then 77 | # Is this process running in Rosetta? 78 | # redirect stderr to devnull to avoid error message when not running in Rosetta 79 | if [[ $(sysctl -n sysctl.proc_translated 2>/dev/null) = 1 ]]; then 80 | target=macos-aarch64 81 | info "Your shell is running in Rosetta 2. Downloading bun for $target instead" 82 | fi 83 | fi 84 | 85 | GITHUB=${GITHUB-"https://github.com"} 86 | github_repo="$GITHUB/DonIsaac/zlint" 87 | 88 | if [[ $# = 0 ]]; then 89 | zlint_uri=$github_repo/releases/latest/download/zlint-$target 90 | else 91 | zlint_uri=$github_repo/releases/download/$1/zlint-$target 92 | fi 93 | 94 | # macos/linux cross-compat mktemp 95 | # https://unix.stackexchange.com/questions/30091/fix-or-alternative-for-mktemp-in-os-x 96 | tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'zlint') 97 | install_dir=/usr/local/bin 98 | 99 | curl --fail --location --progress-bar --output "$tmpdir/zlint" "$zlint_uri" || 100 | error "Failed to download zlint from \"$zlint_uri\"" 101 | chmod +x "$tmpdir/zlint" 102 | 103 | # Check if user can write to install directory 104 | if [[ ! -w $install_dir ]]; then 105 | info "Saving zlint to $install_dir. You will be prompted for your password." 106 | sudo mv "$tmpdir/zlint" "$install_dir" 107 | success "zlint installed to $install_dir/zlint" 108 | else 109 | mv "$tmpdir/zlint" "$install_dir" 110 | success "zlint installed to $install_dir/zlint" 111 | fi 112 | -------------------------------------------------------------------------------- /tasks/lldb/.lldbinit: -------------------------------------------------------------------------------- 1 | 2 | command script import ./tasks/lldb/lldb_pretty_printers.py 3 | type category enable zig.lang 4 | type category enable zig.std 5 | 8 | -------------------------------------------------------------------------------- /tasks/submodules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run this with `just submodules` 3 | mkdir -p zig-out/repos 4 | repos=$(cat test/repos.json) 5 | 6 | while read repository 7 | do 8 | name=$(echo "$repository" | jq -r .name) 9 | repo_url=$(echo "$repository" | jq -r .repo_url) 10 | hash=$(echo "$repository" | jq -r .hash) 11 | 12 | just clone-submodule "zig-out/repos/$name" $repo_url $hash 13 | 14 | done < <(echo "$repos" | jq -c '.[]') 15 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # E2E Tests 2 | 3 | Tl;Dr: 4 | 5 | ```sh 6 | just submodules 7 | just e2e 8 | ``` 9 | 10 | This folder contains E2E & integration tests. They mostly focus on semantic 11 | analysis. They can be run with `just e2e`. 12 | 13 | ## Structure 14 | 15 | These tests are actually compiled as a binary, not as a test suite (e.g. `zig 16 | build test`). `zlint` is linked as a module to this binary. Anything that is 17 | marked `pub` in `src/root.ts` is importiable via `@import("zlint")` within a 18 | test file. 19 | 20 | Several test suites run checks on a set of popular zig codebases. These repos 21 | are configured in `repos.json`. You must run `just submodules` to clone them 22 | before running e2e tests. 23 | 24 | -------------------------------------------------------------------------------- /test/fixtures/config/zlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "unsafe-undefined": "warn" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/simple/fail/syntax_missing_semi.zig: -------------------------------------------------------------------------------- 1 | const x = 1 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/block.zig: -------------------------------------------------------------------------------- 1 | fn empty() void { 2 | const x = {}; 3 | return x; 4 | } 5 | 6 | fn one() void { 7 | var y = 1; 8 | { 9 | y = 1; 10 | } 11 | } 12 | 13 | fn two() void { 14 | var z = 1; 15 | { 16 | const a = 1; 17 | z = a; 18 | } 19 | } 20 | 21 | fn many() void { 22 | var a = 1; 23 | { 24 | const b = 1; 25 | const c = 2; 26 | const d = 3; 27 | const e = 4; 28 | const f = 5; 29 | a += b + c + d + e + f; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/block_comptime.zig: -------------------------------------------------------------------------------- 1 | const x = blk: { 2 | var y = 1; 3 | y += 2; 4 | break :blk y + 1; 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/cond_if.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | // comptime if expression 5 | const msg = if (builtin.mode == .Debug) 6 | "Debug mode" 7 | else 8 | "Release mode"; 9 | 10 | fn simpleIf() void { 11 | var i: u32 = 1; 12 | 13 | // lonely if 14 | if (i % 2 == 0) { 15 | std.debug.print("Even\n", .{}); 16 | } 17 | 18 | if (i > 5) { 19 | const pow = i * i; // should be in new scope 20 | i = pow; 21 | } else { 22 | i = 0; 23 | } 24 | } 25 | 26 | fn comptimeIf() void { 27 | comptime if (builtin.os.tag == .windows) { 28 | @compileError("Windows is not supported"); 29 | }; 30 | } 31 | 32 | fn ifWithPayload() void { 33 | const res: anyerror!u32 = 1; 34 | if (res) |x| { 35 | std.debug.print("value: {d}\n", .{x}); 36 | } else |err| { 37 | std.debug.print("error: {s}\n", .{err}); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/container_error.zig: -------------------------------------------------------------------------------- 1 | const Error = error{ 2 | Foo, 3 | Bar, 4 | }; 5 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/enum_members.zig: -------------------------------------------------------------------------------- 1 | pub const Foo = enum { 2 | a, 3 | b, 4 | c, 5 | 6 | pub const Bar: u32 = 1; 7 | 8 | pub fn isNotA(self: Foo) bool { 9 | return self != Foo.a; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/fibonacci.zig: -------------------------------------------------------------------------------- 1 | // source: https://stackoverflow.com/q/70761612 2 | const std = @import("std"); 3 | const Managed = std.math.big.int.Managed; 4 | 5 | pub fn main() anyerror!void { 6 | const allocator = std.heap.c_allocator; 7 | 8 | var a = try Managed.initSet(allocator, 0); 9 | defer a.deinit(); 10 | var b = try Managed.initSet(allocator, 1); 11 | defer b.deinit(); 12 | var i: u128 = 0; 13 | 14 | var c = try Managed.init(allocator); 15 | defer c.deinit(); 16 | 17 | while (i < 1000000) : (i += 1) { 18 | try c.add(a.toConst(), b.toConst()); 19 | 20 | a.swap(&b); // This is more efficient than using Clone! 21 | b.swap(&c); // This reduced memory leak. 22 | } 23 | 24 | const as = try a.toString(allocator, 10, std.fmt.Case.lower); 25 | defer allocator.free(as); 26 | std.log.info("Fib: {s}", .{as}); 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/fn_comptime.zig: -------------------------------------------------------------------------------- 1 | fn Box(T: type) type { 2 | return struct { 3 | inner: *T, 4 | const Self = @This(); 5 | 6 | pub fn deref(self: *Self) *T { 7 | return self.inner; 8 | } 9 | }; 10 | } 11 | 12 | fn FixedIntArray(comptime size: usize) type { 13 | // since this function has comptime arguments, this scope is comptime, so 14 | // variables can be typed to comptime_int 15 | var l = 0; 16 | l += 1; 17 | if (l > 5) { 18 | @compileError("bad l"); 19 | } 20 | return struct { 21 | inner: [size]u32, 22 | }; 23 | } 24 | 25 | fn add(comptime a: isize, b: usize) usize { 26 | comptime { 27 | const c = 3; 28 | if (a < c) { 29 | @compileError("a cannot be less than 3"); 30 | } 31 | } 32 | const a2: usize = @intCast(a); 33 | return a2 + b; 34 | } 35 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/fn_in_fn.zig: -------------------------------------------------------------------------------- 1 | pub fn foo(a: u32) u32 { 2 | const inner = struct { 3 | fn bar(b: u32) u32 { 4 | return b + 1; 5 | } 6 | }; 7 | 8 | return inner.bar(a); 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/foo.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | var bad: []const u8 = undefined; 4 | 5 | pub const good: ?[]const u8 = null; 6 | 7 | const Foo = struct { 8 | foo: u32 = undefined, 9 | const Bar: u32 = 1; 10 | fn baz(self: *Foo) void { 11 | std.debug.print("{d}\n", .{self.foo}); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/loops_for.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn forOverArr() void { 4 | const arr = [_]u32{ 1, 2, 3, 4, 5 }; 5 | 6 | for (arr) |i| { 7 | const power_of_two = 1 << i; 8 | std.debug.print("2^{} = {}\n", .{ i, power_of_two }); 9 | } 10 | } 11 | 12 | // Copied from https://ziglang.org/documentation/master/#toc-Multidimensional-Arrays 13 | fn forWithMultiBindingClosure() void { 14 | const mat4x4 = [4][4]f32{ 15 | [_]f32{ 1.0, 0.0, 0.0, 0.0 }, 16 | [_]f32{ 0.0, 1.0, 0.0, 1.0 }, 17 | [_]f32{ 0.0, 0.0, 1.0, 0.0 }, 18 | [_]f32{ 0.0, 0.0, 0.0, 1.0 }, 19 | }; 20 | for (mat4x4, 0..) |row, row_index| { 21 | for (row, 0..) |cell, column_index| { 22 | if (row_index == column_index) { 23 | try std.testing.expect(cell == 1.0); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/loops_while.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn simpleWhile() void { 4 | var i: u32 = 0; 5 | 6 | while (i < 5) { 7 | const power_of_two = 1 << i; 8 | std.debug.print("2^{} = {}\n", .{ i, power_of_two }); 9 | i += 1; 10 | } 11 | } 12 | 13 | fn whileWithClosure() void { 14 | var map = std.AutoHashMap(u32, u32){}; 15 | map.put(1, 1); 16 | map.put(2, 2); 17 | const iter = map.iterator(); 18 | while (iter.next()) |ent| { 19 | const k = ent.key_ptr.*; 20 | const v = ent.value_ptr.*; 21 | std.debug.print("{d}: {d}\n", .{ k, v }); 22 | } 23 | } 24 | 25 | // copied from https://ziglang.org/documentation/master/#while 26 | fn whileWithExpr() !void { 27 | var x: usize = 1; 28 | var y: usize = 1; 29 | while (x * y < 2000) : ({ 30 | x *= 2; 31 | y *= 3; 32 | }) { 33 | const my_xy = x * y; 34 | try std.testing.expect(my_xy < 2000); 35 | } 36 | } 37 | 38 | // copied from https://ziglang.org/documentation/master/#while 39 | fn rangeHasNumber(begin: usize, end: usize, number: usize) bool { 40 | var i = begin; 41 | return while (i < end) : (i += 1) { 42 | if (i == number) { 43 | break true; 44 | } 45 | } else false; 46 | } 47 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/stmt_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn add(a: u32, b: u32) u32 { 4 | return a + b; 5 | } 6 | 7 | test add { 8 | try std.testing.expectEqual(2, add(1, 1)); 9 | } 10 | test "add twice" { 11 | try std.testing.expectEqual(3, add(1, add(1, 1))); 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/struct_members.zig: -------------------------------------------------------------------------------- 1 | pub const Foo = struct{ 2 | a: u32, 3 | 4 | const B: u32 = 0; 5 | pub const C = 0; 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/struct_tuple.zig: -------------------------------------------------------------------------------- 1 | const x = u64; 2 | const Foo = struct { u32, i32, x }; 3 | const Namespace = struct { 4 | const Member = u32; 5 | }; 6 | const Bar = struct { Namespace.Member }; 7 | 8 | const f: Foo = .{ 1, 2, 3 }; 9 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/top_level_struct.zig: -------------------------------------------------------------------------------- 1 | a: u32, 2 | b: []const u8 = "", 3 | 4 | const Foo = @This(); 5 | 6 | pub fn new() Foo { 7 | return Foo{ .a = 0, .b = "hello" }; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/unresolved_import.zig: -------------------------------------------------------------------------------- 1 | const foo = @import("this/file/doesnotexist.zig"); 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/writer_interface.zig: -------------------------------------------------------------------------------- 1 | // source: https://www.openmymind.net/Zig-Interfaces/ 2 | const Writer = struct { 3 | // These two fields are the same as before 4 | ptr: *anyopaque, 5 | writeAllFn: *const fn (ptr: *anyopaque, data: []const u8) anyerror!void, 6 | 7 | // This is new 8 | fn init(ptr: anytype) Writer { 9 | const T = @TypeOf(ptr); 10 | const ptr_info = @typeInfo(T); 11 | 12 | const gen = struct { 13 | pub fn writeAll(pointer: *anyopaque, data: []const u8) anyerror!void { 14 | const self: T = @ptrCast(@alignCast(pointer)); 15 | return ptr_info.Pointer.child.writeAll(self, data); 16 | } 17 | }; 18 | 19 | return .{ 20 | .ptr = ptr, 21 | .writeAllFn = gen.writeAll, 22 | }; 23 | } 24 | 25 | // This is the same as before 26 | pub fn writeAll(self: Writer, data: []const u8) !void { 27 | return self.writeAllFn(self.ptr, data); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /test/harness.zig: -------------------------------------------------------------------------------- 1 | pub const TestSuite = @import("harness/TestSuite.zig"); 2 | 3 | const runner = @import("harness/runner.zig"); 4 | pub const getRunner = runner.getRunner; 5 | pub const addTest = runner.addTest; 6 | pub const globalShutdown = runner.globalShutdown; 7 | pub const TestFile = runner.TestFile; 8 | -------------------------------------------------------------------------------- /test/harness/runner.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const utils = @import("../utils.zig"); 4 | const TestSuite = @import("TestSuite.zig"); 5 | 6 | const Allocator = std.mem.Allocator; 7 | const assert = std.debug.assert; 8 | const panic = std.debug.panic; 9 | const print = std.debug.print; 10 | 11 | const TestAllocator = std.heap.GeneralPurposeAllocator(.{ 12 | // .never_unmap = true, 13 | // .retain_metadata = true, 14 | }); 15 | 16 | const is_debug = builtin.mode == .Debug; 17 | var debug_allocator = TestAllocator{}; 18 | var global_runner_instance = TestRunner.new(if (is_debug) 19 | debug_allocator.allocator() 20 | else 21 | std.heap.smp_allocator); 22 | 23 | pub fn getRunner() *TestRunner { 24 | return &global_runner_instance; 25 | } 26 | 27 | pub fn addTest(test_file: TestRunner.TestFile) *TestRunner { 28 | assert(global_runner_instance != null); 29 | return global_runner_instance.?.addTest(test_file); 30 | } 31 | 32 | pub fn globalShutdown() void { 33 | getRunner().deinit(); 34 | 35 | if (is_debug) { 36 | _ = debug_allocator.detectLeaks(); 37 | const status = debug_allocator.deinit(); 38 | if (status == .leak) { 39 | panic("Memory leak detected\n", .{}); 40 | } 41 | } 42 | } 43 | 44 | pub const TestRunner = struct { 45 | tests: std.ArrayListUnmanaged(TestFile) = .{}, 46 | alloc: Allocator, 47 | 48 | pub inline fn new(alloc: Allocator) TestRunner { 49 | return TestRunner{ .alloc = alloc }; 50 | } 51 | 52 | pub inline fn deinit(self: *TestRunner) void { 53 | for (self.tests.items) |test_file| { 54 | if (test_file.deinit) |deinit_fn| { 55 | deinit_fn(self.alloc); 56 | } 57 | } 58 | self.tests.deinit(self.alloc); 59 | } 60 | 61 | pub inline fn addTest(self: *TestRunner, test_file: TestFile) *TestRunner { 62 | self.tests.append(self.alloc, test_file) catch |e| panic("Failed to add test {s}: {any}\n", .{ test_file.name, e }); 63 | return self; 64 | } 65 | 66 | // pub inline fn addSuite(self: *TestRunner, test_suite: TestSuite) *TestRunner { 67 | // const test_file = TestFile { 68 | // const 69 | // } 70 | // } 71 | 72 | pub inline fn runAll(self: *TestRunner) !void { 73 | try utils.TestFolders.globalInit(); 74 | 75 | var last_error: ?anyerror = null; 76 | for (self.tests.items) |test_file| { 77 | if (test_file.globalSetup) |global_setup| { 78 | try global_setup(self.alloc); 79 | } 80 | print("Running test {s}...\n", .{test_file.name}); 81 | test_file.run(self.alloc) catch |e| { 82 | print("Failed to run test {s}: {any}\n", .{ test_file.name, e }); 83 | last_error = e; 84 | }; 85 | } 86 | 87 | if (last_error) |e| { 88 | return e; 89 | } 90 | } 91 | }; 92 | 93 | pub const TestFile = struct { 94 | /// Inlined string (`&'static str`). Never deallocated. 95 | name: []const u8, 96 | globalSetup: ?*const GlobalSetupFn = null, 97 | deinit: ?*const GlobalTeardownFn = null, 98 | run: *const RunFn, 99 | 100 | pub const GlobalSetupFn = fn (alloc: Allocator) anyerror!void; 101 | pub const GlobalTeardownFn = fn (alloc: Allocator) void; 102 | pub const RunFn = fn (alloc: Allocator) anyerror!void; 103 | 104 | fn fromSuite(suite: TestSuite) TestFile { 105 | const gen = struct { 106 | fn deinit(_: Allocator) void { 107 | suite.deinit(); 108 | } 109 | fn run(_: Allocator) anyerror!void { 110 | suite.run(); 111 | } 112 | }; 113 | return TestFile{ 114 | .name = suite.name, 115 | .run = &gen.run, 116 | .deinit = &gen.deinit, 117 | }; 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /test/repos.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "zig", 4 | "repo_url": "https://github.com/ziglang/zig.git", 5 | "hash": "95fdbc579fc1df3c575baded7ec5edc1c3ce6e6d" 6 | }, 7 | { 8 | "name": "bun", 9 | "repo_url": "https://github.com/oven-sh/bun.git", 10 | "hash": "e75d2269431045c5b9b3867d7c7da949c81b19ff" 11 | }, 12 | { 13 | "name": "ghostty", 14 | "repo_url": "https://github.com/ghostty-org/ghostty", 15 | "hash": "6f7977fef186faa9b9afe7707dc21a2eff59883b" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /test/semantic/ecosystem_coverage.zig: -------------------------------------------------------------------------------- 1 | const test_runner = @import("../harness.zig"); 2 | 3 | const std = @import("std"); 4 | const fs = std.fs; 5 | const Allocator = std.mem.Allocator; 6 | const print = std.debug.print; 7 | 8 | const zlint = @import("zlint"); 9 | const Source = zlint.Source; 10 | 11 | const utils = @import("../utils.zig"); 12 | const Repo = utils.Repo; 13 | 14 | const REPOS_DIR = "zig-out/repos"; 15 | 16 | // SAFETY: globalSetup is always run before this is read 17 | var repos: std.json.Parsed([]Repo) = undefined; 18 | 19 | const SemanticError = zlint.semantic.Semantic.Builder.SemanticError; 20 | var is_tty: bool = false; 21 | 22 | pub fn globalSetup(alloc: Allocator) !void { 23 | is_tty = std.io.getStdErr().isTty(); 24 | var repos_dir_fd = fs.cwd().openDir(REPOS_DIR, .{}) catch |e| { 25 | switch (e) { 26 | error.FileNotFound => { 27 | print("Could not find git repos to test, please run `just submodules`", .{}); 28 | return e; 29 | }, 30 | else => return e, 31 | } 32 | }; 33 | repos_dir_fd.close(); 34 | repos = try Repo.load(alloc); 35 | } 36 | 37 | pub fn globalTeardown(_: Allocator) void { 38 | repos.deinit(); 39 | } 40 | 41 | fn isTTY_() bool { 42 | return std.io.getStdErr().isTTY(); 43 | } 44 | 45 | fn testSemantic(alloc: Allocator, source: *const Source) !void { 46 | { 47 | const p = source.pathname orelse ""; 48 | print("ecosystem coverage: {s}", .{p}); 49 | if (is_tty) 50 | print(" \r", .{}) 51 | else 52 | print("\n", .{}); 53 | } 54 | var builder = zlint.semantic.Semantic.Builder.init(alloc); 55 | defer builder.deinit(); 56 | var res = try builder.build(source.text()); 57 | defer res.deinit(); 58 | if (res.hasErrors()) return error.AnalysisFailed; 59 | } 60 | 61 | // const fns: test_runner.TestSuite.TestSuiteFns = .{ 62 | // .test_fn = &testSemantic, 63 | // .setup_fn = &globalSetup, 64 | // .teardown_fn = &globalTeardown, 65 | // }; 66 | 67 | pub fn run(alloc: Allocator) !void { 68 | for (repos.value) |repo| { 69 | const repo_dir = try utils.TestFolders.openRepo(alloc, repo.name); 70 | var suite = try test_runner.TestSuite.init(alloc, repo_dir, "semantic-coverage", repo.name, .{ .test_fn = &testSemantic }); 71 | defer suite.deinit(); 72 | 73 | try suite.run(); 74 | } 75 | } 76 | 77 | pub const SUITE = test_runner.TestFile{ 78 | .name = "semantic_coverage", 79 | .globalSetup = globalSetup, 80 | .deinit = globalTeardown, 81 | .run = run, 82 | }; 83 | -------------------------------------------------------------------------------- /test/snapshots/semantic-coverage/bun.snap: -------------------------------------------------------------------------------- 1 | Passed: 97.40485% (563/578) 2 | Panics: 0% (0/578) 3 | 4 | src/bun.js/test/expect.zig: error.FullMismatch 5 | src/bun.js/webcore/request.zig: error.FullMismatch 6 | src/bun.js/webcore/streams.zig: error.FullMismatch 7 | src/cli/pack_command.zig: error.FullMismatch 8 | src/css/css_parser.zig: error.FullMismatch 9 | src/css/small_list.zig: error.FullMismatch 10 | src/css/values/number.zig: error.FullMismatch 11 | src/install/bun.lock.zig: error.FullMismatch 12 | src/install/extract_tarball.zig: error.FullMismatch 13 | src/install/install.zig: error.FullMismatch 14 | src/install/lockfile.zig: error.FullMismatch 15 | src/install/npm.zig: error.FullMismatch 16 | src/shell/Builtin.zig: error.FullMismatch 17 | src/string.zig: error.FullMismatch 18 | src/sys.zig: error.FullMismatch 19 | -------------------------------------------------------------------------------- /test/snapshots/semantic-coverage/ghostty.snap: -------------------------------------------------------------------------------- 1 | Passed: 99.393936% (492/495) 2 | Panics: 0% (0/495) 3 | 4 | src/cli/list_themes.zig: error.FullMismatch 5 | src/terminal/PageList.zig: error.FullMismatch 6 | src/terminal/Parser.zig: error.FullMismatch 7 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/fail.snap: -------------------------------------------------------------------------------- 1 | Passed: 100% (1/1) 2 | Panics: 0% (0/1) 3 | 4 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/fail/syntax_missing_semi.zig.snap: -------------------------------------------------------------------------------- 1 | 𝙭 syntax error: expected ';' after declaration 2 | ╭─[syntax_missing_semi.zig:1:11] 3 | 1 │ const x = 1 4 | · ─ 5 | ╰──── 6 | 7 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass.snap: -------------------------------------------------------------------------------- 1 | Passed: 100% (17/17) 2 | Panics: 0% (0/17) 3 | 4 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/block_comptime.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "x", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const"], 24 | "references": [ 25 | 26 | ], 27 | "members": [], 28 | "exports": [2], 29 | 30 | }, 31 | { 32 | "name": "y", 33 | "debugName": "", 34 | "token": 7, 35 | "declNode": "simple_var_decl", 36 | "scope": 1, 37 | "flags": ["variable"], 38 | "references": [ 39 | {"symbol":2,"scope":1,"node":"identifier","identifier":"y","flags":["read","write"]}, 40 | {"symbol":2,"scope":1,"node":"identifier","identifier":"y","flags":["read"]}, 41 | 42 | ], 43 | "members": [], 44 | "exports": [], 45 | 46 | }, 47 | ], 48 | "unresolvedReferences": [], 49 | "modules": { 50 | "imports": [ 51 | ], 52 | }, 53 | "scopes": { 54 | "id": 0, 55 | "flags": ["top"], 56 | "bindings": { 57 | "@This()": 0, 58 | "x": 1, 59 | 60 | }, 61 | "children": [ 62 | { 63 | "id": 1, 64 | "flags": ["block","comptime"], 65 | "bindings": { 66 | "y": 2, 67 | 68 | }, 69 | "children": [], 70 | 71 | }, 72 | ], 73 | 74 | }, 75 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/container_error.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "Error", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const","error"], 24 | "references": [ 25 | 26 | ], 27 | "members": [2,3], 28 | "exports": [], 29 | 30 | }, 31 | { 32 | "name": "Bar", 33 | "debugName": "", 34 | "token": 7, 35 | "declNode": "error_set_decl", 36 | "scope": 1, 37 | "flags": ["member","error"], 38 | "references": [ 39 | 40 | ], 41 | "members": [], 42 | "exports": [], 43 | 44 | }, 45 | { 46 | "name": "Foo", 47 | "debugName": "", 48 | "token": 5, 49 | "declNode": "error_set_decl", 50 | "scope": 1, 51 | "flags": ["member","error"], 52 | "references": [ 53 | 54 | ], 55 | "members": [], 56 | "exports": [], 57 | 58 | }, 59 | ], 60 | "unresolvedReferences": [], 61 | "modules": { 62 | "imports": [ 63 | ], 64 | }, 65 | "scopes": { 66 | "id": 0, 67 | "flags": ["top"], 68 | "bindings": { 69 | "@This()": 0, 70 | "Error": 1, 71 | 72 | }, 73 | "children": [ 74 | { 75 | "id": 1, 76 | "flags": ["error"], 77 | "bindings": { 78 | "Bar": 2, 79 | "Foo": 3, 80 | 81 | }, 82 | "children": [], 83 | 84 | }, 85 | ], 86 | 87 | }, 88 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/enum_members.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "Foo", 19 | "debugName": "", 20 | "token": 2, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const","enum"], 24 | "references": [ 25 | {"symbol":1,"scope":2,"node":"identifier","identifier":"Foo","flags":["read"]}, 26 | {"symbol":1,"scope":3,"node":"identifier","identifier":"Foo","flags":["read"]}, 27 | 28 | ], 29 | "members": [2,3,4], 30 | "exports": [5,6], 31 | 32 | }, 33 | { 34 | "name": "a", 35 | "debugName": "", 36 | "token": 6, 37 | "declNode": "container_field_init", 38 | "scope": 1, 39 | "flags": ["member","enum"], 40 | "references": [ 41 | 42 | ], 43 | "members": [], 44 | "exports": [], 45 | 46 | }, 47 | { 48 | "name": "b", 49 | "debugName": "", 50 | "token": 8, 51 | "declNode": "container_field_init", 52 | "scope": 1, 53 | "flags": ["member","enum"], 54 | "references": [ 55 | 56 | ], 57 | "members": [], 58 | "exports": [], 59 | 60 | }, 61 | { 62 | "name": "c", 63 | "debugName": "", 64 | "token": 10, 65 | "declNode": "container_field_init", 66 | "scope": 1, 67 | "flags": ["member","enum"], 68 | "references": [ 69 | 70 | ], 71 | "members": [], 72 | "exports": [], 73 | 74 | }, 75 | { 76 | "name": "Bar", 77 | "debugName": "", 78 | "token": 14, 79 | "declNode": "simple_var_decl", 80 | "scope": 1, 81 | "flags": ["variable","const"], 82 | "references": [ 83 | 84 | ], 85 | "members": [], 86 | "exports": [], 87 | 88 | }, 89 | { 90 | "name": "isNotA", 91 | "debugName": "", 92 | "token": 22, 93 | "declNode": "fn_decl", 94 | "scope": 1, 95 | "flags": ["fn"], 96 | "references": [ 97 | 98 | ], 99 | "members": [], 100 | "exports": [], 101 | 102 | }, 103 | { 104 | "name": "self", 105 | "debugName": "", 106 | "token": 24, 107 | "declNode": "identifier", 108 | "scope": 2, 109 | "flags": ["const","fn_param"], 110 | "references": [ 111 | {"symbol":7,"scope":3,"node":"identifier","identifier":"self","flags":["read"]}, 112 | 113 | ], 114 | "members": [], 115 | "exports": [], 116 | 117 | }, 118 | ], 119 | "unresolvedReferences": [], 120 | "modules": { 121 | "imports": [ 122 | ], 123 | }, 124 | "scopes": { 125 | "id": 0, 126 | "flags": ["top"], 127 | "bindings": { 128 | "@This()": 0, 129 | "Foo": 1, 130 | 131 | }, 132 | "children": [ 133 | { 134 | "id": 1, 135 | "flags": ["enum","block"], 136 | "bindings": { 137 | "a": 2, 138 | "b": 3, 139 | "c": 4, 140 | "Bar": 5, 141 | "isNotA": 6, 142 | 143 | }, 144 | "children": [ 145 | { 146 | "id": 2, 147 | "flags": ["function"], 148 | "bindings": { 149 | "self": 7, 150 | 151 | }, 152 | "children": [ 153 | { 154 | "id": 3, 155 | "flags": ["function","block"], 156 | "bindings": { 157 | 158 | }, 159 | "children": [], 160 | 161 | }, 162 | ], 163 | 164 | }, 165 | ], 166 | 167 | }, 168 | ], 169 | 170 | }, 171 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/fn_in_fn.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1,3], 15 | 16 | }, 17 | { 18 | "name": "foo", 19 | "debugName": "", 20 | "token": 2, 21 | "declNode": "fn_decl", 22 | "scope": 0, 23 | "flags": ["fn"], 24 | "references": [ 25 | 26 | ], 27 | "members": [], 28 | "exports": [], 29 | 30 | }, 31 | { 32 | "name": "a", 33 | "debugName": "", 34 | "token": 4, 35 | "declNode": "identifier", 36 | "scope": 1, 37 | "flags": ["const","fn_param"], 38 | "references": [ 39 | {"symbol":2,"scope":2,"node":"identifier","identifier":"a","flags":["read"]}, 40 | 41 | ], 42 | "members": [], 43 | "exports": [], 44 | 45 | }, 46 | { 47 | "name": "inner", 48 | "debugName": "", 49 | "token": 11, 50 | "declNode": "simple_var_decl", 51 | "scope": 2, 52 | "flags": ["variable","const","struct"], 53 | "references": [ 54 | {"symbol":3,"scope":2,"node":"identifier","identifier":"inner","flags":["call"]}, 55 | 56 | ], 57 | "members": [], 58 | "exports": [4], 59 | 60 | }, 61 | { 62 | "name": "bar", 63 | "debugName": "", 64 | "token": 16, 65 | "declNode": "fn_decl", 66 | "scope": 3, 67 | "flags": ["fn"], 68 | "references": [ 69 | 70 | ], 71 | "members": [], 72 | "exports": [], 73 | 74 | }, 75 | { 76 | "name": "b", 77 | "debugName": "", 78 | "token": 18, 79 | "declNode": "identifier", 80 | "scope": 4, 81 | "flags": ["const","fn_param"], 82 | "references": [ 83 | {"symbol":5,"scope":5,"node":"identifier","identifier":"b","flags":["read"]}, 84 | 85 | ], 86 | "members": [], 87 | "exports": [], 88 | 89 | }, 90 | ], 91 | "unresolvedReferences": [], 92 | "modules": { 93 | "imports": [ 94 | ], 95 | }, 96 | "scopes": { 97 | "id": 0, 98 | "flags": ["top"], 99 | "bindings": { 100 | "@This()": 0, 101 | "foo": 1, 102 | 103 | }, 104 | "children": [ 105 | { 106 | "id": 1, 107 | "flags": ["function"], 108 | "bindings": { 109 | "a": 2, 110 | 111 | }, 112 | "children": [ 113 | { 114 | "id": 2, 115 | "flags": ["function","block"], 116 | "bindings": { 117 | "inner": 3, 118 | 119 | }, 120 | "children": [ 121 | { 122 | "id": 3, 123 | "flags": ["struct","block"], 124 | "bindings": { 125 | "bar": 4, 126 | 127 | }, 128 | "children": [ 129 | { 130 | "id": 4, 131 | "flags": ["function"], 132 | "bindings": { 133 | "b": 5, 134 | 135 | }, 136 | "children": [ 137 | { 138 | "id": 5, 139 | "flags": ["function","block"], 140 | "bindings": { 141 | 142 | }, 143 | "children": [], 144 | 145 | }, 146 | ], 147 | 148 | }, 149 | ], 150 | 151 | }, 152 | ], 153 | 154 | }, 155 | ], 156 | 157 | }, 158 | ], 159 | 160 | }, 161 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/foo.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1,2,3,4], 15 | 16 | }, 17 | { 18 | "name": "std", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const"], 24 | "references": [ 25 | {"symbol":1,"scope":3,"node":"identifier","identifier":"std","flags":["call"]}, 26 | 27 | ], 28 | "members": [], 29 | "exports": [], 30 | 31 | }, 32 | { 33 | "name": "bad", 34 | "debugName": "", 35 | "token": 9, 36 | "declNode": "simple_var_decl", 37 | "scope": 0, 38 | "flags": ["variable"], 39 | "references": [ 40 | 41 | ], 42 | "members": [], 43 | "exports": [], 44 | 45 | }, 46 | { 47 | "name": "good", 48 | "debugName": "", 49 | "token": 20, 50 | "declNode": "simple_var_decl", 51 | "scope": 0, 52 | "flags": ["variable","const"], 53 | "references": [ 54 | 55 | ], 56 | "members": [], 57 | "exports": [], 58 | 59 | }, 60 | { 61 | "name": "Foo", 62 | "debugName": "", 63 | "token": 31, 64 | "declNode": "simple_var_decl", 65 | "scope": 0, 66 | "flags": ["variable","const","struct"], 67 | "references": [ 68 | {"symbol":4,"scope":2,"node":"identifier","identifier":"Foo","flags":["read"]}, 69 | 70 | ], 71 | "members": [5], 72 | "exports": [6,7], 73 | 74 | }, 75 | { 76 | "name": "foo", 77 | "debugName": "", 78 | "token": 35, 79 | "declNode": "container_field_init", 80 | "scope": 1, 81 | "flags": ["member","struct"], 82 | "references": [ 83 | 84 | ], 85 | "members": [], 86 | "exports": [], 87 | 88 | }, 89 | { 90 | "name": "Bar", 91 | "debugName": "", 92 | "token": 42, 93 | "declNode": "simple_var_decl", 94 | "scope": 1, 95 | "flags": ["variable","const"], 96 | "references": [ 97 | 98 | ], 99 | "members": [], 100 | "exports": [], 101 | 102 | }, 103 | { 104 | "name": "baz", 105 | "debugName": "", 106 | "token": 49, 107 | "declNode": "fn_decl", 108 | "scope": 1, 109 | "flags": ["fn"], 110 | "references": [ 111 | 112 | ], 113 | "members": [], 114 | "exports": [], 115 | 116 | }, 117 | { 118 | "name": "self", 119 | "debugName": "", 120 | "token": 51, 121 | "declNode": "ptr_type_aligned", 122 | "scope": 2, 123 | "flags": ["const","fn_param"], 124 | "references": [ 125 | {"symbol":8,"scope":3,"node":"identifier","identifier":"self","flags":["read"]}, 126 | 127 | ], 128 | "members": [], 129 | "exports": [], 130 | 131 | }, 132 | ], 133 | "unresolvedReferences": [], 134 | "modules": { 135 | "imports": [ 136 | { 137 | "specifier": "std", 138 | "node": 3, 139 | "kind": "module", 140 | 141 | }, 142 | ], 143 | }, 144 | "scopes": { 145 | "id": 0, 146 | "flags": ["top"], 147 | "bindings": { 148 | "@This()": 0, 149 | "std": 1, 150 | "bad": 2, 151 | "good": 3, 152 | "Foo": 4, 153 | 154 | }, 155 | "children": [ 156 | { 157 | "id": 1, 158 | "flags": ["struct","block"], 159 | "bindings": { 160 | "foo": 5, 161 | "Bar": 6, 162 | "baz": 7, 163 | 164 | }, 165 | "children": [ 166 | { 167 | "id": 2, 168 | "flags": ["function"], 169 | "bindings": { 170 | "self": 8, 171 | 172 | }, 173 | "children": [ 174 | { 175 | "id": 3, 176 | "flags": ["function","block"], 177 | "bindings": { 178 | 179 | }, 180 | "children": [], 181 | 182 | }, 183 | ], 184 | 185 | }, 186 | ], 187 | 188 | }, 189 | ], 190 | 191 | }, 192 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/stmt_test.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1,2], 15 | 16 | }, 17 | { 18 | "name": "std", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const"], 24 | "references": [ 25 | {"symbol":1,"scope":3,"node":"identifier","identifier":"std","flags":["call"]}, 26 | {"symbol":1,"scope":4,"node":"identifier","identifier":"std","flags":["call"]}, 27 | 28 | ], 29 | "members": [], 30 | "exports": [], 31 | 32 | }, 33 | { 34 | "name": "add", 35 | "debugName": "", 36 | "token": 9, 37 | "declNode": "fn_decl", 38 | "scope": 0, 39 | "flags": ["fn"], 40 | "references": [ 41 | {"symbol":2,"scope":3,"node":"identifier","identifier":"add","flags":["call"]}, 42 | {"symbol":2,"scope":4,"node":"identifier","identifier":"add","flags":["call"]}, 43 | {"symbol":2,"scope":4,"node":"identifier","identifier":"add","flags":["call"]}, 44 | 45 | ], 46 | "members": [], 47 | "exports": [], 48 | 49 | }, 50 | { 51 | "name": "a", 52 | "debugName": "", 53 | "token": 11, 54 | "declNode": "identifier", 55 | "scope": 1, 56 | "flags": ["const","fn_param"], 57 | "references": [ 58 | {"symbol":3,"scope":2,"node":"identifier","identifier":"a","flags":["read"]}, 59 | 60 | ], 61 | "members": [], 62 | "exports": [], 63 | 64 | }, 65 | { 66 | "name": "b", 67 | "debugName": "", 68 | "token": 15, 69 | "declNode": "identifier", 70 | "scope": 1, 71 | "flags": ["const","fn_param"], 72 | "references": [ 73 | {"symbol":4,"scope":2,"node":"identifier","identifier":"b","flags":["read"]}, 74 | 75 | ], 76 | "members": [], 77 | "exports": [], 78 | 79 | }, 80 | ], 81 | "unresolvedReferences": [], 82 | "modules": { 83 | "imports": [ 84 | { 85 | "specifier": "std", 86 | "node": 3, 87 | "kind": "module", 88 | 89 | }, 90 | ], 91 | }, 92 | "scopes": { 93 | "id": 0, 94 | "flags": ["top"], 95 | "bindings": { 96 | "@This()": 0, 97 | "std": 1, 98 | "add": 2, 99 | 100 | }, 101 | "children": [ 102 | { 103 | "id": 1, 104 | "flags": ["function"], 105 | "bindings": { 106 | "a": 3, 107 | "b": 4, 108 | 109 | }, 110 | "children": [ 111 | { 112 | "id": 2, 113 | "flags": ["function","block"], 114 | "bindings": { 115 | 116 | }, 117 | "children": [], 118 | 119 | }, 120 | ], 121 | 122 | }, { 123 | "id": 3, 124 | "flags": ["block","comptime","test"], 125 | "bindings": { 126 | 127 | }, 128 | "children": [], 129 | 130 | }, { 131 | "id": 4, 132 | "flags": ["block","comptime","test"], 133 | "bindings": { 134 | 135 | }, 136 | "children": [], 137 | 138 | }, 139 | ], 140 | 141 | }, 142 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/struct_members.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "Foo", 19 | "debugName": "", 20 | "token": 2, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const","struct"], 24 | "references": [ 25 | 26 | ], 27 | "members": [2], 28 | "exports": [3,4], 29 | 30 | }, 31 | { 32 | "name": "a", 33 | "debugName": "", 34 | "token": 6, 35 | "declNode": "container_field_init", 36 | "scope": 1, 37 | "flags": ["member","struct"], 38 | "references": [ 39 | 40 | ], 41 | "members": [], 42 | "exports": [], 43 | 44 | }, 45 | { 46 | "name": "B", 47 | "debugName": "", 48 | "token": 11, 49 | "declNode": "simple_var_decl", 50 | "scope": 1, 51 | "flags": ["variable","const"], 52 | "references": [ 53 | 54 | ], 55 | "members": [], 56 | "exports": [], 57 | 58 | }, 59 | { 60 | "name": "C", 61 | "debugName": "", 62 | "token": 19, 63 | "declNode": "simple_var_decl", 64 | "scope": 1, 65 | "flags": ["variable","const"], 66 | "references": [ 67 | 68 | ], 69 | "members": [], 70 | "exports": [], 71 | 72 | }, 73 | ], 74 | "unresolvedReferences": [], 75 | "modules": { 76 | "imports": [ 77 | ], 78 | }, 79 | "scopes": { 80 | "id": 0, 81 | "flags": ["top"], 82 | "bindings": { 83 | "@This()": 0, 84 | "Foo": 1, 85 | 86 | }, 87 | "children": [ 88 | { 89 | "id": 1, 90 | "flags": ["struct","block"], 91 | "bindings": { 92 | "a": 2, 93 | "B": 3, 94 | "C": 4, 95 | 96 | }, 97 | "children": [], 98 | 99 | }, 100 | ], 101 | 102 | }, 103 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/struct_tuple.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1,2,3,5,6], 15 | 16 | }, 17 | { 18 | "name": "x", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const"], 24 | "references": [ 25 | {"symbol":1,"scope":1,"node":"identifier","identifier":"x","flags":["type"]}, 26 | 27 | ], 28 | "members": [], 29 | "exports": [], 30 | 31 | }, 32 | { 33 | "name": "Foo", 34 | "debugName": "", 35 | "token": 6, 36 | "declNode": "simple_var_decl", 37 | "scope": 0, 38 | "flags": ["variable","const","struct"], 39 | "references": [ 40 | {"symbol":2,"scope":0,"node":"identifier","identifier":"Foo","flags":["type"]}, 41 | 42 | ], 43 | "members": [], 44 | "exports": [], 45 | 46 | }, 47 | { 48 | "name": "Namespace", 49 | "debugName": "", 50 | "token": 18, 51 | "declNode": "simple_var_decl", 52 | "scope": 0, 53 | "flags": ["variable","const","struct"], 54 | "references": [ 55 | {"symbol":3,"scope":3,"node":"identifier","identifier":"Namespace","flags":["type"]}, 56 | 57 | ], 58 | "members": [], 59 | "exports": [4], 60 | 61 | }, 62 | { 63 | "name": "Member", 64 | "debugName": "", 65 | "token": 23, 66 | "declNode": "simple_var_decl", 67 | "scope": 2, 68 | "flags": ["variable","const"], 69 | "references": [ 70 | 71 | ], 72 | "members": [], 73 | "exports": [], 74 | 75 | }, 76 | { 77 | "name": "Bar", 78 | "debugName": "", 79 | "token": 30, 80 | "declNode": "simple_var_decl", 81 | "scope": 0, 82 | "flags": ["variable","const","struct"], 83 | "references": [ 84 | 85 | ], 86 | "members": [], 87 | "exports": [], 88 | 89 | }, 90 | { 91 | "name": "f", 92 | "debugName": "", 93 | "token": 40, 94 | "declNode": "simple_var_decl", 95 | "scope": 0, 96 | "flags": ["variable","const"], 97 | "references": [ 98 | 99 | ], 100 | "members": [], 101 | "exports": [], 102 | 103 | }, 104 | ], 105 | "unresolvedReferences": [], 106 | "modules": { 107 | "imports": [ 108 | ], 109 | }, 110 | "scopes": { 111 | "id": 0, 112 | "flags": ["top"], 113 | "bindings": { 114 | "@This()": 0, 115 | "x": 1, 116 | "Foo": 2, 117 | "Namespace": 3, 118 | "Bar": 5, 119 | "f": 6, 120 | 121 | }, 122 | "children": [ 123 | { 124 | "id": 1, 125 | "flags": ["struct","block"], 126 | "bindings": { 127 | 128 | }, 129 | "children": [], 130 | 131 | }, { 132 | "id": 2, 133 | "flags": ["struct","block"], 134 | "bindings": { 135 | "Member": 4, 136 | 137 | }, 138 | "children": [], 139 | 140 | }, { 141 | "id": 3, 142 | "flags": ["struct","block"], 143 | "bindings": { 144 | 145 | }, 146 | "children": [], 147 | 148 | }, 149 | ], 150 | 151 | }, 152 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/top_level_struct.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [1,2], 14 | "exports": [3,4], 15 | 16 | }, 17 | { 18 | "name": "a", 19 | "debugName": "", 20 | "token": 0, 21 | "declNode": "container_field_init", 22 | "scope": 0, 23 | "flags": ["member","struct"], 24 | "references": [ 25 | 26 | ], 27 | "members": [], 28 | "exports": [], 29 | 30 | }, 31 | { 32 | "name": "b", 33 | "debugName": "", 34 | "token": 4, 35 | "declNode": "container_field_init", 36 | "scope": 0, 37 | "flags": ["member","struct"], 38 | "references": [ 39 | 40 | ], 41 | "members": [], 42 | "exports": [], 43 | 44 | }, 45 | { 46 | "name": "Foo", 47 | "debugName": "", 48 | "token": 14, 49 | "declNode": "simple_var_decl", 50 | "scope": 0, 51 | "flags": ["variable","const"], 52 | "references": [ 53 | {"symbol":3,"scope":1,"node":"identifier","identifier":"Foo","flags":["type"]}, 54 | {"symbol":3,"scope":2,"node":"identifier","identifier":"Foo","flags":["read"]}, 55 | 56 | ], 57 | "members": [], 58 | "exports": [], 59 | 60 | }, 61 | { 62 | "name": "new", 63 | "debugName": "", 64 | "token": 22, 65 | "declNode": "fn_decl", 66 | "scope": 0, 67 | "flags": ["fn"], 68 | "references": [ 69 | 70 | ], 71 | "members": [], 72 | "exports": [], 73 | 74 | }, 75 | ], 76 | "unresolvedReferences": [], 77 | "modules": { 78 | "imports": [ 79 | ], 80 | }, 81 | "scopes": { 82 | "id": 0, 83 | "flags": ["top"], 84 | "bindings": { 85 | "@This()": 0, 86 | "a": 1, 87 | "b": 2, 88 | "Foo": 3, 89 | "new": 4, 90 | 91 | }, 92 | "children": [ 93 | { 94 | "id": 1, 95 | "flags": ["function"], 96 | "bindings": { 97 | 98 | }, 99 | "children": [ 100 | { 101 | "id": 2, 102 | "flags": ["function","block"], 103 | "bindings": { 104 | 105 | }, 106 | "children": [], 107 | 108 | }, 109 | ], 110 | 111 | }, 112 | ], 113 | 114 | }, 115 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/unresolved_import.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "foo", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const"], 24 | "references": [ 25 | 26 | ], 27 | "members": [], 28 | "exports": [], 29 | 30 | }, 31 | ], 32 | "unresolvedReferences": [], 33 | "modules": { 34 | "imports": [ 35 | { 36 | "specifier": "this/file/doesnotexist.zig", 37 | "node": 3, 38 | "kind": "file", 39 | 40 | }, 41 | ], 42 | }, 43 | "scopes": { 44 | "id": 0, 45 | "flags": ["top"], 46 | "bindings": { 47 | "@This()": 0, 48 | "foo": 1, 49 | 50 | }, 51 | "children": [], 52 | 53 | }, 54 | } -------------------------------------------------------------------------------- /test/test_e2e.zig: -------------------------------------------------------------------------------- 1 | const test_runner = @import("harness/runner.zig"); 2 | 3 | 4 | // Allows recovery from panics in test cases. Errors get saved to that suite's 5 | // snapshot file, and testing continues. 6 | pub const panic = @import("recover").panic; 7 | 8 | // test suites 9 | const semantic_coverage = @import("semantic/ecosystem_coverage.zig"); 10 | const snapshot_coverage = @import("semantic/snapshot_coverage.zig"); 11 | 12 | pub fn main() !void { 13 | const runner = test_runner.getRunner(); 14 | defer runner.deinit(); 15 | try runner 16 | .addTest(semantic_coverage.SUITE) 17 | .addTest(snapshot_coverage.SUITE) 18 | .runAll(); 19 | } 20 | -------------------------------------------------------------------------------- /zls.json: -------------------------------------------------------------------------------- 1 | { 2 | "enable_build_on_save": true, 3 | "build_on_save_step": "check" 4 | } 5 | --------------------------------------------------------------------------------