diff --git a/.cf-studio/config/rules/infrastructure.md b/.cf-studio/config/rules/infrastructure.md index 1c9f1e72f..7876d2967 100644 --- a/.cf-studio/config/rules/infrastructure.md +++ b/.cf-studio/config/rules/infrastructure.md @@ -19,7 +19,7 @@ Use this when changing build tooling, CI, linting, releases, or dependency polic ## Tooling - Use `Makefile` targets as the main local automation surface. Evidence: `Makefile:120-173`, `Makefile:218-320` - Preserve fast PR Clippy and deeper validation split. Evidence: `Makefile:224-246` -- Keep custom Dylint rules aligned with architecture categories. Evidence: `tools/dylint_lints/README.md:16-70` +- Keep custom architecture lints aligned with architecture categories. Run via `cargo gears lint` (lints live in the `cargo-gears` CLI tool). ## CI and Releases - Preserve cross-OS test matrix and DB integration jobs. Evidence: `.github/workflows/ci.yml:85-220` diff --git a/.claude/skills/gear-creator/SKILL.md b/.claude/skills/gear-creator/SKILL.md index 15a6fce7e..66fb173b5 100644 --- a/.claude/skills/gear-creator/SKILL.md +++ b/.claude/skills/gear-creator/SKILL.md @@ -89,7 +89,7 @@ Create files in this order (each layer builds on the previous): 1. `cargo build -p ` — must compile 2. `cargo clippy -p ` — no warnings -3. `make dylint` — architecture lints pass +3. `cargo gears lint` — architecture lints pass 4. `cargo test -p ` — tests pass ## Workflow: Edit an existing gear diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d076f8772..e38cb4080 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -392,45 +392,30 @@ jobs: fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} # Uncomment if required for private repos - dylint: - name: Dylint Tests + lint: + name: Architecture Lints (cargo gears lint) runs-on: ubuntu-latest - env: - RUSTUP_TOOLCHAIN: nightly-2026-04-16 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install Rust nightly toolchain - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly with: - toolchain: ${{ env.RUSTUP_TOOLCHAIN }} - components: llvm-tools-preview,rustc-dev - - - name: Set Rust nightly for Dylint temp builds - run: rustup override set ${{ env.RUSTUP_TOOLCHAIN }} --path /tmp - - - name: Install nextest - uses: taiki-e/install-action@6ef672efc2b5aabc787a9e94baf4989aa02a97df # v2.70.3 - with: - tool: nextest - - - name: Install cargo-dylint and dylint-link from source - run: cargo install --locked cargo-dylint dylint-link + persist-credentials: false - name: Install protoc uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Run dylint tests - working-directory: tools/dylint_lints - env: - RUSTFLAGS: "-C debuginfo=0" - run: cargo test + - name: Install Rust stable toolchain + uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # stable + with: + toolchain: stable - - name: Run dylint on all crates - run: make dylint + - name: Install cargo-gears + run: cargo install --locked cargo-gears + + - name: Run architecture lints + run: cargo gears lint --dylint shear: name: Unused Deps (cargo-shear) @@ -454,13 +439,10 @@ jobs: toolchain: ${{ env.RUSTUP_TOOLCHAIN }} components: llvm-tools-preview,rustc-dev - - name: Install dylint-link from source - run: cargo install --locked dylint-link - - name: Install cargo-shear uses: taiki-e/install-action@6ef672efc2b5aabc787a9e94baf4989aa02a97df # v2.70.3 with: tool: cargo-shear@1.13.1 - - name: cargo shear (workspace root + tools/dylint_lints) + - name: cargo shear (workspace root) run: make shear diff --git a/Cargo.toml b/Cargo.toml index 91f8faf65..b21181cc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,9 +109,6 @@ members = [ exclude = ["tools/fuzz"] resolver = "3" -# Note: dylint_lints is a separate workspace for custom linters -# See tools/dylint_lints/README.md for usage instructions - [workspace.lints.rust] deprecated = "warn" non_ascii_idents = "forbid" diff --git a/Gears.toml b/Gears.toml new file mode 100644 index 000000000..4e8a3ca08 --- /dev/null +++ b/Gears.toml @@ -0,0 +1,25 @@ +# Gears manifest. Used by `cargo gears` CLI for linting and other tasks. +# See https://github.com/constructorfabric/cargo-gears for documentation. + +[apps.gears-rust.dev] +config = "lint.yml" +gears = [] + +[apps.gears-rust.dev.lint] +fmt = false # already handled by `make fmt` +clippy = false # already handled by `make clippy` + +[apps.gears-rust.dev.lint.dylint] +enabled = true +# Remove entries as violations are fixed or cargo-gears-lints is updated. +skip = [ + # New lint not yet addressed in this repo. + "de0504_client_versioning", + # Path exclusion logic in cargo-gears-lints uses "modules/" prefix but this + # repo uses "gears/". Skipped until cargo-gears-lints adds "gears/" support. + "de1101_tests_in_separate_files", + # Scope broadened from /contract/ to /domain/ in cargo-gears-lints. + # Pre-existing code in domain/ layers needs migration. + "de0101_no_serde_in_contract", + "de0102_no_toschema_in_contract", +] diff --git a/Makefile b/Makefile index c071fb2c8..d75b9e881 100644 --- a/Makefile +++ b/Makefile @@ -138,8 +138,7 @@ setup: .setup-stamp cargo install lychee cargo install cargo-geiger cargo install cargo-deny - cargo install cargo-dylint - cargo install dylint-link + cargo install cargo-gears cargo install cargo-fuzz cargo install cargo-hack cargo install gts-validator @@ -169,7 +168,6 @@ setup: .setup-stamp fmt: $(call check_rustup_component,rustfmt) cargo fmt --all --check - cargo fmt --all --check --manifest-path tools/dylint_lints/Cargo.toml # -------- Gear naming validation -------- @@ -213,16 +211,8 @@ validate-gear-names: # | | - Missing documentation warnings | # | | - Ensures clean compilation across all targets and features | # +-------------+----------------------------------------------------------------------+ -# | dylint | - Project-specific architectural conventions (custom lints) | -# | | - DTO declaration and placement (only in api/rest folders) | -# | | - DTO isolation (no references from domain/contract layers) | -# | | - API endpoint versioning requirements (e.g., /users/v1/users) | -# | | - Contract layer purity (no serde, HTTP types, or ToSchema) | -# | | - Layer separation and dependency rules enforcement | -# | | - Use 'make dylint-list' to see all available custom lints | -# +-------------+----------------------------------------------------------------------+ -.PHONY: clippy clippy-deep lychee docs-preview kani geiger safety lint dylint dylint-list dylint-test shear gts-docs cfs-ensure cfs-repair cfs-validate cfs-validate-kits cfs-validate-kit-local cfs-spec-coverage +.PHONY: clippy clippy-deep lychee docs-preview kani geiger safety lint dylint shear gts-docs cfs-ensure cfs-repair cfs-validate cfs-validate-kits cfs-validate-kit-local cfs-spec-coverage CFS ?= cfs CFS_PIPX_SPEC ?= git+https://github.com/constructorfabric/studio.git @@ -297,7 +287,7 @@ cfs-validate-kit-local: cfs-repair # Run markdown checks with 'lychee' lychee: $(call check_tool,lychee) - lychee --exclude-path 'docs/web-docs' docs examples tools/dylint_lints guidelines + lychee --exclude-path 'docs/web-docs' docs examples guidelines # Preview the documentation website with local docs/web-docs content. # Clones the web docs site into .web-docs-preview/ and serves it at localhost:4321. @@ -335,34 +325,15 @@ gts-docs: install-tools: @command -v cargo-nextest >/dev/null 2>&1 || cargo install --locked cargo-nextest -## List all custom project compliance lints (see tools/dylint_lints/README.md) -dylint-list: - @cd tools/dylint_lints && \ - DYLINT_LIBS=$$(find target/release -maxdepth 1 \( -name "libde*@*.so" -o -name "libde*@*.dylib" -o -name "de*@*.dll" \) -type f | sort -u); \ - if [ -z "$$DYLINT_LIBS" ]; then \ - echo "ERROR: No dylint libraries found. Run 'make dylint' first to build them."; \ - exit 1; \ - fi; \ - for lib in $$DYLINT_LIBS; do \ - echo "=== $$lib ==="; \ - cargo dylint list --lib-path "$$lib"; \ - done - -## Test dylint lints on UI test cases (compile and verify violations) -dylint-test: install-tools - @cd tools/dylint_lints && cargo nextest run - -# Run project compliance dylint lints on the workspace (see `make dylint-list`) +# Run architecture lints via cargo-gears (see Gears.toml for configuration). dylint: - $(call check_tool,cargo-dylint) - $(call check_tool,dylint-link) - cargo dylint --all --workspace + $(call check_tool,cargo-gears) + cargo gears lint --dylint # Check for unused dependencies with cargo-shear. shear: $(call check_tool,cargo-shear) cargo +nightly-2026-04-16 shear --expand --deny-warnings - cd tools/dylint_lints && cargo shear --expand --deny-warnings # Run all code safety checks safety: clippy kani lint dylint # geiger @@ -428,7 +399,6 @@ dev-fmt: ## Auto-fix clippy warnings dev-clippy: cargo clippy --workspace --all-targets --all-features --fix --allow-dirty - @cd tools/dylint_lints && cargo clippy --all-targets --workspace # Auto-fix formatting and clippy warnings dev: dev-fmt dev-clippy dev-test @@ -815,14 +785,14 @@ oop-example: cargo run --bin cf-gears-example-server --features oop-example,users-info-example,static-authn,static-authz,static-tenants,static-credstore -- --config config/quickstart.yaml run # Run all quality checks -check: .setup-stamp fmt cfs-validate clippy lychee security dylint-test dylint gts-docs test +check: .setup-stamp fmt cfs-validate clippy lychee security dylint gts-docs test ci_test: fmt clippy ci_docs: lychee gts-docs # Run CI pipeline locally, requires docker -ci: fmt clippy test-no-macros test-macros test-db deny test-users-info-pg lychee gts-docs dylint dylint-test +ci: fmt clippy test-no-macros test-macros test-db deny test-users-info-pg lychee gts-docs dylint # Build the cf-gears-example-server release binary using a toolchain from the rust-toolchain.toml cargo-build: diff --git a/README.md b/README.md index cba37b758..d9529f410 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ See also: **Engineering principles:** - **Spec-Driven Development**: [Specification templates](docs/spec-templates/README.md) (PRD, Design, ADR, Feature) define what gets built *before* code is written. Every gear is well documented. -- **Shift Left**: Custom [dylint](tools/dylint_lints/) architectural lints enforce design rules at compile time, alongside Clippy, [tests](#testing), fuzzing, and security audits in CI +- **Shift Left**: Custom architecture lints (via `cargo gears lint`) enforce design rules at compile time, alongside Clippy, [tests](#testing), fuzzing, and security audits in CI - **Quality First**: 90%+ test coverage target with unit, integration, E2E, performance, and security testing - **Core in Rust**: Compile-time safety, deep static analysis including project-specific lints, so more issues are prevented before review/runtime - **Monorepo**: All the core gears and contracts in one place for atomic refactors, consistent tooling/CI, and realistic local build + E2E testing @@ -145,7 +145,7 @@ See [TOOLKIT UNIFIED SYSTEM](docs/toolkit_unified_system/README.md) and [TOOLKIT Gears apply defense-in-depth security across the entire development lifecycle — from Rust's compile-time safety guarantees and custom architectural lints, through compile-time tenant isolation and PDP/PEP authorization enforcement, to continuous fuzzing, dependency auditing, and automated security scanning in CI. -See **[Security Overview](docs/security/SECURITY.md)** for the full breakdown, including: Secure ORM with compile-time tenant scoping, authentication/authorization architecture (NIST SP 800-162 PDP/PEP model), 90+ Clippy deny-level rules, custom dylint architectural lints, cargo-deny advisory checks, ClusterFuzzLite continuous fuzzing, CodeQL/Scorecard/Snyk/Aikido scanners, and AI-powered PR review bots. +See **[Security Overview](docs/security/SECURITY.md)** for the full breakdown, including: Secure ORM with compile-time tenant scoping, authentication/authorization architecture (NIST SP 800-162 PDP/PEP model), 90+ Clippy deny-level rules, custom architecture lints (via `cargo gears lint`), cargo-deny advisory checks, ClusterFuzzLite continuous fuzzing, CodeQL/Scorecard/Snyk/Aikido scanners, and AI-powered PR review bots. ## FIPS 140-3 support diff --git a/config/lint.yml b/config/lint.yml new file mode 100644 index 000000000..439451610 --- /dev/null +++ b/config/lint.yml @@ -0,0 +1,2 @@ +# Minimal config file required by the Gears manifest for linting. +server: {} diff --git a/docs/ARCHITECTURE_MANIFEST.md b/docs/ARCHITECTURE_MANIFEST.md index 0c7fec074..d1f7b45fe 100644 --- a/docs/ARCHITECTURE_MANIFEST.md +++ b/docs/ARCHITECTURE_MANIFEST.md @@ -52,13 +52,13 @@ The architecture makes the insecure path harder than the secure one. Gear develo #### 3.1.2. Architecture enforced at compile time -Constructor Fabric Gears treats custom static analysis as a core architectural mechanism, not a best-effort coding aid. Architectural boundaries, API conventions, GTS usage rules, and security restrictions are enforced during builds through repository-specific Dylint rules. +Constructor Fabric Gears treats custom static analysis as a core architectural mechanism, not a best-effort coding aid. Architectural boundaries, API conventions, GTS usage rules, and security restrictions are enforced during builds through custom architecture lints shipped in the `cargo-gears` CLI. -**How.** The workspace includes `tools/dylint_lints/`, a dedicated Dylint suite that checks contract-layer purity, DTO placement and schema derives, domain-layer isolation, direct SQL restrictions, versioned REST paths, mandatory `OperationBuilder` metadata, OData extension usage, GTS identifier correctness, and other cross-cutting rules. These lints run alongside the normal Rust toolchain and CI checks, which means architectural violations fail fast before review or runtime. +**How.** The `cargo-gears` CLI (`cargo gears lint`) ships a dedicated lint suite that checks contract-layer purity, DTO placement and schema derives, domain-layer isolation, direct SQL restrictions, versioned REST paths, mandatory `OperationBuilder` metadata, OData extension usage, GTS identifier correctness, and other cross-cutting rules. These lints run alongside the normal Rust toolchain and CI checks, which means architectural violations fail fast before review or runtime. This is a shift-left quality mechanism: the repository pushes correctness, consistency, and architecture conformance into compile-time and CI-time validation rather than relying only on code review. -**Why.** In a large modular platform, architecture decays quickly if it lives only in markdown. Dylint makes the desired structure executable and keeps both human contributors and AI-assisted changes inside the intended design envelope. +**Why.** In a large modular platform, architecture decays quickly if it lives only in markdown. Custom architecture lints make the desired structure executable and keep both human contributors and AI-assisted changes inside the intended design envelope. ### 3.2. Three-tier gear hierarchy @@ -74,7 +74,7 @@ See: [GEARS.md](GEARS.md) Constructor Fabric Gears follows a DDD-light structure in which domain logic is kept free from transport and infrastructure details, while REST/gRPC adapters and infra layers handle boundary-specific concerns. -**How.** The standard gear layout separates SDK contracts, gear bootstrap, domain logic, API adapters, and infrastructure. Domain types and services live under `domain/`, REST DTOs and route wiring stay in API-facing layers, and persistence/integration logic stays in infra. This boundary is reinforced not only by structure but also by custom Dylints and the `#[domain_model]` macro requirement for domain-layer types. +**How.** The standard gear layout separates SDK contracts, gear bootstrap, domain logic, API adapters, and infrastructure. Domain types and services live under `domain/`, REST DTOs and route wiring stay in API-facing layers, and persistence/integration logic stays in infra. This boundary is reinforced not only by structure but also by custom architecture lints (via `cargo gears lint`) and the `#[domain_model]` macro requirement for domain-layer types. **Why.** Business logic stays easier to test, reuse, and evolve because it is not entangled with HTTP, database, or framework details. At the same time, adapter code remains explicit about where transport translation and persistence concerns begin. @@ -132,7 +132,7 @@ Constructor Fabric Gears separates service logic from service packaging. Gear lo Constructor Fabric Gears does not treat HTTP shape, query conventions, and API description as local stylistic choices. Gears follow one API style built around versioned paths, typed route registration, shared middleware, OpenAPI generation, and standard query patterns such as OData for filtering and ordering. -**How.** `OperationBuilder` is the authoritative route-registration mechanism in ToolKit. A route declares method, versioned path, auth posture, license posture, request schema, response schema, tags, summary, and registered error responses in one place. `OpenApiRegistry` collects these declarations into the generated `/openapi.json`. For query shape, ToolKit exposes OData helpers such as `with_odata_filter`, `with_odata_orderby`, and `with_odata_select`, and workspace Dylints enforce that REST endpoints use the standardized extension methods rather than ad-hoc query conventions. +**How.** `OperationBuilder` is the authoritative route-registration mechanism in ToolKit. A route declares method, versioned path, auth posture, license posture, request schema, response schema, tags, summary, and registered error responses in one place. `OpenApiRegistry` collects these declarations into the generated `/openapi.json`. For query shape, ToolKit exposes OData helpers such as `with_odata_filter`, `with_odata_orderby`, and `with_odata_select`, and custom architecture lints enforce that REST endpoints use the standardized extension methods rather than ad-hoc query conventions. This produces one recognizable API dialect across gears: @@ -222,7 +222,7 @@ Rust is a strong fit for CF/Gears implementation because this repository is buil - Libraries such as ToolKit, security layers, transport layers, and registries benefit from predictable performance and explicit interfaces. - **Static analysis as part of architecture** - - Rust's ecosystem, combined with Clippy and custom Dylints, allows many project rules to become enforceable at build time. + - Rust's ecosystem, combined with Clippy and custom architecture lints, allows many project rules to become enforceable at build time. - **Operational efficiency** - A low-footprint runtime makes it practical to run realistic local/edge systems, end-to-end tests, and service combinations without depending on heavyweight environments. @@ -414,7 +414,7 @@ Security in Constructor Fabric Gears spans the language choice, gear boundaries, - `credstore` (`gears/credstore/`) and secrecy-aware types are present for secret handling. - [x] **Static and CI security gates** - - Clippy, custom Dylints, `cargo-deny`, CodeQL, fuzzing, and related scanners are part of the repo. + - Clippy, custom architecture lints (via `cargo gears lint`), `cargo-deny`, CodeQL, fuzzing, and related scanners are part of the CI pipeline. ### 10.2 Tenant Data Model @@ -473,7 +473,7 @@ See more details in: [arch/authorization/DESIGN.md](arch/authorization/DESIGN.md - [x] `OpenApiRegistry` and `OperationBuilder` are implemented in ToolKit. - [x] `/openapi.json` and `/docs` are served by the API gateway. - [x] OData extensions are implemented in `OperationBuilder` for standardized `$filter`, `$select`, and `$orderby` support. -- [x] Workspace Dylints enforce versioned endpoints and standardized OData extension usage. +- [x] Custom architecture lints enforce versioned endpoints and standardized OData extension usage. The important architectural point is that OpenAPI is generated from the same Rust route declarations that wire the running service. Constructor Fabric Gears does not maintain a separate hand-authored HTTP contract description. @@ -482,7 +482,7 @@ The important architectural point is that OpenAPI is generated from the same Rus - [x] GTS schemas can be generated directly from Rust types - [x] Plugin specifications already use this pattern in SDK crates such as `authn-resolver-sdk` and `mini-chat-sdk`. - [x] Generated GTS JSON Schemas are intended for registration in the Types Registry. -- [x] GTS-specific Dylints validate identifier correctness and prevent unsupported schema-generation patterns such as `schema_for!` on GTS structs. +- [x] GTS-specific architecture lints validate identifier correctness and prevent unsupported schema-generation patterns such as `schema_for!` on GTS structs. This is the non-HTTP counterpart to OpenAPI generation. OpenAPI describes REST endpoints; GTS-generated JSON Schema describes platform contracts and typed data beyond REST, including plugin specs, events, and other globally identified contracts. Together they let Constructor Fabric Gears derive both API and non-API contracts from Rust source rather than duplicating schemas manually. diff --git a/docs/REPO_PLAYBOOK.md b/docs/REPO_PLAYBOOK.md index 509486076..6a8188166 100644 --- a/docs/REPO_PLAYBOOK.md +++ b/docs/REPO_PLAYBOOK.md @@ -30,7 +30,7 @@ Purpose: one concise map of repository artifacts that improve developer + AI pro | Architecture overview | [x] `p1` | [docs/ARCHITECTURE_MANIFEST.md](./ARCHITECTURE_MANIFEST.md), [docs/GEARS.md ](./GEARS.md ) | Add “current vs target” split blocks | | System diagrams | [x] `p1` | [docs/img/](./img), [docs/ARCHITECTURE_MANIFEST.md](./ARCHITECTURE_MANIFEST.md), [docs/GEARS.md ](./GEARS.md ) | Add ownership + update cadence per diagram | | Component responsibilities | [x] `p1` | [docs/GEARS.md ](./GEARS.md ), [docs/toolkit_unified_system/README.md](./toolkit_unified_system/README.md) | Add per-gear responsibility cards | -| Gear boundaries | [x] `p1` | [docs/GEARS.md ](./GEARS.md ), [docs/toolkit_unified_system/](./toolkit_unified_system/README.md), [tools/dylint_lints/README.md](../tools/dylint_lints/README.md) | Expand lint coverage for boundary rules | +| Gear boundaries | [x] `p1` | [docs/GEARS.md ](./GEARS.md ), [docs/toolkit_unified_system/](./toolkit_unified_system/README.md), `cargo gears lint` | Expand lint coverage for boundary rules | | Technology choices | [x] `p1` | [README.md](../README.md), [docs/ARCHITECTURE_MANIFEST.md](./ARCHITECTURE_MANIFEST.md), [guidelines/DEPENDENCIES.md](../guidelines/DEPENDENCIES.md) | Add technology decision registry page | | Data flow | [x] `p2` | [docs/GEARS.md ](./GEARS.md ) and [docs/spec-templates/gears-sdlc/DESIGN/template.md](./spec-templates/gears-sdlc/DESIGN/template.md) | Add dedicated sequence-diagram doc set | @@ -40,7 +40,7 @@ Purpose: one concise map of repository artifacts that improve developer + AI pro |---|---|---|---| | Repository structure | [x] `p1` | [README.md](../README.md), [docs/ARCHITECTURE_MANIFEST.md](./ARCHITECTURE_MANIFEST.md), [docs/toolkit_unified_system/](./toolkit_unified_system/README.md) | Keep in sync with workspace changes | | Folder conventions | [x] `p1` | [docs/toolkit_unified_system/](./toolkit_unified_system/README.md) | Add root-level `REPO_STRUCTURE.md` | -| Naming conventions | [x] `p1` | [docs/toolkit_unified_system/](./toolkit_unified_system/README.md), [tools/scripts/validate_gear_names.py](../tools/scripts/validate_gear_names.py), [tools/dylint_lints/](../tools/dylint_lints) | Expand naming rules beyond gears | +| Naming conventions | [x] `p1` | [docs/toolkit_unified_system/](./toolkit_unified_system/README.md), [tools/scripts/validate_gear_names.py](../tools/scripts/validate_gear_names.py), `cargo gears lint` | Expand naming rules beyond gears | | Code organization rules | [x] `p1` | [docs/toolkit_unified_system/](./toolkit_unified_system/README.md), [docs/toolkit_unified_system/README.md](./toolkit_unified_system/README.md) | Add short “golden-path skeleton” page | | Dependency policies | [x] `p1` | [guidelines/DEPENDENCIES.md](../guidelines/DEPENDENCIES.md), [docs/security/SECURITY.md](./security/SECURITY.md) | Add explicit approval policy for new deps | | File naming rules | [x] `p2` | [docs/spec-templates/README.md](./spec-templates/README.md) (ADR/feature naming), gear file layout in [docs/toolkit_unified_system/](./toolkit_unified_system/README.md) | Add global naming matrix | @@ -50,11 +50,11 @@ Purpose: one concise map of repository artifacts that improve developer + AI pro | Item | Status / Phase / ID | Implemented (where) | Planned | |---|---|---|---| | Coding standards | [x] `p1` | [guidelines/README.md](../guidelines/README.md), [CONTRIBUTING.md](../CONTRIBUTING.md) | Add short one-page standards index | -| Style guide | [x] `p1` | clippy rules in [clippy.toml](../clippy.toml) and [Cargo.toml](../Cargo.toml), `cargo fmt` in [Makefile](../Makefile), dylint rules in [tools/dylint_lints/README.md](../tools/dylint_lints/README.md) | Expand language-agnostic style section | -| Lint rules | [x] `p1` | [tools/dylint_lints/README.md](../tools/dylint_lints/README.md), [Makefile](../Makefile), [tools/scripts/ci.py](../tools/scripts/ci.py) | Add lint policy matrix by layer | +| Style guide | [x] `p1` | clippy rules in [clippy.toml](../clippy.toml) and [Cargo.toml](../Cargo.toml), `cargo fmt` in [Makefile](../Makefile), architecture lint rules via `cargo gears lint` | Expand language-agnostic style section | +| Lint rules | [x] `p1` | `cargo gears lint` (in `cargo-gears` CLI), [Makefile](../Makefile), [tools/scripts/ci.py](../tools/scripts/ci.py) | Add lint policy matrix by layer | | Formatting rules | [x] `p1` | [Makefile](../Makefile), [tools/scripts/ci.py](../tools/scripts/ci.py) | Add editor setup snippets | | Documentation standards | [x] `p1` | [docs/spec-templates/README.md](./spec-templates/README.md), [docs/checklists/README.md](./checklists/README.md) | Add docs style/lint enforcement rules | -| Static analysis rules | [x] `p2` | [docs/security/SECURITY.md](./security/SECURITY.md), [tools/dylint_lints/](../tools/dylint_lints), [.github/workflows/codeql.yml](../.github/workflows/codeql.yml) | Add local static-analysis quickstart | +| Static analysis rules | [x] `p2` | [docs/security/SECURITY.md](./security/SECURITY.md), `cargo gears lint`, [.github/workflows/codeql.yml](../.github/workflows/codeql.yml) | Add local static-analysis quickstart | | Code complexity rules | [x] `p2` | Clippy `cognitive_complexity` (threshold: 20) in workspace [Cargo.toml](../Cargo.toml), [clippy.toml](../clippy.toml) | Add per-gear complexity budget | | Commenting rules | [ ] `p3` | Partial conventions in existing guidelines | Add explicit comment policy document | | README standards | [ ] `p3` | Implicit via gear QUICKSTART guidance in [docs/toolkit_unified_system/](./toolkit_unified_system/README.md) | Add README template + required sections | @@ -120,7 +120,7 @@ Purpose: one concise map of repository artifacts that improve developer + AI pro | Integration testing rules | [x] `p1` | [CONTRIBUTING.md](../CONTRIBUTING.md), `ci.py all` flow | Add integration test standards page | | End-to-end testing rules | [x] `p1` | [README.md](../README.md), [tools/scripts/ci.py](../tools/scripts/ci.py), `.github/workflows/e2e.yml` | Add e2e flakiness policy | | Coverage expectations | [x] `p1` | [README.md](../README.md), [CONTRIBUTING.md](../CONTRIBUTING.md), [Makefile](../Makefile) | Enforce threshold gates in CI | -| Test structure | [x] `p2` | [docs/toolkit_unified_system/](./toolkit_unified_system/README.md), [tools/dylint_lints/AGENTS.md](../tools/dylint_lints/AGENTS.md) | Add repository-wide test taxonomy | +| Test structure | [x] `p2` | [docs/toolkit_unified_system/](./toolkit_unified_system/README.md), architecture lints via `cargo gears lint` | Add repository-wide test taxonomy | | Test data management | [ ] `p3` | Not centralized | Add test-fixture lifecycle guide | ## 11) Debugging, Logging & Observability @@ -145,7 +145,7 @@ Purpose: one concise map of repository artifacts that improve developer + AI pro | Dependency security rules | [x] `p1` | [docs/security/SECURITY.md](./security/SECURITY.md), [Makefile](../Makefile), `cargo deny` | Add allow/deny decision log | | Vulnerability response | [x] `p1` | [SECURITY.md](../SECURITY.md) | Add incident severity matrix | | Secure ORM tenant scoping | [x] `p2` | [docs/security/SECURITY.md](./security/SECURITY.md), [docs/toolkit_unified_system/06_authn_authz_secure_orm.md](./toolkit_unified_system/06_authn_authz_secure_orm.md) | Add security-context propagation verification checks | -| Static security linting (Clippy + Dylint) | [x] `p2` | [docs/security/SECURITY.md](./security/SECURITY.md), [tools/dylint_lints/README.md](../tools/dylint_lints/README.md), [clippy.toml](../clippy.toml) | Expand security-focused lint set | +| Static security linting (Clippy + architecture lints) | [x] `p2` | [docs/security/SECURITY.md](./security/SECURITY.md), `cargo gears lint`, [clippy.toml](../clippy.toml) | Expand security-focused lint set | | Secrets handling | [x] `p2` | [docs/security/SECURITY.md](./security/SECURITY.md), [docs/pr-review/README.md](./pr-review/README.md) token guidance | Add repository-wide secrets policy doc | | Data protection rules | [x] `p2` | [docs/security/SECURITY.md](./security/SECURITY.md), secure ORM docs | Add data classification policy | | Access policies | [x] `p2` | [docs/security/SECURITY.md](./security/SECURITY.md), auth architecture docs | Add policy authoring guide | @@ -202,8 +202,8 @@ Purpose: one concise map of repository artifacts that improve developer + AI pro | Code templates | [x] `p2` | [docs/toolkit_unified_system/](./toolkit_unified_system/README.md) gear skeletons/patterns | Add dedicated starter templates folder | | PR templates | [x] `p2` | [CONTRIBUTING.md](../CONTRIBUTING.md) PR description template, [docs/pr-review/code-review-template.md](./pr-review/code-review-template.md) | Add `.github/PULL_REQUEST_TEMPLATE.md` | | Reference implementations | [x] `p2` | [examples/toolkit](../examples/toolkit), [docs/toolkit_unified_system/](./toolkit_unified_system/README.md) | Curate “golden reference gears” list | -| Good examples | [x] `p2` | Lint/gear examples in [tools/dylint_lints/README.md](../tools/dylint_lints/README.md), [examples/](../examples) | Add explicit tagged good examples index | -| Bad examples | [x] `p2` | Dylint bad patterns in [tools/dylint_lints/README.md](../tools/dylint_lints/README.md) + UI tests | Add cross-domain anti-pattern examples | +| Good examples | [x] `p2` | Lint/gear examples in `cargo gears lint` rules, [examples/](../examples) | Add explicit tagged good examples index | +| Bad examples | [x] `p2` | Architecture lint bad patterns + UI tests (in `cargo-gears` CLI) | Add cross-domain anti-pattern examples | | Release checklist | [ ] `p3` | Partial in [docs/RELEASING.md](./RELEASING.md) | Add explicit release checklist doc | | Debug checklist | [ ] `p3` | Not formalized | Add debug triage checklist | | Issue templates | [ ] `p3` | Not found in `.github` | Add GitHub issue templates | @@ -213,12 +213,12 @@ Purpose: one concise map of repository artifacts that improve developer + AI pro | Item | Status / Phase / ID | Implemented (where) | Planned | |---|---|---|---| | API guidelines | [x] `p1` | [guidelines/README.md](../guidelines/README.md), [docs/toolkit_unified_system/](./toolkit_unified_system/README.md), ToolKit docs | Add API design quick-reference | -| API versioning | [x] `p1` | Dylint DE0801 in [tools/dylint_lints/README.md](../tools/dylint_lints/README.md), [CONTRIBUTING.md](../CONTRIBUTING.md) | Add auto-check for docs/version sync | -| Contract rules | [x] `p1` | Dylint DE01xx/DE02xx/DE03xx in [tools/dylint_lints/README.md](../tools/dylint_lints/README.md) | Expand contract lint set | +| API versioning | [x] `p1` | Architecture lint DE0801 (via `cargo gears lint`), [CONTRIBUTING.md](../CONTRIBUTING.md) | Add auto-check for docs/version sync | +| Contract rules | [x] `p1` | Architecture lints DE01xx/DE02xx/DE03xx (via `cargo gears lint`) | Expand contract lint set | | Error handling standards | [x] `p1` | [docs/toolkit_unified_system/05_errors_rfc9457.md](./toolkit_unified_system/05_errors_rfc9457.md), [docs/toolkit_unified_system/](./toolkit_unified_system/README.md) | Add repository-wide error taxonomy | | Configuration management | [x] `p1` | [README.md](../README.md) config section, [docs/toolkit_unified_system/](./toolkit_unified_system/README.md) | Add config schema validation policy | | Data model conventions | [x] `p2` | [docs/toolkit_unified_system/](./toolkit_unified_system/README.md), [docs/toolkit_unified_system/02_gear_layout_and_sdk_pattern.md](./toolkit_unified_system/02_gear_layout_and_sdk_pattern.md) | Add canonical model naming matrix | -| Schema rules | [x] `p2` | GTS + OData + OpenAPI references in ToolKit docs and dylint DE09xx | Add schema compatibility checklist | +| Schema rules | [x] `p2` | GTS + OData + OpenAPI references in ToolKit docs and architecture lints DE09xx | Add schema compatibility checklist | | Migration rules | [x] `p2` | Secure ORM and gear infra patterns in ToolKit docs + [docs/toolkit_unified_system/](./toolkit_unified_system/README.md) | Add explicit DB migration policy doc | | Environment configs | [x] `p2` | [README.md](../README.md) env overrides, [docs/TRACING_SETUP.md](./TRACING_SETUP.md) | Add per-environment config matrix | | Feature flags | [ ] `p3` | Mentioned as target in architecture docs | Add standard feature-flag framework | @@ -249,8 +249,8 @@ Purpose: one concise map of repository artifacts that improve developer + AI pro | Decision records (ADR) | [x] `p1` | [docs/spec-templates/gears-sdlc/ADR/template.md](./spec-templates/gears-sdlc/ADR/template.md), [docs/adrs/](./adrs) | Add ADR index by domain | | Design documents | [x] `p1` | [docs/spec-templates/gears-sdlc/DESIGN/template.md](./spec-templates/gears-sdlc/DESIGN/template.md), gear docs | Add quality gates for design docs | | Common workflows | [x] `p2` | [Makefile](../Makefile), [tools/scripts/ci.py](../tools/scripts/ci.py), [docs/pr-review/README.md](./pr-review/README.md) | Add workflow cookbook | -| Anti-patterns | [x] `p2` | [docs/checklists/](./checklists), [tools/dylint_lints/README.md](../tools/dylint_lints/README.md) | Add unified anti-pattern catalog | -| Common mistakes | [x] `p2` | [tools/dylint_lints/AGENTS.md](../tools/dylint_lints/AGENTS.md) pitfalls, checklists | Add “top mistakes” short guide | +| Anti-patterns | [x] `p2` | [docs/checklists/](./checklists), [architecture lints via cargo gears lint | Add unified anti-pattern catalog | +| Common mistakes | [x] `p2` | [architecture lint rules and checklists (in cargo-gears CLI) | Add “top mistakes” short guide | | Support / escalation paths | [x] `p2` | [SECURITY.md](../SECURITY.md), [CONTRIBUTING.md](../CONTRIBUTING.md) | Add general (non-security) escalation flow | | Proposal process | [x] `p2` | Spec-driven flow in [docs/spec-templates/README.md](./spec-templates/README.md) | Add formal RFC/proposal workflow | | Glossary | [ ] `p3` | Not centralized | Add glossary document | diff --git a/docs/TESTING.md b/docs/TESTING.md index 254cca26e..bc967c1fc 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -20,7 +20,7 @@ build whenever feasible. | **Integration** | Cross-crate or DB-backed logic (SQLite, Postgres, MySQL) | `cargo test -p --features integration` | `#[cfg(feature = "integration")]` | Every PR (`ci.yml` — `integration` job, Ubuntu) | | **E2E** | Full HTTP request → response through a running server | pytest + httpx against `cf-gears-e2e-server` | n/a (Python tests) | PRs to `main`, nightly schedule (`e2e.yml`) | | **Fuzz** | Parser / validator robustness against arbitrary input | `cargo-fuzz` (libFuzzer) | nightly toolchain | PRs + nightly (`clusterfuzzlite.yml`) | -| **Static analysis** | Architectural rules, unsafe code, dependency licenses | clippy, dylint, cargo-deny, cargo-kani, cargo-geiger | varies | Every PR (`ci.yml` — `test`, `security`, `dylint` jobs) | +| **Static analysis** | Architectural rules, unsafe code, dependency licenses | clippy, `cargo gears lint`, cargo-deny, cargo-kani, cargo-geiger | varies | Every PR (`ci.yml` — `test`, `security`, `lint` jobs) | Additional testing categories such as performance (#4054), upgrade / migration (#4117), and long-haul (#4118) or soak testing are expected to be added over time, but they are not yet implemented or enforced as part of the current project test matrix. @@ -170,7 +170,7 @@ The `e2e.yml` workflow runs: Other quality-related GitHub Actions under `.github/workflows` complement the E2E workflow: - **`ci.yml`** runs the main cross-platform quality gates: linting, unit tests, - integration tests, FIPS verification, coverage, security checks, Dylint, and Cypilot + integration tests, FIPS verification, coverage, security checks, architecture lints, and Cypilot validation. - **`fmt.yml`** runs dedicated Rust formatting validation. - **`docs.yml`** checks Markdown links for documentation changes. @@ -237,7 +237,7 @@ python tools/scripts/ci.py fmt # check formatting python tools/scripts/ci.py fmt --fix # auto-format code python tools/scripts/ci.py clippy # run linter python tools/scripts/ci.py clippy --fix # attempt to fix warnings -python tools/scripts/ci.py dylint # custom project compliance lints +python tools/scripts/ci.py lint # custom project compliance lints (via cargo gears lint) python tools/scripts/ci.py audit # security audit python tools/scripts/ci.py deny # license & dependency checks python tools/scripts/ci.py e2e-local # build server + run E2E tests locally @@ -278,10 +278,10 @@ make fmt # formatting check (cargo fmt --all -- --check) make dev-fmt # auto-format (cargo fmt --all) make clippy # linting (clippy --workspace --all-targets --all-features) make lint # compile with -D warnings -make dylint # custom architectural lints +make gears-lint # custom architectural lints (via cargo gears lint) make deny # cargo deny check make kani # Kani formal verification (optional) -make safety # clippy + kani + lint + dylint +make safety # clippy + kani + lint + gears-lint ``` ## 7.3 CI Pipeline Summary @@ -294,7 +294,7 @@ PR opened / updated │ ├── test-fips — FIPS verification / platform-specific FIPS test lanes │ ├── security — cargo-deny │ ├── coverage — cargo-llvm-cov → Codecov upload - │ ├── dylint — custom architectural lints + │ ├── lint — custom architectural lints (cargo gears lint) │ └── cypilot — artifact / specification validation │ ├── fmt.yml — dedicated cargo fmt validation for Rust changes @@ -318,25 +318,25 @@ Nightly (schedule) --- -## 8. Dylint +## 8. Custom Architecture Lints -`Dylint` is the main project-specific lint layer. Unlike generic linting tools such as -`clippy`, it enforces Gears-specific architectural and repository rules: layer -separation, DTO placement, REST conventions, security-sensitive patterns, documentation -constraints, and GTS-related validation. +Custom architecture lints are the main project-specific lint layer. Unlike generic +linting tools such as `clippy`, they enforce Gears-specific architectural and repository +rules: layer separation, DTO placement, REST conventions, security-sensitive patterns, +documentation constraints, and GTS-related validation. + +The lints are shipped as part of the `cargo-gears` CLI (crate `cargo-gears-lints` in the +separate `cf-cli` repository) and are run via `cargo gears lint`. Useful local commands include: ```bash -make dylint # run custom lints across the workspace -make dylint-list # list available Dylint lints -make dylint-test # run lint UI / golden tests -make gts-docs # validate GTS identifiers in docs and schema files +cargo gears lint # run custom lints across the workspace +make gears-lint # Makefile shortcut for the above +make gts-docs # validate GTS identifiers in docs and schema files ``` -The CI `dylint` job both tests the lint crates themselves and applies the lints to the -workspace. For the current lint catalog and development notes, see -[`tools/dylint_lints/README.md`](../tools/dylint_lints/README.md). +The CI `lint` job applies the architecture lints to the workspace on every PR. --- @@ -346,7 +346,7 @@ workspace. For the current lint catalog and development notes, see |------|---------|---------|--------| | **clippy** | Lint for correctness and performance | `make clippy` | `test` | | **rustfmt** | Formatting enforcement | `make fmt` | `test` | -| **dylint** | Project-specific architectural lints (layer separation, DTO placement) | `make dylint` | `dylint` | +| **cargo gears lint** | Project-specific architectural lints (layer separation, DTO placement) | `cargo gears lint` | `lint` | | **cargo-deny** | License compliance, advisories, banned crates | `make deny` | `security` | | **cargo-kani** | Formal verification of unsafe code and invariants | `make kani` | `test` (via `safety`) | | **cargo-geiger** | Audit of `unsafe` usage in dependencies | `make geiger` | manual | @@ -372,6 +372,6 @@ Before opening a PR, verify: - [CONTRIBUTING.md](../CONTRIBUTING.md) — development workflow, commit conventions, PR process - [testing/e2e/README.md](../testing/e2e/README.md) — E2E test guide, fixtures, advanced usage - [fuzz/README.md](../tools/fuzz/README.md) — fuzz target reference, corpus management -- [tools/dylint_lints/README.md](../tools/dylint_lints/README.md) — Dylint lint catalog, commands, and development notes +- `cargo-gears-lints` (in the `cf-cli` repository) — architecture lint catalog and development notes - [guidelines/SECURITY.md](../guidelines/SECURITY.md) — secure coding practices - [docs/QUICKSTART_GUIDE.md](./QUICKSTART_GUIDE.md) — getting started with the project diff --git a/docs/WHY_GEARS.md b/docs/WHY_GEARS.md index 5e7138b35..3e888d063 100644 --- a/docs/WHY_GEARS.md +++ b/docs/WHY_GEARS.md @@ -21,7 +21,7 @@ - [B.2 Spec-driven development with Studio](#b2-spec-driven-development-with-studio) - [B.3 Tenant isolation by default](#b3-tenant-isolation-by-default) - [B.4 Authentication & authorization, built in (NIST SP 800-162 PDP/PEP)](#b4-authentication--authorization-built-in-nist-sp-800-162-pdppep) - - [B.5 Prewritten architecture lints (`dylint`)](#b5-prewritten-architecture-lints-dylint) + - [B.5 Prewritten architecture lints (`cargo gears lint`)](#b5-prewritten-architecture-lints-cargo-gears-lint) - [B.6 Runtime Gears capabilities](#b6-runtime-gears-capabilities) - [B.7 One consistent API dialect: `OperationBuilder` + OpenAPI + OData](#b7-one-consistent-api-dialect-operationbuilder--openapi--odata) - [B.8 Composable gears: one codebase, many deployment shapes](#b8-composable-gears-one-codebase-many-deployment-shapes) @@ -88,7 +88,7 @@ A similar middleware could exist or be built for Go or C#, and mature teams ofte | 13 | **AuthN / AuthZ** | per-service middleware, bespoke | ASP.NET policies | bespoke | **Built-in** PDP/PEP (NIST SP 800-162) | | 14 | **API consistency** | per-team router conventions | attributes + filters | bespoke | **`OperationBuilder`** → uniform REST + OpenAPI | | 15 | **Pagination / filtering** | hand-rolled | OData libs | hand-rolled | **Built-in OData** `$filter`/`$select`/`$orderby` | -| 16 | **Architecture policy** | `go vet` / `golangci-lint` / custom checks | Roslyn analyzers / custom checks | Clippy / custom lints | Prewritten Clippy + `dylint` rules for Gears conventions | +| 16 | **Architecture policy** | `go vet` / `golangci-lint` / custom checks | Roslyn analyzers / custom checks | Clippy / custom lints | Prewritten Clippy + architecture lints (via `cargo gears lint`) for Gears conventions | | 17 | **Multi-tenancy / licensing / quota / usage** | build it yourself | build it yourself | build it yourself | **Pre-integrated, replaceable gears** | | 18 | **Extensible API domain data types** | manual | manual | manual | **GTS** — versioned, schema-validated, autogenerated JSON schemas from Rust code | @@ -463,9 +463,9 @@ Gears ships a real authorization architecture, not a middleware stub: In Go or C#, a mature platform team can centralize this with shared middleware, repositories, analyzers, and code review. Gears' value is that this repo already provides a standard contract and implementation path for its services. -### B.5 Prewritten architecture lints (`dylint`) +### B.5 Prewritten architecture lints (`cargo gears lint`) -This is where Gears uses Rust's linting model as a platform feature. This is not fundamentally different in kind from Go projects using `golangci-lint` / `go vet`, or .NET projects using Roslyn analyzers. The practical benefit is that Gears already ships a suite of custom [`dylint`](https://github.com/constructorfabric/gears-rust/tree/main/tools/dylint_lints) lints for its architecture, and CI can fail the build on violations: +This is where Gears uses Rust's linting model as a platform feature. This is not fundamentally different in kind from Go projects using `golangci-lint` / `go vet`, or .NET projects using Roslyn analyzers. The practical benefit is that Gears already ships a suite of custom architecture lints (run via `cargo gears lint`, provided by the `cargo-gears` CLI), and CI can fail the build on violations: - **Domain-layer isolation** — no infra imports (`sqlx`, `sea_orm`, `axum`, `reqwest`) inside `domain/`. - **Direct-SQL restriction** — raw SQL only in migration infrastructure. @@ -474,11 +474,11 @@ This is where Gears uses Rust's linting model as a platform feature. This is not - **GTS identifier correctness** — valid IDs; no `schema_for!` on GTS structs. - **No unsafe shortcuts** — `unwrap`, avoidable `panic`, unsafe code paths, and unchecked invariants are treated as build-time failures where they would undermine platform guarantees. -Why this matters for Gears: the framework is not just a set of helper libraries; it is a **runtime contract** for secure XaaS systems. `dylint` lets the repository encode rules that ordinary Rust tooling cannot know: which layer may import which crate, which API paths must be versioned, which API metadata is mandatory, where SQL is allowed, and which GTS identifiers are valid. That turns some design-document rules into CI-enforced checks. +Why this matters for Gears: the framework is not just a set of helper libraries; it is a **runtime contract** for secure XaaS systems. `cargo gears lint` lets the repository encode rules that ordinary Rust tooling cannot know: which layer may import which crate, which API paths must be versioned, which API metadata is mandatory, where SQL is allowed, and which GTS identifiers are valid. That turns some design-document rules into CI-enforced checks. Compared with Go/C# alternatives, this is not about one ecosystem being incapable and another being capable. Go has `go vet`, `staticcheck`, and custom analyzers; C# has Roslyn analyzers; both are mature and useful. The difference is that Gears already includes project-specific checks for layer boundaries, route metadata, SQL placement, GTS identifiers, and unsafe shortcuts, wired into the same quality gate as formatting, Clippy, tests, and security checks. -> Documentation in markdown decays. `dylint` makes selected architecture rules executable in CI; that is not a substitute for design review, but it catches violations that reviewers would otherwise have to remember manually. +> Documentation in markdown decays. `cargo gears lint` makes selected architecture rules executable in CI; that is not a substitute for design review, but it catches violations that reviewers would otherwise have to remember manually. ### B.6 Runtime Gears capabilities @@ -561,7 +561,7 @@ Gears also defines a workspace lint floor in `Cargo.toml` and `clippy.toml`. Thi - **AI-generated code guardrails** — the config includes stricter thresholds for LLM-generated code: `single-char-binding-names-threshold = 4`, `large-error-threshold = 128`, plus denials for redundant clones, needless collects, verbose patterns, large stack arrays, `Rc>`, and `LinkedList`. This catches the kind of plausible-but-bloated code that agents and humans both produce under time pressure. - **Tenant-safe ORM use** — `clippy.toml` configures `disallowed-methods` for direct SeaORM `all`, `one`, `count`, update, and delete execution methods, with reasons pointing developers to secure scoped wrappers. -Go and C# teams can enforce many of these rules with `golangci-lint`, `go vet`, Roslyn analyzers, and custom build policy. The practical difference in Gears is that the Rust compiler, Clippy, custom `dylint` rules, and Cargo feature checks are already wired into one standard workspace safety pipeline (`make clippy`, `make lint`, `make dylint`, `make safety`). +Go and C# teams can enforce many of these rules with `golangci-lint`, `go vet`, Roslyn analyzers, and custom build policy. The practical difference in Gears is that the Rust compiler, Clippy, custom architecture lints (via `cargo gears lint`), and Cargo feature checks are already wired into one standard workspace safety pipeline (`make clippy`, `make lint`, `make safety`). ### B.15 Local-first, shift-left development diff --git a/docs/adrs/tests/0001-tests-in-separate-files.md b/docs/adrs/tests/0001-tests-in-separate-files.md index 843319cab..d59a7158f 100644 --- a/docs/adrs/tests/0001-tests-in-separate-files.md +++ b/docs/adrs/tests/0001-tests-in-separate-files.md @@ -5,7 +5,7 @@ date: 2026-04-15 decision-makers: Constructor Fabric Steering Committee --- -# Enforce test code in separate files via dylint lint DE1101 +# Enforce test code in separate files via architecture lint DE1101 **ID**: `cpt-cf-adr-tests-in-separate-files` @@ -30,24 +30,24 @@ Rust modules in the gears-rust monorepo contain inline `#[cfg(test)] mod tests { ## Considered Options * Keep inline tests (Rust Book default) -* Separate test files with a dylint lint +* Separate test files with a architecture lint * Integration tests only (`tests/` directory) ## Decision Outcome -Chosen option: "Separate test files with a dylint lint", because it is the only option that provides automatic enforcement, supports incremental migration, and preserves the ability to test `pub(crate)` internals (via `#[path]` module reference which compiles as part of the crate). +Chosen option: "Separate test files with a architecture lint", because it is the only option that provides automatic enforcement, supports incremental migration, and preserves the ability to test `pub(crate)` internals (via `#[path]` module reference which compiles as part of the crate). ### Consequences * All new gears must follow the `{stem}_tests.rs` companion file convention from day one. -* Existing modules are migrated incrementally by removing entries from `excluded_paths` in `dylint.toml`. +* Existing modules are migrated incrementally by removing entries from `excluded_paths` in the lint configuration in `Gears.toml`. * The `extract_tests.py` migration script must be maintained alongside the lint. * Developers unfamiliar with the convention will see a clear lint error message explaining what to do. -* CI pipeline adds ~15 seconds for the dylint check. +* CI pipeline adds ~15 seconds for the `cargo gears lint` check. ### Confirmation -Compliance is confirmed automatically: CI runs `cargo dylint` which includes lint DE1101. Any inline test code exceeding the threshold or violating the companion file guard produces a build error. The `excluded_paths` list in `dylint.toml` tracks modules not yet migrated. +Compliance is confirmed automatically: CI runs `cargo gears lint` which includes lint DE1101. Any inline test code exceeding the threshold or violating the companion file guard produces a build error. The `excluded_paths` list in the lint configuration in `Gears.toml` tracks modules not yet migrated. ## Pros and Cons of the Options @@ -63,9 +63,9 @@ Follow the standard Rust convention: `#[cfg(test)] mod tests { ... }` inline in * Bad, because PRs mix production and test changes in the same diff. * Bad, because no automatic enforcement — relies on code review. -### Separate test files with a dylint lint +### Separate test files with a architecture lint -Enforce `*_tests.rs` companion files via a custom dylint lint (DE1101). The lint denies inline test blocks exceeding `max_inline_test_lines` (default: 100), denies any inline test when a companion file exists, and validates `#[path]` attributes. +Enforce `*_tests.rs` companion files via a custom architecture lint (DE1101). The lint denies inline test blocks exceeding `max_inline_test_lines` (default: 100), denies any inline test when a companion file exists, and validates `#[path]` attributes. * Good, because production files contain only production code. * Good, because test files are instantly identifiable by naming convention (`*_tests.rs`). @@ -76,7 +76,7 @@ Enforce `*_tests.rs` companion files via a custom dylint lint (DE1101). The lint * Good, because incremental migration via `excluded_paths` — modules are migrated one by one. * Good, because smaller files reduce LLM context window usage — agents process production logic without loading test code. * Bad, because it deviates from Rust Book convention — may surprise Rust developers. -* Bad, because it requires tooling (dylint lint + migration script). +* Bad, because it requires tooling (architecture lint + migration script). * Bad, because navigation between production and test files requires one extra step. ### Integration tests only (`tests/` directory) @@ -92,7 +92,7 @@ Move all tests to the `tests/` directory as integration tests. ## Configuration ```toml -# dylint.toml +# the lint configuration in `Gears.toml` [de1101_tests_in_separate_files] max_inline_test_lines = 100 excluded_paths = [ @@ -126,11 +126,11 @@ mod handler_tests; ### Migration path 1. Run `extract_tests.py ` to automatically split inline tests into companion files. -2. Remove the module from `excluded_paths` in `dylint.toml`. +2. Remove the module from `excluded_paths` in the lint configuration in `Gears.toml`. 3. CI enforces the convention going forward. ## References - [Rust Book ch11-03: Test Organization](https://doc.rust-lang.org/book/ch11-03-test-organization.html) -- [DE1101 lint README](../../../tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/README.md) +- Architecture lint DE1101 (via `cargo gears lint`) - [Unit & Integration Testing Guide](../../toolkit_unified_system/12_unit_testing.md) diff --git a/docs/arch/errors/DECOMPOSITION.md b/docs/arch/errors/DECOMPOSITION.md index 8356b0bcf..619bc2d08 100644 --- a/docs/arch/errors/DECOMPOSITION.md +++ b/docs/arch/errors/DECOMPOSITION.md @@ -55,14 +55,14 @@ Build the `CanonicalError` enum, context types, `Problem` mapping, and `#[resour - [ ] 1.4.4 `From for CanonicalError` → `InvalidArgument` - [ ] 1.4.5 `From for CanonicalError` → `Internal` -#### 1.5 Dylint enforcement rules +#### 1.5 `cargo gears lint` enforcement rules -> Traces to: `cpt-cf-errors-component-dylint-rules`, PRD § 12 Risks — "LLM agents bypass compile checks" +> Traces to: `cpt-cf-errors-component-`cargo gears lint`-rules`, PRD § 12 Risks — "LLM agents bypass compile checks" -- [ ] 1.5.1 Implement a dylint rule — **No direct `Problem` construction**: reject `Problem { ... }` struct literals and direct `IntoResponse` impls that bypass `CanonicalError`; all `Problem` instances must originate from `CanonicalError` via the `From` impl -- [ ] 1.5.2 Implement a dylint rule — **No legacy error patterns**: reject usage of `Problem::new()`, `ErrDef`, `declare_errors!`, or `ErrorCode` -- [ ] 1.5.3 Implement a dylint rule — **No raw status-code error responses**: handlers must return `Result`, not ad-hoc HTTP error responses or gear-specific error enums -- [ ] 1.5.4 Add dylint CI gate to run on all gear code +- [ ] 1.5.1 Implement a architecture lint rule — **No direct `Problem` construction**: reject `Problem { ... }` struct literals and direct `IntoResponse` impls that bypass `CanonicalError`; all `Problem` instances must originate from `CanonicalError` via the `From` impl +- [ ] 1.5.2 Implement a architecture lint rule — **No legacy error patterns**: reject usage of `Problem::new()`, `ErrDef`, `declare_errors!`, or `ErrorCode` +- [ ] 1.5.3 Implement a architecture lint rule — **No raw status-code error responses**: handlers must return `Result`, not ad-hoc HTTP error responses or gear-specific error enums +- [ ] 1.5.4 Add `cargo gears lint` CI gate to run on all gear code #### 1.6 Contract enforcement (Tier 2) diff --git a/docs/arch/errors/DESIGN.md b/docs/arch/errors/DESIGN.md index 0c7d13fb0..224f74d26 100644 --- a/docs/arch/errors/DESIGN.md +++ b/docs/arch/errors/DESIGN.md @@ -187,7 +187,7 @@ Every canonical category has a GTS identifier assigned before any code is writte Any error that does not match a canonical category is mapped to `internal` with a trace ID. No error escapes the system without a canonical category. -> **Note**: Full enforcement of this principle (catching panics, unhandled rejections, and unknown error types in middleware) depends on the error middleware catch-all capability, which is out of scope for the current phase (see PRD §4.2). In the current phase, the principle is upheld by compile-time enforcement (typed enum, Dylint rules) and the single `From for Problem` conversion path. +> **Note**: Full enforcement of this principle (catching panics, unhandled rejections, and unknown error types in middleware) depends on the error middleware catch-all capability, which is out of scope for the current phase (see PRD §4.2). In the current phase, the principle is upheld by compile-time enforcement (typed enum, Architecture lint rules) and the single `From for Problem` conversion path. ### 2.2 Constraints @@ -312,9 +312,9 @@ async fn get_user(Path(id): Path) -> Result, CanonicalError> │ │ #[resource_error] │ macro │ │ └──────────────────────┘ │ ├─────────────────────────────────────────────────┤ -│ dylint_lints/ │ +│ architecture lints (via `cargo gears lint`) │ │ ┌─────────────────┐ │ -│ │ Dylint Rules │ compile-time lint │ +│ │ Architecture Lint Rules │ compile-time lint │ │ └─────────────────┘ │ └─────────────────────────────────────────────────┘ ``` @@ -404,13 +404,13 @@ The `#[resource_error("gts.cf.core.users.user.v1~")] struct UserResourceError;` The macro is a code generator. It does not add new categories or context types. It does not perform any runtime logic beyond delegation to `CanonicalError` constructors. -#### Dylint Rules +#### Architecture Lint Rules -- [ ] `p1` - **ID**: `cpt-cf-errors-component-dylint-rules` +- [ ] `p1` - **ID**: `cpt-cf-errors-component-architecture-lint-rules` **Responsibility scope**: -A set of Dylint lint rules (located in `dylint_lints/`) that enforce canonical error construction patterns at compile time. The rules detect and reject code that bypasses the canonical error system — e.g., constructing `Problem` directly, returning raw HTTP error responses, or using legacy error patterns (`Problem::new()`, `ErrDef`, `declare_errors!`, `ErrorCode`). +A set of architecture lint rules (via `cargo gears lint`) that enforce canonical error construction patterns at compile time. The rules detect and reject code that bypasses the canonical error system — e.g., constructing `Problem` directly, returning raw HTTP error responses, or using legacy error patterns (`Problem::new()`, `ErrDef`, `declare_errors!`, `ErrorCode`). **Rules**: 1. **No direct `Problem` construction** — all `Problem` instances must originate from `CanonicalError` via the `From` impl @@ -419,7 +419,7 @@ A set of Dylint lint rules (located in `dylint_lints/`) that enforce canonical e **Responsibility boundaries**: -Dylint rules are static analysis only. They do not modify code, do not run at runtime, and do not define new error categories or context types. +Architecture lint rules are static analysis only. They do not modify code, do not run at runtime, and do not define new error categories or context types. ##### Related components (by ID) @@ -740,7 +740,7 @@ Not applicable. Errors are transient in-memory values. No persistent storage. | Tier | When | Mechanism | What It Catches | |------|------|-----------|-----------------| -| 1. Compile-time | `cargo build` | Typed enum variants, exhaustive `match`, `#[resource_error]` macro, `GtsSchema` const, Dylint lint rules (`dylint_lints/`), `#[non_exhaustive]` on enum + variants, `pub(crate)` internal constructors | Wrong context type, missing match arm, GTS typos, direct `Problem` construction, legacy error patterns, direct variant construction, bypassing builder API | +| 1. Compile-time | `cargo build` | Typed enum variants, exhaustive `match`, `#[resource_error]` macro, `GtsSchema` const, architecture lint rules (via `cargo gears lint`), `#[non_exhaustive]` on enum + variants, `pub(crate)` internal constructors | Wrong context type, missing match arm, GTS typos, direct `Problem` construction, legacy error patterns, direct variant construction, bypassing builder API | | 2. Test-time | `cargo test` | Showcase tests with `assert_eq!` on full Problem JSON per category; JSON Schema equality assertions per context type | Field renames, default message changes, status code changes, schema drift | | 3. CI-time | PR merge gate | `cargo-semver-checks` on `cf-gears-toolkit-errors`; schema file diffing; snapshot CI gate | Removed types, changed signatures, schema evolution | | 4. Design-time | Architecture | Single `Problem` conversion point; dedicated context constructors; `GtsSchema` generates schemas from types | Ad-hoc JSON construction, missing required fields, schema/code divergence | diff --git a/docs/arch/errors/PRD.md b/docs/arch/errors/PRD.md index 8dd3fdf6e..f2981ada7 100644 --- a/docs/arch/errors/PRD.md +++ b/docs/arch/errors/PRD.md @@ -78,7 +78,7 @@ No gear-specific environment constraints. The canonical error system runs within - Round-trip serialization/deserialization (server → wire → SDK) - Public vs private detail isolation (client-facing context vs server-side logging with trace_id) - Migration of all existing gears to the new error system -- Dylint-level rules enforcement +- `cargo gears lint`-level rules enforcement ### 4.2 Out of Scope @@ -295,7 +295,7 @@ Error construction MUST be O(1) enum + struct allocation with no heap allocation - [ ] No error reaches API consumers outside the canonical vocabulary - [ ] Production error responses for `internal`/`unknown` contain no stack traces, query text, or file paths - [ ] Every error response includes a trace ID -- [ ] Dylint static analysis rules enforce correct error construction patterns (no bypassing canonical errors) +- [ ] `cargo gears lint` static analysis rules enforce correct error construction patterns (no bypassing canonical errors) - [ ] `CanonicalError` variants cannot be constructed directly from outside the crate (`#[non_exhaustive]` on variants, `pub(crate)` internal constructors) ## 10. Dependencies @@ -319,7 +319,7 @@ Error construction MUST be O(1) enum + struct allocation with no heap allocation |------|--------|------------| | CI schema checks become maintenance burden | Devs skip updates, reducing trust | One check per category; auto-generate from error definitions | | 16 categories insufficient long-term | Ad-hoc types outside canonical set | Additive categories (minor version bump) | -| LLM agents bypass compile checks | Contract violated despite CI gates | Dylint lint rules (`dylint_lints/`) that enforce canonical error construction patterns | +| LLM agents bypass compile checks | Contract violated despite CI gates | Architecture lint rules (via `cargo gears lint`) that enforce canonical error construction patterns | ## 13. Open Questions diff --git a/docs/arch/toolkit-contract-binding/DESIGN.md b/docs/arch/toolkit-contract-binding/DESIGN.md index 5d8bae802..d4b5bb63d 100644 --- a/docs/arch/toolkit-contract-binding/DESIGN.md +++ b/docs/arch/toolkit-contract-binding/DESIGN.md @@ -566,7 +566,7 @@ Generated `error_code` values: `NOTIFICATION_NOT_FOUND`, `DELIVERY_UNAVAILABLE`, **Rationale**: The name IS the operational contract. A developer reading `fn process(backend: &dyn NotificationBackend)` knows immediately: this can timeout, this can fail independently, this needs retry logic, this cannot participate in my transaction. No need to open another file, check a configuration, or read documentation. The naming convention eliminates an entire class of architectural misunderstandings. -**Enforcement**: Currently by convention. Future work may add a Dylint lint that rejects traits with incorrect suffixes or transport projections on Extension/Embedded types. +**Enforcement**: Currently by convention. Future work may add an architecture lint (via `cargo gears lint`) that rejects traits with incorrect suffixes or transport projections on Extension/Embedded types. ## 4. Crate Structure diff --git a/docs/checklists/README.md b/docs/checklists/README.md index 0a594f9bd..e12f9e366 100644 --- a/docs/checklists/README.md +++ b/docs/checklists/README.md @@ -136,7 +136,7 @@ These checklists are integrated with the Constructor Studio PR review workflow. - **PRD PRs**: Use `PRD.md` — covers requirements completeness, testability, traceability, and industry alignment - **Design PRs**: Use `DESIGN.md` — covers architecture, trade-offs, API contracts, security, and antipatterns - **ADR PRs**: Use `ADR.md` — covers decision significance, alternatives analysis, and overlap detection -- **Code PRs**: Use `CODING.md` — covers Rust correctness, architecture (ToolKit/SDK pattern), security (secure ORM), clippy/dylint compliance, testing, performance, etc. +- **Code PRs**: Use `CODING.md` — covers Rust correctness, architecture (ToolKit/SDK pattern), security (secure ORM), clippy/architecture-lint compliance, testing, performance, etc. The checklist is auto-selected by the `/cf-gears-pr-review` workflow based on the PR content. Configuration is in `.cf-studio/config/pr-review.toml` under the `[[prompts]]` entries. diff --git a/docs/pr-review/code-review-template.md b/docs/pr-review/code-review-template.md index fa1d8b6fa..b81cc8ff4 100644 --- a/docs/pr-review/code-review-template.md +++ b/docs/pr-review/code-review-template.md @@ -50,7 +50,7 @@ No reviewer comments found. {Assessment of logic, edge cases, error handling.} -### Cargo / Clippy / Dylint / Rustfmt Conformance {icon} +### Cargo / Clippy / Architecture Lints / Rustfmt Conformance {icon} {Assessment of tooling conformance. N/A if no Rust code changed.} diff --git a/docs/security/SECURITY.md b/docs/security/SECURITY.md index 41103c0ab..77c5b60b9 100644 --- a/docs/security/SECURITY.md +++ b/docs/security/SECURITY.md @@ -28,7 +28,7 @@ Gears take a **defense-in-depth** approach to security, combining Rust's compile - [Auth Plugins](#auth-plugins) - [Request Hardening](#request-hardening) - [6. Compile-Time Linting — Clippy](#6-compile-time-linting--clippy) - - [7. Compile-Time Linting — Custom Dylint Rules](#7-compile-time-linting--custom-dylint-rules) + - [7. Compile-Time Linting — Custom Architecture Lints](#7-compile-time-linting--custom-architecture-lints) - [8. Dependency Security — cargo-deny](#8-dependency-security--cargo-deny) - [9. Cryptographic Stack \& FIPS-140-3](#9-cryptographic-stack--fips-140-3) - [Default (non-FIPS) cryptographic stack](#default-non-fips-cryptographic-stack) @@ -210,7 +210,7 @@ Optional M:N, tenant-scoped resource grouping that acts as a **PIP** alongside t ### GTS-Based Attribute Access Control (ABAC) -> Source: [gts-spec](https://github.com/globalTypeSystem/gts-spec/) · [`dylint_lints/de09_gts_layer/`](../../tools/dylint_lints/de09_gts_layer/) · [`gears/system/types-registry/`](../../gears/system/types-registry/) +> Source: [gts-spec](https://github.com/globalTypeSystem/gts-spec/) · [`gears/system/types-registry/`](../../gears/system/types-registry/) Gears use the **Global Type System (GTS)** as the foundation for attribute-based access control. GTS defines a hierarchical identifier scheme for data types and instances: @@ -237,7 +237,7 @@ gts.....v[.]~ | GTS-typed authorization resources | Implemented | | Secure ORM `type_col` auto-injection via PDP | Under development | -Custom dylint rules (`DE0901`, `DE0902`) validate GTS identifier correctness at compile time, preventing malformed type strings from entering the codebase. +Custom architecture lints (`DE0901`, `DE0902`, via `cargo gears lint`) validate GTS identifier correctness at compile time, preventing malformed type strings from entering the codebase. ## 4. Credentials Storage Architecture @@ -354,11 +354,9 @@ The project enforces **90+ Clippy rules at `deny` level**, including the full `p - Stack size threshold of 512 KB - Max 2 boolean fields per struct (prevents boolean blindness) -## 7. Compile-Time Linting — Custom Dylint Rules +## 7. Compile-Time Linting — Custom Architecture Lints -> Source: [`dylint_lints/`](../../tools/dylint_lints/) - -Project-specific architectural lints run on every CI build via `cargo dylint`. These enforce design boundaries that generic linters cannot: +Project-specific architectural lints run on every CI build via `cargo gears lint` (provided by the `cargo-gears` CLI). These enforce design boundaries that generic linters cannot: | ID | Lint | Security Relevance | |---|---|---| @@ -450,7 +448,7 @@ make security # Runs both `deny` (license/advisory) and `fips-policy` **Phase A** (shipped) bans crates not currently in the graph — zero-pain regression gate: future PRs adding `md2`/`md4`/`ripemd`, `chacha20poly1305`/`salsa20`, the Curve25519 family (`x25519-dalek`, `ed25519-dalek`, …), alternative TLS frameworks (`openssl`, `boring`, `native-tls`), or alternative rustls CryptoProviders (`rustls-symcrypt`, `rustls-mbedcrypto-provider`, `rustls-openssl`, `rustls-rustcrypto`, `rustls-graviola`, `rustls-wolfcrypto-provider`, `boring-rustls-provider`) all fail the gate. -**Non-FIPS hasher guard** — Dylint lint **DE0708** (`no_non_fips_hasher`) rejects new `sha2`/`sha1`/`md5` imports outside an explicit allow-list (one entry: file-storage content hashing — see [Non-cryptographic `sha2` and `rand` usage](#non-cryptographic-sha2-and-rand-usage)), preventing unreviewed non-FIPS crypto usage from creeping in. All previous direct use sites have been replaced with inline FNV-1a (a deterministic, non-cryptographic fingerprint): `libs/toolkit-odata/src/pagination.rs` (cursor consistency) and `oidc-authn-plugin/src/infra/token_client.rs` (credential cache key). `sha2` remains in the dependency graph as a Phase B transitive (via `sqlx-core`, `sqlx-postgres`, `lopdf`, `rust-embed-utils`) and will be promoted to Phase A once those pull-throughs are eliminated. +**Non-FIPS hasher guard** — Architecture lint **DE0708** (`no_non_fips_hasher`, via `cargo gears lint`) rejects new `sha2`/`sha1`/`md5` imports outside an explicit allow-list (one entry: file-storage content hashing — see [Non-cryptographic `sha2` and `rand` usage](#non-cryptographic-sha2-and-rand-usage)), preventing unreviewed non-FIPS crypto usage from creeping in. All previous direct use sites have been replaced with inline FNV-1a (a deterministic, non-cryptographic fingerprint): `libs/toolkit-odata/src/pagination.rs` (cursor consistency) and `oidc-authn-plugin/src/infra/token_client.rs` (credential cache key). `sha2` remains in the dependency graph as a Phase B transitive (via `sqlx-core`, `sqlx-postgres`, `lopdf`, `rust-embed-utils`) and will be promoted to Phase A once those pull-throughs are eliminated. **Phase B** (pending transitive cleanup) is documented inline in `deny-fips.toml` — `ring`, non-FIPS `aws-lc-rs`, `chacha20`, `md-5`, `sha1`, `blake2`/`blake3`, `aes`, `hmac`, `hkdf`, etc. — currently pulled by upstream deps (`pingora-rustls`/`ureq`, rustls's default features, `rand`). Each moves to Phase A as its upstream pull-through is replaced. **Tracking**: [ADR 0005 §"Phasing"](fips/adrs/0005-fips-dependency-policy.md) and [FIPS PRD §13 TODO-7](fips/PRD.md#13-open-questions) — promotion to Phase A is the unit of work; no per-crate sub-tickets today. @@ -488,7 +486,7 @@ The HTTP client (`cf-gears-toolkit-http`) exposes the following transport knobs ### Non-cryptographic `sha2` and `rand` usage -Any residual `sha2` / `rand` usage in the tree is **non-cryptographic** and is **not part of the FIPS claim**. Non-cryptographic fingerprints use inline FNV-1a (OData pagination cursor consistency in `libs/toolkit-odata/src/pagination.rs`; the OIDC token-cache key in `oidc-authn-plugin`), and the `rand` ecosystem is pulled in transitively rather than used for key material on the TLS data plane. New `sha2`/`sha1`/`md5` imports are rejected at compile time by Dylint **DE0708** (`no_non_fips_hasher`) outside an explicit allow-list. See the [Non-FIPS hasher guard](#build-time-dependency-graph-policy) note above and [What this does NOT claim](#what-this-does-not-claim) below for the transitive-dependency posture. +Any residual `sha2` / `rand` usage in the tree is **non-cryptographic** and is **not part of the FIPS claim**. Non-cryptographic fingerprints use inline FNV-1a (OData pagination cursor consistency in `libs/toolkit-odata/src/pagination.rs`; the OIDC token-cache key in `oidc-authn-plugin`), and the `rand` ecosystem is pulled in transitively rather than used for key material on the TLS data plane. New `sha2`/`sha1`/`md5` imports are rejected at compile time by architecture lint **DE0708** (`no_non_fips_hasher`, via `cargo gears lint`) outside an explicit allow-list. See the [Non-FIPS hasher guard](#build-time-dependency-graph-policy) note above and [What this does NOT claim](#what-this-does-not-claim) below for the transitive-dependency posture. **DE0708 allow-list entry — file-storage content hashing.** `gears/file-storage/file-storage/src/infra/content/hash.rs` is the single SHA-256 call site in the file-storage gear and is on the DE0708 allow-list. It is used for **content addressing/integrity** — the `expected_hash` upload constraint and version-identity check (SHA-256 is mandated by file-storage ADR-0002) — and to derive the opaque content ETag. It is **not** used for signatures, key derivation, or password storage: the signed-URL signing primitive runs behind a replaceable `SignatureProvider` abstraction (file-storage ADR-0004), so a FIPS deployment swaps the signing module without touching this hasher. All `sha2` usage in the gear is confined to this one reviewable module. @@ -642,8 +640,8 @@ Gears provide a CLI tool for scaffolding new repositories that automatically inh | Inherited Configuration | Description | |---|---| | **Compiler configuration** | `rust-toolchain.toml`, workspace lint rules (`#[deny(warnings)]`, 90+ Clippy rules at deny level), `unsafe_code = "forbid"` | -| **Custom dylint rules** | Architectural boundary enforcement (DE01xx–DE13xx series), GTS validation (DE09xx) | -| **Makefile targets** | `make deny` (cargo-deny), `make fuzz` (continuous fuzzing), `make dylint` (custom lints), `make safety` (full suite) | +| **Custom architecture lints** | Architectural boundary enforcement (DE01xx–DE13xx series), GTS validation (DE09xx), run via `cargo gears lint` | +| **Makefile targets** | `make deny` (cargo-deny), `make fuzz` (continuous fuzzing), `make safety` (full suite) | | **cargo-deny configuration** | `deny.toml` with RustSec advisory checks, license allow-lists, source restrictions | This ensures every new service or gear repository starts with the same defense-in-depth baseline described in this document, eliminating configuration drift across the platform. @@ -659,7 +657,7 @@ The following areas have been identified for future hardening: 2. **Secure ORM type-column auto-injection** — the `ScopableEntity` trait supports a `type_col` dimension, but automatic GTS type constraint injection from PDP → `AccessScope` → SQL `WHERE` is under development 3. **Tenant Resolver access-control plugins** — the `Unauthorized` error variant is reserved in the SDK, but no production plugin enforces caller-vs-target authorization (the static plugin allows any caller to query any configured tenant; the single-tenant plugin uses identity matching only). A policy-backed plugin would enforce fine-grained tenant visibility 4. **Security guidelines in spec templates** — add explicit security checklist sections to PRD and DESIGN templates (threat modeling, data classification, authentication requirements per feature) -5. **Security-focused dylint lints** — extend the `DE07xx` series with additional rules: +5. **Security-focused architecture lints** — extend the `DE07xx` series with additional rules: - Detecting hardcoded secrets or API keys - Enforcing `SecretString` / `SecretValue` usage for sensitive fields - Flagging raw SQL string construction diff --git a/docs/security/fips/PRD.md b/docs/security/fips/PRD.md index 9c5990add..11ab1f230 100644 --- a/docs/security/fips/PRD.md +++ b/docs/security/fips/PRD.md @@ -169,7 +169,7 @@ engineering. sides; cross-provider interop (corecrypto ↔ aws-lc-rs ↔ rustls-cng-crypto) is not in CI. Tracked as TODO-2. - **Native rustls-symcrypt migration on Windows.** SymCrypt is not currently CMVP-validated; migration is conditional on Microsoft obtaining a CMVP certificate. Tracked as TODO-3. -- **`fips140=only`-style source-level enforcement.** A custom `dylint` rule that refuses `use md5;` / `use sha1;` at +- **`fips140=only`-style source-level enforcement.** A custom architecture lint (via `cargo gears lint`) that refuses `use md5;` / `use sha1;` at module scope is deferred to Phase C of the dependency policy ([ADR 0005](adrs/0005-fips-dependency-policy.md)). ## 5. Functional Requirements diff --git a/docs/security/fips/adrs/0005-fips-dependency-policy.md b/docs/security/fips/adrs/0005-fips-dependency-policy.md index dfe5ef121..9c1a5d02f 100644 --- a/docs/security/fips/adrs/0005-fips-dependency-policy.md +++ b/docs/security/fips/adrs/0005-fips-dependency-policy.md @@ -25,7 +25,7 @@ This ADR captures the workaround: **dependency-graph policy enforced at build ti ## Considered Options * **Option A** — `cargo-deny` `[bans]` policy enforced under a separate config — **chosen** -* **Option B** — Workspace-wide custom `dylint` rule that refuses `use md5;` / `use sha1;` / etc. at gear level +* **Option B** — Workspace-wide custom `cargo gears lint` rule that refuses `use md5;` / `use sha1;` / etc. at gear level * **Option C** — Runtime check at `init_crypto_provider`: probe loaded libraries with `dladdr` / `vmmap`, refuse to start if non-validated crypto libs are present * **Option D** — Accept the current gap; document as a known limitation @@ -39,7 +39,7 @@ Key reasons: * `[bans]` enforces at dep-graph resolution time, before any compilation — catches accidental additions in the smallest possible blast radius. * The `[graph] features = ["fips"]` directive scopes the policy to the FIPS build, so non-fips developer workflows are unaffected. * `make fips-policy` is the explicit gate; CI wires it into `make security`. -* Option B (dylint) is complementary but lower priority — catches workspace-internal `use` paths that bypass the graph (rare). Tracked separately as Phase C below if/when needed. +* Option B (`cargo gears lint`) is complementary but lower priority — catches workspace-internal `use` paths that bypass the graph (rare). Tracked separately as Phase C below if/when needed. * Option C (runtime probe) was rejected: brittle (every macOS update changes loader behavior), opaque to operators, and doesn't catch a non-FIPS crypto crate that compiles in but doesn't call `dlopen`. * Option D was rejected: the gap is documented in PRD §7.2 already; closing it via build-time policy adds measurable assurance with low effort. @@ -88,7 +88,7 @@ These will be denied as their upstream usage is removed. Each move from Phase B ### Phase C — Source-level enforcement (deferred) -If even stricter assurance is needed later, a custom `dylint` lint can walk `use` and `extern crate` paths at the source-code level. This catches a forbidden crate that is **in** the dep graph but the workspace's own code does not directly import — covering workspace discipline beyond what `cargo-deny` enforces. Not built today because Phase A + B closes the practical gap. +If even stricter assurance is needed later, a custom `cargo gears lint` lint can walk `use` and `extern crate` paths at the source-code level. This catches a forbidden crate that is **in** the dep graph but the workspace's own code does not directly import — covering workspace discipline beyond what `cargo-deny` enforces. Not built today because Phase A + B closes the practical gap. ## Pros and Cons of the Options @@ -99,13 +99,13 @@ If even stricter assurance is needed later, a custom `dylint` lint can walk `use * Good: `[graph] features = ["fips"]` scopes the policy to FIPS build, no impact on non-fips developer workflow. * Good: phased deny-list approach gives a clear road-map for moving Phase B crates to Phase A as transitives are cleaned. * Neutral: two `*.toml` configs to maintain (`deny.toml` + `deny-fips.toml`); minor cognitive overhead. -* Bad: does not catch a forbidden crate that is in the graph but only used by transitive deps (a workspace member could `use` it via re-export tricks). Phase C / dylint covers that. +* Bad: does not catch a forbidden crate that is in the graph but only used by transitive deps (a workspace member could `use` it via re-export tricks). Phase C / `cargo gears lint` covers that. -### Option B — `dylint` rule (deferred) +### Option B — `cargo gears lint` rule (deferred) * Good: covers source-level imports that `cargo-deny` cannot. * Bad: no protection against transitive crates linked into the binary but `use`d only by transitive code — `cargo-deny` protects against that. -* Bad: dylint rules are more expensive to author and maintain. +* Bad: architecture lint rules are more expensive to author and maintain. * Deferred until measurable signal that workspace-internal imports bypass the graph policy. ### Option C — Runtime probe via dladdr / vmmap diff --git a/docs/slides/1_OVERVIEW.html b/docs/slides/1_OVERVIEW.html index 9fd20e391..537f6bd9d 100644 --- a/docs/slides/1_OVERVIEW.html +++ b/docs/slides/1_OVERVIEW.html @@ -85,7 +85,7 @@

Non-goals

Key defining characteristics

  1. Secure by default (defense-in-depth) — security is structural, not opt-in, validated at build time.
  2. -
  3. Architecture enforced at compile time - use custom lint rules via dylint
  4. +
  5. Architecture enforced at compile time - use custom lint rules via cargo gears lint
  6. Three-tier gears hierarchy — toolkit, System gears, Service gears.
  7. Composable libraries, vendor-controlled deployment — own API + DB, SDK facades local vs. remote.
  8. Local-first shift-left development - run and test everything locally, LLM-friendly
  9. @@ -115,7 +115,7 @@

    Principle 1 — Secure

    Principle 2 — Architecture enforced at compile time

    Custom static analysis is a core architectural mechanism, not a coding aid.

      -
    • tools/dylint_lints/ — a dedicated Dylint suite that checks: +
    • Architecture lints (in cargo-gears CLI) — a dedicated cargo gears lint suite that checks:
      • contract-layer purity, DTO placement & schema derives
      • domain-layer isolation, direct-SQL restrictions
      • @@ -126,13 +126,13 @@

        Principle 2 — Arch
      • Runs alongside Clippy + CI → violations fail fast, before review or runtime
      -

      "Shift-left": architecture that lives in markdown decays; Dylint makes it executable.
      +

      "Shift-left": architecture that lives in markdown decays; `cargo gears lint` makes it executable.
      GTS* = Global Type System identifers like gts.cf.core.events.v1~a.b.c.d.v1~

      -

      Dylint — compile-time architecture validation

      -

      Repository-specific lints in tools/dylint_lints/ make the design executable — not just documented - code won't compile in case of violation:

      +

      `cargo gears lint` — compile-time architecture validation

      +

      Repository-specific lints in tools/architecture lints (in `cargo-gears` CLI) make the design executable — not just documented - code won't compile in case of violation:

      • Domain-layer isolation — no infra imports (sqlx, sea_orm, ...) in domain/
      • Direct-SQL restriction — raw SQL only in migration infrastructure
      • @@ -144,7 +144,7 @@

        Dylint — compile-time ar
      • ...
      -

      Runs Dylint in CI → architecture violations fail the build before review or runtime.

      +

      Runs `cargo gears lint` in CI → architecture violations fail the build before review or runtime.

      @@ -233,7 +233,7 @@

      Principle 7 — Extensible

      Engineering principles (how we work)

      • Spec-Driven Development — PRD → Design & ADR → Feature before code
      • -
      • Code validation — custom Dylints + Clippy + tests + fuzzing + audits in CI
      • +
      • Code validation — custom architecture lints + Clippy + tests + fuzzing + audits in CI
      • Quality First — 90%+ coverage target across unit / integration / E2E / perf / security
      • Core in Rust — compile-time safety + deep static analysis
      • Monorepo — atomic refactors, consistent tooling, realistic local E2E
      • @@ -249,7 +249,7 @@

        Why Rust?

        • Compile-time safety — eliminates broad classes of memory & concurrency bugs
        • Great fit for reusable platform code — predictable perf, explicit interfaces
        • -
        • Static analysis as architecture — Clippy + custom Dylints enforce rules at build
        • +
        • Static analysis as architecture — Clippy + custom architecture lints enforce rules at build
        • Operational efficiency — low footprint → realistic local/edge runs & E2E
        • AI-friendly - great compiler and linters assistant to catch problems early
        • Universal language — cloud/on-prem/edge, kernel + even frontend, mobile
        • @@ -287,7 +287,7 @@

          Repository structure

          │ └─ <service>/ # Business/domain & GenAI gears (mini-chat, file-parser, ...) ├─ apps/ # Executable apps composing gears (example server) ├─ examples/ # Reference gears (users-info, oop-gears, fips-probe) -├─ tools/ # Dylints, CI scripts, fuzz targets +├─ tools/ # CI scripts, fuzz targets └─ docs/ # Manifest, gears registry, toolkit_unified_system, arch, security
      @@ -539,7 +539,7 @@

      Security — defense across the
    • SecurityContext propagation — explicit data, no thread-local magic
    • Outbound boundaryoagw centralizes egress policy + credential injection
    • Credential handlingcredstore + secrecy-aware types
    • -
    • Static & CI gates — Clippy, Dylints, cargo-deny, CodeQL, fuzzing, Scorecard/Snyk/Aikido
    • +
    • Static & CI gates — Clippy, architecture lints, cargo-deny, CodeQL, fuzzing, Scorecard/Snyk/Aikido
    • @@ -582,7 +582,7 @@

      Global Type System (GTS)

    • gts.cf.core.events.event.v1~ style IDs as a platform contract surface
    • GTS JSON Schemas generated directly from Rust types → registered in Types Registry
    • The non-HTTP counterpart to OpenAPI: describes plugin specs, events, permissions
    • -
    • GTS-specific Dylints validate identifier correctness
    • +
    • GTS-specific architecture lints validate identifier correctness
    • @@ -618,7 +618,7 @@

      Developer experience

    • Type-safe RESTOperationBuilder prevents half-wired routes at compile time
    • OpenAPI auto-generated from the same route declarations that run the service
    • GET /cw/docs live Swagger UI on the example server
    • -
    • Architectural Dylints enforce design rules and patterns at build time
    • +
    • Architectural architecture lints enforce design rules and patterns at build time
    • Rich docs: docs/toolkit_unified_system/ (13 topic files) + per-gear specs
    • @@ -743,7 +743,7 @@

      Why Constructor
      1. XaaS-friendly - built-in multi-tenancy, licensing, usage, etc
      2. Security by architecture — AuthN/AuthZ, tenancy, scoped DB access, FIPS 140-3
      3. -
      4. Compile-time governance — linters and Dylints detect violations before runtime
      5. +
      6. Compile-time governance — linters and architecture lints detect violations before runtime
      7. Composable gear model — single code - multiple builds and deployments
      8. Shift-left productivity — all-in-one gears process for local build and test
      9. Extensible by design — custom API data types, plugins, serverless gears
      10. diff --git a/docs/slides/1_OVERVIEW.md b/docs/slides/1_OVERVIEW.md index 586c0c244..7839c51d4 100644 --- a/docs/slides/1_OVERVIEW.md +++ b/docs/slides/1_OVERVIEW.md @@ -86,7 +86,7 @@ Constructor Gears deliberately does **not**: ## Key defining characteristics 1. **Secure by default (defense-in-depth)** — security is structural, not opt-in, validated at build time. -2. **Architecture enforced at compile time** - use custom lint rules via `dylint` +2. **Architecture enforced at compile time** - use custom lint rules via `cargo gears lint` 3. **Three-tier gears hierarchy** — toolkit, System gears, Service gears. 4. **Composable libraries, vendor-controlled deployment** — own API + DB, SDK facades local vs. remote. 5. **Local-first shift-left development** - run and test everything locally, LLM-friendly @@ -120,21 +120,21 @@ The platform owns a **linear security data-path**: Custom static analysis is a **core architectural mechanism**, not a coding aid. -- `tools/dylint_lints/` — a dedicated Dylint suite that checks: +- Architecture lints (in `cargo-gears` CLI) — a dedicated `cargo gears lint` suite that checks: - contract-layer purity, DTO placement & schema derives - domain-layer isolation, direct-SQL restrictions - versioned REST paths, mandatory `OperationBuilder` metadata - OData extension usage, GTS* identifier correctness - Runs alongside Clippy + CI → **violations fail fast, before review or runtime** -> "Shift-left": architecture that lives in markdown decays; Dylint makes it executable. +> "Shift-left": architecture that lives in markdown decays; `cargo gears lint` makes it executable. > GTS* = Global Type System identifers like `gts.cf.core.events.v1~a.b.c.d.v1~` --- -## Dylint — compile-time architecture validation +## `cargo gears lint` — compile-time architecture validation -Repository-specific lints in `tools/dylint_lints/` make the design **executable** — not just documented - code won't compile in case of violation: +Repository-specific architecture lints (via `cargo gears lint`) make the design **executable** — not just documented - code won't compile in case of violation: - **Domain-layer isolation** — no infra imports (`sqlx`, `sea_orm`, ...) in `domain/` - **Direct-SQL restriction** — raw SQL only in migration infrastructure @@ -145,7 +145,7 @@ Repository-specific lints in `tools/dylint_lints/` make the design **executable* - **GTS identifier correctness** — valid IDs; no `schema_for!` on GTS structs - ... -> Runs Dylint in CI → architecture violations **fail the build** before review or runtime. +> Runs `cargo gears lint` in CI → architecture violations **fail the build** before review or runtime. --- @@ -216,7 +216,7 @@ Every backbone concern is a **regular, replaceable gear** with its own SDK: ## Engineering principles (how we work) - **Spec-Driven Development** — PRD → Design & ADR → Feature *before* code -- **Code validation** — custom Dylints + Clippy + tests + fuzzing + audits in CI +- **Code validation** — custom architecture lints + Clippy + tests + fuzzing + audits in CI - **Quality First** — 90%+ coverage target across unit / integration / E2E / perf / security - **Core in Rust** — compile-time safety + deep static analysis - **Monorepo** — atomic refactors, consistent tooling, realistic local E2E @@ -236,7 +236,7 @@ correctness, and maintainability matter more than raw implementation speed. - **Compile-time safety** — eliminates broad classes of memory & concurrency bugs - **Great fit for reusable platform code** — predictable perf, explicit interfaces -- **Static analysis as architecture** — Clippy + custom Dylints enforce rules at build +- **Static analysis as architecture** — Clippy + custom architecture lints enforce rules at build - **Operational efficiency** — low footprint → realistic local/edge runs & E2E - **AI-friendly** - great compiler and linters assistant to catch problems early - **Universal language** — cloud/on-prem/edge, kernel + even frontend, mobile @@ -277,7 +277,7 @@ cyberware-rust/ │ └─ / # Business/domain & GenAI gears (mini-chat, file-parser, ...) ├─ apps/ # Executable apps composing gears (example server) ├─ examples/ # Reference gears (users-info, oop-gears, fips-probe) -├─ tools/ # Dylints, CI scripts, fuzz targets +├─ tools/ # CI scripts, fuzz targets └─ docs/ # Manifest, gears registry, toolkit_unified_system, arch, security ``` @@ -533,7 +533,7 @@ Gateway middleware order: - **`SecurityContext` propagation** — explicit data, no thread-local magic - **Outbound boundary** — `oagw` centralizes egress policy + credential injection - **Credential handling** — `credstore` + `secrecy`-aware types -- **Static & CI gates** — Clippy, Dylints, `cargo-deny`, CodeQL, fuzzing, Scorecard/Snyk/Aikido +- **Static & CI gates** — Clippy, architecture lints, `cargo-deny`, CodeQL, fuzzing, Scorecard/Snyk/Aikido --- @@ -562,7 +562,7 @@ without modifying core code. - `gts.cf.core.events.event.v1~` style IDs as a **platform contract surface** - GTS **JSON Schemas generated directly from Rust types** → registered in Types Registry - The non-HTTP counterpart to OpenAPI: describes **plugin specs, events, permissions** -- GTS-specific **Dylints** validate identifier correctness +- GTS-specific **architecture lints** validate identifier correctness --- @@ -601,7 +601,7 @@ One platform-wide error vocabulary, aligned with the **16 gRPC categories**: - **Type-safe REST** — `OperationBuilder` prevents half-wired routes at compile time - **OpenAPI auto-generated** from the same route declarations that run the service - **`GET /cw/docs`** live Swagger UI on the example server -- **Architectural Dylints** enforce design rules and patterns at build time +- **Architectural architecture lints** enforce design rules and patterns at build time - **Rich docs**: `docs/toolkit_unified_system/` (13 topic files) + per-gear specs --- @@ -733,7 +733,7 @@ On **Windows** (no `make`): `python tools/scripts/ci.py check` 1. **XaaS-friendly** - built-in multi-tenancy, licensing, usage, etc 2. **Security by architecture** — AuthN/AuthZ, tenancy, scoped DB access, FIPS 140-3 -3. **Compile-time governance** — linters and Dylints detect violations before runtime +3. **Compile-time governance** — linters and architecture lints detect violations before runtime 4. **Composable gear model** — single code - multiple builds and deployments 5. **Shift-left productivity** — all-in-one gears process for local build and test 6. **Extensible by design** — custom API data types, plugins, serverless gears diff --git a/docs/slides/2_AUTHN_AUTHZ.html b/docs/slides/2_AUTHN_AUTHZ.html index c8cdc1e9a..b5fcae563 100644 --- a/docs/slides/2_AUTHN_AUTHZ.html +++ b/docs/slides/2_AUTHN_AUTHZ.html @@ -620,7 +620,7 @@

        Not done yet — roadmap

      11. Authorization decision caching — cache PDP decisions + constraints (TTL-bounded)
      12. Multi-Factor Authentication (MFA) — step-up / assurance-level awareness in AuthN
      13. S2S SecurityContext caching — reuse client-credentials identities
      14. -
      15. More dylint rules — widen compile-time architecture enforcement
      16. +
      17. More architecture lints — widen compile-time architecture enforcement
      18. Access Management built-in gear — policy administration (PAP) + a default PDP
      19. diff --git a/docs/slides/2_AUTHN_AUTHZ.md b/docs/slides/2_AUTHN_AUTHZ.md index 6d7f6c012..8e79dc9c6 100644 --- a/docs/slides/2_AUTHN_AUTHZ.md +++ b/docs/slides/2_AUTHN_AUTHZ.md @@ -530,7 +530,7 @@ Being honest about the gaps; each has a clear path: - **Authorization decision caching** — cache PDP decisions + constraints (TTL-bounded) - **Multi-Factor Authentication (MFA)** — step-up / assurance-level awareness in AuthN - **S2S `SecurityContext` caching** — reuse client-credentials identities -- **More dylint rules** — widen compile-time architecture enforcement +- **More architecture lints** — widen compile-time architecture enforcement - **Access Management built-in gear** — policy administration (PAP) + a default PDP > Mostly performance, coverage, and tooling — the core model is already in place. diff --git a/docs/toolkit_unified_system/02_gear_layout_and_sdk_pattern.md b/docs/toolkit_unified_system/02_gear_layout_and_sdk_pattern.md index 712ad9836..5151e178d 100644 --- a/docs/toolkit_unified_system/02_gear_layout_and_sdk_pattern.md +++ b/docs/toolkit_unified_system/02_gear_layout_and_sdk_pattern.md @@ -343,7 +343,7 @@ error: field 'pool' has type 'sqlx::PgPool' which is forbidden (crate 'sqlx'). #### CI enforcement -The [DE0309 lint](../../tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/README.md) runs in CI and **denies** any `struct` or `enum` in `domain/` that is missing the `#[domain_model]` attribute. This ensures the macro cannot be accidentally omitted. +The DE0309 architecture lint (via `cargo gears lint`) runs in CI and **denies** any `struct` or `enum` in `domain/` that is missing the `#[domain_model]` attribute. This ensures the macro cannot be accidentally omitted. ### Gear `src/api/rest/dto.rs` (REST DTOs, OData) diff --git a/docs/toolkit_unified_system/10_checklists_and_templates.md b/docs/toolkit_unified_system/10_checklists_and_templates.md index a228810b4..ba509a8ad 100644 --- a/docs/toolkit_unified_system/10_checklists_and_templates.md +++ b/docs/toolkit_unified_system/10_checklists_and_templates.md @@ -72,7 +72,7 @@ pub struct MyGear { - [ ] Use `db.sea_secure()` for all DB access in handlers/services - [ ] Pass `SecurityContext` to repository methods - [ ] Use `secure_conn.find::(&scope).all(&secure_conn)` for auto-scoped queries -- [ ] Use raw SQL only in `migrations/*.rs` (enforced later via dylint) +- [ ] Use raw SQL only in `migrations/*.rs` (enforced via `cargo gears lint`) - [ ] Add indexes on security columns (tenant_id, resource_id) - [ ] Test with `SecurityContext::test_tenant()` for unit tests diff --git a/docs/toolkit_unified_system/README.md b/docs/toolkit_unified_system/README.md index 1e904b46c..d0156adcf 100644 --- a/docs/toolkit_unified_system/README.md +++ b/docs/toolkit_unified_system/README.md @@ -23,7 +23,7 @@ This folder contains the ToolKit developer documentation, split by topic for foc | Errors, RFC-9457 Problem | `05_errors_rfc9457.md` | | | Lifecycle, background tasks, cancellation | `08_lifecycle_stateful_tasks.md` | | | Out-of-Process / gRPC / SDK pattern | `09_oop_grpc_sdk_pattern.md` | | -| Domain model macro, DDD enforcement | `02_gear_layout_and_sdk_pattern.md` (§ Domain types) | `dylint_lints/de03_domain_layer/de0309_must_have_domain_model/README.md` | +| Domain model macro, DDD enforcement | `02_gear_layout_and_sdk_pattern.md` (§ Domain types) | Architecture lint DE0309 (via `cargo gears lint`) | | Quick checklists, templates | `10_checklists_and_templates.md` | | | Unit & integration testing (philosophy, patterns, infrastructure) | `12_unit_testing.md` | | | E2E testing (philosophy, patterns, infrastructure) | `13_e2e_testing.md` | | diff --git a/docs/web-docs/architecture/index.md b/docs/web-docs/architecture/index.md index c051f51b6..71b0672c1 100644 --- a/docs/web-docs/architecture/index.md +++ b/docs/web-docs/architecture/index.md @@ -89,7 +89,7 @@ with the code by construction, and REST clients can be generated from the same c Security is a layered path with no unscoped shortcut: -1. **Static checks** — custom Dylints enforce architecture rules (DTO placement, domain +1. **Static checks** — custom architecture lints (via `cargo gears lint`) enforce architecture rules (DTO placement, domain isolation, no raw SQL outside migrations, versioned paths, mandatory `OperationBuilder` metadata) at build time. 2. **Authentication** — tokens validated at the gateway; `SecurityContext` injected. diff --git a/docs/web-docs/reference/index.md b/docs/web-docs/reference/index.md index e18d5952a..4c21469bb 100644 --- a/docs/web-docs/reference/index.md +++ b/docs/web-docs/reference/index.md @@ -96,7 +96,7 @@ What you reach for while building, and the entry point for each: (`runtime.type: local | oop`). - **Global Type System (GTS)** — register schemas from Rust types; extend the domain model without changing existing gears. -- **Security baseline / FIPS** — Rust safety, strict Clippy + custom Dylints, `cargo-deny`, +- **Security baseline / FIPS** — Rust safety, strict Clippy + custom architecture lints (via `cargo gears lint`), `cargo-deny`, continuous fuzzing, and `--features fips` for validated crypto on Linux/macOS/Windows. ## What's planned diff --git a/dylint.toml b/dylint.toml index 77992f7f2..eff74b07c 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,19 +1,22 @@ -# Updated: 2026-04-07 by Constructor Tech -# Dylint Configuration for CF/Gears +# Configuration for cargo-gears architecture lints. +# Lints are run via `cargo gears lint --dylint` (see Gears.toml). +# See https://github.com/constructorfabric/cargo-gears for documentation. -[workspace.metadata.dylint] -# Path to the local dylint linters -libraries = [ - { path = "tools/dylint_lints/de*/de*" } +[cargo-gears-lints] + +# DE0708: Non-FIPS hasher allow-list. +# Paths where direct sha2/sha1/md5 imports are permitted (substring-matched). +hasher_allowed_paths = [ + "gears/file-storage/file-storage/src/infra/content/hash.rs", ] # DE1101: Tests must be in separate files. -# Remove entries as gears are migrated. -[de1101_tests_in_separate_files] # Maximum number of inline test lines allowed before the lint enforces splitting # into a separate `*_tests.rs` file. Set to 0 to always require separation. # Default: 100 max_inline_test_lines = 100 + +# Remove entries as gears/libs are migrated to separate test files. excluded_paths = [ # libs/ "libs/toolkit", diff --git a/gears/mini-chat/mini-chat/src/domain/ports/mod.rs b/gears/mini-chat/mini-chat/src/domain/ports/mod.rs index 0496c696c..d42fcde70 100644 --- a/gears/mini-chat/mini-chat/src/domain/ports/mod.rs +++ b/gears/mini-chat/mini-chat/src/domain/ports/mod.rs @@ -21,7 +21,7 @@ use super::error::DomainError; /// /// Identical shape to `oagw_sdk::BodyStream` — conversion at the infra /// boundary is zero-cost. Defined here to keep the domain layer free of -/// HTTP / infra SDK dependencies (enforced by dylint). +/// HTTP / infra SDK dependencies (enforced by `cargo gears lint`). pub type FileStream = Pin>> + Send>>; diff --git a/gears/system/account-management/account-management/src/domain/error_tests.rs b/gears/system/account-management/account-management/src/domain/error_tests.rs index 348186696..f9f4fa24d 100644 --- a/gears/system/account-management/account-management/src/domain/error_tests.rs +++ b/gears/system/account-management/account-management/src/domain/error_tests.rs @@ -291,7 +291,7 @@ fn service_unavailable_without_hint_omits_retry_after() { // by service-layer tests to pin the variant→code/status contract without // going through `AccountManagementError::from(...)` on every assertion. // Production callers MUST go through [`crate::infra::sdk_error_mapping`]; -// this impl block lives in the companion test file (per dylint `DE1101`) so +// this impl block lives in the companion test file (per `cargo gears lint` rule `DE1101`) so // the production [`DomainError`] surface stays free of test-only items. impl DomainError { diff --git a/gears/system/account-management/account-management/src/domain/ports/metrics_tests.rs b/gears/system/account-management/account-management/src/domain/ports/metrics_tests.rs index 9ac998317..972981a9e 100644 --- a/gears/system/account-management/account-management/src/domain/ports/metrics_tests.rs +++ b/gears/system/account-management/account-management/src/domain/ports/metrics_tests.rs @@ -1,7 +1,7 @@ //! Unit tests for the AM observability port traits and label taxonomy. //! //! Kept in a sibling file (not an inline `#[cfg(test)] mod tests`) per -//! the workspace convention enforced by dylint `DE1101`. The tests pin: +//! the workspace convention enforced by `cargo gears lint` rule `DE1101`. The tests pin: //! //! * Every port trait is object-safe and `Arc`-coercible. //! * Closed-set label enums map to the literal strings the legacy diff --git a/gears/system/account-management/account-management/src/infra/canonical_mapping.rs b/gears/system/account-management/account-management/src/infra/canonical_mapping.rs index 40cd604be..080db62a9 100644 --- a/gears/system/account-management/account-management/src/infra/canonical_mapping.rs +++ b/gears/system/account-management/account-management/src/infra/canonical_mapping.rs @@ -2,7 +2,7 @@ //! //! Lives in `infra/` because the classifier reads `sea_orm::DbErr` //! SQLSTATE codes and `toolkit_db::DbError` variant discriminants — -//! both forbidden inside `domain/` by the project-wide Dylint rules +//! both forbidden inside `domain/` by the project-wide `cargo gears lint` rules //! (`DE0301`, `DE0309`). Keeping the classifier here lets //! `domain::error::DomainError` stay pure (no `sea_orm`/`toolkit_db` //! imports, `#[domain_model]` enforced) while still routing DB diff --git a/gears/system/account-management/account-management/src/infra/canonical_mapping_tests.rs b/gears/system/account-management/account-management/src/infra/canonical_mapping_tests.rs index 76a9f3382..3f05ec00f 100644 --- a/gears/system/account-management/account-management/src/infra/canonical_mapping_tests.rs +++ b/gears/system/account-management/account-management/src/infra/canonical_mapping_tests.rs @@ -2,7 +2,7 @@ //! //! Lives in `infra/` so the test code can import `sea_orm::DbErr` //! and `toolkit_db::DbError` directly — both forbidden inside `domain/` -//! by Dylint rules. The tests pin the contract that +//! by `cargo gears lint` rules. The tests pin the contract that //! `with_serializable_retry`'s post-retry classifier and //! `From for DomainError` produce the right typed //! `DomainError` variants for each SQLSTATE / outage signal. diff --git a/gears/system/account-management/account-management/src/infra/error_conv.rs b/gears/system/account-management/account-management/src/infra/error_conv.rs index ac24d8c06..95f54babb 100644 --- a/gears/system/account-management/account-management/src/infra/error_conv.rs +++ b/gears/system/account-management/account-management/src/infra/error_conv.rs @@ -105,7 +105,7 @@ pub(crate) fn is_db_availability_error(err: &DbError) -> bool { // `DbErr::Conn` (handled above) before they would surface as raw // `sqlx::Error`. Deconstructing the wrapped error here would // require depending on `sqlx` directly, which the project-wide - // dylint `de0706_no_direct_sqlx` rule forbids — outside of + // `cargo gears lint` rule `de0706_no_direct_sqlx` forbids — outside of // `toolkit-db`, code must talk to the SecORM abstraction, not raw // `sqlx`. The variant therefore falls through to `Internal`. matches!( diff --git a/gears/system/account-management/account-management/src/infra/rg/checker_tests.rs b/gears/system/account-management/account-management/src/infra/rg/checker_tests.rs index 9ce20149e..7c291171f 100644 --- a/gears/system/account-management/account-management/src/infra/rg/checker_tests.rs +++ b/gears/system/account-management/account-management/src/infra/rg/checker_tests.rs @@ -1,6 +1,6 @@ //! Tests for [`super::RgResourceOwnershipChecker`]. //! -//! Extracted into a companion file per dylint `DE1101` (inline test +//! Extracted into a companion file per `cargo gears lint` rule `DE1101` (inline test //! blocks > 100 lines must move out of the production source file). //! The fakes here are local to the checker's unit tests; the //! cross-gear `SlowRgClient` used by service-level integration diff --git a/gears/system/account-management/account-management/src/infra/types_registry/checker_tests.rs b/gears/system/account-management/account-management/src/infra/types_registry/checker_tests.rs index 051d99142..208cfe62e 100644 --- a/gears/system/account-management/account-management/src/infra/types_registry/checker_tests.rs +++ b/gears/system/account-management/account-management/src/infra/types_registry/checker_tests.rs @@ -1,6 +1,6 @@ //! Tests for [`super::GtsTenantTypeChecker`]. //! -//! Extracted into a companion file per dylint `DE1101` (inline test +//! Extracted into a companion file per `cargo gears lint` rule `DE1101` (inline test //! blocks > 100 lines must move out of the production source file). //! The fakes here are local to the checker's unit tests; the //! cross-gear `SlowRegistry` used by service-level integration diff --git a/gears/system/cluster/docs/ADR/002-async-boundary-no-remote-in-critical-section.md b/gears/system/cluster/docs/ADR/002-async-boundary-no-remote-in-critical-section.md index f1fdf784e..f86770762 100644 --- a/gears/system/cluster/docs/ADR/002-async-boundary-no-remote-in-critical-section.md +++ b/gears/system/cluster/docs/ADR/002-async-boundary-no-remote-in-critical-section.md @@ -46,7 +46,7 @@ Both questions have the same root: **Gears middleware is an async-only, cooperat - Detached tasks (`tokio::spawn` of a release future) are cancelled during runtime shutdown, causing silent leaks in exactly the scenarios release matters most. - TTL on the backend is an unavoidable safety net for every distributed resource; it handles process crash, panic, and forgotten release identically. - The Kleppmann fencing-token argument requires an "unbounded pause of the lock holder while still able to reach the guarded resource". Async + timeouts cannot bound *every* pause source — VM suspend or kernel-scheduler stalls freeze the entire runtime, including any timeout futures. The argument that eliminates the stale-writer scenario is therefore the no-remote-in-critical-section rule: it removes the *guarded resource access* from the critical section, so even an unbounded pause of arbitrary cause cannot produce a stale writer. -- Gears already enforces architectural constraints via dylint (layer rules, no-serde-in-contracts); adding one more rule is cheap. +- Gears already enforces architectural constraints via `cargo gears lint` (layer rules, no-serde-in-contracts); adding one more rule is cheap. ## Considered Options @@ -59,7 +59,7 @@ Both questions have the same root: **Gears middleware is an async-only, cooperat ## Decision Outcome -Chosen options: **Option 4** (no-op `Drop` + explicit async release) combined with **Option 6** (remove fencing tokens; enforce no-remote-in-critical-section via dylint). +Chosen options: **Option 4** (no-op `Drop` + explicit async release) combined with **Option 6** (remove fencing tokens; enforce no-remote-in-critical-section via `cargo gears lint`). The `LockGuard`, `ServiceHandle`, and `LeaderWatch` types have no-op `Drop` implementations. Remote cleanup is exposed as explicit async methods: @@ -69,7 +69,7 @@ The `LockGuard`, `ServiceHandle`, and `LeaderWatch` types have no-op `Drop` impl Consumers that forget to call these rely on the backend TTL for eventual cleanup. The TTL is bounded (seconds, not hours) and identical in behavior to the process-crash case. -The `LockGuard` does not expose a fencing token. Instead, Gears enforces two architectural principles via a dylint rule: +The `LockGuard` does not expose a fencing token. Instead, Gears enforces two architectural principles via a architecture lint rule: 1. All cluster operations are `async fn` invoked within Tokio. Consumers SHOULD wrap them with `tokio::time::timeout` to bound blocking on network calls. 2. Code protected by a `LockGuard` (or inside a database transaction) MUST NOT make additional remote I/O calls. Remote effects MUST occur before `try_lock` or after `release`, never between them. @@ -83,13 +83,13 @@ The TTL safety net composes with both: any pause of duration ≥ TTL produces an This coverage is intentional and complete: bounded pauses (GC) and unbounded pauses (VM suspend) are handled by the same mechanism, because the mechanism doesn't depend on the pause being bounded. -The dylint rule that enforces "no remote I/O in critical sections" is initially scoped to the four cluster backend traits within `try_lock` / `release` scopes. Database-transaction enforcement (treating an open `sqlx::Transaction` as a critical section) is deferred to a follow-up rule extension once the wiring crate and consumer migrations land — the lint surface for cluster locks alone is the high-value target and worth shipping first. +The architecture lint rule that enforces "no remote I/O in critical sections" is initially scoped to the four cluster backend traits within `try_lock` / `release` scopes. Database-transaction enforcement (treating an open `sqlx::Transaction` as a critical section) is deferred to a follow-up rule extension once the wiring crate and consumer migrations land — the lint surface for cluster locks alone is the high-value target and worth shipping first. ### Consequences - Consumers MUST explicitly call `release().await` (or `deregister().await`, `resign().await`) for timely handoff. Forgetting the call leaks the resource until TTL — a bounded but undesirable delay. Linters and code review catch most forgotten calls. - The `LockGuard` API is simpler (no `fencing_token()` method). Provider implementations are significantly simpler: no Lua `INCR` (Redis), no sequence table (Postgres), no annotation CAS (K8s), no `mod_revision` coupling (etcd). -- Consumers whose critical section contained remote I/O must restructure: compute locally, release the lock, then apply remote effects. The dylint rule flags violations at compile time. +- Consumers whose critical section contained remote I/O must restructure: compute locally, release the lock, then apply remote effects. The architecture lint rule flags violations at compile time. - The TTL becomes the operationally-visible bound on forgotten cleanup. Monitoring SHOULD alert on unusually long TTL-expiry rates (indicator of a bug where release is consistently missed). - If a future consumer has a genuine fencing need for a resource with its own concurrency control (e.g., an external storage layer with fencing support), they can generate a monotonic sequence via `ClusterCache::compare_and_swap` at the application level. The primitive does not need to be in the lock API. @@ -97,7 +97,7 @@ The dylint rule that enforces "no remote I/O in critical sections" is initially - Unit tests verify that `Drop` on `LockGuard`, `ServiceHandle`, and `LeaderWatch` performs no I/O (no panics under Tokio; no detached tasks spawned). - Integration tests verify that forgotten release results in TTL-bounded cleanup (lock becomes available within TTL+epsilon). -- Dylint rule `no-remote-in-critical-section` flags violations in unit tests with known-bad inputs; passes on known-good inputs. +- Architecture lint rule `no-remote-in-critical-section` flags violations in unit tests with known-bad inputs; passes on known-good inputs. - Cluster provider implementations are reviewed to confirm no fencing-token generation exists in production code paths. - Unbounded-pause coverage test (per-backend integration suite): simulate a holder pause longer than TTL by suspending the holder process or pausing its async runtime via `pause_runtime_for(ttl + epsilon)`. Assert: (a) backend releases the lock at TTL, (b) a successor acquires within `epsilon`, (c) on resume, the original holder's `release().await` is a benign no-op against the foreign holder, (d) the original holder's subsequent CAS write attempt against the guarded resource returns `CasConflict` when the successor has changed it, and `Ok` only when the resource state matches the holder's pre-pause expected_version. @@ -159,11 +159,11 @@ The dylint rule that enforces "no remote I/O in critical sections" is initially - Good, because it eliminates the scenario fencing protects against at the architectural level, not at the API level. - Good, because the principle (no remote I/O inside critical sections) is a good architectural rule independent of fencing — it prevents deadlocks, bounds critical section duration, and simplifies reasoning about partial-failure scenarios. -- Good, because compile-time enforcement (dylint) catches violations early. Existing workspace dylint rules establish the pattern. +- Good, because compile-time enforcement (`cargo gears lint`) catches violations early. Existing workspace architecture lint rules establish the pattern. - Good, because it removes significant provider implementation complexity. - Good, because if a future consumer needs fencing for a specific external resource, they can implement it at the application level via `ClusterCache::compare_and_swap`. - Bad, because it imposes a restriction on consumers: their critical sections cannot contain remote I/O. Consumers whose existing patterns violate this must refactor. -- Bad, because the dylint rule has to distinguish "remote" traits from "local" traits. Requires a maintained registry of remote-trait signatures. +- Bad, because the architecture lint rule has to distinguish "remote" traits from "local" traits. Requires a maintained registry of remote-trait signatures. - Neutral, because "no remote I/O inside critical sections" is an architectural best practice regardless; formalizing it strengthens the system. ## More Information @@ -216,7 +216,7 @@ async fn update_tenant_rate_limit( } ``` -**Bad pattern — remote I/O inside the critical section (dylint rule rejects this):** +**Bad pattern — remote I/O inside the critical section (architecture lint rule rejects this):** ```rust async fn update_tenant_rate_limit_BAD( @@ -231,13 +231,13 @@ async fn update_tenant_rate_limit_BAD( // If the cache is slow or partitioned, the lock TTL expires while the // holder is still trying to read, creating the classic stale-writer scenario. let current = cluster.cache().get(&format!("oagw/counter/{}", tenant_id)).await?; - // ^^^^^^^^^^^^^^^ dylint: E0001 `no-remote-in-critical-section` + // ^^^^^^^^^^^^^^^ `cargo gears lint`: E0001 `no-remote-in-critical-section` let new_val = increment(¤t.unwrap().value); // WRONG: another remote call inside the critical section. cluster.cache().put(&format!("oagw/counter/{}", tenant_id), &new_val, None).await?; - // ^^^ dylint: E0001 `no-remote-in-critical-section` + // ^^^ `cargo gears lint`: E0001 `no-remote-in-critical-section` guard.release().await?; Ok(()) @@ -303,7 +303,7 @@ This decision directly addresses the following requirements and design elements: - `cpt-cf-clst-fr-leader-resign` — `LeaderWatch::resign(self)` as explicit step-down. - `cpt-cf-clst-fr-sd-register` — `ServiceHandle::deregister(self)` as explicit teardown. - `cpt-cf-clst-nfr-bounded-critical-section` — Async + timeouts + no-remote-in-critical-section structurally bounds critical sections. -- `cpt-cf-clst-constraint-no-remote-in-critical-section` (DESIGN §2.2) — Architectural rule enforced via dylint. +- `cpt-cf-clst-constraint-no-remote-in-critical-section` (DESIGN §2.2) — Architectural rule enforced via `cargo gears lint`. - DESIGN §3.3 lock contract — Method signatures and `Drop` semantics realize this ADR. - DESIGN §3.7 Lifecycle Pattern (Builder/Handle) — Post-shutdown best-effort `Ok` semantics for `release` / `deregister` / `resign` derive from this ADR's release model. diff --git a/gears/system/cluster/docs/DECOMPOSITION.md b/gears/system/cluster/docs/DECOMPOSITION.md index f34cf2da7..0f09a7ca0 100644 --- a/gears/system/cluster/docs/DECOMPOSITION.md +++ b/gears/system/cluster/docs/DECOMPOSITION.md @@ -423,12 +423,12 @@ The remaining out-of-scope elements (lifecycle wiring, standalone and external p - [x] `p3` - **ID**: `cpt-cf-clst-feature-lock-lint` -- **Purpose**: Make the no-remote-I/O-in-critical-section rule enforceable rather than aspirational, via a workspace dylint rule that flags cross-instance remote calls inside a cluster lock's critical section at compile time. Sequenced after the lock primitive so the lint has real `try_lock`/`release` scopes to target. +- **Purpose**: Make the no-remote-I/O-in-critical-section rule enforceable rather than aspirational, via a workspace architecture lint rule (via `cargo gears lint`) that flags cross-instance remote calls inside a cluster lock's critical section at compile time. Sequenced after the lock primitive so the lint has real `try_lock`/`release` scopes to target. - **Depends On**: `cpt-cf-clst-feature-distributed-lock` - **Scope**: - - New dylint crate under `tools/dylint_lints/` (e.g. `de14_cluster/de14XX_no_remote_in_critical_section/`); added to that workspace's members; modeled on the existing `de0707_drop_zeroize` lint. + - New architecture lint rule in the `cargo-gears` CLI (e.g. `de14_cluster/de14XX_no_remote_in_critical_section/`); modeled on the existing `de0707_drop_zeroize` lint. - Lint scope restricted to the four cluster backend traits within `try_lock`/`release` scopes (DB-tx enforcement is a follow-up rule extension). - **Out of scope**: @@ -444,7 +444,7 @@ The remaining out-of-scope elements (lifecycle wiring, standalone and external p - [x] `p3` - `cpt-cf-clst-constraint-no-remote-in-critical-section` - **Domain Model Entities**: - - None (workspace dylint crate; no domain entities). + - None (architecture lint rule; no domain entities). - **API**: - Lint: `DE14XX_NO_REMOTE_IN_CRITICAL_SECTION` (Deny). diff --git a/gears/system/cluster/docs/DESIGN.md b/gears/system/cluster/docs/DESIGN.md index b9d3aa052..c2c9dc8f3 100644 --- a/gears/system/cluster/docs/DESIGN.md +++ b/gears/system/cluster/docs/DESIGN.md @@ -72,7 +72,7 @@ Explicit pub/sub messaging is excluded. The event broker gear provides reliable | ADR | Summary | |-----|---------| | `cpt-cf-clst-adr-provider-compat-perf` (ADR-001) | Provider compatibility and performance analysis — per-primitive routing as operator config, per-backend characteristics, prefix-based routing, subscriber leases as cache not locks | -| `cpt-cf-clst-adr-async-boundary-no-remote-critical` (ADR-002) | Async boundary and no remote I/O in critical sections — no-op `Drop` with explicit async release, fencing tokens removed from public API, dylint enforcement (cluster-trait-scoped) | +| `cpt-cf-clst-adr-async-boundary-no-remote-critical` (ADR-002) | Async boundary and no remote I/O in critical sections — no-op `Drop` with explicit async release, fencing tokens removed from public API, `cargo gears lint` enforcement (cluster-trait-scoped) | | `cpt-cf-clst-adr-watch-event-lifecycle-contract` (ADR-003) | Watch event lifecycle contract for all three watches — union-type `*WatchEvent { value-variant, Lagged, Reset, Closed }` instead of `Result`-based signaling, applied to cache, leader, and service-discovery watches; lightweight key-only cache events as the contract twin of `Lagged`/`Reset` | | `cpt-cf-clst-adr-observability-contract` (ADR-004) | Observability as a versioned naming contract — spans, metrics, log events are part of the SDK contract; cardinality rule forbids keys/names as metric labels | | `cpt-cf-clst-adr-facade-backend-pattern` (ADR-005) | Per-primitive facade-plus-backend-trait pattern, per-primitive `*V1` versioning, no root `Cluster` trait | @@ -87,8 +87,8 @@ Explicit pub/sub messaging is excluded. The event broker gear provides reliable | NFR Summary | Allocated To | Design Response | Verification Approach | |-------------|--------------|-----------------|----------------------| | At most one leader per election name (when bound to `Linearizable` cache) | All backends + SDK defaults | Trait contract enforces single-leader guarantee; capability validation rejects `EventuallyConsistent` cache without explicit opt-in | Multi-task contention smoke tests against `MemCacheBackend`; per-backend integration tests in plugin follow-ups | -| Bounded lock holding (no stale writers) | Consumers + dylint rule | Async + timeouts bound critical section; dylint forbids remote I/O inside `try_lock`/`release` scopes (lint scope is initially restricted to the four cluster backend traits; DB-tx enforcement is a follow-up rule extension) | Dylint rule check; smoke tests for lock release-on-timeout | -| No serde in contract types | SDK crate | Dylint layer rules enforce no serde in trait definitions | `make check` (dylint lints) | +| Bounded lock holding (no stale writers) | Consumers + architecture lint rule | Async + timeouts bound critical section; `cargo gears lint` forbids remote I/O inside `try_lock`/`release` scopes (lint scope is initially restricted to the four cluster backend traits; DB-tx enforcement is a follow-up rule extension) | Architecture lint rule check; smoke tests for lock release-on-timeout | +| No serde in contract types | SDK crate | `cargo gears lint` layer rules enforce no serde in trait definitions | `make check` (architecture lints) | | Watch event delivery — at-most-once with per-key ordering and lifecycle signals | All backends | Union-type events (`*WatchEvent`) carry `Lagged{dropped}`, `Reset`, `Closed(err)` so consumers recover from missed events explicitly | Smoke tests across all three watches verifying each variant is observable | | Backend trait dyn-compatibility | SDK crate | Compile-time assertions (`fn _assert_dyn_compat(_: Arc) {}`) per trait | Build fails if dyn-compat is broken | @@ -109,7 +109,7 @@ Each functional requirement from the PRD maps to the SDK surface and design sect | `cpt-cf-clst-fr-leader-advisory` | Advisory semantics documented on the facade contract (§3.3, §4.1) | | `cpt-cf-clst-fr-lock-acquire` | `DistributedLockV1` acquire-or-fail and acquire-with-wait (§3.3) | | `cpt-cf-clst-fr-lock-release` | Explicit async release with TTL safety net; no-op `Drop` (§2.2 no-remote-in-critical-section, §3.3) | -| `cpt-cf-clst-fr-lock-no-remote` | Dylint rule forbidding remote I/O inside lock critical sections (§2.2, §3.10) | +| `cpt-cf-clst-fr-lock-no-remote` | Architecture lint rule forbidding remote I/O inside lock critical sections (§2.2, §3.10) | | `cpt-cf-clst-fr-sd-register` | `ServiceDiscoveryV1` instance registration with metadata (§3.3) | | `cpt-cf-clst-fr-sd-discover` | State- and metadata-filtered instance listing (§3.3) | | `cpt-cf-clst-fr-sd-watch` | Topology `ServiceDiscoveryWatchEvent` with lifecycle signals (§3.9) | @@ -135,7 +135,7 @@ Each non-functional requirement from the PRD maps to its design response and ver | Requirement | Design Response | |-------------|-----------------| | `cpt-cf-clst-nfr-leader-guarantee` | Single-leader contract bound to `Linearizable` cache; weak-consistency requires explicit opt-in (§3.10, ADR-009) | -| `cpt-cf-clst-nfr-bounded-critical-section` | Async + timeouts plus dylint no-remote-I/O rule bound the critical section (§2.2, §3.10) | +| `cpt-cf-clst-nfr-bounded-critical-section` | Async + timeouts plus architecture lint no-remote-I/O rule bound the critical section (§2.2, §3.10) | | `cpt-cf-clst-nfr-watch-delivery` | At-most-once, per-key-ordered delivery with explicit `Lagged`/`Reset`/`Closed` recovery (§3.9, ADR-003) | | `cpt-cf-clst-nfr-observability` | Versioned spans/metrics/log-event naming contract; cardinality rule (§3.10, ADR-004) | | `cpt-cf-clst-nfr-capability-validation` | Capability requirements validated at resolution/startup (§3.10) | @@ -230,13 +230,13 @@ All three watch event types (`CacheWatchEvent`, `LeaderWatchEvent`, `ServiceWatc - [x] `p1` - **ID**: `cpt-cf-clst-constraint-no-serde` -The `cf-cluster-sdk` crate MUST NOT depend on serde. Serialization concerns belong in plugin implementations. Enforced by dylint lints in the workspace. +The `cf-cluster-sdk` crate MUST NOT depend on serde. Serialization concerns belong in plugin implementations. Enforced by architecture lints in the workspace. #### No Remote I/O in Cluster Critical Sections - [x] `p1` - **ID**: `cpt-cf-clst-constraint-no-remote-in-critical-section` -Code protected by a `LockGuard` MUST NOT make additional remote calls. Remote effects MUST occur before `try_lock` or after `release`, never between them. Together with async + timeouts, this eliminates the Kleppmann fencing scenario at the architectural level. Enforced by a workspace dylint rule scoped to the four cluster backend traits within `try_lock`/`release` scopes; DB-tx enforcement is a follow-up rule extension once the wiring crate and consumer migrations land. See ADR-002. +Code protected by a `LockGuard` MUST NOT make additional remote calls. Remote effects MUST occur before `try_lock` or after `release`, never between them. Together with async + timeouts, this eliminates the Kleppmann fencing scenario at the architectural level. Enforced by a workspace architecture lint rule scoped to the four cluster backend traits within `try_lock`/`release` scopes; DB-tx enforcement is a follow-up rule extension once the wiring crate and consumer migrations land. See ADR-002. #### Backend Trait Dyn-Compatibility diff --git a/gears/system/cluster/docs/TRACEABILITY-AUDIT.md b/gears/system/cluster/docs/TRACEABILITY-AUDIT.md index 361761406..96da4a496 100644 --- a/gears/system/cluster/docs/TRACEABILITY-AUDIT.md +++ b/gears/system/cluster/docs/TRACEABILITY-AUDIT.md @@ -29,7 +29,7 @@ resolution of the two open questions. - **Feature source**: the assignment recorded in [DECOMPOSITION.md](DECOMPOSITION.md) §2 ("Requirements Covered" per feature). - **Marker source**: `@cpt-dod:` markers grepped from `cluster-sdk/src`, - `cluster-sdk/tests`, `cluster-sdk/examples`, and `tools/dylint_lints/de14_cluster`. + `cluster-sdk/tests`, `cluster-sdk/examples`, and architecture lints (in `cargo-gears` CLI). - **Scope key**: `code` = realized by this change's shipped code; `follow-up` = enabling contract shipped here, full realization deferred to the wiring crate / parent host gear per PRD §4.1 (each still maps to a realizing ADR/DESIGN section, diff --git a/gears/system/cluster/docs/features/010-lock-lint.md b/gears/system/cluster/docs/features/010-lock-lint.md index 5da769b59..962680e1a 100644 --- a/gears/system/cluster/docs/features/010-lock-lint.md +++ b/gears/system/cluster/docs/features/010-lock-lint.md @@ -26,7 +26,7 @@ ### 1.1 Overview -Makes the no-remote-I/O-in-critical-section rule enforceable rather than aspirational, via a workspace static-analysis (dylint) rule that flags cross-instance remote calls inside a cluster lock's critical section at compile time. It is sequenced after the lock primitive so the lint has real acquire/release scopes to target. +Makes the no-remote-I/O-in-critical-section rule enforceable rather than aspirational, via a workspace architecture lint rule (via `cargo gears lint`) that flags cross-instance remote calls inside a cluster lock's critical section at compile time. It is sequenced after the lock primitive so the lint has real acquire/release scopes to target. ### 1.2 Purpose @@ -43,7 +43,7 @@ Forbidding remote I/O inside the critical section, combined with async timeouts ### 1.4 References - **PRD**: [PRD.md](../PRD.md) §5.3 (no remote I/O in critical section), §6.1 (bounded critical section NFR) -- **Design**: [DESIGN.md](../DESIGN.md) §2.2 (constraint and dylint scope), §1.2 (NFR allocation) +- **Design**: [DESIGN.md](../DESIGN.md) §2.2 (constraint and lint scope), §1.2 (NFR allocation) - **ADRs**: [ADR-002](../ADR/002-async-boundary-no-remote-in-critical-section.md) - **Dependencies**: - [x] `p2` - `cpt-cf-clst-feature-distributed-lock` @@ -101,7 +101,7 @@ Not applicable — the lint is a static-analysis rule with no runtime entity lif - [ ] `p1` - **ID**: `cpt-cf-clst-dod-lock-lint-rule` -The system **MUST** provide a workspace dylint rule (under the workspace lint tooling, modeled on the existing drop-zeroize lint) that flags, at deny level, cross-instance remote calls inside a cluster lock's critical section, scoped initially to the four cluster backend traits between acquisition and release. +The system **MUST** provide a workspace architecture lint rule (via `cargo gears lint`, modeled on the existing drop-zeroize lint) that flags, at deny level, cross-instance remote calls inside a cluster lock's critical section, scoped initially to the four cluster backend traits between acquisition and release. **Implements**: - `cpt-cf-clst-flow-lock-lint-build-fail` @@ -110,7 +110,7 @@ The system **MUST** provide a workspace dylint rule (under the workspace lint to **Constraints**: `cpt-cf-clst-constraint-no-remote-in-critical-section` **Touches**: -- Entities: workspace dylint rule crate +- Entities: workspace architecture lint rule (in `cargo-gears` CLI) ## 6. Acceptance Criteria diff --git a/gears/system/oagw/docs/ADR/0001-component-architecture.md b/gears/system/oagw/docs/ADR/0001-component-architecture.md index 74218191f..f6dd4b555 100644 --- a/gears/system/oagw/docs/ADR/0001-component-architecture.md +++ b/gears/system/oagw/docs/ADR/0001-component-architecture.md @@ -119,7 +119,7 @@ All services communicate via in-process trait method calls. There is no inter-se * Good, because trait-based isolation enables independent testing of CP and DP (e.g., `MockControlPlaneService`) * Good, because single crate simplifies build, dependency management, and deployment -* Good, because DDD-Light layering keeps domain logic separate from infrastructure, enforced by dylint linters +* Good, because DDD-Light layering keeps domain logic separate from infrastructure, enforced by architecture lints (via `cargo gears lint`) * Good, because migration to separate crates remains possible if needed * Good, because zero overhead — direct Rust function calls, no serialization or RPC diff --git a/gears/system/oagw/docs/adr-component-architecture.md b/gears/system/oagw/docs/adr-component-architecture.md index d37f2e1cb..9981272c4 100644 --- a/gears/system/oagw/docs/adr-component-architecture.md +++ b/gears/system/oagw/docs/adr-component-architecture.md @@ -79,7 +79,7 @@ All services communicate via in-process trait method calls. There is no inter-se - **Testability**: Services are tested in isolation via trait mocking (e.g., `MockControlPlaneService`) - **Performance**: Zero overhead — direct Rust function calls, no serialization or RPC - **Simplicity**: Single crate eliminates multi-crate coordination, versioning, and build complexity -- **Maintainability**: DDD-Light layering (`domain/infra/api`) enforced by dylint linters +- **Maintainability**: DDD-Light layering (`domain/infra/api`) enforced by architecture lints (via `cargo gears lint`) ### Negative diff --git a/gears/system/oagw/oagw/src/infra/metrics.rs b/gears/system/oagw/oagw/src/infra/metrics.rs index ab24058ee..29ffeaca6 100644 --- a/gears/system/oagw/oagw/src/infra/metrics.rs +++ b/gears/system/oagw/oagw/src/infra/metrics.rs @@ -18,7 +18,7 @@ use crate::domain::ports::metric_labels::{METHOD_OTHER, key}; /// so both gears emit the same `http.request.method` vocabulary. /// /// Lives in the infra layer because the domain layer must not depend on -/// transport-level types like `http::Method` (dylint `DE0301`/`DE0308`). +/// transport-level types like `http::Method` (`cargo gears lint` rules `DE0301`/`DE0308`). #[must_use] pub(crate) fn normalize_method(method: &http::Method) -> &'static str { match *method { diff --git a/gears/system/quota-enforcement/docs/DESIGN.md b/gears/system/quota-enforcement/docs/DESIGN.md index c3865bd67..601012908 100644 --- a/gears/system/quota-enforcement/docs/DESIGN.md +++ b/gears/system/quota-enforcement/docs/DESIGN.md @@ -906,7 +906,7 @@ Layered chain: `StorageError → DomainError → CanonicalError`. The SDK error validation errors (`InvalidAmount`, `BulkTooLarge`, `CannotDeleteSeededGlobalPolicy`, …) have no `StorageError` counterpart by construction. - **`From for DomainError`** — every `StorageError` variant has a 1:1 lift; defined alongside - `DomainError` in `domain/error.rs` (no `sea_orm`/`toolkit_db` imports — same Dylint discipline as AM). + `DomainError` in `domain/error.rs` (no `sea_orm`/`toolkit_db` imports — same architecture lint discipline as AM). - **`From for CanonicalError`** — boundary mapping in `quota-enforcement/src/infra/canonical_mapping.rs` (kept out of `domain/` because the lift may classify backend-specific failures via `cf-toolkit-db` helpers, which the `domain/` layer is not permitted to import). Handlers return `ApiResult = Result` and use `?` for diff --git a/guidelines/GTS.md b/guidelines/GTS.md index ee32d7521..9deb19c99 100644 --- a/guidelines/GTS.md +++ b/guidelines/GTS.md @@ -1065,7 +1065,7 @@ Types that **must not be extended**. No derived types are allowed. 6. **DB storage**: base fields in columns, extension data in `JSONB` or `TEXT` 7. **Error types**: use GTS identifiers as RFC 9457 `type` URIs for machine-readable error classification 8. **Access control**: structure identifiers so that wildcard policies can grant/revoke access at the vendor, package, or namespace level -9. **Dylint enforcement**: GTS-specific lints validate identifier correctness and prevent unsupported patterns at compile time +9. **Architecture lint enforcement**: GTS-specific lints (via `cargo gears lint`) validate identifier correctness and prevent unsupported patterns at compile time 10. **Constants as GTS instances**: discriminator fields and string constants that select behavior, routing, or authorization should be GTS well-known instances — not raw strings or Rust enums (see [section 6.6](#66-gts-well-known-instances-for-constants-and-discriminator-values)) 11. **Rust naming**: in new code prefer `type_id`/`TYPE_ID`/`GtsTypeId` naming; treat `schema_id` names as deprecated compatibility aliases. The `schema_id = "..."` macro attribute produces a compile-time deprecation warning — use `type_id = "..."` instead 12. **Schema dialect**: handwritten GTS JSON Schemas and fixtures must target Draft-07 (`"$schema": "http://json-schema.org/draft-07/schema#"`) and avoid post-Draft-07 keywords; use `definitions` (not `$defs`) for local reusable subschemas diff --git a/libs/toolkit-macros/src/domain_model.rs b/libs/toolkit-macros/src/domain_model.rs index b83bf64c8..fced8f04a 100644 --- a/libs/toolkit-macros/src/domain_model.rs +++ b/libs/toolkit-macros/src/domain_model.rs @@ -720,7 +720,7 @@ mod tests { #[test] fn test_allowed_infra_in_domain_layer() { - // "infra::Repo" is now allowed (architectural decision left to dylint) + // "infra::Repo" is now allowed (architectural decision left to `cargo gears lint`) let input: DeriveInput = parse_quote! { pub struct GoodModel { pub repo: infra::Repo, @@ -736,7 +736,7 @@ mod tests { #[test] fn test_allowed_api_in_domain_layer() { - // "api::Handler" is now allowed (architectural decision left to dylint) + // "api::Handler" is now allowed (architectural decision left to `cargo gears lint`) let input: DeriveInput = parse_quote! { pub struct GoodModel { pub handler: api::Handler, diff --git a/libs/toolkit-macros/tests/ui/pass/domain_model_infra_allowed.rs b/libs/toolkit-macros/tests/ui/pass/domain_model_infra_allowed.rs index c5da3ace4..27fdb851f 100644 --- a/libs/toolkit-macros/tests/ui/pass/domain_model_infra_allowed.rs +++ b/libs/toolkit-macros/tests/ui/pass/domain_model_infra_allowed.rs @@ -7,7 +7,7 @@ mod infra { // `infra::` paths are allowed in domain models. // Architectural enforcement (preventing infra in domain layer) is handled -// by dylint rules, not by the macro itself. +// by `cargo gears lint` rules, not by the macro itself. #[domain_model] pub struct GoodModel { pub repo: infra::UserRepository, diff --git a/libs/toolkit/src/domain/mod.rs b/libs/toolkit/src/domain/mod.rs index 137b1461f..7526a70dc 100644 --- a/libs/toolkit/src/domain/mod.rs +++ b/libs/toolkit/src/domain/mod.rs @@ -43,7 +43,7 @@ //! clear error messages at macro expansion time, similar to how `#[api_dto]` validates //! its arguments. //! -//! Additional enforcement is provided by Dylint lints: +//! Additional enforcement is provided by `cargo gears lint` rules: //! - `DE0301`: Prohibits infrastructure imports in domain layer //! - `DE0308`: Prohibits HTTP types in domain layer diff --git a/studio-kit-gears/artifacts/PR-CODE-REVIEW-TEMPLATE/template.md b/studio-kit-gears/artifacts/PR-CODE-REVIEW-TEMPLATE/template.md index 31a33e5bb..4b92f1c8d 100644 --- a/studio-kit-gears/artifacts/PR-CODE-REVIEW-TEMPLATE/template.md +++ b/studio-kit-gears/artifacts/PR-CODE-REVIEW-TEMPLATE/template.md @@ -50,7 +50,7 @@ No reviewer comments found. {Assessment of logic, edge cases, error handling.} -### Cargo / Clippy / Dylint / Rustfmt Conformance {icon} +### Cargo / Clippy / Architecture Lints / Rustfmt Conformance {icon} {Assessment of tooling conformance. N/A if no Rust code changed.} diff --git a/tools/dylint_lints/.cargo/config.toml b/tools/dylint_lints/.cargo/config.toml deleted file mode 100644 index 8b3f5749f..000000000 --- a/tools/dylint_lints/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.'cfg(all())'] -linker = "dylint-link" \ No newline at end of file diff --git a/tools/dylint_lints/AGENTS.md b/tools/dylint_lints/AGENTS.md deleted file mode 100644 index 6aeefb186..000000000 --- a/tools/dylint_lints/AGENTS.md +++ /dev/null @@ -1,274 +0,0 @@ -# Agent Guide: Adding Dylint Lints - -## Quick Start - -1. **Initialize**: `cargo dylint new ` in `dylint_lints/` -2. **Configure**: Update `Cargo.toml` with dependencies and example targets -3. **Implement**: Write lint logic in `src/lib.rs` -4. **Test**: Create UI test files in `ui/` with corresponding `.stderr` files. If the `main.rs` and `main.stderr` are empty, remove them. -5. **Register**: Add to workspace in `dylint_lints/Cargo.toml` - -## Lint Pass Selection - -### Pre-Expansion Lint (`declare_pre_expansion_lint!`) -**Use when**: Checking derive attributes before macro expansion - -**Characteristics**: -- Runs before proc macros expand -- Uses `EarlyLintPass` with AST (`rustc_ast`) -- Can see `#[derive(...)]` attributes directly -- Required for detecting serde/utoipa derives - -**Example**: `de0101_no_serde_in_contract`, `de0102_no_toschema_in_contract` - -```rust -dylint_linting::declare_pre_expansion_lint! { - pub LINT_NAME, - Deny, - "description" -} - -impl EarlyLintPass for LintName { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - // Check derive attributes before macro expansion - } -} -``` - -### Early Lint Pass (`declare_early_lint!`) -**Use when**: Checking syntax/structure before type checking - -**Characteristics**: -- Runs after macro expansion but before type resolution -- Uses `EarlyLintPass` with AST (`rustc_ast`) -- No type information available -- Fast, syntax-level checks - -**Example**: Naming conventions, syntax patterns - -### Late Lint Pass (`declare_late_lint!`) -**Use when**: Need type information or semantic analysis - -**Characteristics**: -- Runs after type checking -- Uses `LateLintPass` with HIR (`rustc_hir`) -- Full type information available -- Can check trait implementations, method calls, etc. - -**Example**: Type-based checks, semantic validation - -## Implementation Pattern (Pre-Expansion) - -### 1. Crate Structure -``` -de0xxx_lint_name/ -├── Cargo.toml # Dependencies + example targets -├── src/lib.rs # Lint implementation -└── ui/ # UI tests - ├── test1.rs - ├── test1.stderr - ├── test2.rs - └── test2.stderr -``` - -### 2. Cargo.toml Configuration -Common dependencies (`clippy_utils`, `dylint_linting`, `dylint_testing`, `lint_utils`) are defined in the workspace `Cargo.toml`. Reference them using `.workspace = true`: - -```toml -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -# Add trait/macro crates needed for tests - -[[example]] -name = "test_case_name" -path = "ui/test_case_name.rs" -``` - -**Note**: Only add lint-specific dependencies to individual `Cargo.toml` files. Keep common dependencies in the workspace to avoid duplication. - -## Testing Options - -### ui_examples vs ui_test vs ui_test_example - -**Use `ui_test_examples`** (Recommended): -- Tests all example targets defined in `Cargo.toml` -- Each example is a separate test case -- Examples live in `ui/` directory -- Best for multiple independent test scenarios -- Used by: `de0101`, `de0102` - -```rust -#[test] -fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); -} -``` - -**Use `ui_test`**: -- Tests all `.rs` files in a directory -- No need for `[[example]]` targets in `Cargo.toml` -- Files share dependencies from `[dev-dependencies]` -- Good for many small test cases - -```rust -#[test] -fn ui() { - dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui"); -} -``` - -**Use `ui_test_example`**: -- Tests a single specific example target -- Useful for focused testing during development -- Can be combined with `ui_test_examples` - -```rust -#[test] -fn specific_case() { - dylint_testing::ui_test_example(env!("CARGO_PKG_NAME"), "example_name"); -} -``` - -**Choose `ui_test_examples` when**: -- Your tests need external dependencies (e.g., serde, utoipa) -- You want explicit test case organization -- Test cases are logically distinct scenarios - -**Choose `ui_test` when**: -- All tests have no external dependencies -- You have many small, similar test cases -- You want simpler `Cargo.toml` configuration - -## UI Testing - -### Test File Structure -```rust -mod contract { - use target_crate::TargetTrait; - - #[derive(Debug, Clone, TargetTrait)] - // Should trigger DEXXX - description of what triggers - pub struct Example { - pub field: String, - } -} - -fn main() {} -``` - -### Comment Annotations for Test Validation - -**Purpose**: Validate that test comments match actual lint behavior in `.stderr` files. - -**Comment Format**: -- `// Should trigger DEXXX - description` - Marks code that MUST trigger the lint -- `// Should not trigger DEXXX - description` - Marks code that MUST NOT trigger the lint - -**Placement Rules**: -- Place comment on the line **immediately before** where the error is reported -- For multiline spans (structs, enums, functions), the error is reported on the **first line** of the item -- NOT on the attribute line (e.g., `#[derive(...)]`), but on the item declaration line - -**Example - Correct**: -```rust -#[derive(Debug, Clone)] -// Should trigger DE0203 - DTOs must have serde derives -pub struct UserDto { // Error reported HERE - pub id: String, -} -``` - -**Example - Incorrect**: -```rust -// Should trigger DE0203 - DTOs must have serde derives -#[derive(Debug, Clone)] // Comment expects error on next line (derive) -pub struct UserDto { // But error is actually reported HERE - pub id: String, -} -``` - -**Required Unit Test**: -Every lint MUST include this test to enforce comment/stderr alignment: - -```rust -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DEXXX", // Lint code - "description of what triggers" // Must match comment text - ); - } -} -``` - -This test validates: -1. Every "Should trigger" comment has a corresponding error in `.stderr` -2. Every "Should not trigger" comment has NO error in `.stderr` -3. Every error in `.stderr` has a corresponding "Should trigger" comment - -### Generating .stderr Files -1. Run tests: `cargo test --lib ui_examples` -2. Copy normalized stderr from test output -3. Create `.stderr` file with `$DIR/` placeholder for paths -4. Line numbers must match exactly -5. Add comment annotations as described above - -### Example .stderr -``` -error: contract type should not derive `TargetTrait` (DEXXX) - --> $DIR/test_case.rs:5:5 - | -LL | / pub struct Example { -LL | | pub field: String, -LL | | } - | |_^ - | - = help: helpful suggestion here - = note: `#[deny(lint_name)]` on by default - -error: aborting due to 1 previous error - -``` - -## Shared Utilities - -### lint_utils Crate -- `is_in_contract_gear_ast()`: Check if AST item is in contract/ directory -- Add new helpers as needed for common patterns - -## Checklist - -- [ ] Run `cargo dylint new ` -- [ ] Update `Cargo.toml` with dependencies -- [ ] Add example targets for each test case -- [ ] Implement lint with appropriate pass type -- [ ] Create UI test files in `ui/` with comment annotations -- [ ] Generate `.stderr` golden files -- [ ] Add `test_comment_annotations_match_stderr` unit test -- [ ] Verify all tests pass: `cargo test --lib` -- [ ] Add to workspace `members` in root `Cargo.toml` -- [ ] Document lint behavior in doc comments - -## Common Pitfalls - -1. **Wrong lint pass**: Pre-expansion for derives, late for types -2. **Gear detection**: Must handle both `mod contract {}` and `contract/` directories -3. **Line numbers**: `.stderr` files must match exact line numbers including `#[allow(dead_code)]` -4. **Empty tests**: Include test case with no violations (empty `.stderr`) -5. **Workspace**: Don't forget to add new crate to workspace members -6. **Test verification**: Always verify correct package tests are running with `-p` flag -7. **simulated_dir**: Only works with EarlyLintPass, not LateLintPass diff --git a/tools/dylint_lints/Cargo.lock b/tools/dylint_lints/Cargo.lock deleted file mode 100644 index cce238ef3..000000000 --- a/tools/dylint_lints/Cargo.lock +++ /dev/null @@ -1,5798 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "serde", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "annotate-snippets" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92570a3f9c98e7e84df84b71d0965ac99b1871fcd75a3773a3bd1bad13f64cf7" -dependencies = [ - "anstyle", - "memchr", - "unicode-width", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "arc-swap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" -dependencies = [ - "rustversion", -] - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-lc-rs" -version = "1.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core 0.5.6", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit 0.8.4", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bigdecimal" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" -dependencies = [ - "autocfg", - "libm", - "num-bigint", - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "borrow-or-share" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" - -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "cargo_metadata" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "cc" -version = "1.2.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cf-gears-system-sdk-directory" -version = "0.1.37" -dependencies = [ - "anyhow", - "async-trait", -] - -[[package]] -name = "cf-gears-system-sdks" -version = "0.1.37" -dependencies = [ - "cf-gears-system-sdk-directory", -] - -[[package]] -name = "cf-gears-toolkit" -version = "0.6.13" -dependencies = [ - "anyhow", - "arc-swap", - "async-trait", - "axum 0.8.9", - "cf-gears-system-sdks", - "cf-gears-toolkit-canonical-errors", - "cf-gears-toolkit-db", - "cf-gears-toolkit-gts", - "cf-gears-toolkit-macros", - "cf-gears-toolkit-odata", - "cf-gears-toolkit-sdk", - "cf-gears-toolkit-utils", - "dashmap", - "figment", - "futures-core", - "futures-util", - "gts", - "http", - "inventory", - "nix 0.31.2", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", - "parking_lot", - "rustls", - "sea-orm-migration", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tonic", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", - "urlencoding", - "utoipa", - "uuid", -] - -[[package]] -name = "cf-gears-toolkit-canonical-errors" -version = "0.7.4" -dependencies = [ - "axum 0.8.9", - "cf-gears-toolkit-canonical-errors-macro", - "http", - "serde", - "serde_json", - "thiserror 2.0.18", - "tracing", - "utoipa", -] - -[[package]] -name = "cf-gears-toolkit-canonical-errors-macro" -version = "0.6.1" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "cf-gears-toolkit-db" -version = "0.8.4" -dependencies = [ - "anyhow", - "async-trait", - "bigdecimal", - "cf-gears-toolkit-db-macros", - "cf-gears-toolkit-odata", - "cf-gears-toolkit-security", - "cf-gears-toolkit-utils", - "chrono", - "dashmap", - "dirs", - "figment", - "rust_decimal", - "ryu", - "sea-orm", - "sea-orm-migration", - "serde", - "serde_json", - "thiserror 2.0.18", - "time", - "tokio", - "tracing", - "url", - "uuid", - "xxhash-rust", -] - -[[package]] -name = "cf-gears-toolkit-db-macros" -version = "0.6.3" -dependencies = [ - "heck 0.5.0", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "cf-gears-toolkit-gts" -version = "0.1.2" -dependencies = [ - "anyhow", - "cf-gears-toolkit-gts-macros", - "gts", - "gts-macros", - "inventory", - "schemars", - "serde", - "serde_json", -] - -[[package]] -name = "cf-gears-toolkit-gts-macros" -version = "0.1.2" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "cf-gears-toolkit-macros" -version = "0.6.3" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "cf-gears-toolkit-odata" -version = "0.8.2" -dependencies = [ - "base64", - "bigdecimal", - "cf-gears-toolkit-canonical-errors", - "chrono", - "chrono-tz", - "peg", - "serde", - "serde_json", - "thiserror 2.0.18", - "uuid", -] - -[[package]] -name = "cf-gears-toolkit-sdk" -version = "0.7.0" -dependencies = [ - "cf-gears-toolkit-odata", - "cf-gears-toolkit-security", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "cf-gears-toolkit-security" -version = "0.7.2" -dependencies = [ - "postcard", - "secrecy", - "serde", - "thiserror 2.0.18", - "uuid", -] - -[[package]] -name = "cf-gears-toolkit-utils" -version = "0.6.3" -dependencies = [ - "humantime", - "regex", - "secrecy", - "serde", - "zeroize", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - -[[package]] -name = "chrono-tz" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" -dependencies = [ - "chrono", - "phf", -] - -[[package]] -name = "clippy_utils" -version = "0.1.97" -source = "git+https://github.com/rust-lang/rust-clippy?rev=f6d310692116e9a527ce6d0b3526c965d9c5d7b9#f6d310692116e9a527ce6d0b3526c965d9c5d7b9" -dependencies = [ - "arrayvec", - "itertools 0.12.1", - "rustc_apfloat", - "serde", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.18", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "compiletest_rs" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f150fe9105fcd2a57cad53f0c079a24de65195903ef670990f5909f695eac04c" -dependencies = [ - "diff", - "filetime", - "getopts", - "lazy_static", - "libc", - "log", - "miow", - "regex", - "rustfix", - "serde", - "serde_derive", - "serde_json", - "tester", - "windows-sys 0.59.0", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "de0101_no_serde_in_contract" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "serde", -] - -[[package]] -name = "de0102_no_toschema_in_contract" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "utoipa", -] - -[[package]] -name = "de0103_no_http_types_in_contract" -version = "0.1.0" -dependencies = [ - "axum 0.7.9", - "clippy_utils", - "dylint_linting", - "dylint_testing", - "http", - "lint_utils", -] - -[[package]] -name = "de0104_no_api_dto_in_contract" -version = "0.1.0" -dependencies = [ - "cf-gears-toolkit", - "cf-gears-toolkit-macros", - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "serde", - "utoipa", -] - -[[package]] -name = "de0110_no_schema_for_on_gts_structs" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "gts", - "gts-macros", - "lint_utils", - "schemars", - "serde", - "serde_json", -] - -[[package]] -name = "de0201_dtos_only_in_api_rest" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", -] - -[[package]] -name = "de0202_dtos_not_referenced_outside_api" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", -] - -[[package]] -name = "de0203_dtos_must_use_api_dto" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "serde", -] - -[[package]] -name = "de0204_dtos_must_have_toschema_derive" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "serde", - "utoipa", -] - -[[package]] -name = "de0205_operation_builder" -version = "0.1.0" -dependencies = [ - "cf-gears-toolkit", - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", -] - -[[package]] -name = "de0301_no_infra_in_domain" -version = "0.1.0" -dependencies = [ - "anyhow", - "axum 0.7.9", - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "sea-orm", - "sqlx", - "thiserror 2.0.18", - "uuid", -] - -[[package]] -name = "de0308_no_http_in_domain" -version = "0.1.0" -dependencies = [ - "anyhow", - "axum 0.7.9", - "clippy_utils", - "dylint_linting", - "dylint_testing", - "http", - "lint_utils", -] - -[[package]] -name = "de0309_must_have_domain_model" -version = "0.1.0" -dependencies = [ - "cf-gears-toolkit", - "cf-gears-toolkit-macros", - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", -] - -[[package]] -name = "de0503_plugin_client_suffix" -version = "0.1.0" -dependencies = [ - "async-trait", - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", -] - -[[package]] -name = "de0504_client_versioning" -version = "0.1.0" -dependencies = [ - "async-trait", - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", -] - -[[package]] -name = "de0706_no_direct_sqlx" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "sea-orm", - "sqlx", -] - -[[package]] -name = "de0707_drop_zeroize" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", -] - -[[package]] -name = "de0708_no_non_fips_hasher" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "sha2", -] - -[[package]] -name = "de0801_api_endpoint_version" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", -] - -[[package]] -name = "de0802_use_odata_ext" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", -] - -[[package]] -name = "de0803_api_snake_case" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "serde", -] - -[[package]] -name = "de0901_gts_string_pattern" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "gts", - "gts-macros", - "lint_utils", - "schemars", - "serde", - "serde_json", -] - -[[package]] -name = "de0902_no_schema_for_on_gts_structs" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "gts", - "gts-macros", - "lint_utils", - "schemars", - "serde", - "serde_json", -] - -[[package]] -name = "de1101_tests_in_separate_files" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "serde", -] - -[[package]] -name = "de1201_docs_rs_all_features" -version = "0.1.0" -dependencies = [ - "cargo_metadata", - "dylint_linting", - "serde", - "serde_json", -] - -[[package]] -name = "de1301_no_print_macros" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "tokio", -] - -[[package]] -name = "de1302_error_from_to_string" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "thiserror 2.0.18", -] - -[[package]] -name = "de1303_no_primitive_type_alias" -version = "0.1.0" -dependencies = [ - "clippy_utils", - "dylint_linting", - "dylint_testing", - "lint_utils", - "serde_json", - "uuid", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dylint" -version = "6.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c738fb72ea7d248df2995a31b3698beb63d0f1f9ca5a5dc0188c4b64cd0e86e4" -dependencies = [ - "anstyle", - "anyhow", - "cargo_metadata", - "dylint_internal", - "log", - "once_cell", - "semver", - "serde", - "serde_json", - "tempfile", -] - -[[package]] -name = "dylint_internal" -version = "6.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9796d3441d7894cbaf4992640799efffc1661978f0cc1266ec788c32254fdfb3" -dependencies = [ - "anstyle", - "anyhow", - "bitflags", - "cargo_metadata", - "git2", - "home", - "log", - "regex", - "serde", - "tar", - "thiserror 2.0.18", - "toml", -] - -[[package]] -name = "dylint_linting" -version = "6.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c213635b3eaa84f43b9b497377f17d9a8d4feb413bab1d82e03ad1c73e4f6d8c" -dependencies = [ - "cargo_metadata", - "dylint_internal", - "paste", - "rustversion", - "serde", - "thiserror 2.0.18", - "toml", -] - -[[package]] -name = "dylint_testing" -version = "6.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5f50ee06be9ebfb5b9538ba0d7bb08adc33e8f31c901e7d398de2a9b1ae290" -dependencies = [ - "anyhow", - "cargo_metadata", - "compiletest_rs", - "dylint", - "dylint_internal", - "env_logger", - "once_cell", - "regex", - "serde_json", - "tempfile", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] - -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" -dependencies = [ - "serde", -] - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "encoding_rs_io" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" -dependencies = [ - "encoding_rs", -] - -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "fancy-regex" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "figment" -version = "0.10.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" -dependencies = [ - "atomic", - "pear", - "serde", - "serde_yaml", - "uncased", - "version_check", -] - -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fluent-uri" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" -dependencies = [ - "borrow-or-share", - "ref-cast", - "serde", -] - -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fraction" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" -dependencies = [ - "lazy_static", - "num", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "git2" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" -dependencies = [ - "bitflags", - "libc", - "libgit2-sys", - "log", - "openssl-probe", - "openssl-sys", - "url", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gts" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78d5445277d7f0df598295f7e53608540ae5fe0b830d5010f8f7cce21c69d09f" -dependencies = [ - "gts-id", - "jsonschema", - "schemars", - "serde", - "serde-saphyr", - "serde_json", - "shellexpand", - "thiserror 2.0.18", - "tracing", - "uuid", - "walkdir", -] - -[[package]] -name = "gts-id" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85a48bfcfaa558f7eaee74e9a7d153d0924eac2d4d287f93323b756fe2700fe" -dependencies = [ - "thiserror 2.0.18", -] - -[[package]] -name = "gts-macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a84d8b5e87ab37b5539bb5e9d830b40235918f375405569c219f4de9b3f7d4" -dependencies = [ - "gts-id", - "proc-macro2", - "quote", - "serde_json", - "syn 2.0.117", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", -] - -[[package]] -name = "inherent" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "inlinable_string" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" - -[[package]] -name = "inventory" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" -dependencies = [ - "rustversion", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jsonschema" -version = "0.40.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba783d17473c27cfd4d1d72785dc1c26d5faba8072f50fec4ebea179bec8f33d" -dependencies = [ - "ahash 0.8.12", - "bytecount", - "data-encoding", - "email_address", - "fancy-regex", - "fraction", - "getrandom 0.3.4", - "idna", - "itoa", - "num-cmp", - "num-traits", - "percent-encoding", - "referencing", - "regex", - "regex-syntax", - "serde", - "serde_json", - "unicode-general-category", - "uuid-simd", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.185" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" - -[[package]] -name = "libgit2-sys" -version = "0.18.3+1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" -dependencies = [ - "cc", - "libc", - "libssh2-sys", - "libz-sys", - "openssl-sys", - "pkg-config", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" -dependencies = [ - "bitflags", - "libc", - "plain", - "redox_syscall 0.7.4", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libssh2-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-sys" -version = "1.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "lint_utils" -version = "0.1.0" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix 0.29.0", - "serde", - "winapi", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "miow" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nix" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "opentelemetry" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "opentelemetry-http" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry", - "reqwest", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" -dependencies = [ - "http", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost", - "reqwest", - "thiserror 2.0.18", - "tokio", - "tonic", - "tracing", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost", - "tonic", - "tonic-prost", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" -dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "opentelemetry", - "percent-encoding", - "rand 0.9.4", - "thiserror 2.0.18", - "tokio", - "tokio-stream", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ouroboros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" -dependencies = [ - "aliasable", - "ouroboros_macro", - "static_assertions", -] - -[[package]] -name = "ouroboros_macro" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pear" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" -dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi", -] - -[[package]] -name = "pear_codegen" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" -dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "peg" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" -dependencies = [ - "peg-macros", - "peg-runtime", -] - -[[package]] -name = "peg-macros" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" -dependencies = [ - "peg-runtime", - "proc-macro2", - "quote", -] - -[[package]] -name = "peg-runtime" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pgvector" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" -dependencies = [ - "serde", -] - -[[package]] -name = "phf" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "version_check", - "yansi", -] - -[[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_syscall" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "referencing" -version = "0.40.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef39a30a317e883d1ef4c43aa849f90f480d90bb24904fd38266e61d6be58f2" -dependencies = [ - "ahash 0.8.12", - "fluent-uri", - "getrandom 0.3.4", - "hashbrown 0.16.1", - "parking_lot", - "percent-encoding", - "serde_json", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rkyv" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rust_decimal" -version = "1.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.5", - "rkyv", - "serde", - "serde_json", - "wasm-bindgen", -] - -[[package]] -name = "rustc_apfloat" -version = "0.2.3+llvm-462a31f5a5ab" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486c2179b4796f65bfe2ee33679acf0927ac83ecf583ad6c91c3b4570911b9ad" -dependencies = [ - "bitflags", - "smallvec", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustfix" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fa69b198d894d84e23afde8e9ab2af4400b2cba20d6bf2b428a8b01c222c5a" -dependencies = [ - "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" -dependencies = [ - "aws-lc-rs", - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "saphyr-parser-bw" -version = "0.0.611" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dec0c833db75dc98957956b303fe447ffc5eb13f2325ef4c2350f7f3aa69e3" -dependencies = [ - "arraydeque", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", - "uuid", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sea-bae" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" -dependencies = [ - "heck 0.4.1", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sea-orm" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc312fedd460a47ea563911761d254a84e7b51d8cc73ec92c929e78f33fa957" -dependencies = [ - "async-stream", - "async-trait", - "bigdecimal", - "chrono", - "derive_more", - "futures-util", - "log", - "mac_address", - "ouroboros", - "pgvector", - "rust_decimal", - "sea-orm-macros", - "sea-query", - "sea-query-binder", - "serde", - "serde_json", - "sqlx", - "strum", - "thiserror 2.0.18", - "time", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sea-orm-cli" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da80ebcdb44571e86f03a2bdcb5532136a87397f366f38bbce64673fc5e6a450" -dependencies = [ - "chrono", - "glob", - "regex", - "sea-schema", - "sqlx", - "tokio", - "tracing", - "tracing-subscriber", - "url", -] - -[[package]] -name = "sea-orm-macros" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9a3f90e336ec74803e8eb98c61bc98754c1adfba3b4f84d946237b752b1c88" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "sea-bae", - "syn 2.0.117", - "unicode-ident", -] - -[[package]] -name = "sea-orm-migration" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c577f2959277e936c1d08109acd1e08fc36a95ef29ec028190ba82cad8f96e" -dependencies = [ - "async-trait", - "sea-orm", - "sea-orm-cli", - "sea-schema", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "sea-query" -version = "0.32.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" -dependencies = [ - "bigdecimal", - "chrono", - "inherent", - "ordered-float", - "rust_decimal", - "sea-query-derive", - "serde_json", - "time", - "uuid", -] - -[[package]] -name = "sea-query-binder" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" -dependencies = [ - "bigdecimal", - "chrono", - "rust_decimal", - "sea-query", - "serde_json", - "sqlx", - "time", - "uuid", -] - -[[package]] -name = "sea-query-derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" -dependencies = [ - "darling", - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.117", - "thiserror 2.0.18", -] - -[[package]] -name = "sea-schema" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2239ff574c04858ca77485f112afea1a15e53135d3097d0c86509cef1def1338" -dependencies = [ - "futures", - "sea-query", - "sea-query-binder", - "sea-schema-derive", - "sqlx", -] - -[[package]] -name = "sea-schema-derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "secrecy" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" -dependencies = [ - "serde", - "zeroize", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-saphyr" -version = "0.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83ad47c2f14654528a89495f8d0dbc64173176f8512c7c72386cbe81009f661" -dependencies = [ - "ahash 0.8.12", - "annotate-snippets", - "base64", - "encoding_rs_io", - "getrandom 0.3.4", - "nohash-hasher", - "num-traits", - "saphyr-parser-bw", - "serde", - "smallvec", - "zmij", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shellexpand" -version = "3.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" -dependencies = [ - "dirs", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlx" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" -dependencies = [ - "base64", - "bigdecimal", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink", - "indexmap", - "log", - "memchr", - "once_cell", - "percent-encoding", - "rust_decimal", - "serde", - "serde_json", - "sha2", - "smallvec", - "thiserror 2.0.18", - "time", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sqlx-macros" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.117", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" -dependencies = [ - "dotenvy", - "either", - "heck 0.5.0", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 2.0.117", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" -dependencies = [ - "atoi", - "base64", - "bigdecimal", - "bitflags", - "byteorder", - "bytes", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand 0.8.5", - "rsa", - "rust_decimal", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.18", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" -dependencies = [ - "atoi", - "base64", - "bigdecimal", - "bitflags", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "num-bigint", - "once_cell", - "rand 0.8.5", - "rust_decimal", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.18", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "thiserror 2.0.18", - "time", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tar" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - -[[package]] -name = "tester" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8bf7e0eb2dd7b4228cc1b6821fc5114cd6841ae59f652a85488c016091e5f" -dependencies = [ - "cfg-if", - "getopts", - "libc", - "num_cpus", - "term", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tonic" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" -dependencies = [ - "async-trait", - "axum 0.8.9", - "base64", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "socket2", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-prost" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" -dependencies = [ - "bytes", - "prost", - "tonic", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "indexmap", - "pin-project-lite", - "slab", - "sync_wrapper", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" -dependencies = [ - "js-sys", - "opentelemetry", - "smallvec", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "time", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "utoipa" -version = "5.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" -dependencies = [ - "indexmap", - "serde", - "serde_json", - "utoipa-gen", -] - -[[package]] -name = "utoipa-gen" -version = "5.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "uuid", -] - -[[package]] -name = "uuid" -version = "1.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "serde_core", - "sha1_smol", - "wasm-bindgen", -] - -[[package]] -name = "uuid-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "serde", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "xxhash-rust" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tools/dylint_lints/Cargo.toml b/tools/dylint_lints/Cargo.toml deleted file mode 100644 index 87b1060a8..000000000 --- a/tools/dylint_lints/Cargo.toml +++ /dev/null @@ -1,85 +0,0 @@ -[workspace] -members = [ - "lint_utils", - "de01_contract_layer/de0101_no_serde_in_contract", - "de01_contract_layer/de0102_no_toschema_in_contract", - "de01_contract_layer/de0103_no_http_types_in_contract", - "de01_contract_layer/de0104_no_api_dto_in_contract", - "de01_contract_layer/de0110_no_schema_for_on_gts_structs", - "de02_api_layer/de0201_dtos_only_in_api_rest", - "de02_api_layer/de0202_dtos_not_referenced_outside_api", - "de02_api_layer/de0203_dtos_must_use_api_dto", - "de02_api_layer/de0204_dtos_must_have_toschema_derive", - "de02_api_layer/de0205_operation_builder", - "de03_domain_layer/de0301_no_infra_in_domain", - "de03_domain_layer/de0308_no_http_in_domain", - "de05_client_layer/de0503_plugin_client_suffix", - "de07_security/de0706_no_direct_sqlx", - "de08_rest_api_conventions/de0801_api_endpoint_version", - "de08_rest_api_conventions/de0802_use_odata_ext", - "de08_rest_api_conventions/de0803_api_snake_case", - "de09_gts_layer/de0901_gts_string_pattern", - "de09_gts_layer/de0902_no_schema_for_on_gts_structs", - "de03_domain_layer/de0309_must_have_domain_model", - "de05_client_layer/de0504_client_versioning", - "de07_security/de0707_drop_zeroize", - "de07_security/de0708_no_non_fips_hasher", - "de12_documentation/de1201_docs_rs_all_features", - "de13_common_patterns/de1301_no_print_macros", - "de13_common_patterns/de1302_error_from_to_string", - "de13_common_patterns/de1303_no_primitive_type_alias", - "de11_testing/de1101_tests_in_separate_files", -] -resolver = "3" - -[workspace.package] -version = "0.2.0" -edition = "2024" -license = "Apache-2.0" -authors = ["Constructor Fabric"] - -# This workspace contains dylint linter crates for Gears architecture enforcement -# -# Current linters: -# - DE0101-DE0104: Contract layer validation (serde, toschema, http types, api_dto) -# - DE0201-DE0204: API layer validation (DTOs location, references, derives) -# - DE0503-DE0504: Client layer validation (naming conventions, versioning) -# - DE0801-DE0803: REST API conventions (endpoints, odata, snake_case) -# - DE0901-DE0902: GTS layer validation (string patterns, schema_for) - -[workspace.lints.rust] -unsafe_code = "forbid" -unused_extern_crates = "warn" - -[workspace.dependencies] -clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "f6d310692116e9a527ce6d0b3526c965d9c5d7b9" } -dylint_linting = "6" -dylint_testing = "6" -lint_utils = { path = "lint_utils" } -tokio = { version = "1.44", features = ["macros", "rt"] } -# NOTE: Keep gts version in sync with /Cargo.toml workspace dependencies -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -utoipa = "5.2" -toolkit-macros = { package = "cf-gears-toolkit-macros", path = "../../libs/toolkit-macros" } -toolkit = { package = "cf-gears-toolkit", path = "../../libs/toolkit" } -gts = "0.10.0" -gts-macros = "0.10.0" -schemars = { version = "1.2", features = ["derive"] } - -# Dev dependencies for lint tests -sea-orm = "1.0" -sqlx = "0.8" -axum = "0.7" -uuid = "1.0" -anyhow = "1.0" -thiserror = "2.0" -http = "1.0" -sha2 = "0.10" - -[workspace.lints.clippy] -all = "warn" -pedantic = "warn" - -[workspace.metadata.cargo-shear] -ignored-paths = ["**/ui/**"] diff --git a/tools/dylint_lints/PROMPT.md b/tools/dylint_lints/PROMPT.md deleted file mode 100644 index a7ef3a9b3..000000000 --- a/tools/dylint_lints/PROMPT.md +++ /dev/null @@ -1,13 +0,0 @@ -Your job is to take a lint specification, ensure its design and constraints are complete, and implement it. -Start by understanding the current project context, then ask questions one at a time to refine the idea. Always assume the lint needs to prevent workarounds by being comprehensive about how it detects failing scenarios. Once you understand what you're building, present the design in small sections (200-300 words), checking after each section whether it looks right so far. Once the design is complete, show some passing and failing examples for the lint to clarify the expected behavior of the lint. Once the design is approved and implementation is complete, populate the lint's README.md with documentation that explains and demonstrates the lint. - -The lints should follow the norms established in the `https://github.com/constructorfabric/DNA` repository, which is a separate repository from this one you should clone to a temporary location, and specifically adhere to the `RUST.md` norms that are contained within the repository. - -Key Principles -- One question at a time - Don't overwhelm with multiple questions -- Multiple choice preferred - Easier to answer than open-ended when possible -- Explore alternatives - Always propose 2-3 approaches before settling, lead with your recommendation and explain why -- Be flexible - Go back and clarify when something doesn't make sense - -Implement the following lint: - diff --git a/tools/dylint_lints/README.md b/tools/dylint_lints/README.md deleted file mode 100644 index dc3bd4ca1..000000000 --- a/tools/dylint_lints/README.md +++ /dev/null @@ -1,338 +0,0 @@ -# Gears Dylint Linters - -Custom [dylint](https://github.com/trailofbits/dylint) linters enforcing Gears' architectural patterns, layer separation, and REST API conventions. - -## Quick Start - -```bash -# From workspace root -make dylint # Run Dylint lints on Rust code (auto-rebuilds if changed) -make dylint-list # Show all available Dylint lints -make dylint-test # Test UI cases (compile & verify violations) -make gts-docs # Validate GTS identifiers in docs (.md, .json, .yaml, .yml) -make gts-docs-test # Run unit tests for GTS validator -``` - -## What This Checks - -### Contract Layer (DE01xx) -- ✅ DE0101: No Serde in Contract -- ✅ DE0102: No ToSchema in Contract -- ✅ DE0103: No HTTP Types in Contract - -### API Layer (DE02xx) -- ✅ DE0201: DTOs Only in API Rest Folder -- ✅ DE0202: DTOs Not Referenced Outside API -- ✅ DE0203: DTOs Must Have Serde Derives -- ✅ DE0204: DTOs Must Have ToSchema Derive -- ✅ DE0205: Operation builder must have tag and summary - -### Domain Layer (DE03xx) -- ✅ DE0301: No Infra in Domain -- ✅ DE0308: No HTTP Types in Domain -- ✅ DE0309: Must Have Domain Model - -### Infrastructure/storage Layer (DE04xx) -- TODO - -### Client/gateway Layer (DE05xx) -- ✅ DE0503: Plugin Client Suffix - -### Gear structure (DE06xx) -- TODO - -### Security (DE07xx) -- ✅ DE0706: No Direct SQLx -- ✅ DE0707: Drop Zeroize (sensitive types) -- ✅ DE0708: No Non-FIPS Hasher Imports (sha2/sha1/md5 outside allow-list) - -### REST Conventions (DE08xx) -- ✅ DE0801: API Endpoint Must Have Version -- ✅ DE0802: Use OData Extension Methods - -### GTS (DE09xx) -- ✅ DE0901: GTS String Pattern Validator (Rust source code) -- ✅ DE0902: No `schema_for!` on GTS Structs (Rust source code) -- ✅ DE0903: GTS Documentation Validator (`.md`, `.json`, `.yaml`, `.yml` files) - -### Error handling (DE10xx) -- TODO - -### Testing (DE11xx) -- TODO - -### Documentation (DE12xx) -- ✅ DE1201: Publishable crates must set `package.metadata.docs.rs.all-features = true` - -### Common patterns (DE13xx) -- ✅ DE1301: No Print/Debug Macros in libraries/gears -- ✅ DE1302: No `.to_string()` in Error From impls (preserve error chain) -- ✅ DE1303: No `pub type X = primitive`; use newtype for type safety - -## Examples - -Each lint includes bad/good examples in source comments. View them: - -```bash -# Show lint implementation with examples -cat contract_lints/src/de01_contract_layer/de0101_no_serde_in_contract.rs -``` - -Example output: - -```rust -//! ## Example: Bad -//! -//! // src/contract/user.rs - WRONG -//! #[derive(Serialize, Deserialize)] // ❌ Serde in contract -//! pub struct User { ... } -//! -//! ## Example: Good -//! -//! // src/contract/user.rs - CORRECT -//! #[derive(Debug, Clone)] // ✅ No serde -//! pub struct User { ... } -//! -//! // src/api/rest/dto.rs - CORRECT -//! #[derive(Serialize, Deserialize)] // ✅ Serde in DTO -//! pub struct UserDto { ... } -``` - -## Development - -### Project Structure - -```text -dylint_lints/ -├── contract_lints/ # Main lint crate -│ ├── src/ -│ │ ├── de01_contract_layer/ -│ │ ├── de02_api_layer/ -│ │ ├── de08_rest_api_conventions/ -│ │ ├── lib.rs # Lint registration -│ │ └── utils.rs # Helper functions -│ └── ui/ # Test cases -│ ├── de0101_contract_serde.rs -│ ├── de0203_dto_serde_derives.rs -│ ├── de0801_api_versioning.rs -│ ├── good_contract.rs # Correct patterns -│ └── ... (see ui/README.md) -├── Cargo.toml -├── rust-toolchain.toml # Nightly required -└── README.md -``` - -### Adding a New Lint - -1. Create file in appropriate category (e.g., `src/de02_api_layer/de0205_my_lint.rs`) - -2. Implement the lint: - -```rust -//! DE0205: My Lint Description -//! -//! ## Example: Bad -//! // ... bad code example -//! -//! ## Example: Good -//! // ... good code example - -use rustc_hir::{Item, ItemKind}; -use rustc_lint::{LateContext, LintContext}; - -rustc_session::declare_lint! { - pub MY_LINT, - Deny, - "description of what this checks" -} - -pub fn check<'tcx>(cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { - // Implementation -} -``` - -3. Register in `lib.rs`: - -```rust -mod de02_api_layer { - pub mod de0205_my_lint; -} - -impl<'tcx> LateLintPass<'tcx> for ContractLints { - fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { - de02_api_layer::de0205_my_lint::check(cx, item); - } -} -``` - -4. Add test case in `ui/` directory (optional but recommended): - -```rust -// ui/de0205_my_lint.rs -mod api { - // Should trigger - violation example - pub struct BadPattern { } - - // Should NOT trigger - correct pattern - pub struct GoodPattern { } -} -fn main() {} -``` - -5. Test: - -```bash -make dylint # Run on workspace code -make dylint-test # List test cases - compare with your violations -``` - -### Useful Patterns - -**Check if in specific gear:** - -```rust -use crate::utils::is_in_api_rest_folder; - -if !is_in_api_rest_folder(cx, item.owner_id.def_id) { - return; -} -``` - -**Check derives:** - -```rust -let attrs = cx.tcx.hir_attrs(item.hir_id()); -for attr in attrs { - if attr.has_name(Symbol::intern("derive")) { - // Check derive attributes - } -} -``` - -**Lint with help:** - -```rust -cx.span_lint(MY_LINT, item.span, |diag| { - diag.primary_message("Error message"); - diag.help("Suggestion on how to fix"); -}); -``` - -## GTS Validators (DE09xx) - -GTS (Global Type System) identifiers are validated by complementary tools that cover different file types: - -| Lint | Scope | Tool | Command | -|------|-------|------|---------| -| **DE0901** | GTS string patterns in Rust | Dylint (Rust) | `make dylint` | -| **DE0902** | No `schema_for!` on GTS structs | Dylint (Rust) | `make dylint` | -| **DE0903** | GTS in docs (`.md`, `.json`, `.yaml`, `.yml`) | Rust CLI | `make gts-docs` | - -### DE0901: GTS String Pattern Validator - -A Dylint lint that validates GTS identifiers in Rust source files during compilation. - -**What it checks:** -- `schema_id = "..."` in `#[struct_to_gts_schema(...)]` attributes -- Arguments to `gts_make_instance_id("...")` -- Any string literal starting with `gts.` -- GTS parts in permission strings (e.g., `"read:gts.cf.core.type.v1~"`) - -**How to run:** -```bash -make dylint # Runs DE0901 along with other Dylint lints -``` - -**Location:** [`de09_gts_layer/de0901_gts_string_pattern/`](de09_gts_layer/de0901_gts_string_pattern/) - -### DE0902: No `schema_for!` on GTS Structs - -A Dylint lint that prevents using `schemars::schema_for!()` on GTS-wrapped structs. - -**Why:** GTS structs must use `gts_schema_with_refs_as_string()` for correct `$id` and `$ref` handling. - -**Location:** [`de09_gts_layer/de0902_no_schema_for_on_gts_structs/`](de09_gts_layer/de0902_no_schema_for_on_gts_structs/) - -### DE0903: Documentation Validator - -A standalone CLI tool that validates GTS identifiers in documentation and configuration files. Distributed via crates.io as `gts-validator` (install with `cargo install gts-validator`). - -**What it checks:** - -- All `.md`, `.json`, `.yaml`, `.yml` files in `docs/`, `gears/`, `libs/`, `examples/` -- Skips intentionally invalid examples (marked with "bad", "invalid", "❌", etc.) -- Allows wildcards in pattern/filter contexts -- Optionally validates vendor consistency with `--vendor` flag - -**How to run:** -```bash -# Quick check (from workspace root) -make gts-docs - -# Direct CLI with options -gts-validator --vendor cf,vendor,example,fabrikam --exclude "target/*" docs gears libs examples -``` - -**Exit codes:** -- `0` - All GTS identifiers are valid -- `1` - Invalid GTS identifiers found (fails CI) - -### GTS Identifier Format - -A GTS identifier follows this structure: -```text -gts.~[~]* - -Where each segment = vendor.org.package.type.version -``` - -**Examples:** -```text -gts.cf.toolkit.plugins.plugin.v1~ # Schema (type definition) -gts.cf.toolkit.plugins.plugin.v1~vendor.pkg.gear.plugin.v1 # Instance (chained) -gts.hx.core.errors.err.v1~hx.odata.errors.invalid.v1 # Error code -``` - -**Validation Rules:** - -| Rule | Valid ✓ | Invalid ✗ | -|------|---------|-----------| -| Must start with `gts.` | `gts.cf.core.type.v1~` | `cf.core.type.v1~` | -| Schema IDs end with `~` | `gts.cf.core.type.v1~` | `gts.cf.core.type.v1` | -| 5 components per segment | `cf.core.pkg.type.v1` | `cf.core.type.v1` (4) | -| No hyphens | `my_type` | `my-type` | -| Version format | `v1`, `v1.0`, `v2.1` | `1.0`, `version1` | -| No wildcards (except patterns) | `gts.cf.core.type.v1~` | `gts.cf.*.type.v1~` | - -**When wildcards ARE allowed:** -- In `$filter` queries: `$filter=type_id eq 'gts.cf.*'` -- In pattern methods: `.with_pattern("gts.cf.core.*")` -- In permission patterns: `.resource_pattern("gts.cf.core.type.v1~*")` - -## Troubleshooting - -**"dylint library not found"** -```bash -cd dylint_lints && cargo build --release -``` - -**"feature may not be used on stable"** -Dylint requires nightly. The `rust-toolchain.toml` in `dylint_lints/` sets this automatically. - -**Lint not triggering** -- Check file path matches pattern (e.g., `*/api/rest/*`) -- Verify lint is registered in `lib.rs` -- Rebuild: `cd dylint_lints && cargo build --release` - -**Changes not reflected** -Use `make dylint` - it auto-rebuilds if sources changed. - -## Resources - -- [Makefile](../../Makefile) - Tool comparison table (line 60) -- [Dylint Docs](https://github.com/trailofbits/dylint) -- [Clippy Lint Development](https://doc.rust-lang.org/nightly/clippy/development/index.html) - -## License - -Apache-2.0 diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/.gitignore b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/Cargo.toml b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/Cargo.toml deleted file mode 100644 index d437c850c..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "de0101_no_serde_in_contract" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "description goes here" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "serialize" -path = "ui/serialize.rs" - -[[example]] -name = "no_serde_derive" -path = "ui/no_serde_derive.rs" - -[[example]] -name = "serialize_deserialize" -path = "ui/serialize_deserialize.rs" - -[[example]] -name = "deserialize" -path = "ui/deserialize.rs" - -[[example]] -name = "qualified_paths" -path = "ui/qualified_paths.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -serde.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/README.md b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/README.md deleted file mode 100644 index 08e4110a7..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# DE0101: No Serde in Contract - -### What it does - -Checks that structs and enums in contract gears do not derive `Serialize` or `Deserialize` from serde. - -### Why is this bad? - -Contract models should remain independent of serialization concerns. They represent pure domain logic and should not be coupled to any specific serialization format or library. Use DTOs (Data Transfer Objects) in the API layer for serialization instead. - -This separation provides: -- **Clear separation of concerns**: Domain logic vs. API representation -- **Flexibility**: Different API endpoints can use different serialization strategies -- **Protection**: Contract models stay stable when API format changes - -### Example - -```rust -// ❌ Bad - contract model derives serde traits -mod contract { - use serde::Serialize; - - #[derive(Serialize)] - pub struct User { - pub id: String - } -} -``` - -Use instead: - -```rust -// ✅ Good - contract model without serde -mod contract { - pub struct User { - pub id: String - } -} - -// Separate DTO in API layer -mod api { - use serde::Serialize; - - #[derive(Serialize)] - pub struct UserDto { - pub id: String - } -} -``` - -### Configuration - -This lint is configured to **deny** by default. - -### See Also - -- [DE0102](../de0102_no_toschema_in_contract) - No ToSchema in Contract -- [DE0103](../de0103_no_http_types_in_contract) - No HTTP Types in Contract diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/src/lib.rs b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/src/lib.rs deleted file mode 100644 index 1957e96e3..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/src/lib.rs +++ /dev/null @@ -1,114 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{Item, ItemKind}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; - -use lint_utils::is_in_contract_path; - -dylint_linting::declare_pre_expansion_lint! { - /// ### What it does - /// - /// Checks that structs and enums in contract gears do not derive Serialize or Deserialize. - /// - /// ### Why is this bad? - /// - /// Contract models should remain independent of serialization concerns. - /// Use DTOs (Data Transfer Objects) in the API layer for serialization instead. - /// - /// ### Example - /// - /// ```rust - /// // Bad - contract model derives serde traits - /// mod contract { - /// use serde::Serialize; - /// #[derive(Serialize)] - /// pub struct User { pub id: String } - /// } - /// ``` - /// - /// Use instead: - /// - /// ```rust - /// // Good - contract model without serde - /// mod contract { - /// pub struct User { pub id: String } - /// } - /// - /// // Separate DTO in API layer - /// mod api { - /// use serde::Serialize; - /// #[derive(Serialize)] - /// pub struct UserDto { pub id: String } - /// } - /// ``` - pub DE0101_NO_SERDE_IN_CONTRACT, - Deny, - "contract models should not have serde derives (DE0101)" -} - -impl EarlyLintPass for De0101NoSerdeInContract { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - // Only check structs and enums - if !matches!(item.kind, ItemKind::Struct(..) | ItemKind::Enum(..)) { - return; - } - - if !is_in_contract_path(cx.sess().source_map(), item.span) { - return; - } - - // Check for serde derives - lint_utils::check_derive_attrs(item, |meta_item, attr| { - let segments = lint_utils::get_derive_path_segments(meta_item); - - // Check if this is a serde Serialize or Deserialize - // Handles: Serialize, serde::Serialize, ::serde::Serialize - let is_serialize = lint_utils::is_serde_trait(&segments, "Serialize"); - let is_deserialize = lint_utils::is_serde_trait(&segments, "Deserialize"); - - if is_serialize { - span_lint_and_then( - cx, - DE0101_NO_SERDE_IN_CONTRACT, - attr.span, - "contract type should not derive `Serialize` (DE0101)", - |diag| { - diag.help( - "remove serde derives from contract models; use DTOs in the API layer", - ); - }, - ); - } else if is_deserialize { - span_lint_and_then( - cx, - DE0101_NO_SERDE_IN_CONTRACT, - attr.span, - "contract type should not derive `Deserialize` (DE0101)", - |diag| { - diag.help( - "remove serde derives from contract models; use DTOs in the API layer", - ); - }, - ); - } - }); - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0101", "Serde in contract"); - } -} diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/deserialize.rs b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/deserialize.rs deleted file mode 100644 index cc1c8fea7..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/deserialize.rs +++ /dev/null @@ -1,21 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -use serde::Deserialize; - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, Deserialize)] -pub struct Order { - pub id: String, - pub total: f64, -} - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, Deserialize)] -pub enum UserRole { - Admin, - User, - Guest, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/deserialize.stderr b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/deserialize.stderr deleted file mode 100644 index e04781952..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/deserialize.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: contract type should not derive `Deserialize` (DE0101) - --> $DIR/deserialize.rs:6:1 - | -LL | #[derive(Debug, Clone, Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - = note: `#[deny(de0101_no_serde_in_contract)]` on by default - -error: contract type should not derive `Deserialize` (DE0101) - --> $DIR/deserialize.rs:14:1 - | -LL | #[derive(Debug, Clone, Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/no_serde_derive.rs b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/no_serde_derive.rs deleted file mode 100644 index c89aac2b8..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/no_serde_derive.rs +++ /dev/null @@ -1,19 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -#[allow(dead_code)] -// Should not trigger DE0101 - Serde in contract -#[derive(Debug, Clone, PartialEq)] -pub struct Invoice { - pub id: String, - pub amount: i64, -} - -#[allow(dead_code)] -// Should not trigger DE0101 - Serde in contract -#[derive(Clone, PartialEq)] -pub enum OrderStatus { - Pending, - Confirmed, - Shipped, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/no_serde_derive.stderr b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/no_serde_derive.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/qualified_paths.rs b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/qualified_paths.rs deleted file mode 100644 index 4ba0b1ab0..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/qualified_paths.rs +++ /dev/null @@ -1,23 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, serde::Serialize)] -pub struct WithQualifiedSerialize { - pub id: String, -} - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, serde::Deserialize)] -pub struct WithQualifiedDeserialize { - pub id: String, -} - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(serde::Serialize, serde::Deserialize)] -pub struct WithBothQualified { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/qualified_paths.stderr b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/qualified_paths.stderr deleted file mode 100644 index 6976ff1da..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/qualified_paths.stderr +++ /dev/null @@ -1,35 +0,0 @@ -error: contract type should not derive `Serialize` (DE0101) - --> $DIR/qualified_paths.rs:4:1 - | -LL | #[derive(Debug, Clone, serde::Serialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - = note: `#[deny(de0101_no_serde_in_contract)]` on by default - -error: contract type should not derive `Deserialize` (DE0101) - --> $DIR/qualified_paths.rs:11:1 - | -LL | #[derive(Debug, serde::Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: contract type should not derive `Serialize` (DE0101) - --> $DIR/qualified_paths.rs:18:1 - | -LL | #[derive(serde::Serialize, serde::Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: contract type should not derive `Deserialize` (DE0101) - --> $DIR/qualified_paths.rs:18:1 - | -LL | #[derive(serde::Serialize, serde::Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: aborting due to 4 previous errors - diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize.rs b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize.rs deleted file mode 100644 index d6e005afb..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize.rs +++ /dev/null @@ -1,29 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -use serde::Serialize; - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, Serialize)] -pub struct User { - pub id: String, - pub name: String, -} - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, Serialize)] -pub struct Product { - pub id: String, - pub price: f64, -} - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, Serialize)] -pub enum UserRole { - Admin, - User, - Guest, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize.stderr b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize.stderr deleted file mode 100644 index ad85dc39c..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error: contract type should not derive `Serialize` (DE0101) - --> $DIR/serialize.rs:6:1 - | -LL | #[derive(Debug, Clone, Serialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - = note: `#[deny(de0101_no_serde_in_contract)]` on by default - -error: contract type should not derive `Serialize` (DE0101) - --> $DIR/serialize.rs:14:1 - | -LL | #[derive(Debug, Clone, Serialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: contract type should not derive `Serialize` (DE0101) - --> $DIR/serialize.rs:22:1 - | -LL | #[derive(Debug, Clone, Serialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: aborting due to 3 previous errors - diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize_deserialize.rs b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize_deserialize.rs deleted file mode 100644 index fde0210ff..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize_deserialize.rs +++ /dev/null @@ -1,54 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -use serde::{Deserialize, Serialize}; - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct User { - pub id: String, - pub name: String, -} - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, Serialize)] -pub struct Product { - pub id: String, - pub price: f64, -} - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, Deserialize)] -pub struct Order { - pub id: String, - pub total: f64, -} - -#[allow(dead_code)] -// Should not trigger DE0101 - Serde in contract -#[derive(Debug, Clone, PartialEq)] -pub struct Invoice { - pub id: String, - pub amount: i64, -} - -#[allow(dead_code)] -// Should trigger DE0101 - Serde in contract -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum UserRole { - Admin, - User, - Guest, -} - -#[allow(dead_code)] -// Should not trigger DE0101 - Serde in contract -#[derive(Debug, Clone, PartialEq)] -pub enum OrderStatus { - Pending, - Confirmed, - Shipped, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize_deserialize.stderr b/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize_deserialize.stderr deleted file mode 100644 index d20886479..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0101_no_serde_in_contract/ui/serialize_deserialize.stderr +++ /dev/null @@ -1,51 +0,0 @@ -error: contract type should not derive `Serialize` (DE0101) - --> $DIR/serialize_deserialize.rs:6:1 - | -LL | #[derive(Debug, Clone, Serialize, Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - = note: `#[deny(de0101_no_serde_in_contract)]` on by default - -error: contract type should not derive `Deserialize` (DE0101) - --> $DIR/serialize_deserialize.rs:6:1 - | -LL | #[derive(Debug, Clone, Serialize, Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: contract type should not derive `Serialize` (DE0101) - --> $DIR/serialize_deserialize.rs:14:1 - | -LL | #[derive(Debug, Clone, Serialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: contract type should not derive `Deserialize` (DE0101) - --> $DIR/serialize_deserialize.rs:22:1 - | -LL | #[derive(Debug, Clone, Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: contract type should not derive `Serialize` (DE0101) - --> $DIR/serialize_deserialize.rs:38:1 - | -LL | #[derive(Debug, Clone, Serialize, Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: contract type should not derive `Deserialize` (DE0101) - --> $DIR/serialize_deserialize.rs:38:1 - | -LL | #[derive(Debug, Clone, Serialize, Deserialize)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: remove serde derives from contract models; use DTOs in the API layer - -error: aborting due to 6 previous errors - diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/.gitignore b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/Cargo.toml b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/Cargo.toml deleted file mode 100644 index 695d66046..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "de0102_no_toschema_in_contract" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Contract models should not have ToSchema derive (DE0102)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "toschema" -path = "ui/toschema.rs" - -[[example]] -name = "no_toschema_derive" -path = "ui/no_toschema_derive.rs" - -[[example]] -name = "mixed_derives" -path = "ui/mixed_derives.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -utoipa = "5.2" - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/README.md b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/README.md deleted file mode 100644 index a6e27de81..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# DE0102: No ToSchema in Contract - -### What it does - -Checks that structs and enums in contract gears do not derive `ToSchema` from utoipa (OpenAPI schema generation). - -### Why is this bad? - -Contract models should remain independent of API documentation concerns. OpenAPI schema generation is a presentation layer responsibility and should not be mixed with domain models. Use DTOs (Data Transfer Objects) in the API layer for schema generation instead. - -This separation provides: -- **Clear separation of concerns**: Domain logic vs. API documentation -- **Flexibility**: API schemas can differ from internal models -- **Protection**: Contract models stay stable when API documentation changes -- **API versioning**: Different versions can have different schemas - -### Example - -```rust -// ❌ Bad - contract model derives ToSchema -mod contract { - use utoipa::ToSchema; - - #[derive(ToSchema)] - pub struct User { - pub id: String - } -} -``` - -Use instead: - -```rust -// ✅ Good - contract model without ToSchema -mod contract { - pub struct User { - pub id: String - } -} - -// Separate DTO in API layer with ToSchema -mod api { - use utoipa::ToSchema; - - #[derive(ToSchema)] - pub struct UserDto { - pub id: String - } -} -``` - -### Configuration - -This lint is configured to **deny** by default. - -### See Also - -- [DE0101](../de0101_no_serde_in_contract) - No Serde in Contract -- [DE0103](../de0103_no_http_types_in_contract) - No HTTP Types in Contract diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/src/lib.rs b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/src/lib.rs deleted file mode 100644 index bca1cec1f..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/src/lib.rs +++ /dev/null @@ -1,102 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{Item, ItemKind}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; - -use lint_utils::is_in_contract_path; - -dylint_linting::declare_pre_expansion_lint! { - /// ### What it does - /// - /// Checks that structs and enums in contract gears do not derive ToSchema. - /// - /// ### Why is this bad? - /// - /// Contract models should remain independent of OpenAPI documentation concerns. - /// ToSchema is for API documentation and should only be used on DTOs in the API layer. - /// - /// ### Example - /// - /// ```rust - /// // Bad - contract model derives ToSchema - /// mod contract { - /// use utoipa::ToSchema; - /// #[derive(ToSchema)] - /// pub struct Product { pub id: String } - /// } - /// ``` - /// - /// Use instead: - /// - /// ```rust - /// // Good - contract model without ToSchema - /// mod contract { - /// pub struct Product { pub id: String } - /// } - /// - /// // Separate DTO in API layer - /// mod api { - /// use utoipa::ToSchema; - /// use serde::{Serialize, Deserialize}; - /// #[derive(Serialize, Deserialize, ToSchema)] - /// pub struct ProductDto { pub id: String } - /// } - /// ``` - pub DE0102_NO_TOSCHEMA_IN_CONTRACT, - Deny, - "contract models should not have ToSchema derive (DE0102)" -} - -impl EarlyLintPass for De0102NoToschemaInContract { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - // Only check structs and enums - if !matches!(item.kind, ItemKind::Struct(..) | ItemKind::Enum(..)) { - return; - } - - if !is_in_contract_path(cx.sess().source_map(), item.span) { - return; - } - - // Check for ToSchema derives - lint_utils::check_derive_attrs(item, |meta_item, attr| { - let segments = lint_utils::get_derive_path_segments(meta_item); - - // Check if this is a utoipa ToSchema - // Handles: ToSchema, utoipa::ToSchema, ::utoipa::ToSchema - if lint_utils::is_utoipa_trait(&segments, "ToSchema") { - span_lint_and_then( - cx, - DE0102_NO_TOSCHEMA_IN_CONTRACT, - attr.span, - "contract type should not derive `ToSchema` (DE0102)", - |diag| { - diag.help("ToSchema is an OpenAPI concern; use DTOs in api/rest/ instead"); - }, - ); - } - }); - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0102", - "ToSchema in contract", - ); - } -} diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/mixed_derives.rs b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/mixed_derives.rs deleted file mode 100644 index 60d522580..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/mixed_derives.rs +++ /dev/null @@ -1,39 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -use utoipa::ToSchema; - -#[allow(dead_code)] -// Should trigger DE0102 - ToSchema in contract -#[derive(Debug, Clone, ToSchema)] -pub struct Product { - pub id: String, - pub name: String, - pub price: f64, -} - -#[allow(dead_code)] -// Should not trigger DE0102 - ToSchema in contract -#[derive(Debug, Clone, PartialEq)] -pub struct Order { - pub id: String, - pub total: f64, -} - -#[allow(dead_code)] -// Should trigger DE0102 - ToSchema in contract -#[derive(Debug, Clone, ToSchema)] -pub enum Status { - Active, - Inactive, - Pending, -} - -#[allow(dead_code)] -// Should not trigger DE0102 - ToSchema in contract -#[derive(Clone, PartialEq)] -pub enum Priority { - High, - Medium, - Low, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/mixed_derives.stderr b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/mixed_derives.stderr deleted file mode 100644 index 1bd2187ad..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/mixed_derives.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: contract type should not derive `ToSchema` (DE0102) - --> $DIR/mixed_derives.rs:6:1 - | -LL | #[derive(Debug, Clone, ToSchema)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: ToSchema is an OpenAPI concern; use DTOs in api/rest/ instead - = note: `#[deny(de0102_no_toschema_in_contract)]` on by default - -error: contract type should not derive `ToSchema` (DE0102) - --> $DIR/mixed_derives.rs:23:1 - | -LL | #[derive(Debug, Clone, ToSchema)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: ToSchema is an OpenAPI concern; use DTOs in api/rest/ instead - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/no_toschema_derive.rs b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/no_toschema_derive.rs deleted file mode 100644 index 8ed393af5..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/no_toschema_derive.rs +++ /dev/null @@ -1,20 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -#[allow(dead_code)] -// Should not trigger DE0102 - ToSchema in contract -#[derive(Debug, Clone, PartialEq)] -pub struct Product { - pub id: String, - pub name: String, - pub price: f64, -} - -#[allow(dead_code)] -// Should not trigger DE0102 - ToSchema in contract -#[derive(Clone, PartialEq)] -pub enum Status { - Active, - Inactive, - Pending, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/no_toschema_derive.stderr b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/no_toschema_derive.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/toschema.rs b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/toschema.rs deleted file mode 100644 index 4c95584fc..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/toschema.rs +++ /dev/null @@ -1,30 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -use utoipa::ToSchema; - -#[allow(dead_code)] -// Should trigger DE0102 - ToSchema in contract -#[derive(Debug, Clone, ToSchema)] -pub struct Product { - pub id: String, - pub name: String, - pub price: f64, -} - -#[allow(dead_code)] -// Should trigger DE0102 - ToSchema in contract -#[derive(Debug, Clone, ToSchema)] -pub struct Order { - pub id: String, - pub total: f64, -} - -#[allow(dead_code)] -// Should trigger DE0102 - ToSchema in contract -#[derive(Debug, Clone, ToSchema)] -pub enum Status { - Active, - Inactive, - Pending, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/toschema.stderr b/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/toschema.stderr deleted file mode 100644 index ffd0b48d2..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0102_no_toschema_in_contract/ui/toschema.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error: contract type should not derive `ToSchema` (DE0102) - --> $DIR/toschema.rs:6:1 - | -LL | #[derive(Debug, Clone, ToSchema)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: ToSchema is an OpenAPI concern; use DTOs in api/rest/ instead - = note: `#[deny(de0102_no_toschema_in_contract)]` on by default - -error: contract type should not derive `ToSchema` (DE0102) - --> $DIR/toschema.rs:15:1 - | -LL | #[derive(Debug, Clone, ToSchema)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: ToSchema is an OpenAPI concern; use DTOs in api/rest/ instead - -error: contract type should not derive `ToSchema` (DE0102) - --> $DIR/toschema.rs:23:1 - | -LL | #[derive(Debug, Clone, ToSchema)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: ToSchema is an OpenAPI concern; use DTOs in api/rest/ instead - -error: aborting due to 3 previous errors - diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/.gitignore b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/Cargo.toml b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/Cargo.toml deleted file mode 100644 index 08ca7c87a..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "de0103_no_http_types_in_contract" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Contract modules should not reference HTTP types (DE0103)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "http_imports" -path = "ui/http_imports.rs" - -[[example]] -name = "no_http_imports" -path = "ui/no_http_imports.rs" - -[[example]] -name = "mixed_imports" -path = "ui/mixed_imports.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -http = "1.0" -axum = "0.7" - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/README.md b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/README.md deleted file mode 100644 index e2320e716..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# DE0103: No HTTP Types in Contract - -### What it does - -Checks that contract gears do not use HTTP-specific types such as `StatusCode`, `HeaderMap`, `Response`, `Request`, etc. - -### Why is this bad? - -Contract models represent pure domain logic and should be transport-agnostic. Using HTTP-specific types couples your domain layer to a specific transport protocol (HTTP), making it harder to: -- Reuse domain logic with different transports (gRPC, WebSocket, message queues) -- Test domain logic in isolation -- Maintain clean architecture boundaries - -HTTP types belong in the API layer, not the contract layer. - -### Detected Types - -The lint detects usage of common HTTP types from popular Rust web frameworks: -- `axum`: StatusCode, HeaderMap, Response, Request, Body -- `hyper`: StatusCode, HeaderMap, Response, Request, Body -- `http`: StatusCode, HeaderMap, Response, Request -- And other HTTP-related types - -### Example - -```rust -// ❌ Bad - contract uses HTTP types -mod contract { - use axum::http::StatusCode; - - pub struct UserService { - pub status: StatusCode, - } - - pub fn create_user() -> (StatusCode, String) { - (StatusCode::OK, "user created".to_string()) - } -} -``` - -Use instead: - -```rust -// ✅ Good - contract uses domain types -mod contract { - pub enum UserCreationResult { - Success(String), - AlreadyExists, - ValidationError(String), - } - - pub fn create_user() -> UserCreationResult { - UserCreationResult::Success("user-id-123".to_string()) - } -} - -// Map to HTTP in API layer -mod api { - use axum::http::StatusCode; - use crate::contract; - - pub async fn create_user_handler() -> (StatusCode, String) { - match contract::create_user() { - UserCreationResult::Success(id) => (StatusCode::CREATED, id), - UserCreationResult::AlreadyExists => (StatusCode::CONFLICT, "User exists".into()), - UserCreationResult::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg), - } - } -} -``` - -### Configuration - -This lint is configured to **deny** by default. - -### See Also - -- [DE0101](../de0101_no_serde_in_contract) - No Serde in Contract -- [DE0102](../de0102_no_toschema_in_contract) - No ToSchema in Contract diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/src/lib.rs b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/src/lib.rs deleted file mode 100644 index ea52ac237..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/src/lib.rs +++ /dev/null @@ -1,154 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{Item, ItemKind, UseTree, UseTreeKind}; -use rustc_lint::EarlyLintPass; - -use lint_utils::is_in_contract_module_ast; - -dylint_linting::declare_early_lint! { - /// ### What it does - /// - /// Checks that contract gears do not import HTTP-specific types. - /// - /// ### Why is this bad? - /// - /// Contract gears should be transport-agnostic. HTTP is just one possible - /// transport layer. Using HTTP types in contracts couples the domain logic - /// to a specific transport mechanism. - /// - /// ### Example - /// - /// ```rust - /// // Bad - HTTP types in contract - /// mod contract { - /// use http::StatusCode\; - /// - /// pub struct OrderResult { - /// pub status: StatusCode, // ❌ HTTP-specific - /// } - /// } - /// ``` - /// - /// Use instead: - /// - /// ```rust - /// // Good - domain types in contract - /// mod contract { - /// pub enum OrderStatus { - /// Pending, - /// Confirmed, - /// Shipped, - /// } - /// - /// pub struct OrderResult { - /// pub status: OrderStatus, // ✅ Domain type - /// } - /// } - /// - /// // HTTP types in API layer - /// mod api { - /// use http::StatusCode\; - /// // HTTP layer converts between HTTP and domain types - /// } - /// ``` - pub DE0103_NO_HTTP_TYPES_IN_CONTRACT, - Deny, - "contract gears should not reference HTTP-specific types (DE0103)" -} - -const HTTP_TYPE_PATTERNS: &[&str] = &[ - "axum::http", - "http::StatusCode", - "http::Method", - "http::HeaderMap", - "http::HeaderName", - "http::HeaderValue", - "http::Request", - "http::Response", - "hyper::StatusCode", - "hyper::Method", -]; - -fn use_tree_to_string(tree: &UseTree) -> String { - match &tree.kind { - UseTreeKind::Simple(..) | UseTreeKind::Glob(_) => tree - .prefix - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"), - UseTreeKind::Nested { items, .. } => { - let prefix = tree - .prefix - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - for (nested_tree, _) in items { - let nested_str = use_tree_to_string(nested_tree); - if !nested_str.is_empty() { - return format!("{}::{}", prefix, nested_str); - } - } - prefix - } - } -} - -fn check_use_in_contract(cx: &rustc_lint::EarlyContext<'_>, item: &Item) { - let ItemKind::Use(use_tree) = &item.kind else { - return; - }; - - let path_str = use_tree_to_string(use_tree); - for pattern in HTTP_TYPE_PATTERNS { - if path_str.contains(pattern) { - span_lint_and_then( - cx, - DE0103_NO_HTTP_TYPES_IN_CONTRACT, - item.span, - "contract module imports HTTP type (DE0103)", - |diag| { - diag.help( - "contract gears should be transport-agnostic; move HTTP types to api/rest/", - ); - }, - ); - break; - } - } -} - -impl EarlyLintPass for De0103NoHttpTypesInContract { - fn check_item(&mut self, cx: &rustc_lint::EarlyContext<'_>, item: &Item) { - // Check use statements in file-based contract gears - if matches!(item.kind, ItemKind::Use(_)) && is_in_contract_module_ast(cx, item) { - check_use_in_contract(cx, item); - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0103", - "HTTP types in contract", - ); - } -} diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/http_imports.rs b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/http_imports.rs deleted file mode 100644 index b53e1da3a..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/http_imports.rs +++ /dev/null @@ -1,20 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -// Should trigger DE0103 - HTTP types in contract -use http::StatusCode; -// Should trigger DE0103 - HTTP types in contract -use http::Method; -// Should trigger DE0103 - HTTP types in contract -use axum::http::HeaderMap; - -#[allow(dead_code)] -pub struct OrderResult { - pub status: StatusCode, -} - -#[allow(dead_code)] -pub struct RequestInfo { - pub method: Method, - pub headers: HeaderMap, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/http_imports.stderr b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/http_imports.stderr deleted file mode 100644 index 5d3ae78bc..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/http_imports.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error: contract module imports HTTP type (DE0103) - --> $DIR/http_imports.rs:3:1 - | -LL | use http::StatusCode; - | ^^^^^^^^^^^^^^^^^^^^^ - | - = help: contract gears should be transport-agnostic; move HTTP types to api/rest/ - = note: `#[deny(de0103_no_http_types_in_contract)]` on by default - -error: contract module imports HTTP type (DE0103) - --> $DIR/http_imports.rs:5:1 - | -LL | use http::Method; - | ^^^^^^^^^^^^^^^^^ - | - = help: contract gears should be transport-agnostic; move HTTP types to api/rest/ - -error: contract module imports HTTP type (DE0103) - --> $DIR/http_imports.rs:7:1 - | -LL | use axum::http::HeaderMap; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: contract gears should be transport-agnostic; move HTTP types to api/rest/ - -error: aborting due to 3 previous errors - diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/mixed_imports.rs b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/mixed_imports.rs deleted file mode 100644 index 65643e91a..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/mixed_imports.rs +++ /dev/null @@ -1,20 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -// Should not trigger DE0103 - HTTP types in contract -use std::collections::HashMap; -// Should trigger DE0103 - HTTP types in contract -use http::StatusCode; - -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum OrderStatus { - Pending, - Confirmed, -} - -#[allow(dead_code)] -pub struct OrderResult { - pub status: StatusCode, - pub metadata: HashMap, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/mixed_imports.stderr b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/mixed_imports.stderr deleted file mode 100644 index c6120fba3..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/mixed_imports.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: contract module imports HTTP type (DE0103) - --> $DIR/mixed_imports.rs:5:1 - | -LL | use http::StatusCode; - | ^^^^^^^^^^^^^^^^^^^^^ - | - = help: contract gears should be transport-agnostic; move HTTP types to api/rest/ - = note: `#[deny(de0103_no_http_types_in_contract)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/no_http_imports.rs b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/no_http_imports.rs deleted file mode 100644 index b66c035c3..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/no_http_imports.rs +++ /dev/null @@ -1,18 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -#[derive(Debug, Clone)] -#[allow(dead_code)] -// Should not trigger DE0103 - HTTP types in contract -pub enum OrderStatus { - Pending, - Confirmed, - Shipped, -} - -#[derive(Debug, Clone)] -#[allow(dead_code)] -// Should not trigger DE0103 - HTTP types in contract -pub struct OrderResult { - pub status: OrderStatus, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/no_http_imports.stderr b/tools/dylint_lints/de01_contract_layer/de0103_no_http_types_in_contract/ui/no_http_imports.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/.gitignore b/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/Cargo.toml b/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/Cargo.toml deleted file mode 100644 index bb841a1f8..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "de0104_no_api_dto_in_contract" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Prevents use of api_dto macro in contract layer" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -toolkit-macros.workspace = true -toolkit.workspace = true -serde.workspace = true -utoipa.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true - -[[example]] -name = "with_api_dto" -path = "ui/with_api_dto.rs" - -[[example]] -name = "without_api_dto" -path = "ui/without_api_dto.rs" diff --git a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/README.md b/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/README.md deleted file mode 100644 index 4cf13e06c..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# de0104_no_api_dto_in_contract - -## What it does - -Checks that structs and enums in contract gears do not use the `api_dto` attribute macro. - -## Why is this bad? - -Contract models should remain independent of API serialization concerns. The `api_dto` macro is specifically designed for API DTOs (Data Transfer Objects) and should only be used in the API layer, not in contract models. - -Using `api_dto` in contract gears violates the separation of concerns between the contract layer (domain models) and the API layer (data transfer objects). This separation ensures: - -- **Layer independence**: Contract models can evolve without being tied to API representation -- **Clear boundaries**: API concerns (serialization, validation, OpenAPI schema) stay in the API layer -- **Reusability**: Contract models can be used across different API versions or transport layers - -## Known problems - -None. - -## Example - -```rust -// Bad - contract model uses api_dto -mod contract { - #[toolkit_macros::api_dto(request, response)] - pub struct User { - pub id: String, - pub name: String, - } -} -``` - -Use instead: - -```rust -// Good - contract model without api_dto -mod contract { - pub struct User { - pub id: String, - pub name: String, - } -} - -// Separate DTO in API layer -mod api { - mod rest { - #[toolkit_macros::api_dto(request, response)] - pub struct UserDto { - pub id: String, - pub name: String, - } - } -} -``` diff --git a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/src/lib.rs b/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/src/lib.rs deleted file mode 100644 index 840a35b3f..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/src/lib.rs +++ /dev/null @@ -1,108 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{Item, ItemKind}; -use rustc_lint::{EarlyContext, EarlyLintPass}; - -use lint_utils::is_in_contract_module_ast; - -dylint_linting::declare_pre_expansion_lint! { - /// ### What it does - /// - /// Checks that structs and enums in contract gears do not use the `api_dto` attribute macro. - /// - /// ### Why is this bad? - /// - /// Contract models should remain independent of API serialization concerns. - /// The `api_dto` macro is specifically designed for API DTOs (Data Transfer Objects) - /// and should only be used in the API layer, not in contract models. - /// - /// ### Example - /// - /// ```rust - /// // Bad - contract model uses api_dto - /// mod contract { - /// #[toolkit_macros::api_dto(request, response)] - /// pub struct User { pub id: String } - /// } - /// ``` - /// - /// Use instead: - /// - /// ```rust - /// // Good - contract model without api_dto - /// mod contract { - /// pub struct User { pub id: String } - /// } - /// - /// // Separate DTO in API layer - /// mod api { - /// #[toolkit_macros::api_dto(request, response)] - /// pub struct UserDto { pub id: String } - /// } - /// ``` - pub DE0104_NO_API_DTO_IN_CONTRACT, - Deny, - "contract models should not use api_dto macro (DE0104)" -} - -impl EarlyLintPass for De0104NoApiDtoInContract { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - // Only check structs and enums - if !matches!(item.kind, ItemKind::Struct(..) | ItemKind::Enum(..)) { - return; - } - - if !is_in_contract_module_ast(cx, item) { - return; - } - - // Check for api_dto attribute macro - for attr in &item.attrs { - if let rustc_ast::AttrKind::Normal(attr_item) = &attr.kind { - let path = &attr_item.item.path; - let segments: Vec<&str> = path - .segments - .iter() - .map(|s| s.ident.name.as_str()) - .collect(); - - // Check if this is an api_dto attribute - // Handles: api_dto, toolkit_macros::api_dto, ::toolkit_macros::api_dto - let is_api_dto = matches!( - segments.as_slice(), - ["api_dto"] | [.., "toolkit_macros", "api_dto"] - ); - - if is_api_dto { - span_lint_and_then( - cx, - DE0104_NO_API_DTO_IN_CONTRACT, - attr.span, - "contract type should not use `api_dto` macro (DE0104)", - |diag| { - diag.help("api_dto is for API DTOs; use plain structs in contract/ and create DTOs in api/rest/"); - }, - ); - } - } - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0104", "api_dto in contract"); - } -} diff --git a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/with_api_dto.rs b/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/with_api_dto.rs deleted file mode 100644 index 472b47377..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/with_api_dto.rs +++ /dev/null @@ -1,30 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -#![allow(dead_code)] - -// Should trigger DE0104 - api_dto in contract -#[toolkit_macros::api_dto(request, response)] -pub struct User { - pub id: String, - pub name: String, -} - -// Should trigger DE0104 - api_dto in contract -#[toolkit_macros::api_dto(response)] -pub struct Product { - pub id: String, - pub price: f64, -} - -// Should trigger DE0104 - api_dto in contract -#[toolkit_macros::api_dto(request)] -pub enum OrderStatus { - Pending, - Completed, -} - -// Should not trigger DE0104 - api_dto in contract -pub struct ValidContract { - pub field: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/with_api_dto.stderr b/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/with_api_dto.stderr deleted file mode 100644 index 17cd61aae..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/with_api_dto.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error: contract type should not use `api_dto` macro (DE0104) - --> $DIR/with_api_dto.rs:5:1 - | -LL | #[toolkit_macros::api_dto(request, response)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: api_dto is for API DTOs; use plain structs in contract/ and create DTOs in api/rest/ - = note: `#[deny(de0104_no_api_dto_in_contract)]` on by default - -error: contract type should not use `api_dto` macro (DE0104) - --> $DIR/with_api_dto.rs:12:1 - | -LL | #[toolkit_macros::api_dto(response)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: api_dto is for API DTOs; use plain structs in contract/ and create DTOs in api/rest/ - -error: contract type should not use `api_dto` macro (DE0104) - --> $DIR/with_api_dto.rs:19:1 - | -LL | #[toolkit_macros::api_dto(request)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: api_dto is for API DTOs; use plain structs in contract/ and create DTOs in api/rest/ - -error: aborting due to 3 previous errors - diff --git a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/without_api_dto.rs b/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/without_api_dto.rs deleted file mode 100644 index 34569b472..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/without_api_dto.rs +++ /dev/null @@ -1,16 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/ -#![allow(dead_code)] - -// Should not trigger DE0104 - api_dto in contract -#[toolkit_macros::api_dto(request, response)] -pub struct UserDto { - pub id: String, - pub name: String, -} - -// Should not trigger DE0104 - api_dto in contract -pub struct PlainStruct { - pub field: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/without_api_dto.stderr b/tools/dylint_lints/de01_contract_layer/de0104_no_api_dto_in_contract/ui/without_api_dto.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/Cargo.toml b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/Cargo.toml deleted file mode 100644 index cdddc4dfd..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "de0110_no_schema_for_on_gts_structs" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Prohibit using schemars::schema_for!() on GTS-wrapped structs" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "contract_gts_struct_schema_for" -path = "ui/gts_struct_schema_for.rs" - -[[example]] -name = "contract_non_gts_struct_schema_for" -path = "ui/non_gts_struct_schema_for.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -gts-macros.workspace = true -gts.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/README.md b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/README.md deleted file mode 100644 index a34475f5a..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# DE0110: No `schema_for!` on GTS Structs - -## What it does - -Detects usage of `schemars::schema_for!()` macro on GTS-wrapped structs (those using `#[struct_to_gts_schema]`). - -## Why is this bad? - -GTS-wrapped structs **must** use `gts_json_schema_with_refs()` for schema generation because: - -1. **Performance**: It is static (computed at compile time), so it's faster -2. **Correct `$id`**: It automatically sets the correct `$id` field, no need to do it manually -3. **Proper `$ref`s**: It generates proper schema with `$ref` references, while `schema_for!` inlines everything - -## Example - -```rust -// BAD - uses schemars::schema_for!() on a GTS struct -use schemars::schema_for; - -#[struct_to_gts_schema(...)] -pub struct MyPluginSpec { ... } - -let schema = schema_for!(MyPluginSpec); // ❌ Will trigger DE0110 -``` - -Use instead: - -```rust -// GOOD - uses GTS-provided method -#[struct_to_gts_schema(...)] -pub struct MyPluginSpec { ... } - -let schema = MyPluginSpec::gts_json_schema_with_refs(); // ✅ Correct -``` - -## Detection - -The lint detects `schema_for!` macro invocations where the type argument has the `#[struct_to_gts_schema]` attribute. Types with this attribute implement the `gts::GtsSchema` trait and have a `GTS_SCHEMA_ID` constant. diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/src/lib.rs b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/src/lib.rs deleted file mode 100644 index ab708f2c4..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/src/lib.rs +++ /dev/null @@ -1,157 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_hir; -extern crate rustc_middle; -extern crate rustc_span; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_hir::{Expr, ExprKind, GenericArg}; -use rustc_lint::{LateContext, LateLintPass, LintContext}; -use rustc_middle::ty::{self, Ty}; -use rustc_span::Symbol; - -dylint_linting::declare_late_lint! { - /// ### What it does - /// - /// Detects usage of `schemars::schema_for!()` macro on GTS-wrapped structs. - /// - /// ### Why is this bad? - /// - /// GTS-wrapped structs (those using `#[struct_to_gts_schema]`) must use - /// `gts_json_schema_with_refs()` for schema generation because: - /// - /// 1. **Performance**: It is static (computed at compile time), so it's faster - /// 2. **Correct `$id`**: It automatically sets the correct `$id` field - /// 3. **Proper `$ref`s**: It generates proper schema with `$ref` references, - /// while `schema_for!` inlines everything - /// - /// ### Example - /// - /// ```rust - /// // Bad - uses schema_for! on a GTS struct - /// #[struct_to_gts_schema(...)] - /// pub struct MyPluginSpec { ... } - /// - /// let schema = schemars::schema_for!(MyPluginSpec); - /// ``` - /// - /// Use instead: - /// - /// ```rust - /// // Good - uses GTS-provided method - /// #[struct_to_gts_schema(...)] - /// pub struct MyPluginSpec { ... } - /// - /// let schema = MyPluginSpec::gts_json_schema_with_refs(); - /// ``` - pub DE0110_NO_SCHEMA_FOR_ON_GTS_STRUCTS, - Deny, - "GTS structs must use gts_json_schema_with_refs() instead of schema_for!() (DE0110)" -} - -/// Check if a type has the gts_schema_with_refs_as_string method. -/// GTS types generated by `#[struct_to_gts_schema]` have this method. -fn is_gts_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { - // Get the ADT (struct/enum) definition if this is one - if let ty::Adt(adt_def, _) = ty.kind() { - let def_id = adt_def.did(); - - // Check if this type has a method named gts_schema_with_refs_as_string - let gts_method = Symbol::intern("gts_schema_with_refs_as_string"); - - for item in cx.tcx.inherent_impls(def_id).iter() { - for &assoc_item_def_id in cx.tcx.associated_item_def_ids(*item) { - let assoc_item = cx.tcx.associated_item(assoc_item_def_id); - if assoc_item.name() == gts_method { - return true; - } - } - } - } - false -} - -/// Extract type name for error message -fn get_type_name<'tcx>(ty: Ty<'tcx>) -> String { - if let ty::Adt(adt_def, _) = ty.kind() { - adt_def.variant(0u32.into()).name.to_string() - } else { - ty.to_string() - } -} - -impl<'tcx> LateLintPass<'tcx> for De0110NoSchemaForOnGtsStructs { - fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { - // The schema_for! macro expands to: - // schemars::gen::SchemaGenerator::default().into_root_schema_for::() - // - // We look for method calls to `into_root_schema_for` with a GTS type as type parameter. - - if let ExprKind::MethodCall(segment, _receiver, _args, _span) = expr.kind { - let method_name = segment.ident.name.as_str(); - - // Check for into_root_schema_for (from schema_for! macro expansion) - if method_name == "into_root_schema_for" { - // Only report if this comes from a schema_for! macro invocation, - // not from derive macro expansions - let callsite_span = expr.span.source_callsite(); - - // Check if the callsite is a schema_for! macro call by looking at the source - let source_map = cx.sess().source_map(); - let snippet = source_map - .span_to_snippet(callsite_span) - .unwrap_or_default(); - if !snippet.contains("schema_for!") { - return; - } - - // Check the generic type argument - if let Some(args) = segment.args { - for arg in args.args { - if let GenericArg::Type(hir_ty) = arg - && let Some(ty) = cx.typeck_results().node_type_opt(hir_ty.hir_id) - && is_gts_type(cx, ty) - { - let type_name = get_type_name(ty); - span_lint_and_then( - cx, - DE0110_NO_SCHEMA_FOR_ON_GTS_STRUCTS, - callsite_span, - format!( - "do not use `schema_for!({})` on GTS-wrapped struct (DE0110)", - type_name - ), - |diag| { - diag.help(format!( - "use `{}::gts_json_schema_with_refs()` instead for proper `$id` and `$ref` handling", - type_name - )); - }, - ); - return; - } - } - } - } - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0110", - "schema_for on GTS struct", - ); - } -} diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs deleted file mode 100644 index 9beaad997..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Test case: schema_for! on GTS-wrapped struct should trigger DE0110 - -use gts_macros::struct_to_gts_schema; - -/// A GTS-wrapped struct (has struct_to_gts_schema attribute) -#[derive(Debug, Clone)] -#[struct_to_gts_schema( - dir_path = "schemas", - base = true, - type_id = "gts.cf.core.test.plugin.v1~", - description = "Test plugin specification", - properties = "id,vendor" -)] -pub struct MyGtsPluginSpecV1 { - pub id: gts::GtsInstanceId, - pub vendor: String, -} - -fn main() { - // Should trigger DE0110 - schema_for on GTS struct - let _schema = schemars::schema_for!(MyGtsPluginSpecV1); -} diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr deleted file mode 100644 index 8d29bdfec..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: do not use `schema_for!(MyGtsPluginSpecV1)` on GTS-wrapped struct (DE0110) - --> $DIR/gts_struct_schema_for.rs:21:19 - | -LL | let _schema = schemars::schema_for!(MyGtsPluginSpecV1); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: use `MyGtsPluginSpecV1::gts_json_schema_with_refs()` instead for proper `$id` and `$ref` handling - = note: `#[deny(de0110_no_schema_for_on_gts_structs)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.rs b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.rs deleted file mode 100644 index 9ac0c8100..000000000 --- a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Test case: schema_for! on regular (non-GTS) struct should NOT trigger DE0110 - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// A regular struct (NOT GTS-wrapped, no struct_to_gts_schema attribute) -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct RegularDto { - pub id: String, - pub name: String, -} - -fn main() { - // Should not trigger DE0110 - schema_for on non-GTS struct - let _schema = schemars::schema_for!(RegularDto); -} diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.stderr b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/.gitignore b/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/Cargo.toml b/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/Cargo.toml deleted file mode 100644 index 8132f5733..000000000 --- a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "de0201_dtos_only_in_api_rest" -version = "0.1.0" -edition.workspace = true -authors = ["Constructor Fabric"] -description = "description goes here" -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true - -[[example]] -name = "dto_in_domain" -path = "ui/dto_in_domain.rs" - -[[example]] -name = "dto_in_api_rest" -path = "ui/dto_in_api_rest.rs" - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/README.md b/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/README.md deleted file mode 100644 index 1ad87ef1c..000000000 --- a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# DE0201: DTOs Only in API Rest Folder - -### What it does - -Checks that types with DTO suffixes (e.g., `UserDto`, `ProductDto`) are only defined in `*/api/rest/*.rs` files. - -### Why is this bad? - -DTOs (Data Transfer Objects) are specifically designed for API communication and should be colocated with the API layer code. Defining DTOs outside the API folder can lead to: -- **Confusion**: Unclear which types are for API vs. internal use -- **Coupling**: Non-API code depending on API-specific structures -- **Organization**: Scattered API concerns across the codebase - -### Example - -```rust -// ❌ Bad - DTO defined in domain folder -// File: src/domain/user.rs -pub struct UserDto { - pub id: String, - pub name: String, -} -``` - -Use instead: - -```rust -// ✅ Good - DTO defined in api/rest folder -// File: src/api/rest/dto.rs -pub struct UserDto { - pub id: String, - pub name: String, -} - -// Domain types stay in domain folder -// File: src/domain/user.rs -pub struct User { - pub id: String, - pub name: String, - pub email: String, // May have fields DTOs don't expose -} -``` - -### Configuration - -This lint is configured to **deny** by default. - -Types matching the pattern `*Dto` (case-insensitive) must be in files matching `*/api/rest/*.rs`. diff --git a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/src/lib.rs b/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/src/lib.rs deleted file mode 100644 index ee35863c0..000000000 --- a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/src/lib.rs +++ /dev/null @@ -1,77 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::ItemKind; -use rustc_lint::{EarlyLintPass, LintContext}; - -dylint_linting::declare_early_lint! { - /// DE0201: DTOs Only in API Rest Folder - /// - /// Types with DTO suffixes must be defined only in `*/api/rest/*.rs` files. - pub DE0201_DTOS_ONLY_IN_API_REST, - Deny, - "DTO types should only be defined in */api/rest/* files (DE0201)" -} - -impl EarlyLintPass for De0201DtosOnlyInApiRest { - fn check_item(&mut self, cx: &rustc_lint::EarlyContext<'_>, item: &rustc_ast::Item) { - // Only check structs and enums - if !matches!(item.kind, ItemKind::Struct(..) | ItemKind::Enum(..)) { - return; - } - - // Check if item name ends with "Dto" - let (item_name, span) = match &item.kind { - ItemKind::Struct(ident, ..) => { - let span = item.span.with_hi(ident.span.hi()); - (ident.name.as_str(), span) - } - ItemKind::Enum(ident, ..) => { - let span = item.span.with_hi(ident.span.hi()); - (ident.name.as_str(), span) - } - _ => return, - }; - - if !item_name.to_lowercase().ends_with("dto") { - return; - } - - // Check if the file is in api/rest folder (supports simulated_dir for tests) - if !lint_utils::is_in_api_rest_folder(cx.sess().source_map(), item.span) { - span_lint_and_then( - cx, - DE0201_DTOS_ONLY_IN_API_REST, - span, - format!( - "DTO type `{}` is defined outside of api/rest folder (DE0201)", - item_name - ), - |diag| { - diag.help("move DTO types to src/api/rest/dto.rs"); - }, - ); - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0201", - "DTOs only in api/rest", - ); - } -} diff --git a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_api_rest.rs b/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_api_rest.rs deleted file mode 100644 index cfc1d5dc3..000000000 --- a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_api_rest.rs +++ /dev/null @@ -1,9 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/ -#![allow(dead_code)] - -// Should not trigger DE0201 - DTOs only in api/rest -pub struct UserDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_api_rest.stderr b/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_api_rest.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_domain.rs b/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_domain.rs deleted file mode 100644 index b1e97d66f..000000000 --- a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_domain.rs +++ /dev/null @@ -1,9 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/domain/ -#![allow(dead_code)] - -// Should trigger DE0201 - DTOs only in api/rest -pub struct UserDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_domain.stderr b/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_domain.stderr deleted file mode 100644 index c88e61904..000000000 --- a/tools/dylint_lints/de02_api_layer/de0201_dtos_only_in_api_rest/ui/dto_in_domain.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTO type `UserDto` is defined outside of api/rest folder (DE0201) - --> $DIR/dto_in_domain.rs:5:1 - | -LL | pub struct UserDto { - | ^^^^^^^^^^^^^^^^^^ - | - = help: move DTO types to src/api/rest/dto.rs - = note: `#[deny(de0201_dtos_only_in_api_rest)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/.gitignore b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/Cargo.toml b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/Cargo.toml deleted file mode 100644 index 147d51ae8..000000000 --- a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "de0202_dtos_not_referenced_outside_api" -version = "0.1.0" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true - -[[example]] -name = "import_dto_in_contract" -path = "ui/import_dto_in_contract.rs" - -[[example]] -name = "import_dto_in_domain" -path = "ui/import_dto_in_domain.rs" - -[[example]] -name = "import_dto_in_api" -path = "ui/import_dto_in_api.rs" diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/README.md b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/README.md deleted file mode 100644 index de09bd3c8..000000000 --- a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# DE0202: DTOs Not Referenced Outside API - -### What it does - -Checks that DTO types defined in the API layer are not referenced in domain, contract, or infrastructure layers. - -### Why is this bad? - -DTOs are API-specific representations designed for external communication. When non-API layers depend on DTOs, it creates: -- **Wrong dependencies**: Domain logic should not know about API representations -- **Tight coupling**: Changes to API formats affect domain logic -- **Poor architecture**: Violates clean architecture principles -- **Testing difficulties**: Harder to test domain logic in isolation - -The data flow should be: Contract → Domain → API (with DTOs), never the reverse. - -### Example - -```rust -// ❌ Bad - domain layer uses DTO -// File: src/domain/user_service.rs -use crate::api::rest::UserDto; - -pub fn process_user(dto: UserDto) { // Wrong layer dependency - // domain logic -} -``` - -Use instead: - -```rust -// ✅ Good - domain uses contract types, API converts -// File: src/contract/user.rs -pub struct User { - pub id: String, - pub name: String, -} - -// File: src/domain/user_service.rs -use crate::contract::User; - -pub fn process_user(user: User) { // ✅ Uses contract type - // domain logic -} - -// File: src/api/rest/handlers.rs -use crate::contract::User; -use crate::api::rest::UserDto; - -pub async fn create_user(dto: UserDto) -> Result<()> { - // Convert DTO to contract type - let user = User { - id: dto.id, - name: dto.name, - }; - - // Pass contract type to domain - domain::process_user(user) -} -``` - -### Configuration - -This lint is configured to **deny** by default. - -It checks that paths in domain, contract, and infra layers do not reference types ending with `Dto`. diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/src/lib.rs b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/src/lib.rs deleted file mode 100644 index 911280494..000000000 --- a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/src/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_hir; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_hir::{Item, ItemKind}; -use rustc_lint::{LateContext, LateLintPass, LintContext}; - -dylint_linting::declare_late_lint! { - /// DE0202: DTOs not referenced outside API - /// - /// DTO types must not be imported by contract, domain, or infra gears. - /// DTOs are API layer implementation details. - pub DE0202_DTOS_NOT_REFERENCED_OUTSIDE_API, - Deny, - "DTO types should not be imported outside of api layer (DE0202)" -} - -impl<'tcx> LateLintPass<'tcx> for De0202DtosNotReferencedOutsideApi { - fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { - // Only check use statements - let ItemKind::Use(path, _) = &item.kind else { - return; - }; - - // Check if we're in a forbidden module (contract, domain, infra) - let sm = cx.sess().source_map(); - let span = cx.tcx.def_span(item.owner_id.def_id); - - let in_forbidden = lint_utils::is_in_contract_path(sm, span) - || lint_utils::is_in_domain_path(sm, span) - || lint_utils::is_in_infra_path(sm, span); - if !in_forbidden { - return; - } - - // Check if the import path references api::rest::dto - let path_str = path_to_string(path); - - // Only check imports from api::rest::dto or api::rest - if !path_str.contains("api::rest::dto") && !path_str.contains("api::rest") { - return; - } - - // Check if importing a DTO type - let segments: Vec<&str> = path_str.split("::").collect(); - if let Some(last) = segments.last() { - let is_dto = last.ends_with("Dto") - || last.ends_with("Request") - || last.ends_with("Response") - || last.ends_with("Query"); - - if is_dto { - let module_type = if lint_utils::is_in_contract_path(sm, span) { - "contract" - } else if lint_utils::is_in_domain_path(sm, span) { - "domain" - } else { - "infra" - }; - - span_lint_and_then( - cx, - DE0202_DTOS_NOT_REFERENCED_OUTSIDE_API, - item.span, - format!( - "{} module imports DTO type `{}` from api layer (DE0202)", - module_type, last - ), - |diag| { - diag.help( - "DTOs are API layer details; use contract models or domain types instead", - ); - }, - ); - } - } - } -} - -fn path_to_string(path: &rustc_hir::UsePath<'_>) -> String { - path.segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::") -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0202", - "DTOs not referenced outside api", - ); - } -} diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_api.rs b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_api.rs deleted file mode 100644 index 533943b54..000000000 --- a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_api.rs +++ /dev/null @@ -1,15 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/ -#![allow(unused)] - -mod api { - pub mod rest { - pub mod dto { - pub struct UserDto; - } - - // Should not trigger DE0202 - DTOs not referenced outside api - use crate::api::rest::dto::UserDto; - } -} - -fn main() {} diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_api.stderr b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_api.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_contract.rs b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_contract.rs deleted file mode 100644 index 24a4c8603..000000000 --- a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_contract.rs +++ /dev/null @@ -1,19 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -#![allow(unused)] - -mod api { - pub mod rest { - pub mod dto { - pub struct UserDto; - } - } -} - -// Should trigger DE0202 - DTOs not referenced outside api -use crate::api::rest::dto::UserDto; - -pub fn get_user() -> UserDto { - UserDto -} - -fn main() {} diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_contract.stderr b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_contract.stderr deleted file mode 100644 index e97b5ba19..000000000 --- a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_contract.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: contract module imports DTO type `UserDto` from api layer (DE0202) - --> $DIR/import_dto_in_contract.rs:13:1 - | -LL | use crate::api::rest::dto::UserDto; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs are API layer details; use contract models or domain types instead - = note: `#[deny(de0202_dtos_not_referenced_outside_api)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_domain.rs b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_domain.rs deleted file mode 100644 index 0eb8fa6c9..000000000 --- a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_domain.rs +++ /dev/null @@ -1,17 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/domain/ -#![allow(unused)] - -mod api { - pub mod rest { - pub mod dto { - pub struct UserDto; - } - } -} - -// Should trigger DE0202 - DTOs not referenced outside api -use crate::api::rest::dto::UserDto; - -pub struct UserService; - -fn main() {} diff --git a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_domain.stderr b/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_domain.stderr deleted file mode 100644 index c3693459b..000000000 --- a/tools/dylint_lints/de02_api_layer/de0202_dtos_not_referenced_outside_api/ui/import_dto_in_domain.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: domain module imports DTO type `UserDto` from api layer (DE0202) - --> $DIR/import_dto_in_domain.rs:13:1 - | -LL | use crate::api::rest::dto::UserDto; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs are API layer details; use contract models or domain types instead - = note: `#[deny(de0202_dtos_not_referenced_outside_api)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/Cargo.toml b/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/Cargo.toml deleted file mode 100644 index 7192e5b29..000000000 --- a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "de0203_dtos_must_use_api_dto" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "description goes here" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "missing_api_dto" -path = "ui/missing_api_dto.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -serde.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/README.md b/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/README.md deleted file mode 100644 index e17bb6e5f..000000000 --- a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# DE0203: DTOs Must Use api_dto Macro - -## What it does - -Checks that all DTO types (structs/enums ending with `Dto`) in the API layer use the `#[toolkit_macros::api_dto(...)]` macro instead of manually adding serde derives. - -## Why is this bad? - -The `api_dto` macro ensures consistent serialization behavior across all DTOs by automatically adding: -- `Serialize` and `Deserialize` derives (based on `request`/`response` arguments) -- `ToSchema` derive for OpenAPI documentation -- `#[serde(rename_all = "snake_case")]` for consistent field naming - -Manually adding these derives: -- **Inconsistent**: Different DTOs may have different configurations -- **Error-prone**: Easy to forget ToSchema or snake_case renaming -- **Maintenance burden**: Changes to DTO standards require updating every DTO -- **Missing features**: May not include all required derives and attributes - -## Example - -```rust -// ❌ Bad - DTO with manual derives -// File: src/api/rest/dto.rs -use serde::{Serialize, Deserialize}; - -#[derive(Serialize, Deserialize)] -pub struct UserDto { - pub id: String, - pub name: String, -} -``` - -```rust -// ❌ Bad - DTO with manual derives and ToSchema -// File: src/api/rest/dto.rs -use serde::{Serialize, Deserialize}; -use utoipa::ToSchema; - -#[derive(Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub struct UserDto { - pub id: String, - pub name: String, -} -``` - -Use instead: - -```rust -// ✅ Good - DTO using api_dto macro for request and response -// File: src/api/rest/dto.rs -#[toolkit_macros::api_dto(request, response)] -pub struct UserDto { - pub id: String, - pub name: String, -} - -// ✅ Good - DTO for request only -#[toolkit_macros::api_dto(request)] -pub struct CreateUserReq { - pub name: String, - pub email: String, -} - -// ✅ Good - DTO for response only -#[toolkit_macros::api_dto(response)] -pub struct UserResponseDto { - pub id: String, - pub name: String, -} -``` - -## Configuration - -This lint is configured to **deny** by default. - -It checks all types with names ending in `Dto` (case-insensitive) in `*/api/rest/*.rs` files. - -## See Also - -- [DE0201](../de0201_dtos_only_in_api_rest) - DTOs Only in API Rest Folder -- [DE0204](../de0204_dtos_must_have_toschema_derive) - DTOs Must Have ToSchema Derive diff --git a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/src/lib.rs b/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/src/lib.rs deleted file mode 100644 index e7f500155..000000000 --- a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/src/lib.rs +++ /dev/null @@ -1,104 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{Item, ItemKind}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; - -use lint_utils::is_in_api_rest_folder; - -dylint_linting::declare_pre_expansion_lint! { - /// DE0203: DTOs Must Use api_dto Macro - /// - /// All DTO types in `api/rest` MUST use the `#[toolkit_macros::api_dto(...)]` macro. - /// The macro ensures consistent serialization behavior by automatically adding - /// serde derives, ToSchema, and snake_case renaming. - /// - /// ### Example: Bad - /// - /// ```rust,ignore - /// // src/api/rest/dto.rs - /// #[derive(Debug, Clone, Serialize, Deserialize)] // ❌ Manual derives instead of api_dto - /// pub struct UserDto { - /// pub id: String, - /// } - /// ``` - /// - /// ### Example: Good - /// - /// ```rust,ignore - /// // src/api/rest/dto.rs - /// #[toolkit_macros::api_dto(request, response)] // ✅ Uses api_dto macro - /// pub struct UserDto { - /// pub id: String, - /// } - /// ``` - pub DE0203_DTOS_MUST_USE_API_DTO, - Deny, - "DTO types must use the api_dto macro (DE0203)" -} - -impl EarlyLintPass for De0203DtosMustUseApiDto { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - check_dto_uses_api_dto(cx, item); - } -} - -fn check_dto_uses_api_dto(cx: &EarlyContext<'_>, item: &Item) { - // Only check structs and enums - if !matches!(item.kind, ItemKind::Struct(..) | ItemKind::Enum(..)) { - return; - } - - // Only check items in api/rest folder - if !is_in_api_rest_folder(cx.sess().source_map(), item.span) { - return; - } - - // Check if the type name ends with "Dto" suffix (case-insensitive) - let item_name = match &item.kind { - ItemKind::Struct(ident, _, _) => ident.name.as_str(), - ItemKind::Enum(ident, _, _) => ident.name.as_str(), - _ => return, - }; - let item_name_lower = item_name.to_lowercase(); - if !item_name_lower.ends_with("dto") { - return; - } - - // Check for api_dto macro - if lint_utils::has_api_dto_attribute(item) { - return; - } - - // Report missing api_dto macro - span_lint_and_then( - cx, - DE0203_DTOS_MUST_USE_API_DTO, - item.span, - "api/rest DTO type must use the api_dto macro (DE0203)", - |diag| { - diag.help("Use #[toolkit_macros::api_dto(request)] for request DTOs, #[toolkit_macros::api_dto(response)] for response DTOs, or #[toolkit_macros::api_dto(request, response)] for both"); - }, - ); -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0203", - "DTOs must use api_dto", - ); - } -} diff --git a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/ui/missing_api_dto.rs b/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/ui/missing_api_dto.rs deleted file mode 100644 index 407b6a6b3..000000000 --- a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/ui/missing_api_dto.rs +++ /dev/null @@ -1,18 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/ -use serde::{Deserialize, Serialize}; - -#[allow(dead_code)] -#[derive(Debug, Clone, Serialize, Deserialize)] -// Should trigger DE0203 - DTOs must use api_dto -pub struct UserDto { - pub id: String, -} - -#[allow(dead_code)] -#[derive(Debug, Clone)] -// Should trigger DE0203 - DTOs must use api_dto -pub struct ProductDto { - pub name: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/ui/missing_api_dto.stderr b/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/ui/missing_api_dto.stderr deleted file mode 100644 index b94f2783d..000000000 --- a/tools/dylint_lints/de02_api_layer/de0203_dtos_must_use_api_dto/ui/missing_api_dto.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error: api/rest DTO type must use the api_dto macro (DE0203) - --> $DIR/missing_api_dto.rs:7:1 - | -LL | / pub struct UserDto { -LL | | pub id: String, -LL | | } - | |_^ - | - = help: Use #[toolkit_macros::api_dto(request)] for request DTOs, #[toolkit_macros::api_dto(response)] for response DTOs, or #[toolkit_macros::api_dto(request, response)] for both - = note: `#[deny(de0203_dtos_must_use_api_dto)]` on by default - -error: api/rest DTO type must use the api_dto macro (DE0203) - --> $DIR/missing_api_dto.rs:14:1 - | -LL | / pub struct ProductDto { -LL | | pub name: String, -LL | | } - | |_^ - | - = help: Use #[toolkit_macros::api_dto(request)] for request DTOs, #[toolkit_macros::api_dto(response)] for response DTOs, or #[toolkit_macros::api_dto(request, response)] for both - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/.gitignore b/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/Cargo.toml b/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/Cargo.toml deleted file mode 100644 index 51e02daa1..000000000 --- a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "de0204_dtos_must_have_toschema_derive" -version = "0.1.0" -authors = ["Constructor Fabric"] -edition.workspace = true - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "missing_toschema" -path = "ui/missing_toschema.rs" - -[[example]] -name = "has_toschema" -path = "ui/has_toschema.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -serde.workspace = true -utoipa = "5.2.0" - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/README.md b/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/README.md deleted file mode 100644 index 859e955c4..000000000 --- a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# DE0204: DTOs Must Have ToSchema Derive - -### What it does - -Checks that all DTO types (structs/enums ending with `Dto`) in the API layer derive `ToSchema` from utoipa for OpenAPI documentation. - -### Why is this bad? - -DTOs are the API's public contract and should be documented in OpenAPI specs. A DTO without `ToSchema`: -- **Missing API documentation**: Won't appear in Swagger/OpenAPI docs -- **Incomplete API contract**: Clients can't discover the schema -- **Likely a mistake**: Forgot to add derive or expose in docs -- **Inconsistent**: Other DTOs have ToSchema, this one should too - -OpenAPI documentation is essential for: -- API discoverability -- Client code generation -- Integration testing -- API versioning tracking - -### Example - -```rust -// ❌ Bad - DTO without ToSchema -// File: src/api/rest/dto.rs -use serde::{Serialize, Deserialize}; - -#[derive(Serialize, Deserialize)] -pub struct UserDto { - pub id: String, - pub name: String, -} -``` - -Use instead: - -```rust -// ✅ Good - DTO with ToSchema -// File: src/api/rest/dto.rs -use serde::{Serialize, Deserialize}; -use utoipa::ToSchema; - -#[derive(Serialize, Deserialize, ToSchema)] -pub struct UserDto { - pub id: String, - pub name: String, -} - -// ✅ Also good - with documentation -#[derive(Serialize, Deserialize, ToSchema)] -#[schema(example = json!({"id": "123", "name": "John"}))] -pub struct ProductDto { - /// Unique product identifier - pub id: String, - /// Product display name - pub name: String, -} -``` - -### Configuration - -This lint is configured to **deny** by default. - -It checks all types with names ending in `Dto` (case-insensitive) in `*/api/rest/*.rs` files. diff --git a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/src/lib.rs b/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/src/lib.rs deleted file mode 100644 index e85c755a5..000000000 --- a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/src/lib.rs +++ /dev/null @@ -1,113 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{Item, ItemKind}; -use rustc_lint::{EarlyContext, EarlyLintPass}; - -dylint_linting::declare_pre_expansion_lint! { - /// DE0204: DTOs Must Have ToSchema Derive - /// - /// All DTO types MUST derive `utoipa::ToSchema` for OpenAPI documentation. - /// DTOs in api/rest need schema definitions for API documentation. - /// - /// ### Example: Bad - /// - /// ```rust,ignore - /// // src/api/rest/dto.rs - /// use serde::{Deserialize, Serialize}; - /// - /// #[derive(Debug, Serialize, Deserialize)] // ❌ Missing ToSchema - /// pub struct UserDto { - /// pub id: String, - /// } - /// ``` - /// - /// ### Example: Good - /// - /// ```rust,ignore - /// // src/api/rest/dto.rs - /// use serde::{Deserialize, Serialize}; - /// use utoipa::ToSchema; - /// - /// #[derive(Debug, Serialize, Deserialize, ToSchema)] // ✅ Has ToSchema - /// pub struct UserDto { - /// pub id: String, - /// } - /// ``` - pub DE0204_DTOS_MUST_HAVE_TOSCHEMA_DERIVE, - Deny, - "DTO types must derive ToSchema for OpenAPI documentation (DE0204)" -} - -impl EarlyLintPass for De0204DtosMustHaveToschemaDerive { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - check_dto_toschema_derive(cx, item); - } -} - -fn check_dto_toschema_derive(cx: &EarlyContext<'_>, item: &Item) { - // Only check structs and enums - if !matches!(item.kind, ItemKind::Struct(..) | ItemKind::Enum(..)) { - return; - } - - // Check if the type name ends with "Dto" suffix (case-insensitive) - let item_name = match &item.kind { - ItemKind::Struct(ident, _, _) => ident.name.as_str(), - ItemKind::Enum(ident, _, _) => ident.name.as_str(), - _ => return, - }; - let item_name_lower = item_name.to_lowercase(); - if !item_name_lower.ends_with("dto") { - return; - } - - // Check for api_dto macro which adds ToSchema derive automatically - if lint_utils::has_api_dto_attribute(item) { - return; - } - - // Check for ToSchema derive - let mut has_toschema = false; - lint_utils::check_derive_attrs(item, |meta_item, _attr| { - let segments = lint_utils::get_derive_path_segments(meta_item); - // Check for ToSchema (bare or utoipa::ToSchema) - if lint_utils::is_utoipa_trait(&segments, "ToSchema") { - has_toschema = true; - } - }); - - // Report missing derive - if !has_toschema { - span_lint_and_then( - cx, - DE0204_DTOS_MUST_HAVE_TOSCHEMA_DERIVE, - item.span, - "api/rest type is missing required ToSchema derive (DE0204)", - |diag| { - diag.help("DTOs in api/rest must derive ToSchema for OpenAPI documentation"); - }, - ); - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0204", - "DTOs must have ToSchema derive", - ); - } -} diff --git a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/has_toschema.rs b/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/has_toschema.rs deleted file mode 100644 index 29c1547af..000000000 --- a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/has_toschema.rs +++ /dev/null @@ -1,19 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/ -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -#[allow(dead_code)] -// Should not trigger DE0204 - DTOs must have ToSchema derive -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct UserDto { - pub id: String, -} - -#[allow(dead_code)] -// Should not trigger DE0204 - DTOs must have ToSchema derive -#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] -pub struct ProductDto { - pub name: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/has_toschema.stderr b/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/has_toschema.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/missing_toschema.rs b/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/missing_toschema.rs deleted file mode 100644 index 470c9e452..000000000 --- a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/missing_toschema.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/ -use serde::{Deserialize, Serialize}; - -#[allow(dead_code)] -#[derive(Debug, Serialize, Deserialize)] -// Should trigger DE0204 - DTOs must have ToSchema derive -pub struct UserDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/missing_toschema.stderr b/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/missing_toschema.stderr deleted file mode 100644 index 4a47c4bfa..000000000 --- a/tools/dylint_lints/de02_api_layer/de0204_dtos_must_have_toschema_derive/ui/missing_toschema.stderr +++ /dev/null @@ -1,13 +0,0 @@ -error: api/rest type is missing required ToSchema derive (DE0204) - --> $DIR/missing_toschema.rs:7:1 - | -LL | / pub struct UserDto { -LL | | pub id: String, -LL | | } - | |_^ - | - = help: DTOs in api/rest must derive ToSchema for OpenAPI documentation - = note: `#[deny(de0204_dtos_must_have_toschema_derive)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/.gitignore b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/Cargo.toml b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/Cargo.toml deleted file mode 100644 index 2724064e5..000000000 --- a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "de0205_operation_builder" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Requires operation builders to have tag and summary (DE0205)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "missing_tag" -path = "ui/missing_tag.rs" - -[[example]] -name = "invalid_tag_format" -path = "ui/invalid_tag_format.rs" - -[[example]] -name = "valid_operation" -path = "ui/valid_operation.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -toolkit = { workspace = true } - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/README.md b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/README.md deleted file mode 100644 index c901aefe0..000000000 --- a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# DE0205 – Operation builder must have tag and summary - -## What it does - -Ensures that all `OperationBuilder` instances call both `.tag(...)` and -`.summary(...)` with properly formatted values. - -- **Tags** must contain whitespace-separated words where each word starts with - a capital letter. Tags must be string literals or references to `const` - string items. -- **Summaries** must be non-empty string literals or const strings. - -## Why is this bad? - -Operation builders without tags or summaries, or with improperly formatted -tags, make it difficult to organize and categorize API endpoints in OpenAPI -documentation and UI. Proper documentation is essential for API usability. - -## Example - -```rust -// Bad – missing summary and incorrect tag casing -OperationBuilder::post("/users") - .operation_id("create_user") - .tag("simple resource registry"); -``` - -Use instead: - -```rust -// Good – properly formatted tag and summary -OperationBuilder::post("/users") - .operation_id("create_user") - .tag("User Management") - .summary("Create a new user"); -``` diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/src/lib.rs b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/src/lib.rs deleted file mode 100644 index 159424c3c..000000000 --- a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/src/lib.rs +++ /dev/null @@ -1,277 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; -extern crate rustc_hir; -extern crate rustc_span; - -use clippy_utils::consts::{ConstEvalCtxt, Constant}; -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_lint::{LateContext, LateLintPass}; -use rustc_span::Span; - -dylint_linting::declare_late_lint! { - /// DE0205: Operation builder must have tag and summary - /// - /// Ensures that all `OperationBuilder` instances call both `.tag(...)` and `.summary(...)` - /// with properly formatted values. Tags must contain whitespace-separated words where each - /// word starts with a capital letter. Tags must be string literals or references to `const` - /// string items. Summaries must be non-empty string literals or const strings. - /// - /// ### Why is this bad? - /// - /// Operation builders without tags or summaries, or with improperly formatted tags, - /// make it difficult to organize and categorize API endpoints in OpenAPI documentation - /// and UI. Proper documentation is essential for API usability. - /// - /// ### Example - /// - /// ```rust - /// // Invalid - missing summary and bad tag format - /// OperationBuilder::post("/users") - /// .operation_id("create_user") - /// .tag("simple resource registry"); - /// ``` - /// - /// Use instead: - /// - /// ```rust - /// // Proper tag format and summary - /// OperationBuilder::post("/users") - /// .operation_id("create_user") - /// .tag("User Management") - /// .summary("Create a new user"); - /// ``` - pub DE0205_OPERATION_BUILDER, - Deny, - "operation builder must have tag and summary (DE0205)" -} - -impl<'tcx> LateLintPass<'tcx> for De0205OperationBuilder { - fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx rustc_hir::Stmt<'tcx>) { - // Check statements for complete builder chains - if let rustc_hir::StmtKind::Let(local) = stmt.kind { - if let Some(init) = local.init { - check_complete_builder_chain(cx, init); - } - } else if let rustc_hir::StmtKind::Semi(expr) | rustc_hir::StmtKind::Expr(expr) = stmt.kind - { - check_complete_builder_chain(cx, expr); - } - } - - fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx rustc_hir::Expr<'tcx>) { - // Validate tag/summary format when called - if let rustc_hir::ExprKind::MethodCall(path, receiver, args, _span) = expr.kind - && is_operation_builder_type(cx, receiver) - { - let method_name = path.ident.name.as_str(); - - match method_name { - "tag" => { - if let Some(tag_arg) = args.first() { - if let Some(tag_string) = extract_tag_value(cx, tag_arg) { - if !is_valid_tag_format(&tag_string) { - span_lint_and_then( - cx, - DE0205_OPERATION_BUILDER, - tag_arg.span, - "tag format is invalid", - |diag| { - diag.help("tags must contain whitespace-separated words, each starting with a capital letter"); - diag.note("example: \"User Management\", \"Simple Resource Registry\""); - }, - ); - } - } else { - span_lint_and_then( - cx, - DE0205_OPERATION_BUILDER, - tag_arg.span, - "tag must be a string literal or const string", - |diag| { - diag.help("use a string literal like `.tag(\"Your Tag\")` or a const string"); - }, - ); - } - } - } - "summary" => { - if let Some(summary_arg) = args.first() { - if let Some(summary_string) = extract_tag_value(cx, summary_arg) { - if summary_string.trim().is_empty() { - span_lint_and_then( - cx, - DE0205_OPERATION_BUILDER, - summary_arg.span, - "summary cannot be empty", - |diag| { - diag.help("provide a meaningful summary for the operation"); - }, - ); - } - } else { - span_lint_and_then( - cx, - DE0205_OPERATION_BUILDER, - summary_arg.span, - "summary must be a string literal or const string", - |diag| { - diag.help("use a string literal like `.summary(\"Your summary\")` or a const string"); - }, - ); - } - } - } - _ => {} - } - } - } -} - -fn check_complete_builder_chain(cx: &LateContext<'_>, expr: &rustc_hir::Expr<'_>) { - // Only check if this expression contains an OperationBuilder constructor - if contains_operation_builder_constructor(expr) { - let mut has_tag = false; - let mut has_summary = false; - - // Walk the expression tree to find tag and summary calls - check_builder_chain(expr, &mut has_tag, &mut has_summary); - - // Report missing calls - let builder_span = get_builder_constructor_span(expr); - if !has_tag || !has_summary { - let msg = match (has_tag, has_summary) { - (false, false) => "operation builder missing .tag() and .summary() calls", - (false, true) => "operation builder missing .tag() call", - (true, false) => "operation builder missing .summary() call", - (true, true) => "", - }; - span_lint_and_then( - cx, - DE0205_OPERATION_BUILDER, - builder_span, - msg, - |diag| match (has_tag, has_summary) { - (false, false) => { - diag.help("add .tag(\"Your Tag\") with properly formatted tag"); - diag.note("tags must contain whitespace-separated words, each starting with a capital letter"); - diag.help("add .summary(\"Your summary\") with a meaningful description"); - } - (false, true) => { - diag.help("add .tag(\"Your Tag\") with properly formatted tag"); - diag.note("tags must contain whitespace-separated words, each starting with a capital letter"); - } - (true, false) => { - diag.help("add .summary(\"Your summary\") with a meaningful description"); - } - (true, true) => {} - }, - ); - } - } -} - -fn contains_operation_builder_constructor(expr: &rustc_hir::Expr<'_>) -> bool { - match expr.kind { - rustc_hir::ExprKind::Call(func, _) => { - if let rustc_hir::ExprKind::Path(qpath) = &func.kind - && let rustc_hir::QPath::TypeRelative(ty, segment) = qpath - && let rustc_hir::TyKind::Path(rustc_hir::QPath::Resolved(_, path)) = &ty.kind - { - let type_str = format!("{:?}", path); - let method_name = segment.ident.name.as_str(); - return type_str.contains("OperationBuilder") - && type_str.contains("toolkit") - && matches!( - method_name, - "get" | "post" | "put" | "delete" | "patch" | "head" | "options" - ); - } - false - } - rustc_hir::ExprKind::MethodCall(_, receiver, _, _) => { - contains_operation_builder_constructor(receiver) - } - _ => false, - } -} - -fn get_builder_constructor_span(expr: &rustc_hir::Expr<'_>) -> Span { - match expr.kind { - rustc_hir::ExprKind::Call(_, _) => expr.span, - rustc_hir::ExprKind::MethodCall(_, receiver, _, _) => { - get_builder_constructor_span(receiver) - } - _ => expr.span, - } -} - -fn check_builder_chain(expr: &rustc_hir::Expr<'_>, has_tag: &mut bool, has_summary: &mut bool) { - if let rustc_hir::ExprKind::MethodCall(path, receiver, _, _) = expr.kind { - let method_name = path.ident.name.as_str(); - if method_name == "tag" { - *has_tag = true; - } else if method_name == "summary" { - *has_summary = true; - } - check_builder_chain(receiver, has_tag, has_summary); - } -} - -fn is_operation_builder_type(cx: &LateContext<'_>, expr: &rustc_hir::Expr<'_>) -> bool { - let ty = cx.typeck_results().expr_ty(expr); - let type_str = format!("{:?}", ty); - type_str.contains("OperationBuilder") && type_str.contains("toolkit") -} - -fn extract_tag_value(cx: &LateContext<'_>, expr: &rustc_hir::Expr<'_>) -> Option { - if let rustc_hir::ExprKind::Lit(lit) = expr.kind - && let rustc_ast::LitKind::Str(symbol, _) = lit.node - { - return Some(symbol.to_string()); - } - - if let Some(Constant::Str(s)) = ConstEvalCtxt::new(cx).eval(expr) { - return Some(s); - } - - None -} - -fn is_valid_tag_format(tag: &str) -> bool { - if tag.is_empty() { - return false; - } - - // Split by whitespace and check each word - let words: Vec<&str> = tag.split_whitespace().collect(); - - // Must have at least one word - if words.is_empty() { - return false; - } - - // Each word must start with a capital letter - for word in words { - if word.is_empty() || !word.chars().next().unwrap().is_uppercase() { - return false; - } - } - - true -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0205", "Operation builder"); - } -} diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/invalid_tag_format.rs b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/invalid_tag_format.rs deleted file mode 100644 index 2abc05033..000000000 --- a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/invalid_tag_format.rs +++ /dev/null @@ -1,48 +0,0 @@ -// simulated_dir=gears/simple-resource-registry/simple-resource-registry/src/api/rest - -use toolkit::api::OperationBuilder; - -const INVALID_TAG: &str = "simple resource registry"; - -fn invalid_tag_formats() { - let _router1: OperationBuilder<_, _, ()> = OperationBuilder::post("/resources") - .operation_id("create_resource") - // Should trigger DE0205 - Operation builder tag - .tag("simple resource registry") // lowercase words - .summary("Create a resource"); - - let _router2: OperationBuilder<_, _, ()> = OperationBuilder::get("/resources/{id}") - .operation_id("get_resource") - // Should trigger DE0205 - Operation builder tag - .tag("Simple resource registry") // mixed case - .summary("Get a resource"); - - let _router3: OperationBuilder<_, _, ()> = OperationBuilder::put("/resources/{id}") - .operation_id("update_resource") - // Should trigger DE0205 - Operation builder tag - .tag("registry") // single lowercase word - .summary("Update a resource"); - - let _router4: OperationBuilder<_, _, ()> = OperationBuilder::delete("/resources/{id}") - .operation_id("delete_resource") - // Should trigger DE0205 - Operation builder tag - .tag("") // empty string - .summary("Delete a resource"); - - let tag_name = "Dynamic Tag"; - let _router5: OperationBuilder<_, _, ()> = OperationBuilder::get("/resources") - .operation_id("list_resources") - // Should trigger DE0205 - Operation builder tag - .tag(tag_name) // variable, not string literal or const - .summary("List resources"); - - let _router6: OperationBuilder<_, _, ()> = OperationBuilder::patch("/resources/{id}") - .operation_id("patch_resource") - // Should trigger DE0205 - Operation builder tag - .tag(INVALID_TAG) // const with invalid format - .summary("Patch a resource"); -} - -fn main() { - invalid_tag_formats(); -} diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/invalid_tag_format.stderr b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/invalid_tag_format.stderr deleted file mode 100644 index c1625f26d..000000000 --- a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/invalid_tag_format.stderr +++ /dev/null @@ -1,56 +0,0 @@ -error: tag format is invalid - --> $DIR/invalid_tag_format.rs:11:14 - | -LL | .tag("simple resource registry") // lowercase words - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: tags must contain whitespace-separated words, each starting with a capital letter - = note: example: "User Management", "Simple Resource Registry" - = note: `#[deny(de0205_operation_builder)]` on by default - -error: tag format is invalid - --> $DIR/invalid_tag_format.rs:17:14 - | -LL | .tag("Simple resource registry") // mixed case - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: tags must contain whitespace-separated words, each starting with a capital letter - = note: example: "User Management", "Simple Resource Registry" - -error: tag format is invalid - --> $DIR/invalid_tag_format.rs:23:14 - | -LL | .tag("registry") // single lowercase word - | ^^^^^^^^^^ - | - = help: tags must contain whitespace-separated words, each starting with a capital letter - = note: example: "User Management", "Simple Resource Registry" - -error: tag format is invalid - --> $DIR/invalid_tag_format.rs:29:14 - | -LL | .tag("") // empty string - | ^^ - | - = help: tags must contain whitespace-separated words, each starting with a capital letter - = note: example: "User Management", "Simple Resource Registry" - -error: tag must be a string literal or const string - --> $DIR/invalid_tag_format.rs:36:14 - | -LL | .tag(tag_name) // variable, not string literal or const - | ^^^^^^^^ - | - = help: use a string literal like `.tag("Your Tag")` or a const string - -error: tag format is invalid - --> $DIR/invalid_tag_format.rs:42:14 - | -LL | .tag(INVALID_TAG) // const with invalid format - | ^^^^^^^^^^^ - | - = help: tags must contain whitespace-separated words, each starting with a capital letter - = note: example: "User Management", "Simple Resource Registry" - -error: aborting due to 6 previous errors - diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/main.stderr b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/main.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/missing_tag.rs b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/missing_tag.rs deleted file mode 100644 index f0f990624..000000000 --- a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/missing_tag.rs +++ /dev/null @@ -1,20 +0,0 @@ -// simulated_dir=gears/simple-resource-registry/simple-resource-registry/src/api/rest - -use toolkit::api::OperationBuilder; - -fn test_operations() { - let router1: OperationBuilder<_, _, ()> = - // Should trigger DE0205 - Operation builder - OperationBuilder::post("/resources").operation_id("create_resource"); - - let router2: OperationBuilder<_, _, ()> = - // Should trigger DE0205 - Operation builder - OperationBuilder::get("/resources/{id}").operation_id("get_resource"); - - _ = router1; - _ = router2; -} - -fn main() { - test_operations(); -} diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/missing_tag.stderr b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/missing_tag.stderr deleted file mode 100644 index b1948978b..000000000 --- a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/missing_tag.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error: operation builder missing .tag() and .summary() calls - --> $DIR/missing_tag.rs:8:9 - | -LL | OperationBuilder::post("/resources").operation_id("create_resource"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: add .tag("Your Tag") with properly formatted tag - = note: tags must contain whitespace-separated words, each starting with a capital letter - = help: add .summary("Your summary") with a meaningful description - = note: `#[deny(de0205_operation_builder)]` on by default - -error: operation builder missing .tag() and .summary() calls - --> $DIR/missing_tag.rs:12:9 - | -LL | OperationBuilder::get("/resources/{id}").operation_id("get_resource"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: add .tag("Your Tag") with properly formatted tag - = note: tags must contain whitespace-separated words, each starting with a capital letter - = help: add .summary("Your summary") with a meaningful description - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/valid_operation.rs b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/valid_operation.rs deleted file mode 100644 index 2ead31d38..000000000 --- a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/valid_operation.rs +++ /dev/null @@ -1,55 +0,0 @@ -// simulated_dir=gears/simple-resource-registry/simple-resource-registry/src/api/rest - -use toolkit::api::OperationBuilder; - -const VALID_TAG: &str = "Simple Resource Registry"; -const ANOTHER_VALID_TAG: &str = "User Management"; - -fn valid_operations() { - // Should not trigger DE0205 - Operation builder with tag and summary - let router1: OperationBuilder<_, _, ()> = OperationBuilder::post("/resources") - .operation_id("create_resource") - .summary("Create a new resource") - .tag("Simple Resource Registry"); // proper format - - // Should not trigger DE0205 - Operation builder with tag and summary - let router2: OperationBuilder<_, _, ()> = OperationBuilder::get("/resources/{id}") - .operation_id("get_resource") - .summary("Get resource by ID") - .tag("Registry"); // single capital word - - // Should not trigger DE0205 - Operation builder with tag and summary - let router3: OperationBuilder<_, _, ()> = OperationBuilder::put("/resources/{id}") - .operation_id("update_resource") - .summary("Update an existing resource") - .tag("User Management System"); // multiple capital words - - // Should not trigger DE0205 - Operation builder with tag and summary - let router4: OperationBuilder<_, _, ()> = OperationBuilder::delete("/resources/{id}") - .operation_id("delete_resource") - .summary("Delete a resource") - .tag("API V1 Resources"); // capital with numbers - - // Should not trigger DE0205 - Operation builder with const tag and summary - let router5: OperationBuilder<_, _, ()> = OperationBuilder::patch("/resources/{id}") - .operation_id("patch_resource") - .summary("Partially update a resource") - .tag(VALID_TAG); // const with valid format - - // Should not trigger DE0205 - Operation builder with const tag and summary - let router6: OperationBuilder<_, _, ()> = OperationBuilder::get("/resources/all") - .operation_id("list_resources") - .summary("List all resources") - .tag(ANOTHER_VALID_TAG); // const with valid format - - _ = router1; - _ = router2; - _ = router3; - _ = router4; - _ = router5; - _ = router6; -} - -fn main() { - valid_operations(); -} diff --git a/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/valid_operation.stderr b/tools/dylint_lints/de02_api_layer/de0205_operation_builder/ui/valid_operation.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/Cargo.toml b/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/Cargo.toml deleted file mode 100644 index 3d595ae3b..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "de0301_no_infra_in_domain" -version = "0.1.0" -authors = ["Hypernetix"] -description = "Domain modules should not import infrastructure dependencies (DE0301)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "bad_infra_import" -path = "ui/bad_infra_import.rs" - -[[example]] -name = "good_domain_0301" -path = "ui/good_domain_0301.rs" - -[[example]] -name = "mixed_imports_0301" -path = "ui/mixed_imports_0301.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -sea-orm.workspace = true -sqlx.workspace = true -axum.workspace = true -uuid.workspace = true -anyhow.workspace = true -thiserror.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/README.md b/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/README.md deleted file mode 100644 index f4648bae2..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# DE0301: No Infrastructure in Domain - -## What it does - -Checks that domain modules do not import infrastructure dependencies. - -## Why is this bad? - -Domain modules should contain pure business logic and depend only on abstractions (ports), not concrete implementations: -- **Violates Dependency Inversion Principle**: Domain depends on low-level details -- **Harder to test**: Requires infrastructure setup for domain tests -- **Tight coupling**: Changes to infrastructure affect domain logic -- **Prevents portability**: Cannot easily swap infrastructure implementations - -## Example - -```rust -// ❌ Bad - infrastructure imports in domain -// File: src/domain/users.rs -use crate::infra::storage::UserRepository; // concrete implementation -use sea_orm::*; // database framework -use sqlx::*; // database driver - -pub struct UserService { - repo: UserRepository, // concrete type -} -``` - -Use instead: - -```rust -// ✅ Good - domain depends on abstractions -// File: src/domain/users.rs -use std::sync::Arc; -use uuid::Uuid; - -pub trait UsersRepository: Send + Sync { - async fn find_by_id(&self, id: Uuid) -> Result; -} - -pub struct UserService { - repo: Arc, // trait object -} -``` - -## Configuration - -This lint is configured to **deny** by default. - -It checks all imports in `*/domain/*.rs` files for references to: -- `sea_orm`, `sqlx` (database frameworks) -- `infra::*` (infrastructure gears) - -## See Also - -- [DE0308](../de0308_no_http_in_domain) - No HTTP in Domain Layer diff --git a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/src/lib.rs b/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/src/lib.rs deleted file mode 100644 index b7cacd414..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/src/lib.rs +++ /dev/null @@ -1,315 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use lint_utils::{is_in_contract_module_ast, is_in_domain_path, use_tree_to_strings}; -use rustc_ast::{Item, ItemKind, Ty, TyKind}; -use rustc_lint::{EarlyLintPass, LintContext}; - -dylint_linting::declare_early_lint! { - /// ### What it does - /// - /// Checks that domain modules do not import infrastructure dependencies. - /// - /// ### Why is this bad? - /// - /// Domain modules should contain pure business logic and depend only on abstractions (ports), - /// not concrete implementations. Importing infrastructure code (database, HTTP, external APIs) - /// violates the Dependency Inversion Principle and makes domain logic harder to test. - /// - /// ### Example - /// - /// ```rust - /// // Bad - infrastructure imports in domain - /// mod domain { - /// use crate::infra::storage::UserRepository; // ❌ concrete implementation - /// use sea_orm::*; // ❌ database framework - /// use sqlx::*; // ❌ database driver - /// } - /// ``` - /// - /// Use instead: - /// - /// ```rust - /// // Good - domain depends on abstractions - /// mod domain { - /// use std::sync::Arc; - /// - /// pub trait UsersRepository: Send + Sync { - /// async fn find_by_id(&self, id: Uuid) -> Result; - /// } - /// - /// pub struct Service { - /// repo: Arc, // ✅ trait object - /// } - /// } - /// ``` - pub DE0301_NO_INFRA_IN_DOMAIN, - Deny, - "domain gears should not import infrastructure dependencies (DE0301)" -} - -/// Forbidden import patterns for domain layer -const INFRA_PATTERNS: &[&str] = &[ - // Infrastructure layer - "crate::infra", - "crate::infrastructure", - // Database frameworks (direct access forbidden) - "sea_orm", - "sqlx", - // ToolKit infrastructure crates (should not leak into domain) - "toolkit_db", - "toolkit_db_macros", - "toolkit_transport_grpc", - // HTTP/Web frameworks (only used ones: axum, hyper, http) - "axum", - "hyper", - "http", - // API layer - "crate::api", - // External service clients - "reqwest", - "tonic", - // File system (should be abstracted) - "std::fs", - "tokio::fs", -]; - -/// Check if a path matches an infrastructure pattern. -/// Returns the matched pattern if path equals pattern exactly or starts with "pattern::" -/// This avoids false positives like "http_client" matching "http". -fn matches_infra_pattern(path: &str) -> Option<&'static str> { - for pattern in INFRA_PATTERNS { - // Patterns containing "::" are already specific (e.g., "crate::infra") - // Other patterns need exact match or "::" suffix to avoid false positives - if pattern.contains("::") { - if path.starts_with(pattern) { - return Some(pattern); - } - } else if path == *pattern || path.starts_with(&format!("{pattern}::")) { - return Some(pattern); - } - } - None -} - -fn check_use_in_domain(cx: &rustc_lint::EarlyContext<'_>, item: &Item) { - let ItemKind::Use(use_tree) = &item.kind else { - return; - }; - - for path_str in use_tree_to_strings(use_tree) { - if let Some(pattern) = matches_infra_pattern(&path_str) { - span_lint_and_then( - cx, - DE0301_NO_INFRA_IN_DOMAIN, - item.span, - format!("domain module imports infrastructure dependency `{pattern}` (DE0301)"), - |diag| { - diag.help( - "domain should depend only on abstractions; move infrastructure code to infra/ layer", - ); - }, - ); - return; - } - } -} - -fn check_type_in_domain(cx: &rustc_lint::EarlyContext<'_>, ty: &Ty) { - match &ty.kind { - TyKind::Path(_, path) => { - // Check the path itself - let path_str = path - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - if matches_infra_pattern(&path_str).is_some() { - span_lint_and_then( - cx, - DE0301_NO_INFRA_IN_DOMAIN, - ty.span, - format!("domain module uses infrastructure type `{path_str}` (DE0301)"), - |diag| { - diag.help( - "domain should depend only on abstractions; move infrastructure code to infra/ layer", - ); - }, - ); - return; - } - - // Recursively check generic arguments (e.g., Option) - for segment in &path.segments { - if let Some(args) = &segment.args - && let rustc_ast::GenericArgs::AngleBracketed(ref angle_args) = **args - { - for arg in &angle_args.args { - if let rustc_ast::AngleBracketedArg::Arg(rustc_ast::GenericArg::Type( - inner_ty, - )) = arg - { - check_type_in_domain(cx, inner_ty); - } - } - } - } - } - // Handle references: &sqlx::PgPool - TyKind::Ref(_, mut_ty) => { - check_type_in_domain(cx, &mut_ty.ty); - } - // Handle slices: [sqlx::PgPool] - TyKind::Slice(inner_ty) => { - check_type_in_domain(cx, inner_ty); - } - // Handle arrays: [sqlx::PgPool; 10] - TyKind::Array(inner_ty, _) => { - check_type_in_domain(cx, inner_ty); - } - // Handle raw pointers: *const sqlx::PgPool - TyKind::Ptr(mut_ty) => { - check_type_in_domain(cx, &mut_ty.ty); - } - // Handle tuples: (sqlx::PgPool, String) - TyKind::Tup(types) => { - for inner_ty in types { - check_type_in_domain(cx, inner_ty); - } - } - // Handle trait objects: dyn sqlx::Database - TyKind::TraitObject(bounds, _) => { - for bound in bounds { - if let rustc_ast::GenericBound::Trait(trait_ref) = bound { - // Check the trait path itself - let path = &trait_ref.trait_ref.path; - let path_str = path - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - if matches_infra_pattern(&path_str).is_some() { - span_lint_and_then( - cx, - DE0301_NO_INFRA_IN_DOMAIN, - ty.span, - format!( - "domain module uses infrastructure trait `{path_str}` (DE0301)" - ), - |diag| { - diag.help( - "domain should depend only on abstractions; move infrastructure code to infra/ layer", - ); - }, - ); - return; - } - } - } - } - // Handle impl Trait: impl sqlx::Executor - TyKind::ImplTrait(_, bounds) => { - for bound in bounds { - if let rustc_ast::GenericBound::Trait(trait_ref) = bound { - // Check the trait path itself - let path = &trait_ref.trait_ref.path; - let path_str = path - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - if matches_infra_pattern(&path_str).is_some() { - span_lint_and_then( - cx, - DE0301_NO_INFRA_IN_DOMAIN, - ty.span, - format!( - "domain module uses infrastructure trait `{path_str}` (DE0301)" - ), - |diag| { - diag.help( - "domain should depend only on abstractions; move infrastructure code to infra/ layer", - ); - }, - ); - return; - } - } - } - } - _ => {} - } -} - -impl EarlyLintPass for De0301NoInfraInDomain { - fn check_item(&mut self, cx: &rustc_lint::EarlyContext<'_>, item: &Item) { - // Skip if not in domain path or if in contract module (contracts can have infra types) - if !is_in_domain_path(cx.sess().source_map(), item.span) - || is_in_contract_module_ast(cx, item) - { - return; - } - - match &item.kind { - // Check use statements - ItemKind::Use(_) => { - check_use_in_domain(cx, item); - } - // Check struct fields - ItemKind::Struct(_, _, variant_data) => { - for field in variant_data.fields() { - check_type_in_domain(cx, &field.ty); - } - } - // Check enum variants - ItemKind::Enum(_, _, enum_def) => { - for variant in &enum_def.variants { - for field in variant.data.fields() { - check_type_in_domain(cx, &field.ty); - } - } - } - // Check function signatures - ItemKind::Fn(fn_item) => { - // Check parameters - for param in &fn_item.sig.decl.inputs { - check_type_in_domain(cx, ¶m.ty); - } - // Check return type - if let rustc_ast::FnRetTy::Ty(ret_ty) = &fn_item.sig.decl.output { - check_type_in_domain(cx, ret_ty); - } - } - // Check type aliases - ItemKind::TyAlias(ty_alias) => { - if let Some(ty) = &ty_alias.ty { - check_type_in_domain(cx, ty); - } - } - _ => {} - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0301", "infra in domain"); - } -} diff --git a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/bad_infra_import.rs b/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/bad_infra_import.rs deleted file mode 100644 index facf3c768..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/bad_infra_import.rs +++ /dev/null @@ -1,16 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/domain/service.rs -// Test file for DE0301: No Infrastructure Dependencies in Domain -// This file simulates being in a domain/ directory -#![allow(unused_imports)] -#![allow(dead_code)] - -// Should trigger DE0301 - infra in domain -use sea_orm::entity::*; - -// Should trigger DE0301 - infra in domain -use sqlx::Pool; - -// Should trigger DE0301 - infra in domain -use axum::http::StatusCode; - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/bad_infra_import.stderr b/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/bad_infra_import.stderr deleted file mode 100644 index ff0d12e6d..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/bad_infra_import.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error: domain module imports infrastructure dependency `sea_orm` (DE0301) - --> $DIR/bad_infra_import.rs:8:1 - | -LL | use sea_orm::entity::*; - | ^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: domain should depend only on abstractions; move infrastructure code to infra/ layer - = note: `#[deny(de0301_no_infra_in_domain)]` on by default - -error: domain module imports infrastructure dependency `sqlx` (DE0301) - --> $DIR/bad_infra_import.rs:11:1 - | -LL | use sqlx::Pool; - | ^^^^^^^^^^^^^^^ - | - = help: domain should depend only on abstractions; move infrastructure code to infra/ layer - -error: domain module imports infrastructure dependency `axum` (DE0301) - --> $DIR/bad_infra_import.rs:14:1 - | -LL | use axum::http::StatusCode; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: domain should depend only on abstractions; move infrastructure code to infra/ layer - -error: aborting due to 3 previous errors - diff --git a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/good_domain_0301.rs b/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/good_domain_0301.rs deleted file mode 100644 index b45ae1e8e..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/good_domain_0301.rs +++ /dev/null @@ -1,21 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/domain/service.rs -// Test file for DE0301: No Infrastructure Dependencies in Domain -// This file simulates being in a domain/ directory - should NOT trigger -#![allow(unused_imports)] -#![allow(dead_code)] - -// Should not trigger DE0301 - infra in domain -use std::sync::Arc; - -// Should not trigger DE0301 - infra in domain -use uuid::Uuid; - -// Should not trigger DE0301 - infra in domain -use anyhow::Result; - -// Domain trait - this is correct -pub trait UsersRepository: Send + Sync { - fn find_by_id(&self, id: Uuid) -> Result<()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/good_domain_0301.stderr b/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/good_domain_0301.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/mixed_imports_0301.rs b/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/mixed_imports_0301.rs deleted file mode 100644 index 39e643186..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/mixed_imports_0301.rs +++ /dev/null @@ -1,15 +0,0 @@ -// simulated_dir=/cf-gears/gears/another_module/domain/ -// Test file for DE0301: Mixed imports - some valid, some violating -#![allow(unused_imports)] -#![allow(dead_code)] - -// Should not trigger DE0301 - infra in domain -use std::collections::HashMap; - -// Should trigger DE0301 - infra in domain -use sea_orm::DatabaseConnection; - -// Should not trigger DE0301 - infra in domain -use thiserror::Error; - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/mixed_imports_0301.stderr b/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/mixed_imports_0301.stderr deleted file mode 100644 index 6109f2528..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0301_no_infra_in_domain/ui/mixed_imports_0301.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: domain module imports infrastructure dependency `sea_orm` (DE0301) - --> $DIR/mixed_imports_0301.rs:10:1 - | -LL | use sea_orm::DatabaseConnection; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: domain should depend only on abstractions; move infrastructure code to infra/ layer - = note: `#[deny(de0301_no_infra_in_domain)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/Cargo.toml b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/Cargo.toml deleted file mode 100644 index 09883827b..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "de0308_no_http_in_domain" -version = "0.1.0" -authors = ["Hypernetix"] -description = "Domain modules should not reference HTTP types or status codes (DE0308)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "bad_http_import" -path = "ui/bad_http_import.rs" - -[[example]] -name = "good_domain_0308" -path = "ui/good_domain_0308.rs" - -[[example]] -name = "bad_http_type_in_struct" -path = "ui/bad_http_type_in_struct.rs" - -[[example]] -name = "nested_generic_http_type" -path = "ui/nested_generic_http_type.rs" - -[[example]] -name = "trait_bounds_http" -path = "ui/trait_bounds_http.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -http = "1.0" -axum.workspace = true -anyhow.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/README.md b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/README.md deleted file mode 100644 index 256af6c72..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# DE0308: No HTTP in Domain - -## What it does - -Checks that domain modules do not reference HTTP types or status codes. - -## Why is this bad? - -Domain modules should be transport-agnostic: -- **HTTP is just one transport**: Domain logic should work with any protocol (gRPC, WebSockets, CLI) -- **Tight coupling**: Domain becomes dependent on web layer -- **Harder to reuse**: Cannot use domain logic in non-HTTP contexts -- **Violates separation of concerns**: HTTP is a delivery detail, not business logic - -## Example - -```rust -// ❌ Bad - HTTP types in domain -// File: src/domain/error.rs -use http::StatusCode; - -pub enum DomainError { - NotFound(StatusCode), // HTTP leaking into domain -} -``` - -```rust -// ❌ Bad - HTTP status in domain function -use axum::http::StatusCode; - -pub fn validate_user() -> StatusCode { - StatusCode::OK // Domain should not return HTTP types -} -``` - -Use instead: - -```rust -// ✅ Good - domain errors are transport-agnostic -// File: src/domain/error.rs -use thiserror::Error; -use uuid::Uuid; - -#[derive(Error, Debug)] -pub enum DomainError { - #[error("User not found: {id}")] - UserNotFound { id: Uuid }, - - #[error("Email '{email}' already exists")] - EmailAlreadyExists { email: String }, - - #[error("Validation failed: {field}: {message}")] - Validation { field: String, message: String }, - - #[error("Database error: {message}")] - Database { message: String }, -} -``` - -```rust -// ✅ Good - API layer handles HTTP mapping -// File: src/api/rest/error.rs -use toolkit::api::problem::Problem; -use crate::domain::error::DomainError; - -impl From for Problem { - fn from(e: DomainError) -> Self { - match &e { - DomainError::UserNotFound { id } => { - ErrorCode::user_not_found_v1() - .with_context(format!("User {id} not found"), "/", None) - } - DomainError::Validation { .. } => { - ErrorCode::validation_error_v1() - .with_context(e.to_string(), "/", None) - } - _ => ErrorCode::internal_error_v1() - .with_context("Internal error", "/", None) - } - } -} -``` - -## Configuration - -This lint is configured to **deny** by default. - -It checks all imports in `*/domain/*.rs` files for references to `http` crate types. - -### See Also - -- [DE0301](../de0301_no_infra_in_domain) - No Infrastructure in Domain Layer diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/src/lib.rs b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/src/lib.rs deleted file mode 100644 index aae7f98bd..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/src/lib.rs +++ /dev/null @@ -1,273 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use lint_utils::{is_in_contract_module_ast, is_in_domain_path, use_tree_to_strings}; -use rustc_ast::{Item, ItemKind, Ty, TyKind}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; - -dylint_linting::declare_early_lint! { - /// ### What it does - /// - /// Checks that domain modules do not reference HTTP types or status codes. - /// - /// ### Why is this bad? - /// - /// Domain modules should be transport-agnostic. HTTP is just one possible - /// transport layer. Referencing HTTP types in domain code couples the business - /// logic to a specific transport mechanism. - /// - /// ### Example - /// - /// ```rust,ignore - /// // Bad - HTTP types in domain - /// mod domain { - /// use http::StatusCode; - /// - /// pub fn check_result() -> StatusCode { - /// StatusCode::OK // ❌ HTTP-specific - /// } - /// } - /// ``` - /// - /// Use instead: - /// - /// ```rust,ignore - /// // Good - domain errors converted in API layer - /// mod domain { - /// pub enum DomainResult { - /// Success, - /// NotFound, - /// InvalidData, - /// } - /// } - /// ``` - pub DE0308_NO_HTTP_IN_DOMAIN, - Deny, - "domain gears should not reference HTTP types or status codes (DE0308)" -} - -/// HTTP-related patterns forbidden in domain code -/// Only includes frameworks actually used in the project: axum, hyper, http -const HTTP_PATTERNS: &[&str] = &["http", "axum", "hyper"]; - -/// Check if a path matches an HTTP pattern. -/// Returns true only if path equals pattern exactly or starts with "pattern::" -/// This avoids false positives like "http_client" matching "http". -fn matches_http_pattern(path: &str) -> Option<&'static str> { - for pattern in HTTP_PATTERNS { - if path == *pattern || path.starts_with(&format!("{pattern}::")) { - return Some(pattern); - } - } - None -} - -fn check_use_item(cx: &EarlyContext<'_>, item: &Item, tree: &rustc_ast::UseTree) { - for path_str in use_tree_to_strings(tree) { - if let Some(pattern) = matches_http_pattern(&path_str) { - span_lint_and_then( - cx, - DE0308_NO_HTTP_IN_DOMAIN, - item.span, - format!("domain module imports HTTP type `{pattern}` (DE0308)"), - |diag| { - diag.help("domain should be transport-agnostic; handle HTTP in api/ layer"); - }, - ); - return; - } - } -} - -fn check_type_in_domain(cx: &rustc_lint::EarlyContext<'_>, ty: &Ty) { - match &ty.kind { - TyKind::Path(_, path) => { - // Check the path itself - let path_str = path - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - if matches_http_pattern(&path_str).is_some() { - span_lint_and_then( - cx, - DE0308_NO_HTTP_IN_DOMAIN, - ty.span, - format!("domain module uses HTTP type `{}` (DE0308)", path_str), - |diag| { - diag.help("domain should be transport-agnostic; handle HTTP in api/ layer"); - }, - ); - return; - } - - // Recursively check generic arguments (e.g., Option) - for segment in &path.segments { - if let Some(args) = &segment.args - && let rustc_ast::GenericArgs::AngleBracketed(ref angle_args) = **args - { - for arg in &angle_args.args { - if let rustc_ast::AngleBracketedArg::Arg(rustc_ast::GenericArg::Type( - inner_ty, - )) = arg - { - check_type_in_domain(cx, inner_ty); - } - } - } - } - } - // Handle references: &http::Request - TyKind::Ref(_, mut_ty) => { - check_type_in_domain(cx, &mut_ty.ty); - } - // Handle slices: [http::StatusCode] - TyKind::Slice(inner_ty) => { - check_type_in_domain(cx, inner_ty); - } - // Handle arrays: [http::StatusCode; 10] - TyKind::Array(inner_ty, _) => { - check_type_in_domain(cx, inner_ty); - } - // Handle raw pointers: *const http::Request - TyKind::Ptr(mut_ty) => { - check_type_in_domain(cx, &mut_ty.ty); - } - // Handle tuples: (http::Request, String) - TyKind::Tup(types) => { - for inner_ty in types { - check_type_in_domain(cx, inner_ty); - } - } - // Handle trait objects: dyn http::Service - TyKind::TraitObject(bounds, _) => { - for bound in bounds { - if let rustc_ast::GenericBound::Trait(trait_ref) = bound { - // Check the trait path itself - let path = &trait_ref.trait_ref.path; - let path_str = path - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - if matches_http_pattern(&path_str).is_some() { - span_lint_and_then( - cx, - DE0308_NO_HTTP_IN_DOMAIN, - ty.span, - format!("domain module uses HTTP trait `{}` (DE0308)", path_str), - |diag| { - diag.help( - "domain should be transport-agnostic; handle HTTP in api/ layer", - ); - }, - ); - return; - } - } - } - } - // Handle impl Trait: impl http::Service - TyKind::ImplTrait(_, bounds) => { - for bound in bounds { - if let rustc_ast::GenericBound::Trait(trait_ref) = bound { - // Check the trait path itself - let path = &trait_ref.trait_ref.path; - let path_str = path - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - if matches_http_pattern(&path_str).is_some() { - span_lint_and_then( - cx, - DE0308_NO_HTTP_IN_DOMAIN, - ty.span, - format!("domain module uses HTTP trait `{}` (DE0308)", path_str), - |diag| { - diag.help( - "domain should be transport-agnostic; handle HTTP in api/ layer", - ); - }, - ); - return; - } - } - } - } - _ => {} - } -} - -impl EarlyLintPass for De0308NoHttpInDomain { - fn check_item(&mut self, cx: &rustc_lint::EarlyContext<'_>, item: &Item) { - // Skip if not in domain path or if in contract module (contracts can have HTTP types) - if !is_in_domain_path(cx.sess().source_map(), item.span) - || is_in_contract_module_ast(cx, item) - { - return; - } - - match &item.kind { - // Check use statements - ItemKind::Use(use_tree) => { - check_use_item(cx, item, use_tree); - } - // Check struct fields - ItemKind::Struct(_, _, variant_data) => { - for field in variant_data.fields() { - check_type_in_domain(cx, &field.ty); - } - } - // Check enum variants - ItemKind::Enum(_, _, enum_def) => { - for variant in &enum_def.variants { - for field in variant.data.fields() { - check_type_in_domain(cx, &field.ty); - } - } - } - // Check function signatures - ItemKind::Fn(fn_item) => { - // Check parameters - for param in &fn_item.sig.decl.inputs { - check_type_in_domain(cx, ¶m.ty); - } - // Check return type - if let rustc_ast::FnRetTy::Ty(ret_ty) = &fn_item.sig.decl.output { - check_type_in_domain(cx, ret_ty); - } - } - // Check type aliases - ItemKind::TyAlias(ty_alias) => { - if let Some(ty) = &ty_alias.ty { - check_type_in_domain(cx, ty); - } - } - _ => {} - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0308", "HTTP in domain"); - } -} diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_import.rs b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_import.rs deleted file mode 100644 index 6d51f44b4..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_import.rs +++ /dev/null @@ -1,12 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/domain/service.rs -// Test file for DE0308: No HTTP in Domain -#![allow(unused_imports)] -#![allow(dead_code)] - -// Should trigger DE0308 - HTTP in domain -use http::StatusCode; - -// Should trigger DE0308 - HTTP in domain -use axum::http::HeaderMap; - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_import.stderr b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_import.stderr deleted file mode 100644 index c4be9c2e5..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_import.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: domain module imports HTTP type `http` (DE0308) - --> $DIR/bad_http_import.rs:7:1 - | -LL | use http::StatusCode; - | ^^^^^^^^^^^^^^^^^^^^^ - | - = help: domain should be transport-agnostic; handle HTTP in api/ layer - = note: `#[deny(de0308_no_http_in_domain)]` on by default - -error: domain module imports HTTP type `axum` (DE0308) - --> $DIR/bad_http_import.rs:10:1 - | -LL | use axum::http::HeaderMap; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: domain should be transport-agnostic; handle HTTP in api/ layer - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_type_in_struct.rs b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_type_in_struct.rs deleted file mode 100644 index 24caf178d..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_type_in_struct.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/domain/service.rs -#![feature(register_tool)] -#![register_tool(dylint)] -#![allow(dead_code)] - -pub struct Hello { - // Should trigger DE0308 - HTTP in domain - param1: http::StatusCode, -} - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_type_in_struct.stderr b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_type_in_struct.stderr deleted file mode 100644 index 26da8a517..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/bad_http_type_in_struct.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: domain module uses HTTP type `http::StatusCode` (DE0308) - --> $DIR/bad_http_type_in_struct.rs:8:13 - | -LL | param1: http::StatusCode, - | ^^^^^^^^^^^^^^^^ - | - = help: domain should be transport-agnostic; handle HTTP in api/ layer - = note: `#[deny(de0308_no_http_in_domain)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/good_domain_0308.rs b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/good_domain_0308.rs deleted file mode 100644 index 3189fa156..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/good_domain_0308.rs +++ /dev/null @@ -1,19 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/domain/service.rs -// Test file for DE0308: Good domain code - no HTTP types -#![allow(unused_imports)] -#![allow(dead_code)] - -// Should not trigger DE0308 - HTTP in domain -use std::sync::Arc; - -// Should not trigger DE0308 - HTTP in domain -use anyhow::Result; - -// Domain error enum - this is correct -pub enum DomainResult { - Success, - NotFound, - InvalidData, -} - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/good_domain_0308.stderr b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/good_domain_0308.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/nested_generic_http_type.rs b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/nested_generic_http_type.rs deleted file mode 100644 index 5ad08085c..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/nested_generic_http_type.rs +++ /dev/null @@ -1,15 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/domain/service.rs -#![feature(register_tool)] -#![register_tool(dylint)] -#![allow(dead_code)] - -pub struct Config { - // Should trigger DE0308 - HTTP in domain - status: Option, - // Should trigger DE0308 - HTTP in domain - headers: Vec, - // Should trigger DE0308 - HTTP in domain - response: &'static http::Response, -} - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/nested_generic_http_type.stderr b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/nested_generic_http_type.stderr deleted file mode 100644 index 16dae79be..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/nested_generic_http_type.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error: domain module uses HTTP type `http::StatusCode` (DE0308) - --> $DIR/nested_generic_http_type.rs:8:20 - | -LL | status: Option, - | ^^^^^^^^^^^^^^^^ - | - = help: domain should be transport-agnostic; handle HTTP in api/ layer - = note: `#[deny(de0308_no_http_in_domain)]` on by default - -error: domain module uses HTTP type `http::HeaderMap` (DE0308) - --> $DIR/nested_generic_http_type.rs:10:18 - | -LL | headers: Vec, - | ^^^^^^^^^^^^^^^ - | - = help: domain should be transport-agnostic; handle HTTP in api/ layer - -error: domain module uses HTTP type `http::Response` (DE0308) - --> $DIR/nested_generic_http_type.rs:12:24 - | -LL | response: &'static http::Response, - | ^^^^^^^^^^^^^^^^^^^^^^ - | - = help: domain should be transport-agnostic; handle HTTP in api/ layer - -error: aborting due to 3 previous errors - diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/trait_bounds_http.rs b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/trait_bounds_http.rs deleted file mode 100644 index f4fe9736b..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/trait_bounds_http.rs +++ /dev/null @@ -1,18 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/domain/service.rs -#![feature(register_tool)] -#![register_tool(dylint)] -#![allow(dead_code)] - -use std::fmt::Display; - -// Should trigger DE0308 - HTTP in domain -pub fn make_status() -> impl Display + axum::http::header::IntoHeaderName { - "content-type" -} - -// Should trigger DE0308 - HTTP in domain -pub fn make_header() -> impl axum::http::header::IntoHeaderName { - "x-custom" -} - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/trait_bounds_http.stderr b/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/trait_bounds_http.stderr deleted file mode 100644 index 24c7de080..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0308_no_http_in_domain/ui/trait_bounds_http.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: domain module uses HTTP trait `axum::http::header::IntoHeaderName` (DE0308) - --> $DIR/trait_bounds_http.rs:9:25 - | -LL | pub fn make_status() -> impl Display + axum::http::header::IntoHeaderName { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: domain should be transport-agnostic; handle HTTP in api/ layer - = note: `#[deny(de0308_no_http_in_domain)]` on by default - -error: domain module uses HTTP trait `axum::http::header::IntoHeaderName` (DE0308) - --> $DIR/trait_bounds_http.rs:14:25 - | -LL | pub fn make_header() -> impl axum::http::header::IntoHeaderName { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: domain should be transport-agnostic; handle HTTP in api/ layer - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/Cargo.toml b/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/Cargo.toml deleted file mode 100644 index f50e2273b..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "de0309_must_have_domain_model" -version = "0.1.0" -authors = ["Hypernetix"] -description = "Domain structs must have #[domain_model] attribute (DE0309)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "missing_domain_model" -path = "ui/missing_domain_model.rs" - -[[example]] -name = "has_domain_model" -path = "ui/has_domain_model.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -toolkit.workspace = true -toolkit-macros.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/README.md b/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/README.md deleted file mode 100644 index ac7a79473..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# DE0309: Must Have Domain Model Attribute - -## What it does - -Checks that all struct and enum types in domain modules have the `#[domain_model]` attribute. - -## Why is this important? - -The `#[domain_model]` macro provides **compile-time validation** of Domain-Driven Design (DDD) boundaries. It ensures that domain types don't contain infrastructure dependencies such as: - -- HTTP types (`http::StatusCode`, `axum::*`) -- Database types (`sqlx::PgPool`, `sea_orm::*`) -- File system types (`std::fs::*`, `tokio::fs::*`) -- External service clients (`reqwest::*`, `tonic::*`) - -By requiring this attribute on all domain types, we guarantee that infrastructure concerns cannot leak into the domain layer. - -## Example - -### Bad - -```rust -// src/domain/user.rs - -pub struct User { // Missing #[domain_model] - pub id: Uuid, - pub email: String, -} -``` - -### Good - -```rust -// src/domain/user.rs -use toolkit_macros::domain_model; - -#[domain_model] -pub struct User { - pub id: Uuid, - pub email: String, -} -``` - -## Configuration - -This lint is configured to **deny** by default. - -It checks all `struct` and `enum` definitions in files whose path contains `/domain/`. - -## TDD Approach - -This lint is designed for Test-Driven Development: - -1. **Add the lint** - CI will fail for all domain types without the attribute -2. **Fix each violation** - Add `#[domain_model]` to all domain types -3. **CI passes** - All domain types are now validated at compile time - -## See Also - -- [`#[domain_model]` macro documentation](../../../../libs/toolkit-macros/src/domain_model.rs) -- [Domain Layer Architecture](../../../../docs/toolkit_unified_system/02_gear_layout_and_sdk_pattern.md) diff --git a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/src/lib.rs b/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/src/lib.rs deleted file mode 100644 index 7c8031dbf..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/src/lib.rs +++ /dev/null @@ -1,130 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use lint_utils::is_in_domain_path; -use rustc_ast::{Item, ItemKind}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; - -dylint_linting::declare_pre_expansion_lint! { - /// DE0309: Domain Structs Must Have `#[domain_model]` Attribute - /// - /// All struct and enum types in the domain layer MUST have the `#[domain_model]` - /// attribute to ensure compile-time validation of DDD boundaries. - /// - /// ### Why is this important? - /// - /// The `#[domain_model]` macro enforces that domain types don't contain - /// infrastructure dependencies (HTTP types, database types, etc.) at compile time. - /// This provides stronger guarantees than import-based lints and prevents - /// infrastructure leakage into the domain layer. - /// - /// ### Example: Bad - /// - /// ```rust,ignore - /// // src/domain/user.rs - /// pub struct User { // Missing #[domain_model] - /// pub id: Uuid, - /// pub email: String, - /// } - /// ``` - /// - /// ### Example: Good - /// - /// ```rust,ignore - /// // src/domain/user.rs - /// use toolkit_macros::domain_model; - /// - /// #[domain_model] - /// pub struct User { - /// pub id: Uuid, - /// pub email: String, - /// } - /// ``` - pub DE0309_MUST_HAVE_DOMAIN_MODEL, - Deny, - "domain types must have #[domain_model] attribute for DDD boundary enforcement (DE0309)" -} - -impl EarlyLintPass for De0309MustHaveDomainModel { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - check_domain_model_attribute(cx, item); - } -} - -fn check_domain_model_attribute(cx: &EarlyContext<'_>, item: &Item) { - // Only check structs and enums - if !matches!(item.kind, ItemKind::Struct(..) | ItemKind::Enum(..)) { - return; - } - - // Only check items in domain path - if !is_in_domain_path(cx.sess().source_map(), item.span) { - return; - } - - // Check if the item has #[domain_model] attribute - if has_domain_model_attribute(item) { - return; - } - - // Get item kind and name for error message - let (item_keyword, item_name) = match &item.kind { - ItemKind::Struct(ident, ..) => ("struct", ident.name.as_str()), - ItemKind::Enum(ident, ..) => ("enum", ident.name.as_str()), - _ => return, - }; - - span_lint_and_then( - cx, - DE0309_MUST_HAVE_DOMAIN_MODEL, - item.span, - format!("domain type `{item_name}` is missing required #[domain_model] attribute (DE0309)"), - |diag| { - diag.help(format!( - "add #[domain_model] attribute to enforce DDD boundaries at compile time: \ - use toolkit_macros::domain_model; #[domain_model] pub {item_keyword} ..." - )); - }, - ); -} - -/// Check if an item has the `#[domain_model]` or `#[toolkit::domain_model]` attribute. -fn has_domain_model_attribute(item: &Item) -> bool { - for attr in &item.attrs { - if let rustc_ast::AttrKind::Normal(attr_item) = &attr.kind { - let path = &attr_item.item.path; - let segments: Vec<&str> = path - .segments - .iter() - .map(|s| s.ident.name.as_str()) - .collect(); - - // Match: domain_model, toolkit::domain_model, toolkit_macros::domain_model - if segments.last() == Some(&"domain_model") { - return true; - } - } - } - false -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0309", - "domain_model attribute", - ); - } -} diff --git a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/has_domain_model.rs b/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/has_domain_model.rs deleted file mode 100644 index e2eaca20d..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/has_domain_model.rs +++ /dev/null @@ -1,34 +0,0 @@ -// simulated_dir=/cf-gears/gears/example/src/domain/ - -// Test: Domain structs WITH #[domain_model] should NOT trigger lint - -// For testing purposes, we define a dummy domain_model attribute -// In real code, this comes from toolkit_macros -#[allow(dead_code)] -mod toolkit { - pub use toolkit_macros::domain_model; -} - -use toolkit::domain_model; - -// Should not trigger DE0309 - domain_model attribute -#[domain_model] -pub struct User { - pub id: i64, - pub email: String, -} - -// Should not trigger DE0309 - domain_model attribute -#[domain_model] -pub enum UserStatus { - Active, - Inactive, -} - -// Should not trigger DE0309 - domain_model attribute -#[domain_model] -pub struct ServiceConfig { - pub timeout_ms: u64, -} - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/has_domain_model.stderr b/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/has_domain_model.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/missing_domain_model.rs b/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/missing_domain_model.rs deleted file mode 100644 index 58950a28b..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/missing_domain_model.rs +++ /dev/null @@ -1,22 +0,0 @@ -// simulated_dir=/cf-gears/gears/example/src/domain/ - -// Test: Domain structs without #[domain_model] should trigger lint - -// Should trigger DE0309 - domain_model attribute -pub struct User { - pub id: i64, - pub email: String, -} - -// Should trigger DE0309 - domain_model attribute -pub enum UserStatus { - Active, - Inactive, -} - -// Should trigger DE0309 - domain_model attribute -pub struct ServiceConfig { - pub timeout_ms: u64, -} - -fn main() {} diff --git a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/missing_domain_model.stderr b/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/missing_domain_model.stderr deleted file mode 100644 index da2f6da7b..000000000 --- a/tools/dylint_lints/de03_domain_layer/de0309_must_have_domain_model/ui/missing_domain_model.stderr +++ /dev/null @@ -1,35 +0,0 @@ -error: domain type `User` is missing required #[domain_model] attribute (DE0309) - --> $DIR/missing_domain_model.rs:6:1 - | -LL | / pub struct User { -LL | | pub id: i64, -LL | | pub email: String, -LL | | } - | |_^ - | - = help: add #[domain_model] attribute to enforce DDD boundaries at compile time: use toolkit_macros::domain_model; #[domain_model] pub struct ... - = note: `#[deny(de0309_must_have_domain_model)]` on by default - -error: domain type `UserStatus` is missing required #[domain_model] attribute (DE0309) - --> $DIR/missing_domain_model.rs:12:1 - | -LL | / pub enum UserStatus { -LL | | Active, -LL | | Inactive, -LL | | } - | |_^ - | - = help: add #[domain_model] attribute to enforce DDD boundaries at compile time: use toolkit_macros::domain_model; #[domain_model] pub enum ... - -error: domain type `ServiceConfig` is missing required #[domain_model] attribute (DE0309) - --> $DIR/missing_domain_model.rs:18:1 - | -LL | / pub struct ServiceConfig { -LL | | pub timeout_ms: u64, -LL | | } - | |_^ - | - = help: add #[domain_model] attribute to enforce DDD boundaries at compile time: use toolkit_macros::domain_model; #[domain_model] pub struct ... - -error: aborting due to 3 previous errors - diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/.gitignore b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/Cargo.toml b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/Cargo.toml deleted file mode 100644 index b98517479..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/Cargo.toml +++ /dev/null @@ -1,55 +0,0 @@ -[package] -name = "de0503_plugin_client_suffix" -version = "0.1.0" -authors.workspace = true -edition.workspace = true -license.workspace = true -description = "Dylint lint enforcing *Client suffix for plugin client traits instead of *Api or *PluginApi" -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "plugin_api_bad-sdk" -path = "ui/plugin_api_bad.rs" - -[[example]] -name = "plugin_client_good-sdk" -path = "ui/plugin_client_good.rs" - -[[example]] -name = "generic_parameters_api-sdk" -path = "ui/generic_parameters_api.rs" - -[[example]] -name = "generic_parameters_good-sdk" -path = "ui/generic_parameters_good.rs" - -[[example]] -name = "versioned_api-sdk" -path = "ui/versioned_api.rs" - -[[example]] -name = "plugin_client_v1_good-sdk" -path = "ui/plugin_client_v1_good.rs" - -[[example]] -name = "malformed_versioned_api-sdk" -path = "ui/malformed_versioned_api.rs" - -[[example]] -name = "client_api_bad-sdk" -path = "ui/client_api_bad.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -async-trait = "0.1" - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/README.md b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/README.md deleted file mode 100644 index 437c285c1..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/README.md +++ /dev/null @@ -1,68 +0,0 @@ - -# DE0503: Plugin Client Trait Suffix - -### What it does - -Checks that plugin client traits in `*-sdk` crates use the `*Client` suffix instead of `*Api` or `*PluginApi`. - -### Why is this bad? - -In the SDK pattern used by Gears, `*-sdk` crates define public API traits for consumers (often wired through the ClientHub). If those traits use inconsistent suffixes like `*Api` or `*PluginApi`: - -- **The role of the trait is unclear**: is it a server-side API surface or a client interface? -- **Naming becomes inconsistent across SDK crates**: harder to find and standardize clients. -- **Refactors become noisy**: multiple patterns (`Api`, `PluginApi`, `Client`) spread across the codebase. - -### Example - -```rust -// ❌ Bad - plugin client trait using *PluginApi suffix -use async_trait::async_trait; - -#[async_trait] -pub trait TenantResolverPluginApi: Send + Sync { - async fn get_root_tenant(&self) -> Result<(), ()>; -} -``` - -```rust -// ❌ Bad - plugin client trait using *Api suffix -use async_trait::async_trait; - -#[async_trait] -pub trait TenantResolverApi: Send + Sync { - async fn get_root_tenant(&self) -> Result<(), ()>; -} -``` - -Use instead: - -```rust -// ✅ Good - uses *Client / *PluginClient suffix -use async_trait::async_trait; - -#[async_trait] -pub trait TenantResolverPluginClient: Send + Sync { - async fn get_root_tenant(&self) -> Result<(), ()>; -} -``` - -### Configuration - -This lint is configured to **deny** by default. - -It only applies to code inside `*-sdk` crates. - -In practice, it enables itself when either: - -- the crate name ends with `-sdk` or `_sdk`, or -- the source file path contains a `-sdk/` path segment - -It reports a violation when a trait name: - -- ends with `PluginApi`, or -- ends with `Api` (and looks like a plugin/client trait) - -### See Also - -- [Issue #181](https://github.com/constructorfabric/gears-rust/issues/181) diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/src/lib.rs b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/src/lib.rs deleted file mode 100644 index 10cf14170..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/src/lib.rs +++ /dev/null @@ -1,151 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; -extern crate rustc_span; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{Item, ItemKind}; -use rustc_lint::{EarlyContext, EarlyLintPass}; -use rustc_span::Span; - -dylint_linting::declare_early_lint! { - /// ### What it does - /// - /// Checks that plugin client traits use the `*Client` suffix instead of `*Api` or `*PluginApi`. - /// - /// # Why is this bad? - /// - /// Inconsistent naming makes it harder to identify client traits - /// and violates the project's architectural conventions. - /// - /// # Scope - /// This lint only applies to `*-sdk` crates where plugin client traits are defined. - /// - /// # Example - /// ```rust,ignore - /// // Bad (in a *-sdk crate) - /// pub trait ThrPluginApi: Send + Sync { - /// async fn get_root_tenant(&self) -> Result; - /// } - /// - /// // Good - /// pub trait ThrPluginClient: Send + Sync { - /// async fn get_root_tenant(&self) -> Result; - /// } - /// ``` - /// - /// Use instead: - /// - /// ```rust - /// // Good - uses Client suffix - /// #[async_trait] - /// pub trait ThrPluginClient: Send + Sync { - /// async fn get_data(&self) -> Result; - /// } - /// ``` - pub DE0503_PLUGIN_CLIENT_SUFFIX, - Deny, - "plugin client traits should use *PluginClient suffix, not *Api or *PluginApi (DE0503)" -} - -impl EarlyLintPass for De0503PluginClientSuffix { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - // Only check trait definitions - let ItemKind::Trait(trait_data) = &item.kind else { - return; - }; - - // Only apply this lint to *-sdk crates - if !lint_utils::is_in_sdk_crate(cx, item.span) { - return; - } - - let trait_name = trait_data.ident.name.as_str(); - if trait_name.is_empty() { - return; - } - - // Strip version suffix (valid or malformed) to check the base name - let version = lint_utils::parse_version_suffix(trait_name); - let base_name = version.base; - - // Check if base name ends with "PluginApi" or just "Api" - if base_name.ends_with("PluginApi") { - emit_lint(cx, item.span, trait_name, "PluginApi", "PluginClient"); - } else if base_name.ends_with("Api") { - let base_without_api = base_name.strip_suffix("Api").unwrap_or(base_name); - let name_lower = base_name.to_lowercase(); - - let is_plugin_api = - base_without_api.ends_with("Plugin") || name_lower.contains("plugin"); - - if is_plugin_api && !base_without_api.ends_with("Client") { - emit_lint(cx, item.span, trait_name, "Api", "PluginClient"); - } else if base_without_api.ends_with("Client") { - // Trait like SomeClientApi — already has Client suffix, just drop Api - emit_lint(cx, item.span, trait_name, "ClientApi", "Client"); - } - } - } -} - -fn emit_lint( - cx: &EarlyContext<'_>, - span: Span, - trait_name: &str, - wrong_suffix: &str, - suggested_suffix: &str, -) { - let version = lint_utils::parse_version_suffix(trait_name); - - let suggestion = if version.base.ends_with(wrong_suffix) { - let base = version.base.strip_suffix(wrong_suffix).unwrap(); - if version.has_valid_version() { - format!("{base}{suggested_suffix}{}", version.version_suffix) - } else if version.has_malformed_version() { - // Only suggest Vn if digits don't start with 0 (e.g., ThrPluginApi2 -> ThrPluginClientV2) - if !version.malformed_digits.starts_with('0') { - format!("{base}{suggested_suffix}V{}", version.malformed_digits) - } else { - format!("{base}{suggested_suffix}") - } - } else { - format!("{base}{suggested_suffix}") - } - } else { - format!("{trait_name}Client") - }; - - span_lint_and_then( - cx, - DE0503_PLUGIN_CLIENT_SUFFIX, - span, - format!( - "plugin client trait `{trait_name}` should use `*{suggested_suffix}` suffix, not `*{wrong_suffix}` (DE0503)" - ), - |diag| { - diag.help(format!( - "rename trait to `{suggestion}` to follow plugin client naming conventions" - )); - }, - ); -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0503", - "plugin client traits should use", - ); - } -} diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/client_api_bad.rs b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/client_api_bad.rs deleted file mode 100644 index 0389dde55..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/client_api_bad.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `SomeClient` suffix -pub trait SomeClientApi: Send + Sync { - async fn get_data(&self) -> Result<(), ()>; -} - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `ThrPluginClientV1` suffix -pub trait ThrPluginClientApiV1: Send + Sync { - async fn resolve(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/client_api_bad.stderr b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/client_api_bad.stderr deleted file mode 100644 index 9e2effa67..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/client_api_bad.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error: plugin client trait `SomeClientApi` should use `*Client` suffix, not `*ClientApi` (DE0503) - --> $DIR/client_api_bad.rs:6:1 - | -LL | / pub trait SomeClientApi: Send + Sync { -LL | | async fn get_data(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `SomeClient` to follow plugin client naming conventions - = note: `#[deny(de0503_plugin_client_suffix)]` on by default - -error: plugin client trait `ThrPluginClientApiV1` should use `*Client` suffix, not `*ClientApi` (DE0503) - --> $DIR/client_api_bad.rs:12:1 - | -LL | / pub trait ThrPluginClientApiV1: Send + Sync { -LL | | async fn resolve(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `ThrPluginClientV1` to follow plugin client naming conventions - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/generic_parameters_api.rs b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/generic_parameters_api.rs deleted file mode 100644 index 1a6c65309..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/generic_parameters_api.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `ThrPluginClient` suffix -pub trait ThrPluginApi: Send + Sync { - async fn get_data(&self) -> Result; -} - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `DataPluginClient` suffix -pub trait DataPluginApi: Send + Sync -where - T: Send + Sync, - E: std::error::Error, -{ - async fn process(&self) -> Result; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/generic_parameters_api.stderr b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/generic_parameters_api.stderr deleted file mode 100644 index a192a4eaf..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/generic_parameters_api.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error: plugin client trait `ThrPluginApi` should use `*PluginClient` suffix, not `*PluginApi` (DE0503) - --> $DIR/generic_parameters_api.rs:6:1 - | -LL | / pub trait ThrPluginApi: Send + Sync { -LL | | async fn get_data(&self) -> Result; -LL | | } - | |_^ - | - = help: rename trait to `ThrPluginClient` to follow plugin client naming conventions - = note: `#[deny(de0503_plugin_client_suffix)]` on by default - -error: plugin client trait `DataPluginApi` should use `*PluginClient` suffix, not `*PluginApi` (DE0503) - --> $DIR/generic_parameters_api.rs:12:1 - | -LL | / pub trait DataPluginApi: Send + Sync -LL | | where -LL | | T: Send + Sync, -LL | | E: std::error::Error, -LL | | { -LL | | async fn process(&self) -> Result; -LL | | } - | |_^ - | - = help: rename trait to `DataPluginClient` to follow plugin client naming conventions - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/generic_parameters_good.rs b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/generic_parameters_good.rs deleted file mode 100644 index e507965ff..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/generic_parameters_good.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -// Should NOT trigger - properly named with generics -#[async_trait] -pub trait ThrPluginClient: Send + Sync { - async fn get_data(&self) -> Result; -} - -// Should NOT trigger - properly named with multiple generics -#[async_trait] -pub trait DataPluginClient: Send + Sync -where - T: Send + Sync, - E: std::error::Error, -{ - async fn process(&self) -> Result; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/malformed_versioned_api.rs b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/malformed_versioned_api.rs deleted file mode 100644 index 578383ab2..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/malformed_versioned_api.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `ThrPluginClientV2` suffix -pub trait ThrPluginApi2: Send + Sync { - async fn get_data(&self) -> Result<(), ()>; -} - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `DataPluginClient` suffix -pub trait DataPluginApiV: Send + Sync { - async fn process(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/malformed_versioned_api.stderr b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/malformed_versioned_api.stderr deleted file mode 100644 index 9eced4287..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/malformed_versioned_api.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error: plugin client trait `ThrPluginApi2` should use `*PluginClient` suffix, not `*PluginApi` (DE0503) - --> $DIR/malformed_versioned_api.rs:6:1 - | -LL | / pub trait ThrPluginApi2: Send + Sync { -LL | | async fn get_data(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `ThrPluginClientV2` to follow plugin client naming conventions - = note: `#[deny(de0503_plugin_client_suffix)]` on by default - -error: plugin client trait `DataPluginApiV` should use `*PluginClient` suffix, not `*PluginApi` (DE0503) - --> $DIR/malformed_versioned_api.rs:12:1 - | -LL | / pub trait DataPluginApiV: Send + Sync { -LL | | async fn process(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `DataPluginClient` to follow plugin client naming conventions - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_api_bad.rs b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_api_bad.rs deleted file mode 100644 index 7af093f8c..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_api_bad.rs +++ /dev/null @@ -1,17 +0,0 @@ -#![allow(dead_code)] - -use async_trait::async_trait; - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `ThrPluginClient` suffix -pub trait ThrPluginApi: Send + Sync { - async fn get_root_tenant(&self) -> Result<(), ()>; -} - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `OagwPluginClient` suffix -pub trait OagwPluginApi: Send + Sync { - async fn execute(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_api_bad.stderr b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_api_bad.stderr deleted file mode 100644 index 2449b0dc5..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_api_bad.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error: plugin client trait `ThrPluginApi` should use `*PluginClient` suffix, not `*PluginApi` (DE0503) - --> $DIR/plugin_api_bad.rs:7:1 - | -LL | / pub trait ThrPluginApi: Send + Sync { -LL | | async fn get_root_tenant(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `ThrPluginClient` to follow plugin client naming conventions - = note: `#[deny(de0503_plugin_client_suffix)]` on by default - -error: plugin client trait `OagwPluginApi` should use `*PluginClient` suffix, not `*PluginApi` (DE0503) - --> $DIR/plugin_api_bad.rs:13:1 - | -LL | / pub trait OagwPluginApi: Send + Sync { -LL | | async fn execute(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `OagwPluginClient` to follow plugin client naming conventions - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_client_good.rs b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_client_good.rs deleted file mode 100644 index 9c57ea3c8..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_client_good.rs +++ /dev/null @@ -1,23 +0,0 @@ -#![allow(dead_code)] - -use async_trait::async_trait; - -// Should not trigger DE0503 - PluginApi suffix -#[async_trait] -pub trait ThrPluginClient: Send + Sync { - async fn get_root_tenant(&self) -> Result<(), ()>; -} - -// Should not trigger DE0503 - PluginApi suffix -#[async_trait] -pub trait OagwPluginClient: Send + Sync { - async fn execute(&self) -> Result<(), ()>; -} - -// Should not trigger DE0503 - PluginApi suffix -#[async_trait] -pub trait TenantResolverClient: Send + Sync { - async fn list_tenants(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_client_v1_good.rs b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_client_v1_good.rs deleted file mode 100644 index c88c6cb82..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/plugin_client_v1_good.rs +++ /dev/null @@ -1,22 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -// Should not trigger DE0503 - PluginClientV1 suffix is valid -#[async_trait] -pub trait TenantResolverPluginClientV1: Send + Sync { - async fn get_root_tenant(&self) -> Result<(), ()>; -} - -// Should not trigger DE0503 - ClientV1 suffix is valid -#[async_trait] -pub trait TenantResolverClientV1: Send + Sync { - async fn list_tenants(&self) -> Result<(), ()>; -} - -// Should not trigger DE0503 - PluginClientV2 suffix is valid -#[async_trait] -pub trait TenantResolverPluginClientV2: Send + Sync { - async fn get_root_tenant(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/versioned_api.rs b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/versioned_api.rs deleted file mode 100644 index 3cb409ceb..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/versioned_api.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `ThrPluginClientV1` suffix -pub trait ThrPluginApiV1: Send + Sync { - async fn get_data(&self) -> Result<(), ()>; -} - -#[async_trait] -// Should trigger DE0503 - plugin client traits should use `DataPluginClientV2` suffix -pub trait DataPluginApiV2: Send + Sync { - async fn process(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/versioned_api.stderr b/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/versioned_api.stderr deleted file mode 100644 index 668fbd8a7..000000000 --- a/tools/dylint_lints/de05_client_layer/de0503_plugin_client_suffix/ui/versioned_api.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error: plugin client trait `ThrPluginApiV1` should use `*PluginClient` suffix, not `*PluginApi` (DE0503) - --> $DIR/versioned_api.rs:6:1 - | -LL | / pub trait ThrPluginApiV1: Send + Sync { -LL | | async fn get_data(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `ThrPluginClientV1` to follow plugin client naming conventions - = note: `#[deny(de0503_plugin_client_suffix)]` on by default - -error: plugin client trait `DataPluginApiV2` should use `*PluginClient` suffix, not `*PluginApi` (DE0503) - --> $DIR/versioned_api.rs:12:1 - | -LL | / pub trait DataPluginApiV2: Send + Sync { -LL | | async fn process(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `DataPluginClientV2` to follow plugin client naming conventions - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/Cargo.toml b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/Cargo.toml deleted file mode 100644 index 2389f9cb8..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "de0504_client_versioning" -version = "0.1.0" -authors.workspace = true -edition.workspace = true -license.workspace = true -description = "Dylint lint enforcing version suffixes (V1, V2, etc.) for Client traits in non-system gears" -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "non_system_missing_version-sdk" -path = "ui/non_system_missing_version.rs" - -[[example]] -name = "non_system_versioned_good-sdk" -path = "ui/non_system_versioned_good.rs" - -[[example]] -name = "invalid_version_suffix-sdk" -path = "ui/invalid_version_suffix.rs" - -[[example]] -name = "generic_parameters-sdk" -path = "ui/generic_parameters.rs" - -[[example]] -name = "non_client_traits_good-sdk" -path = "ui/non_client_traits_good.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -async-trait = "0.1" - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/src/lib.rs b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/src/lib.rs deleted file mode 100644 index 89dbdfa11..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/src/lib.rs +++ /dev/null @@ -1,249 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; -extern crate rustc_span; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{Item, ItemKind}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; -use rustc_span::Span; - -dylint_linting::declare_early_lint! { - /// ### What it does - /// - /// Enforces that Client and PluginClient traits in non-system gears have version suffixes (V1, V2, etc.). - /// - /// # Why is this bad? - /// - /// Non-system gears require explicit versioning for their public API contracts to enable - /// parallel versions and clear upgrade paths. System gears are exempt because they follow - /// different versioning rules managed at the platform level. - /// - /// # Scope - /// - **Applies to**: All SDK crates in `gears/*` (except `gears/system/*`) and `examples/*` - /// - **Does NOT apply to**: System gears only (`gears/system/*`) - /// - /// # Example - /// ```rust,ignore - /// // Bad (in gears/simple_user_settings or examples/*) - /// pub trait UsersInfoClient: Send + Sync { - /// async fn get_user(&self) -> Result; - /// } - /// - /// // Good (in gears/simple_user_settings or examples/*) - /// pub trait UsersInfoClientV1: Send + Sync { - /// async fn get_user(&self) -> Result; - /// } - /// - /// // OK (in gears/system/* - exempt from versioning) - /// pub trait TypesRegistryClient: Send + Sync { - /// async fn register(&self) -> Result<(), Error>; - /// } - /// ``` - pub DE0504_CLIENT_VERSIONING, - Deny, - "Client and PluginClient traits in non-system gears must have version suffixes (V1, V2, etc.) (DE0504)" -} - -impl EarlyLintPass for De0504ClientVersioning { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - // Only check trait definitions - let ItemKind::Trait(trait_data) = &item.kind else { - return; - }; - - // Only apply this lint to *-sdk crates or UI test examples - if !lint_utils::is_in_sdk_crate(cx, item.span) { - return; - } - - // EXEMPTION: Skip system gears (gears/system/*) from versioning requirements. - // UI tests always run for testing purposes even if they simulate system gears. - if is_system_module(cx, item.span) && !is_ui_test(cx, item.span) { - return; - } - - let trait_name = trait_data.ident.name.as_str(); - if trait_name.is_empty() { - return; - } - - let version = lint_utils::parse_version_suffix(trait_name); - - // Only match traits whose base name ends with "Client" to avoid false positives - // on helper traits like ClientEventHandler, ClientConfiguration, etc. - if !version.base.ends_with("Client") { - return; - } - - // If it has a valid version suffix (V1, V2, etc.), it's fine - if version.has_valid_version() { - return; - } - - emit_lint(cx, item.span, trait_name, &version); - } -} - -fn is_ui_test(cx: &EarlyContext<'_>, span: Span) -> bool { - let Some(file_path) = lint_utils::filename_str(cx.sess().source_map(), span) else { - return false; - }; - lint_utils::is_temp_path(&file_path) -} - -/// Checks if the file is part of a system gear (gears/system/*). -fn is_system_module(cx: &EarlyContext<'_>, span: Span) -> bool { - let Some(file_path) = lint_utils::filename_str(cx.sess().source_map(), span) else { - return false; - }; - file_path.contains("gears/system/") || file_path.contains("gears\\system\\") -} - -fn emit_lint( - cx: &EarlyContext<'_>, - span: Span, - trait_name: &str, - version: &lint_utils::VersionParts<'_>, -) { - let suggestion = - if version.has_malformed_version() && !version.malformed_digits.starts_with('0') { - // Trailing digits without V prefix: suggest inserting V - // e.g., UsersInfoClient2 -> UsersInfoClientV2 - format!("{}V{}", version.base, version.malformed_digits) - } else { - // No version, bare V, V0, or leading-zero digits: suggest appending V1 to base - format!("{}V1", version.base) - }; - - span_lint_and_then( - cx, - DE0504_CLIENT_VERSIONING, - span, - format!( - "Client trait `{trait_name}` in non-system gear must have a version suffix (DE0504)" - ), - |diag| { - diag.help(format!( - "rename trait to `{suggestion}` to indicate API version" - )); - }, - ); -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0504", "Client trait"); - } - - // NOTE: Positive-case testing (lint fires on bad code) is covered by UI tests in ui/ - // (non_system_missing_version.rs, invalid_version_suffix.rs, generic_parameters.rs). - // Integration tests in tests/system_module_exemption.rs verify the system gear - // exemption works with real crate paths, which cannot be tested through UI tests. - - // --- Unit tests for lint_utils::parse_version_suffix --- - // Placed here because lint_utils can't run unit tests directly (rustc_private linking). - - fn assert_version( - name: &str, - expected_base: &str, - expected_suffix: &str, - expected_malformed: &str, - ) { - let v = lint_utils::parse_version_suffix(name); - assert_eq!( - v.base, expected_base, - "parse_version_suffix({name:?}): base mismatch" - ); - assert_eq!( - v.version_suffix, expected_suffix, - "parse_version_suffix({name:?}): version_suffix mismatch" - ); - assert_eq!( - v.malformed_digits, expected_malformed, - "parse_version_suffix({name:?}): malformed_digits mismatch" - ); - } - - #[test] - fn test_parse_version_suffix_empty_and_single_char() { - assert_version("", "", "", ""); - // Single "V" is just a name, not a bare-V suffix (requires len > 1) - assert_version("V", "V", "", ""); - assert_version("A", "A", "", ""); - assert_version("1", "", "", "1"); - } - - #[test] - fn test_parse_version_suffix_valid_versions() { - assert_version("FooClientV1", "FooClient", "V1", ""); - assert_version("FooClientV2", "FooClient", "V2", ""); - assert_version("FooClientV10", "FooClient", "V10", ""); - assert_version("FooClientV99", "FooClient", "V99", ""); - assert_version("V1", "", "V1", ""); - } - - #[test] - fn test_parse_version_suffix_rejected_versions() { - // V0: version zero is invalid - assert_version("FooClientV0", "FooClient", "", ""); - // V00: leading zero - assert_version("FooClientV00", "FooClient", "", ""); - // V01: leading zero - assert_version("FooClientV01", "FooClient", "", ""); - // V0 standalone - assert_version("V0", "", "", ""); - } - - #[test] - fn test_parse_version_suffix_bare_v() { - assert_version("FooClientV", "FooClient", "", ""); - assert_version("VV", "V", "", ""); - } - - #[test] - fn test_parse_version_suffix_malformed_digits() { - assert_version("FooClient2", "FooClient", "", "2"); - assert_version("FooClient123", "FooClient", "", "123"); - assert_version("Client1", "Client", "", "1"); - } - - #[test] - fn test_parse_version_suffix_no_suffix() { - assert_version("FooClient", "FooClient", "", ""); - assert_version("ThrPluginApi", "ThrPluginApi", "", ""); - assert_version("SomeTraitName", "SomeTraitName", "", ""); - } - - #[test] - fn test_version_parts_helpers() { - let v = lint_utils::parse_version_suffix("FooClientV1"); - assert!(v.has_valid_version()); - assert!(!v.has_malformed_version()); - - let v = lint_utils::parse_version_suffix("FooClient2"); - assert!(!v.has_valid_version()); - assert!(v.has_malformed_version()); - - let v = lint_utils::parse_version_suffix("FooClient"); - assert!(!v.has_valid_version()); - assert!(!v.has_malformed_version()); - - let v = lint_utils::parse_version_suffix("FooClientV0"); - assert!(!v.has_valid_version()); - assert!(!v.has_malformed_version()); - - let v = lint_utils::parse_version_suffix("FooClientV"); - assert!(!v.has_valid_version()); - assert!(!v.has_malformed_version()); - } -} diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/tests/system_gear_exemption.rs b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/tests/system_gear_exemption.rs deleted file mode 100644 index 5b2381f92..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/tests/system_gear_exemption.rs +++ /dev/null @@ -1,132 +0,0 @@ -/// Integration tests to verify system gears are exempt from versioning requirements. -/// -/// These tests ensure that Client traits in gears/system/* do NOT trigger DE0504, -/// while Client traits in non-system gears and examples compile cleanly (because -/// they already have V1 suffixes from the refactoring). -/// -/// Positive-case testing (lint fires on bad code) is covered by UI tests in ui/. -use std::process::Command; - -fn workspace_root() -> std::path::PathBuf { - // Navigate from CARGO_MANIFEST_DIR (de0504_client_versioning/) up to workspace root (versions/) - let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); - manifest - .parent() // de05_client_layer - .and_then(|p| p.parent()) // dylint_lints - .and_then(|p| p.parent()) // versions (workspace root) - .expect("Failed to find workspace root from CARGO_MANIFEST_DIR") - .to_path_buf() -} - -#[test] -fn test_system_gears_are_exempt() { - let output = Command::new("cargo") - .args([ - "check", - "-p", - "cf-gears-tenant-resolver-sdk", - "--message-format=json", - ]) - .current_dir(workspace_root()) - .output() - .expect("Failed to run cargo check on system gear"); - - let stderr = String::from_utf8_lossy(&output.stderr); - let stdout = String::from_utf8_lossy(&output.stdout); - - assert!( - output.status.success(), - "System gear tenant_resolver-sdk should compile successfully.\n\ - Stderr: {}\nStdout: {}", - stderr, - stdout - ); - - let has_de0504_error = stdout.lines().chain(stderr.lines()).any(|line| { - line.contains("de0504_client_versioning") - || line.contains("DE0504") - || (line.contains("Client trait") && line.contains("version suffix")) - }); - - assert!( - !has_de0504_error, - "System gear tenant_resolver-sdk should NOT trigger DE0504 for TenantResolverClient\n\ - System gears (gears/system/*) are exempt from versioning requirements.\n\ - Stderr: {}\nStdout: {}", - stderr, stdout - ); -} - -#[test] -fn test_non_system_gears_require_versioning() { - let output = Command::new("cargo") - .args([ - "check", - "-p", - "cf-gears-simple-user-settings-sdk", - "--message-format=json", - ]) - .current_dir(workspace_root()) - .output() - .expect("Failed to run cargo check on non-system gear"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!( - output.status.success(), - "Non-system gear simple_user_settings-sdk should compile successfully.\n\ - If this fails, the V1 refactoring is incomplete.\n\ - Stderr: {}\nStdout: {}", - stderr, - stdout - ); - - let has_de0504_error = stdout.lines().chain(stderr.lines()).any(|line| { - line.contains("de0504_client_versioning") - || (line.contains("must have a version suffix") && line.contains("DE0504")) - }); - - assert!( - !has_de0504_error, - "Non-system gear simple_user_settings-sdk should compile without DE0504 errors \ - because it has V1 suffixes.\n\ - If this fails, the V1 refactoring is incomplete.\n\ - Stderr: {}\nStdout: {}", - stderr, stdout - ); -} - -#[test] -fn test_examples_require_versioning() { - let output = Command::new("cargo") - .args(["check", "-p", "users-info-sdk", "--message-format=json"]) - .current_dir(workspace_root()) - .output() - .expect("Failed to run cargo check on example"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!( - output.status.success(), - "Example users-info-sdk should compile successfully.\n\ - If this fails, the V1 refactoring is incomplete.\n\ - Stderr: {}\nStdout: {}", - stderr, - stdout - ); - - let has_de0504_error = stdout.lines().chain(stderr.lines()).any(|line| { - line.contains("de0504_client_versioning") - || (line.contains("must have a version suffix") && line.contains("DE0504")) - }); - - assert!( - !has_de0504_error, - "Example user_info-sdk should compile without DE0504 errors because it has V1 suffixes.\n\ - If this fails, the V1 refactoring is incomplete.\n\ - Stderr: {}\nStdout: {}", - stderr, stdout - ); -} diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/generic_parameters.rs b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/generic_parameters.rs deleted file mode 100644 index 8e3c58cd5..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/generic_parameters.rs +++ /dev/null @@ -1,26 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -#[async_trait] -// Should trigger DE0504 - Client trait `UsersInfoClient` with generic parameter missing version suffix -pub trait UsersInfoClient: Send + Sync { - async fn get_user(&self) -> Result; -} - -#[async_trait] -// Should trigger DE0504 - Client trait `CalculatorClient` with multiple generic parameters missing version suffix -pub trait CalculatorClient: Send + Sync -where - T: Send + Sync, - E: std::error::Error, -{ - async fn calculate(&self) -> Result; -} - -#[async_trait] -// Should NOT trigger - properly versioned with generics -pub trait DataClientV1: Send + Sync { - async fn get_data(&self) -> Result; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/generic_parameters.stderr b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/generic_parameters.stderr deleted file mode 100644 index 44a2a0ce5..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/generic_parameters.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error: Client trait `UsersInfoClient` in non-system gear must have a version suffix (DE0504) - --> $DIR/generic_parameters.rs:6:1 - | -LL | / pub trait UsersInfoClient: Send + Sync { -LL | | async fn get_user(&self) -> Result; -LL | | } - | |_^ - | - = help: rename trait to `UsersInfoClientV1` to indicate API version - = note: `#[deny(de0504_client_versioning)]` on by default - -error: Client trait `CalculatorClient` in non-system gear must have a version suffix (DE0504) - --> $DIR/generic_parameters.rs:12:1 - | -LL | / pub trait CalculatorClient: Send + Sync -LL | | where -LL | | T: Send + Sync, -LL | | E: std::error::Error, -LL | | { -LL | | async fn calculate(&self) -> Result; -LL | | } - | |_^ - | - = help: rename trait to `CalculatorClientV1` to indicate API version - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/invalid_version_suffix.rs b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/invalid_version_suffix.rs deleted file mode 100644 index de4f3522c..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/invalid_version_suffix.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -#[async_trait] -// Should trigger DE0504 - Client trait `UsersInfoClient2` ends with digit but missing V prefix -pub trait UsersInfoClient2: Send + Sync { - async fn get_user(&self) -> Result<(), ()>; -} - -#[async_trait] -// Should trigger DE0504 - Client trait `Client123` ends with digits but missing V prefix -pub trait Client123: Send + Sync { - async fn calculate(&self) -> Result<(), ()>; -} - -#[async_trait] -// Should trigger DE0504 - Client trait `UsersInfoClientV0` has zero version -pub trait UsersInfoClientV0: Send + Sync { - async fn get_user_v0(&self) -> Result<(), ()>; -} - -#[async_trait] -// Should trigger DE0504 - Client trait `UsersInfoClientV` has bare V without digits -pub trait UsersInfoClientV: Send + Sync { - async fn list_users(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/invalid_version_suffix.stderr b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/invalid_version_suffix.stderr deleted file mode 100644 index 3d256f45b..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/invalid_version_suffix.stderr +++ /dev/null @@ -1,43 +0,0 @@ -error: Client trait `UsersInfoClient2` in non-system gear must have a version suffix (DE0504) - --> $DIR/invalid_version_suffix.rs:6:1 - | -LL | / pub trait UsersInfoClient2: Send + Sync { -LL | | async fn get_user(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `UsersInfoClientV2` to indicate API version - = note: `#[deny(de0504_client_versioning)]` on by default - -error: Client trait `Client123` in non-system gear must have a version suffix (DE0504) - --> $DIR/invalid_version_suffix.rs:12:1 - | -LL | / pub trait Client123: Send + Sync { -LL | | async fn calculate(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `ClientV123` to indicate API version - -error: Client trait `UsersInfoClientV0` in non-system gear must have a version suffix (DE0504) - --> $DIR/invalid_version_suffix.rs:18:1 - | -LL | / pub trait UsersInfoClientV0: Send + Sync { -LL | | async fn get_user_v0(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `UsersInfoClientV1` to indicate API version - -error: Client trait `UsersInfoClientV` in non-system gear must have a version suffix (DE0504) - --> $DIR/invalid_version_suffix.rs:24:1 - | -LL | / pub trait UsersInfoClientV: Send + Sync { -LL | | async fn list_users(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `UsersInfoClientV1` to indicate API version - -error: aborting due to 4 previous errors - diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_client_traits_good.rs b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_client_traits_good.rs deleted file mode 100644 index 5c84f7edf..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_client_traits_good.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -// Should not trigger DE0504 - Client trait naming does not apply, base does not end with Client -#[async_trait] -pub trait ClientEventHandler: Send + Sync { - async fn handle(&self) -> Result<(), ()>; -} - -// Should not trigger DE0504 - Client trait naming does not apply, base does not end with Client -#[async_trait] -pub trait ClientConfiguration: Send + Sync { - async fn configure(&self) -> Result<(), ()>; -} - -// Should not trigger DE0504 - Client trait naming does not apply, base does not end with Client -#[async_trait] -pub trait ApiClientAdapter: Send + Sync { - async fn adapt(&self) -> Result<(), ()>; -} - -// Should not trigger DE0504 - Client trait naming does not apply, no Client suffix -#[async_trait] -pub trait DataProcessor: Send + Sync { - async fn process(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_system_missing_version.rs b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_system_missing_version.rs deleted file mode 100644 index 10120fadf..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_system_missing_version.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -#[async_trait] -// Should trigger DE0504 - Client trait `UsersInfoClient` in non-system gear must have a version suffix -pub trait UsersInfoClient: Send + Sync { - async fn get_user(&self) -> Result<(), ()>; -} - -#[async_trait] -// Should trigger DE0504 - Client trait `CalculatorPluginClient` in non-system gear must have a version suffix -pub trait CalculatorPluginClient: Send + Sync { - async fn calculate(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_system_missing_version.stderr b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_system_missing_version.stderr deleted file mode 100644 index 1bcc8bc7e..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_system_missing_version.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error: Client trait `UsersInfoClient` in non-system gear must have a version suffix (DE0504) - --> $DIR/non_system_missing_version.rs:6:1 - | -LL | / pub trait UsersInfoClient: Send + Sync { -LL | | async fn get_user(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `UsersInfoClientV1` to indicate API version - = note: `#[deny(de0504_client_versioning)]` on by default - -error: Client trait `CalculatorPluginClient` in non-system gear must have a version suffix (DE0504) - --> $DIR/non_system_missing_version.rs:12:1 - | -LL | / pub trait CalculatorPluginClient: Send + Sync { -LL | | async fn calculate(&self) -> Result<(), ()>; -LL | | } - | |_^ - | - = help: rename trait to `CalculatorPluginClientV1` to indicate API version - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_system_versioned_good.rs b/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_system_versioned_good.rs deleted file mode 100644 index deb58c9d1..000000000 --- a/tools/dylint_lints/de05_client_layer/de0504_client_versioning/ui/non_system_versioned_good.rs +++ /dev/null @@ -1,22 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; - -// Should not trigger DE0504 - has V1 suffix -#[async_trait] -pub trait UsersInfoClientV1: Send + Sync { - async fn get_user(&self) -> Result<(), ()>; -} - -// Should not trigger DE0504 - has V2 suffix -#[async_trait] -pub trait CalculatorPluginClientV2: Send + Sync { - async fn calculate(&self) -> Result<(), ()>; -} - -// Should not trigger DE0504 - has V1 suffix -#[async_trait] -pub trait SimpleUserSettingsClientV1: Send + Sync { - async fn get_settings(&self) -> Result<(), ()>; -} - -fn main() {} diff --git a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/Cargo.toml b/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/Cargo.toml deleted file mode 100644 index 4f98505b5..000000000 --- a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "de0706_no_direct_sqlx" -version = "0.1.0" -authors = ["Hypernetix"] -description = "Prohibits direct usage of sqlx; use Sea-ORM or SecORM instead (DE0706)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "bad_direct_sqlx" -path = "ui/bad_direct_sqlx.rs" - -[[example]] -name = "bad_sqlx_types" -path = "ui/bad_sqlx_types.rs" - -[[example]] -name = "good_sea_orm" -path = "ui/good_sea_orm.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -sqlx.workspace = true -sea-orm.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/README.md b/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/README.md deleted file mode 100644 index 6c4577b81..000000000 --- a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# DE0706: No Direct sqlx Usage - -## What it does - -Prohibits direct usage of the `sqlx` crate in the codebase. - -## Why is this bad? - -Direct sqlx usage bypasses important architectural layers: -- **Skips security enforcement**: SecureConn and AccessScope are not applied -- **Bypasses query building**: Loses type-safe query construction -- **Inconsistent patterns**: Makes codebase harder to maintain -- **No audit logging**: Loses automatic operation tracking -- **No tenant isolation**: Multi-tenant security controls are bypassed - -## Example - -```rust -// ❌ Bad - direct sqlx usage -use sqlx::PgPool; -use sqlx::query; - -let pool = PgPool::connect(&db_url).await?; -let users = sqlx::query("SELECT * FROM users") - .fetch_all(&pool) - .await?; -``` - -```rust -// ❌ Bad - sqlx query macros -use sqlx::query_as; - -let user = query_as!(User, "SELECT * FROM users WHERE id = $1", id) - .fetch_one(&pool) - .await?; -``` - -Use instead: - -```rust -// ✅ Good - sea-orm with type-safe queries -use sea_orm::{EntityTrait, QueryFilter, ColumnTrait}; -use crate::infra::storage::entity::user::{Entity as UserEntity, Column}; - -let users = UserEntity::find() - .all(&conn) - .await?; - -let user = UserEntity::find() - .filter(Column::Id.eq(id)) - .one(&conn) - .await?; -``` - -```rust -// ✅ Good - SecureConn with access scope -use toolkit_db::secure::SecureEntityExt; -use toolkit_security::AccessScope; - -let users = UserEntity::find() - .secure() // Enable security layer - .scope_with(&scope) // Apply tenant/access control - .all(conn) - .await - .map_err(db_err)?; -``` - -## Configuration - -This lint is configured to **deny** by default. - -It detects: -- `use sqlx::*` imports -- `use sqlx::{...}` nested imports -- `extern crate sqlx` declarations - -## See Also - -- [DE0301](../../de03_domain_layer/de0301_no_infra_in_domain) - No Infrastructure in Domain -- [DE0308](../../de03_domain_layer/de0308_no_http_in_domain) - No HTTP in Domain diff --git a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/src/lib.rs b/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/src/lib.rs deleted file mode 100644 index 02fc5c66a..000000000 --- a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/src/lib.rs +++ /dev/null @@ -1,269 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use lint_utils::{ - is_in_cf_gears_server_path, is_in_contract_module_ast, is_in_toolkit_db_path, - use_tree_to_strings, -}; -use rustc_ast::{Item, ItemKind, Ty, TyKind}; -use rustc_lint::{EarlyLintPass, LintContext}; - -dylint_linting::declare_early_lint! { - /// ### What it does - /// - /// Prohibits direct usage of the `sqlx` crate. Projects should use Sea-ORM - /// or SecORM abstractions instead for database operations. - /// - /// ### Why is this bad? - /// - /// Direct sqlx usage bypasses important architectural layers: - /// - Skips security enforcement (SecureConn, AccessScope) - /// - Bypasses query building abstractions and type safety - /// - Makes it harder to maintain consistent patterns across the codebase - /// - Loses automatic audit logging and tenant isolation - /// - /// ### Known Exclusions - /// - /// This lint does NOT apply to `libs/toolkit-db/` which is the internal - /// wrapper library that provides the Sea-ORM/SecORM abstraction layer. - /// - /// ### Example - /// - /// ```rust,ignore - /// // Bad - direct sqlx usage - /// use sqlx::PgPool; - /// sqlx::query("SELECT * FROM users").fetch_all(&pool).await?; - /// ``` - /// - /// Use instead: - /// - /// ```rust,ignore - /// // Good - use Sea-ORM with SecureConn - /// use sea_orm::EntityTrait; - /// UserEntity::find().secure().scope_with(&scope).all(conn).await?; - /// ``` - pub DE0706_NO_DIRECT_SQLX, - Deny, - "direct sqlx usage is prohibited; use Sea-ORM or SecORM instead (DE0706)" -} - -/// Sqlx crate pattern to detect -const SQLX_PATTERN: &str = "sqlx"; - -/// Check if a path string matches the sqlx crate pattern. -/// Matches "sqlx" exactly or any qualified path starting with "sqlx::" (e.g., "sqlx::PgPool"). -fn is_sqlx_path(path: &str) -> bool { - path == SQLX_PATTERN || path.starts_with("sqlx::") -} - -/// Find any sqlx path in the use tree (handles grouped imports like `use {sqlx::PgPool, other};`) -fn find_sqlx_path(tree: &rustc_ast::UseTree) -> Option { - use_tree_to_strings(tree) - .into_iter() - .find(|path| is_sqlx_path(path)) -} - -/// Recursively check a type AST node for sqlx usage. -/// Handles qualified paths like `sqlx::pool::Pool` in struct fields, -/// function parameters, return types, and type aliases. -fn check_type_for_sqlx(cx: &rustc_lint::EarlyContext<'_>, ty: &Ty) { - match &ty.kind { - TyKind::Path(_, path) => { - let path_str = path - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - if is_sqlx_path(&path_str) { - span_lint_and_then( - cx, - DE0706_NO_DIRECT_SQLX, - ty.span, - format!("direct sqlx type usage detected: `{path_str}` (DE0706)"), - |diag| { - diag.help("use Sea-ORM EntityTrait or SecORM abstractions instead"); - diag.note("sqlx bypasses security enforcement and architectural patterns"); - }, - ); - return; - } - - // Recursively check generic arguments (e.g., Option) - for segment in &path.segments { - if let Some(args) = &segment.args - && let rustc_ast::GenericArgs::AngleBracketed(ref angle_args) = **args - { - for arg in &angle_args.args { - if let rustc_ast::AngleBracketedArg::Arg(rustc_ast::GenericArg::Type( - inner_ty, - )) = arg - { - check_type_for_sqlx(cx, inner_ty); - } - } - } - } - } - TyKind::Ref(_, mut_ty) => { - check_type_for_sqlx(cx, &mut_ty.ty); - } - TyKind::Slice(inner_ty) | TyKind::Array(inner_ty, _) => { - check_type_for_sqlx(cx, inner_ty); - } - TyKind::Ptr(mut_ty) => { - check_type_for_sqlx(cx, &mut_ty.ty); - } - TyKind::Tup(types) => { - for inner_ty in types { - check_type_for_sqlx(cx, inner_ty); - } - } - TyKind::TraitObject(bounds, _) | TyKind::ImplTrait(_, bounds) => { - for bound in bounds { - if let rustc_ast::GenericBound::Trait(trait_ref) = bound { - let path = &trait_ref.trait_ref.path; - let path_str = path - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - if is_sqlx_path(&path_str) { - span_lint_and_then( - cx, - DE0706_NO_DIRECT_SQLX, - ty.span, - format!("direct sqlx trait usage detected: `{path_str}` (DE0706)"), - |diag| { - diag.help("use Sea-ORM EntityTrait or SecORM abstractions instead"); - diag.note( - "sqlx bypasses security enforcement and architectural patterns", - ); - }, - ); - return; - } - } - } - } - _ => {} - } -} - -fn check_use_for_sqlx(cx: &rustc_lint::EarlyContext<'_>, item: &Item) { - let ItemKind::Use(use_tree) = &item.kind else { - return; - }; - - if let Some(path_str) = find_sqlx_path(use_tree) { - span_lint_and_then( - cx, - DE0706_NO_DIRECT_SQLX, - item.span, - format!("direct sqlx import detected: `{}` (DE0706)", path_str), - |diag| { - diag.help("use Sea-ORM EntityTrait or SecORM abstractions instead"); - diag.note("sqlx bypasses security enforcement and architectural patterns"); - }, - ); - } -} - -impl EarlyLintPass for De0706NoDirectSqlx { - fn check_item(&mut self, cx: &rustc_lint::EarlyContext<'_>, item: &Item) { - // Skip libs/toolkit-db/ - this is the internal wrapper library - // that legitimately uses sqlx to provide the abstraction layer - if is_in_toolkit_db_path(cx.sess().source_map(), item.span) { - return; - } - - // Skip apps/cf-gears-example-server/ - it needs sqlx driver linkage workaround - if is_in_cf_gears_server_path(cx.sess().source_map(), item.span) { - return; - } - - // Skip contract/ gears - they may need sqlx types for test fixtures - if is_in_contract_module_ast(cx, item) { - return; - } - - match &item.kind { - // Check use statements for sqlx imports - ItemKind::Use(_) => { - check_use_for_sqlx(cx, item); - } - // Check extern crate declarations - ItemKind::ExternCrate(rename, ident) => { - let is_sqlx = match rename { - Some(sym) => sym.as_str() == SQLX_PATTERN, - None => ident.name.as_str() == SQLX_PATTERN, - }; - - if is_sqlx { - span_lint_and_then( - cx, - DE0706_NO_DIRECT_SQLX, - item.span, - "extern crate sqlx is prohibited (DE0706)", - |diag| { - diag.help("use Sea-ORM EntityTrait or SecORM abstractions instead"); - diag.note( - "sqlx bypasses security enforcement and architectural patterns", - ); - }, - ); - } - } - // Check struct fields for sqlx types - ItemKind::Struct(_, _, variant_data) => { - for field in variant_data.fields() { - check_type_for_sqlx(cx, &field.ty); - } - } - // Check enum variant fields for sqlx types - ItemKind::Enum(_, _, enum_def) => { - for variant in &enum_def.variants { - for field in variant.data.fields() { - check_type_for_sqlx(cx, &field.ty); - } - } - } - // Check function parameter and return types - ItemKind::Fn(fn_item) => { - for param in &fn_item.sig.decl.inputs { - check_type_for_sqlx(cx, ¶m.ty); - } - if let rustc_ast::FnRetTy::Ty(ret_ty) = &fn_item.sig.decl.output { - check_type_for_sqlx(cx, ret_ty); - } - } - // Check type alias targets - ItemKind::TyAlias(ty_alias) => { - if let Some(ty) = &ty_alias.ty { - check_type_for_sqlx(cx, ty); - } - } - _ => {} - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0706", "sqlx"); - } -} diff --git a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_direct_sqlx.rs b/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_direct_sqlx.rs deleted file mode 100644 index 286f19427..000000000 --- a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_direct_sqlx.rs +++ /dev/null @@ -1,13 +0,0 @@ -// Test file for DE0706: No Direct sqlx Usage -// This file demonstrates BAD patterns that should trigger the lint -#![allow(unused_imports, dead_code, clippy::single_component_path_imports)] - -// Should trigger DE0706 - sqlx -use sqlx; - -// Should trigger DE0706 - sqlx -use sqlx::Error; - -fn main() { - // These imports should all be flagged by the lint -} diff --git a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_direct_sqlx.stderr b/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_direct_sqlx.stderr deleted file mode 100644 index 3b3b5ed85..000000000 --- a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_direct_sqlx.stderr +++ /dev/null @@ -1,21 +0,0 @@ -error: direct sqlx import detected: `sqlx` (DE0706) - --> $DIR/bad_direct_sqlx.rs:6:1 - | -LL | use sqlx; - | ^^^^^^^^^ - | - = help: use Sea-ORM EntityTrait or SecORM abstractions instead - = note: sqlx bypasses security enforcement and architectural patterns - = note: `#[deny(de0706_no_direct_sqlx)]` on by default - -error: direct sqlx import detected: `sqlx::Error` (DE0706) - --> $DIR/bad_direct_sqlx.rs:9:1 - | -LL | use sqlx::Error; - | ^^^^^^^^^^^^^^^^ - | - = help: use Sea-ORM EntityTrait or SecORM abstractions instead - = note: sqlx bypasses security enforcement and architectural patterns - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_sqlx_types.rs b/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_sqlx_types.rs deleted file mode 100644 index 69343c9d7..000000000 --- a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_sqlx_types.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Test file for DE0706: No Direct sqlx Usage via type annotations -// This file demonstrates BAD patterns using qualified paths in types -#![allow(unused_imports)] -#![allow(dead_code)] - -struct Database { - // Should trigger DE0706 - sqlx - err: sqlx::Error, -} - -// Should trigger DE0706 - sqlx -fn handle_error(_err: &sqlx::Error) {} - -// Should trigger DE0706 - sqlx -type DbError = sqlx::Error; - -enum DbResult { - // Should trigger DE0706 - sqlx - Err(sqlx::Error), -} - -// Should trigger DE0706 - sqlx -fn nested_generic() -> Option { - None -} - -fn main() {} diff --git a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_sqlx_types.stderr b/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_sqlx_types.stderr deleted file mode 100644 index 192f6ac5b..000000000 --- a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/bad_sqlx_types.stderr +++ /dev/null @@ -1,48 +0,0 @@ -error: direct sqlx type usage detected: `sqlx::Error` (DE0706) - --> $DIR/bad_sqlx_types.rs:8:10 - | -LL | err: sqlx::Error, - | ^^^^^^^^^^^ - | - = help: use Sea-ORM EntityTrait or SecORM abstractions instead - = note: sqlx bypasses security enforcement and architectural patterns - = note: `#[deny(de0706_no_direct_sqlx)]` on by default - -error: direct sqlx type usage detected: `sqlx::Error` (DE0706) - --> $DIR/bad_sqlx_types.rs:12:24 - | -LL | fn handle_error(_err: &sqlx::Error) {} - | ^^^^^^^^^^^ - | - = help: use Sea-ORM EntityTrait or SecORM abstractions instead - = note: sqlx bypasses security enforcement and architectural patterns - -error: direct sqlx type usage detected: `sqlx::Error` (DE0706) - --> $DIR/bad_sqlx_types.rs:15:16 - | -LL | type DbError = sqlx::Error; - | ^^^^^^^^^^^ - | - = help: use Sea-ORM EntityTrait or SecORM abstractions instead - = note: sqlx bypasses security enforcement and architectural patterns - -error: direct sqlx type usage detected: `sqlx::Error` (DE0706) - --> $DIR/bad_sqlx_types.rs:19:9 - | -LL | Err(sqlx::Error), - | ^^^^^^^^^^^ - | - = help: use Sea-ORM EntityTrait or SecORM abstractions instead - = note: sqlx bypasses security enforcement and architectural patterns - -error: direct sqlx type usage detected: `sqlx::Error` (DE0706) - --> $DIR/bad_sqlx_types.rs:23:31 - | -LL | fn nested_generic() -> Option { - | ^^^^^^^^^^^ - | - = help: use Sea-ORM EntityTrait or SecORM abstractions instead - = note: sqlx bypasses security enforcement and architectural patterns - -error: aborting due to 5 previous errors - diff --git a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/good_sea_orm.rs b/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/good_sea_orm.rs deleted file mode 100644 index a8bee09cd..000000000 --- a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/good_sea_orm.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Test file for DE0706: No Direct sqlx Usage -// This file demonstrates GOOD patterns that should NOT trigger the lint -#![allow(unused_imports)] -#![allow(dead_code)] - -// Using sea_orm is allowed -use sea_orm::EntityTrait; - -fn main() { - // Sea-ORM usage is the preferred pattern -} diff --git a/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/good_sea_orm.stderr b/tools/dylint_lints/de07_security/de0706_no_direct_sqlx/ui/good_sea_orm.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de07_security/de0707_drop_zeroize/Cargo.toml b/tools/dylint_lints/de07_security/de0707_drop_zeroize/Cargo.toml deleted file mode 100644 index 4e88d470c..000000000 --- a/tools/dylint_lints/de07_security/de0707_drop_zeroize/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -# Created: 2026-03-13 by Constructor Tech -# Updated: 2026-03-17 by Constructor Tech -[package] -name = "de0707_drop_zeroize" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Manual byte-zeroing in Drop may be optimized away; use the zeroize crate instead (DE0707)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "bad_drop_zeroing" -path = "ui/bad_drop_zeroing.rs" - -[[example]] -name = "good_no_zeroing" -path = "ui/good_no_zeroing.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de07_security/de0707_drop_zeroize/README.md b/tools/dylint_lints/de07_security/de0707_drop_zeroize/README.md deleted file mode 100644 index c5e8b4123..000000000 --- a/tools/dylint_lints/de07_security/de0707_drop_zeroize/README.md +++ /dev/null @@ -1,79 +0,0 @@ -Created: 2026-03-18 by Constructor Tech -Updated: 2026-03-18 by Constructor Tech - -# DE0707: No Manual Byte-Zeroing in Drop - -## What it does - -Detects manual byte-zeroing inside `impl Drop` implementations: - -- `*ptr = 0` (deref-assign to zero) -- `slice.fill(0)` or `vec.fill(0)` -- `std::ptr::write_bytes(ptr, 0, len)` - -These patterns may be **silently optimized away** by the LLVM dead-store elimination pass. - -## Why is this bad? - -The LLVM optimizer can legally remove writes to memory that are never read again before the memory is freed. Manual zeroing in `Drop::drop` is almost always a dead store from the optimizer's perspective. Sensitive data (keys, tokens, passwords) may remain in memory after the struct is dropped. - -The `zeroize` and `secrecy` crates use compiler memory fences to prevent this optimization. - -## Example - -### Bad - -```rust -struct SecretKey { - data: Vec, -} - -impl Drop for SecretKey { - fn drop(&mut self) { - self.data.fill(0); // LLVM may remove this! - } -} -``` - -```rust -impl Drop for RawBuffer { - fn drop(&mut self) { - unsafe { - std::ptr::write_bytes(self.data, 0, self.len); // May be optimized away - } - } -} -``` - -### Good - -```rust -use zeroize::Zeroize; - -impl Drop for SecretKey { - fn drop(&mut self) { - self.data.zeroize(); // Uses compiler fence; won't be optimized away - } -} -``` - -```rust -use secrecy::{ExposeSecret, SecretBox}; - -pub type SecretKey = SecretBox>; // Zeroization built-in -``` - -## Limitations - -- Only inspects the immediate body of `Drop::drop`; zeroing delegated to a helper function is not detected. -- Only flags when the target type is `u8` (byte buffers); `fill(255)` or other values are not flagged. -- Zeroing outside `Drop` (e.g. in a `reset()` method) is allowed. - -## Configuration - -This lint is configured to **deny** by default. - -## See Also - -- [zeroize crate](https://crates.io/crates/zeroize) -- [secrecy crate](https://crates.io/crates/secrecy) diff --git a/tools/dylint_lints/de07_security/de0707_drop_zeroize/src/lib.rs b/tools/dylint_lints/de07_security/de0707_drop_zeroize/src/lib.rs deleted file mode 100644 index 51749ab82..000000000 --- a/tools/dylint_lints/de07_security/de0707_drop_zeroize/src/lib.rs +++ /dev/null @@ -1,300 +0,0 @@ -// Created: 2026-03-13 by Constructor Tech -// Updated: 2026-03-17 by Constructor Tech -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; -extern crate rustc_hir; -extern crate rustc_middle; -extern crate rustc_span; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_hir::def_id::DefId; -use rustc_hir::{self as hir, Expr, ExprKind, ImplItemKind, ItemKind}; -use rustc_lint::{LateContext, LateLintPass}; -use rustc_middle::ty::{Ty, TypeckResults}; -use rustc_span::symbol::Symbol; - -/// Pre-interned symbols used in `is_ptr_write_bytes`. Initialized once at first call -/// rather than re-interning on every lint invocation. -static SYM_CORE: std::sync::LazyLock = std::sync::LazyLock::new(|| Symbol::intern("core")); -static SYM_STD: std::sync::LazyLock = std::sync::LazyLock::new(|| Symbol::intern("std")); -static SYM_WRITE_BYTES: std::sync::LazyLock = - std::sync::LazyLock::new(|| Symbol::intern("write_bytes")); - -dylint_linting::declare_late_lint! { - /// ### What it does - /// - /// Detects manual byte-zeroing (`*b = 0` or `.fill(0)`) inside `impl Drop` - /// implementations, which the LLVM optimizer may legally eliminate. - /// - /// ### Why is this bad? - /// - /// The LLVM optimizer performs dead-store elimination: if it can prove that - /// a write to memory is never read again before the memory is freed, it may - /// remove the write entirely. Manual zeroing in `Drop::drop` is almost always - /// a dead store from the optimizer's perspective. The `secrecy` and `zeroize` - /// crates work around this using a compiler memory fence to prevent removal. - /// - /// ### Example - /// - /// ```rust,ignore - /// // Bad - may be silently optimized away - /// impl Drop for SecretKey { - /// fn drop(&mut self) { - /// self.data.fill(0); // LLVM may remove this! - /// } - /// } - /// ``` - /// - /// Use instead: - /// - /// ```rust,ignore - /// // Good (preferred for secrets) - secrecy provides zeroization + redacted Debug - /// use secrecy::{ExposeSecret, SecretBox}; - /// pub type SecretKey = SecretBox>; - /// - /// // Good (alternative) - zeroize when only wiping is needed - /// use zeroize::Zeroize; - /// impl Drop for SecretKey { - /// fn drop(&mut self) { - /// self.data.zeroize(); - /// } - /// } - /// ``` - /// - /// ### Limitations - /// - /// This lint only inspects the immediate body of `Drop::drop` and does **not** - /// perform interprocedural analysis. Zeroing delegated to a helper function will - /// not be detected: - /// - /// ```rust,ignore - /// fn secure_erase(buf: &mut Vec) { - /// buf.fill(0); // not flagged — outside Drop::drop - /// } - /// - /// impl Drop for SecretKey { - /// fn drop(&mut self) { - /// secure_erase(&mut self.data); // not flagged — indirect call - /// } - /// } - /// ``` - /// - /// The helper call itself escapes the lint, but the underlying zeroing is still - /// at risk: LLVM may inline `secure_erase` and then eliminate the dead store. - /// Use `zeroize` or `secrecy` in all cases to ensure the compiler fence is in place. - pub DE0707_DROP_ZEROIZE, - Deny, - "manual byte-zeroing in Drop may be optimized away; use `secrecy::SecretBox` or the `zeroize` crate (DE0707)" -} - -/// Returns true if `expr` is the integer literal `0` (with any type suffix, e.g. `0u8`). -fn is_zero_literal(expr: &Expr<'_>) -> bool { - if let ExprKind::Lit(lit) = expr.kind - && let rustc_ast::ast::LitKind::Int(n, _) = lit.node - { - return n.get() == 0; - } - false -} - -/// Returns true if `ty` is a raw pointer or reference to `u8` (`*mut u8`, `*const u8`, -/// `&u8`, or `&mut u8`). Used to validate `*ptr = 0` deref-assign patterns. -fn is_u8_ptr_or_ref(ty: Ty<'_>) -> bool { - let pointee = match ty.kind() { - rustc_middle::ty::TyKind::RawPtr(pointee, _) => pointee, - rustc_middle::ty::TyKind::Ref(_, pointee, _) => pointee, - _ => return false, - }; - matches!( - pointee.kind(), - rustc_middle::ty::TyKind::Uint(rustc_middle::ty::UintTy::U8) - ) -} - -/// Returns true if the adjusted type (after auto-deref coercions) has `u8` as its element -/// type — i.e., `[u8]` or `[u8; N]`. Used to validate `slice.fill(0)` patterns. -fn has_u8_element(ty: Ty<'_>) -> bool { - // peel_refs strips &/&mut wrappers left over after auto-deref - let ty = ty.peel_refs(); - match ty.kind() { - rustc_middle::ty::TyKind::Slice(elem) | rustc_middle::ty::TyKind::Array(elem, _) => { - matches!( - elem.kind(), - rustc_middle::ty::TyKind::Uint(rustc_middle::ty::UintTy::U8) - ) - } - _ => false, - } -} - -/// Returns true if `def_id` resolves to `core::ptr::write_bytes` or its intrinsic definition, -/// guarding against user-defined functions with the same name. -/// -/// Checks the crate origin (must be `core` or `std`) and the item name to avoid matching -/// user-defined `write_bytes` helpers. -fn is_ptr_write_bytes(cx: &LateContext<'_>, def_id: DefId) -> bool { - let krate = cx.tcx.crate_name(def_id.krate); - if krate != *SYM_CORE && krate != *SYM_STD { - return false; - } - cx.tcx.item_name(def_id) == *SYM_WRITE_BYTES -} - -struct ZeroingVisitor<'tcx, 'cx> { - cx: &'cx LateContext<'tcx>, - /// Typeck results for the `fn drop` body being walked. - typeck: &'tcx TypeckResults<'tcx>, -} - -impl<'tcx> hir::intravisit::Visitor<'tcx> for ZeroingVisitor<'tcx, '_> { - fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { - match expr.kind { - // Pattern: *buf = 0 (deref-assign to zero). - // Only flagged when the inner expression is a `u8` pointer/reference. - ExprKind::Assign(lhs, rhs, _) => { - if let ExprKind::Unary(hir::UnOp::Deref, inner) = lhs.kind - && is_zero_literal(rhs) - { - let inner_ty = self.typeck.expr_ty(inner); - if is_u8_ptr_or_ref(inner_ty) { - span_lint_and_then( - self.cx, - DE0707_DROP_ZEROIZE, - expr.span, - "manual byte-zeroing in `Drop::drop` may be eliminated by the optimizer (DE0707)", - |diag| { - diag.help( - "use `secrecy::SecretBox` or `zeroize`: `.zeroize()` / `#[derive(ZeroizeOnDrop)]`", - ); - diag.note( - "LLVM dead-store elimination can legally remove writes that are never read; `zeroize` uses a compiler fence to prevent this", - ); - }, - ); - } - } - } - // Pattern: slice.fill(0). - // Only flagged when the method resolves to core/std (not a custom `fill` method) - // and the auto-deref'd receiver type is a `[u8]` or `[u8; N]` byte slice. - ExprKind::MethodCall(seg, recv, args, _) => { - if seg.ident.name.as_str() == "fill" - && let Some(arg) = args.first() - && is_zero_literal(arg) - { - let method_in_std = - self.typeck - .type_dependent_def_id(expr.hir_id) - .is_some_and(|did| { - let krate = self.cx.tcx.crate_name(did.krate); - krate == *SYM_CORE || krate == *SYM_STD - }); - // Use adjusted type so Vec auto-derefs to [u8] - let recv_ty = self.typeck.expr_ty_adjusted(recv); - if method_in_std && has_u8_element(recv_ty) { - span_lint_and_then( - self.cx, - DE0707_DROP_ZEROIZE, - expr.span, - "manual byte-zeroing in `Drop::drop` may be eliminated by the optimizer (DE0707)", - |diag| { - diag.help( - "use `secrecy::SecretBox` or `zeroize`: `.zeroize()` / `#[derive(ZeroizeOnDrop)]`", - ); - diag.note( - "LLVM dead-store elimination can legally remove writes that are never read; `zeroize` uses a compiler fence to prevent this", - ); - }, - ); - } - } - } - // Pattern: ptr::write_bytes(ptr, 0, len). - // Only flagged when the function resolves to `core::ptr::write_bytes`, - // not a user-defined helper with the same name. - ExprKind::Call(func, args) => { - if args.len() >= 2 - && let Some(fill_byte) = args.get(1) - && is_zero_literal(fill_byte) - && let ExprKind::Path(qpath) = &func.kind - && let Some(def_id) = self.cx.qpath_res(qpath, func.hir_id).opt_def_id() - && is_ptr_write_bytes(self.cx, def_id) - { - span_lint_and_then( - self.cx, - DE0707_DROP_ZEROIZE, - expr.span, - "manual byte-zeroing in `Drop::drop` may be eliminated by the optimizer (DE0707)", - |diag| { - diag.help( - "use `secrecy::SecretBox` or `zeroize`: `.zeroize()` / `#[derive(ZeroizeOnDrop)]`", - ); - diag.note( - "LLVM dead-store elimination can legally remove writes that are never read; `zeroize` uses a compiler fence to prevent this", - ); - }, - ); - } - } - _ => {} - } - // Always recurse so nested blocks (for loops, unsafe blocks, closures) are visited. - hir::intravisit::walk_expr(self, expr); - } -} - -impl<'tcx> LateLintPass<'tcx> for De0707DropZeroize { - fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'tcx>) { - let ItemKind::Impl(impl_block) = item.kind else { - return; - }; - - // Only examine `impl Drop for X` blocks — resolved semantically via lang items - // to prevent false positives from a custom `Drop` trait with the same name. - let Some(_) = impl_block.of_trait else { - return; - }; - let impl_def_id = item.owner_id.def_id; - let impl_trait_ref = cx.tcx.impl_trait_ref(impl_def_id).instantiate_identity(); - let Some(drop_trait_did) = cx.tcx.lang_items().drop_trait() else { - return; - }; - if impl_trait_ref.def_id != drop_trait_did { - return; - } - - // Walk every `fn drop` body looking for byte-zeroing patterns. - for item_ref in impl_block.items { - let node = cx.tcx.hir_node_by_def_id(item_ref.owner_id.def_id); - let hir::Node::ImplItem(impl_item) = node else { - continue; - }; - if impl_item.ident.name.as_str() != "drop" { - continue; - } - let ImplItemKind::Fn(_, body_id) = impl_item.kind else { - continue; - }; - let body = cx.tcx.hir_body(body_id); - let typeck = cx.tcx.typeck(item_ref.owner_id.def_id); - let mut visitor = ZeroingVisitor { cx, typeck }; - hir::intravisit::walk_expr(&mut visitor, body.value); - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0707", "manual zeroing"); - } -} diff --git a/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/bad_drop_zeroing.rs b/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/bad_drop_zeroing.rs deleted file mode 100644 index 3bc4d0c88..000000000 --- a/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/bad_drop_zeroing.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Created: 2026-03-13 by Constructor Tech -// Updated: 2026-03-13 by Constructor Tech -#![allow(dead_code, unused_variables, unsafe_code)] - -struct SecretKey { - data: Vec, -} - -impl Drop for SecretKey { - fn drop(&mut self) { - // Should trigger DE0707 - manual zeroing - self.data.fill(0); - } -} - -struct SecretBytes { - buf: [u8; 32], -} - -impl Drop for SecretBytes { - fn drop(&mut self) { - for b in self.buf.iter_mut() { - // Should trigger DE0707 - manual zeroing - *b = 0; - } - } -} - -struct RawBuffer { - data: *mut u8, - len: usize, -} - -impl Drop for RawBuffer { - fn drop(&mut self) { - unsafe { - // Should trigger DE0707 - manual zeroing - std::ptr::write_bytes(self.data, 0, self.len); - } - } -} - -fn main() {} diff --git a/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/bad_drop_zeroing.stderr b/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/bad_drop_zeroing.stderr deleted file mode 100644 index 9d3803b7d..000000000 --- a/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/bad_drop_zeroing.stderr +++ /dev/null @@ -1,30 +0,0 @@ -error: manual byte-zeroing in `Drop::drop` may be eliminated by the optimizer (DE0707) - --> $DIR/bad_drop_zeroing.rs:12:9 - | -LL | self.data.fill(0); - | ^^^^^^^^^^^^^^^^^ - | - = help: use `secrecy::SecretBox` or `zeroize`: `.zeroize()` / `#[derive(ZeroizeOnDrop)]` - = note: LLVM dead-store elimination can legally remove writes that are never read; `zeroize` uses a compiler fence to prevent this - = note: `#[deny(de0707_drop_zeroize)]` on by default - -error: manual byte-zeroing in `Drop::drop` may be eliminated by the optimizer (DE0707) - --> $DIR/bad_drop_zeroing.rs:24:13 - | -LL | *b = 0; - | ^^^^^^ - | - = help: use `secrecy::SecretBox` or `zeroize`: `.zeroize()` / `#[derive(ZeroizeOnDrop)]` - = note: LLVM dead-store elimination can legally remove writes that are never read; `zeroize` uses a compiler fence to prevent this - -error: manual byte-zeroing in `Drop::drop` may be eliminated by the optimizer (DE0707) - --> $DIR/bad_drop_zeroing.rs:38:13 - | -LL | std::ptr::write_bytes(self.data, 0, self.len); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: use `secrecy::SecretBox` or `zeroize`: `.zeroize()` / `#[derive(ZeroizeOnDrop)]` - = note: LLVM dead-store elimination can legally remove writes that are never read; `zeroize` uses a compiler fence to prevent this - -error: aborting due to 3 previous errors - diff --git a/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/good_no_zeroing.rs b/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/good_no_zeroing.rs deleted file mode 100644 index e4e89a0c3..000000000 --- a/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/good_no_zeroing.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Created: 2026-03-13 by Constructor Tech -// Updated: 2026-03-13 by Constructor Tech -#![allow(dead_code)] - -struct SecretKey { - data: Vec, -} - -// Good - Drop that does NOT manually zero bytes -impl Drop for SecretKey { - fn drop(&mut self) { - // No manual zeroing; would use zeroize crate in real code - let _ = self.data.len(); - } -} - -// Good - zeroing outside of Drop is fine (not a security issue) -fn clear_buffer(buf: &mut [u8]) { - buf.fill(0); -} - -// Good - Drop with unrelated mutation (not zeroing) -struct Timer { - id: u32, -} - -impl Drop for Timer { - fn drop(&mut self) { - self.id = 999; - } -} - -// Good - non-Drop impl with fill(0) is not flagged -struct Buffer { - data: Vec, -} - -impl Buffer { - fn reset(&mut self) { - self.data.fill(0); - } -} - -// Good - fill(255) is not zeroing; only fill(0) is flagged -struct SentinelBuffer { - data: Vec, -} - -impl Drop for SentinelBuffer { - fn drop(&mut self) { - self.data.fill(255); // non-zero fill — not flagged - } -} - -// Good - Drop calls a helper function (helper may zero, but the Drop body itself doesn't) -struct ManagedSecret { - data: Vec, -} - -fn secure_erase(buf: &mut [u8]) { - buf.fill(0); // outside Drop — not flagged -} - -impl Drop for ManagedSecret { - fn drop(&mut self) { - secure_erase(&mut self.data); // indirect call — not flagged - } -} - -fn main() {} diff --git a/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/good_no_zeroing.stderr b/tools/dylint_lints/de07_security/de0707_drop_zeroize/ui/good_no_zeroing.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/Cargo.toml b/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/Cargo.toml deleted file mode 100644 index b67a51e9a..000000000 --- a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "de0708_no_non_fips_hasher" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Prohibits sha2/sha1/md5 imports outside an explicit allow-list (DE0708)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "bad_sha2_import" -path = "ui/bad_sha2_import.rs" - -[[example]] -name = "good_allowed_path" -path = "ui/good_allowed_path.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -sha2.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/README.md b/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/README.md deleted file mode 100644 index 08f20ce07..000000000 --- a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# DE0708: No Non-FIPS Hasher Imports - -Prohibits imports of non-FIPS-validated hash crates (`sha2`, `sha1`, `md5`) outside an explicit allow-list. - -## Why - -These crates use pure-Rust RustCrypto implementations that are not FIPS-validated. They are Phase B entries in `deny-fips.toml` (present in the dependency graph via transitives). This lint prevents new *direct* usage from creeping in without review. - -## Allow-list - -Currently empty — all direct call sites have been replaced. To add an exception, update `is_in_hasher_allow_list()` in `lint_utils/src/lib.rs`. - -## Example - -```rust -// Bad — triggers DE0708 -use sha2::{Digest, Sha256}; - -// Good — route through the validated crypto provider, -// or add to the allow-list with a SECURITY.md §9 disclaimer. -``` - -## References - -- `docs/security/SECURITY.md` §9 — FIPS dependency policy and non-crypto disclaimers -- `deny-fips.toml` — Phase A/B dependency bans diff --git a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/src/lib.rs b/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/src/lib.rs deleted file mode 100644 index 75ab3e636..000000000 --- a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/src/lib.rs +++ /dev/null @@ -1,102 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use lint_utils::{is_in_hasher_allow_list, use_tree_to_strings}; -use rustc_ast::{Item, ItemKind}; -use rustc_lint::{EarlyLintPass, LintContext}; - -dylint_linting::declare_early_lint! { - /// ### What it does - /// - /// Prohibits imports of non-FIPS-validated hash crates (`sha2`, `sha1`, `md5`) - /// outside an explicit allow-list of source files. - /// - /// ### Why is this bad? - /// - /// These crates use pure-Rust RustCrypto implementations that are not - /// FIPS-validated. While they are Phase B entries in `deny-fips.toml` - /// (present in the dependency graph via transitives), new *direct* usage - /// should not be introduced without review. - /// - /// ### Known Exclusions - /// - /// None — all direct call sites have been replaced. The allow-list in - /// `lint_utils::is_in_hasher_allow_list` is empty but can be extended - /// if a legitimate usage is introduced. - /// - /// ### Example - /// - /// ```rust,ignore - /// // Bad — direct sha2 import in application code - /// use sha2::{Digest, Sha256}; - /// ``` - /// - /// Use instead: request a review and add the file to the DE0708 allow-list - /// in `lint_utils::is_in_hasher_allow_list` if the usage is non-cryptographic, - /// or route the operation through the validated crypto provider. - pub DE0708_NO_NON_FIPS_HASHER, - Deny, - "non-FIPS-validated hasher import (sha2/sha1/md5) outside allow-list (DE0708)" -} - -/// Crate names to detect (as they appear in `use` statements — hyphens become underscores). -const BANNED_CRATES: &[&str] = &["sha2", "sha1", "md5"]; - -/// Check if a resolved use-path matches one of the banned hasher crates. -fn is_banned_path(path: &str) -> bool { - BANNED_CRATES - .iter() - .any(|crate_name| path == *crate_name || path.starts_with(&format!("{}::", crate_name))) -} - -/// Find the first banned path in a use tree (handles grouped imports). -fn find_banned_path(tree: &rustc_ast::UseTree) -> Option { - use_tree_to_strings(tree) - .into_iter() - .find(|path| is_banned_path(path)) -} - -impl EarlyLintPass for De0708NoNonFipsHasher { - fn check_item(&mut self, cx: &rustc_lint::EarlyContext<'_>, item: &Item) { - // Skip files in the allow-list - if is_in_hasher_allow_list(cx.sess().source_map(), item.span) { - return; - } - - let ItemKind::Use(use_tree) = &item.kind else { - return; - }; - - if let Some(path_str) = find_banned_path(use_tree) { - span_lint_and_then( - cx, - DE0708_NO_NON_FIPS_HASHER, - item.span, - format!("non-FIPS-validated hasher import detected: `{path_str}` (DE0708)"), - |diag| { - diag.help( - "these crates use pure-Rust RustCrypto; add to the DE0708 allow-list if usage is non-cryptographic", - ); - diag.note("see docs/security/SECURITY.md §9 for the FIPS dependency policy"); - }, - ); - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0708", "non-FIPS hasher"); - } -} diff --git a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/bad_sha2_import.rs b/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/bad_sha2_import.rs deleted file mode 100644 index 727c6ee20..000000000 --- a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/bad_sha2_import.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![allow(unused_imports, dead_code)] - -// Should trigger DE0708 - non-FIPS hasher -use sha2::{Digest, Sha256}; - -fn main() {} diff --git a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/bad_sha2_import.stderr b/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/bad_sha2_import.stderr deleted file mode 100644 index 6a6fff7ac..000000000 --- a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/bad_sha2_import.stderr +++ /dev/null @@ -1,12 +0,0 @@ -error: non-FIPS-validated hasher import detected: `sha2::Digest` (DE0708) - --> $DIR/bad_sha2_import.rs:4:1 - | -LL | use sha2::{Digest, Sha256}; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: these crates use pure-Rust RustCrypto; add to the DE0708 allow-list if usage is non-cryptographic - = note: see docs/security/SECURITY.md §9 for the FIPS dependency policy - = note: `#[deny(de0708_no_non_fips_hasher)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/good_allowed_path.rs b/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/good_allowed_path.rs deleted file mode 100644 index 9bb5118b0..000000000 --- a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/good_allowed_path.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![allow(unused_imports, dead_code)] - -// Should not trigger DE0708 - non-FIPS hasher -use std::hash::{DefaultHasher, Hasher}; - -fn main() {} diff --git a/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/good_allowed_path.stderr b/tools/dylint_lints/de07_security/de0708_no_non_fips_hasher/ui/good_allowed_path.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/.gitignore b/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/Cargo.toml b/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/Cargo.toml deleted file mode 100644 index 2ba5e1edf..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "de0801_api_endpoint_version" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "DE0801: API endpoints must follow /{service-name}/v{N}/{resource} format" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "valid_endpoints" -path = "ui/valid_endpoints.rs" - -[[example]] -name = "invalid_endpoints" -path = "ui/invalid_endpoints.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/README.md b/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/README.md deleted file mode 100644 index 74d227a6c..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# DE0801: API Endpoint Must Have Service Name and Version - -### What it does - -Checks that all API endpoints follow the format `/{service-name}/v{N}/{resource}` where: -- `{service-name}` is in kebab-case (lowercase letters, numbers, dashes) -- `v{N}` is a version number (v1, v2, v10, etc.) -- `{resource}` is the resource path in kebab-case - -### Why is this bad? - -Consistent API endpoint structure is essential for: -- **Service identification**: Clearly identify which microservice owns an endpoint -- **API versioning**: Support multiple API versions simultaneously -- **Discoverability**: Predictable URL patterns for API consumers -- **Routing**: Easier to implement API gateways and load balancers -- **Documentation**: Clear organization in API documentation - -Without this structure: -- Unclear which service owns which endpoints -- Difficult to version APIs without breaking changes -- Inconsistent API design across services -- Poor developer experience - -### Validation Rules - -1. **Service name** (first segment): - - Must be kebab-case (lowercase letters, numbers, dashes) - - Cannot start or end with a dash - - Examples: `user-service`, `api-v2`, `product-catalog` - -2. **Version** (second segment): - - Must be `v` followed by digits only - - Examples: `v1`, `v2`, `v10` - - Not allowed: `V1`, `version1`, `v1.0` - -3. **Resource** (third segment onwards): - - Must be kebab-case - - Path parameters like `{id}` are allowed - - Examples: `users`, `user-profiles`, `orders/{order-id}` - -### Example - -```rust -// ❌ Bad - various violations -use toolkit::api::OperationBuilder; - -// Missing service name and version -OperationBuilder::get("/users"); - -// Missing service name (version first) -OperationBuilder::get("/v1/products"); - -// Service name not kebab-case (has underscore) -OperationBuilder::post("/some_service/v1/products"); - -// Uppercase letters in service name -OperationBuilder::get("/SomeService/v1/users"); - -// Uppercase version -OperationBuilder::get("/my-service/V1/products"); - -// Resource name not kebab-case -OperationBuilder::get("/my-service/v1/Products"); -``` - -Use instead: - -```rust -// ✅ Good - correct format -use toolkit::api::OperationBuilder; - -// Basic endpoint -OperationBuilder::get("/my-service/v1/users") - .handler(list_users) - .build(); - -// With path parameters -OperationBuilder::get("/my-service/v1/users/{id}") - .handler(get_user); - -// With sub-resources -OperationBuilder::post("/user-service/v2/users/{id}/profile") - .handler(update_profile); - -// Different versions coexist -OperationBuilder::get("/api-gateway/v1/health"); -OperationBuilder::get("/api-gateway/v2/health"); -``` - -### Configuration - -This lint is configured to **deny** by default. - -It checks all calls to `OperationBuilder` HTTP methods (get, post, put, delete, patch). - -### See Also - -- REST API best practices -- Semantic versioning -- API gateway routing patterns diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/src/lib.rs b/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/src/lib.rs deleted file mode 100644 index fddda28a1..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/src/lib.rs +++ /dev/null @@ -1,307 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; -extern crate rustc_hir; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_hir::{Expr, ExprKind}; -use rustc_lint::{LateContext, LateLintPass}; - -dylint_linting::declare_late_lint! { - /// ### What it does - /// - /// Checks that API endpoints follow the format `/{service-name}/v{N}/{resource}`. - /// - /// ### Why is this bad? - /// - /// Consistent API structure ensures proper versioning and organization. - /// Service names help identify different microservices/gears, and versions - /// allow for API evolution without breaking changes. - /// - /// ### Example - /// - /// ```rust,ignore - /// // Bad - no service name or version - /// OperationBuilder::get("/users") - /// - /// // Bad - no service name before version - /// OperationBuilder::get("/v1/users") - /// - /// // Bad - service name uses underscore - /// OperationBuilder::post("/some_service/v1/users") - /// ``` - /// - /// Use instead: - /// - /// ```rust,ignore - /// // Good - correct format - /// OperationBuilder::get("/my-service/v1/users") - /// - /// // Good - with path parameters - /// OperationBuilder::get("/my-service/v1/users/{id}") - /// - /// // Good - with sub-resources - /// OperationBuilder::post("/my-service/v2/users/{id}/profile") - /// ``` - pub DE0801_API_ENDPOINT_VERSION, - Deny, - "API endpoints must follow /{service-name}/v{N}/{resource} format (DE0801)" -} - -impl<'tcx> LateLintPass<'tcx> for De0801ApiEndpointVersion { - fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { - if let ExprKind::Call(func, args) = &expr.kind - && let ExprKind::Path(qpath) = &func.kind - { - let is_operation_builder_http_method = match qpath { - rustc_hir::QPath::TypeRelative(ty, segment) => { - let method_name = segment.ident.name.as_str(); - let is_http_method = HTTP_METHODS.contains(&method_name); - - if is_http_method { - type_contains_operation_builder(ty) - } else { - false - } - } - rustc_hir::QPath::Resolved(_, path) => { - let segments: Vec<&str> = path - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect(); - - if segments.len() >= 2 { - let has_op_builder = segments.contains(&"OperationBuilder"); - let last_is_http_method = segments - .last() - .map(|s| HTTP_METHODS.contains(s)) - .unwrap_or(false); - has_op_builder && last_is_http_method - } else { - false - } - } - }; - - if is_operation_builder_http_method && let Some(path_arg) = args.first() { - check_path_argument(cx, path_arg); - } - } - } -} - -/// Result of path validation -#[derive(Debug, PartialEq)] -enum PathValidationError { - /// No service name before version - MissingServiceName, - /// Service name is not in kebab-case - InvalidServiceName(String), - /// Missing version segment - MissingVersion, - /// Invalid version format (not v{N}) - InvalidVersionFormat(String), - /// Missing resource after version - MissingResource, - /// Resource or sub-resource is not in kebab-case - InvalidResourceName(String), -} - -/// Check if a segment is a valid kebab-case identifier -fn is_valid_kebab_case(segment: &str) -> bool { - if segment.is_empty() { - return false; - } - - if segment.starts_with('-') || segment.ends_with('-') { - return false; - } - - segment - .chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') -} - -/// Check if a segment is a valid version (v{N}) -fn is_valid_version(segment: &str) -> bool { - if !segment.starts_with('v') { - return false; - } - - let after_v = &segment[1..]; - if after_v.is_empty() { - return false; - } - - after_v.chars().all(|c| c.is_ascii_digit()) -} - -/// Check if a segment is a path parameter like {id} -fn is_path_param(segment: &str) -> bool { - segment.starts_with('{') && segment.ends_with('}') -} - -/// Validate that a path follows the format: /{service-name}/v{N}/{resource} -fn validate_api_path(path: &str) -> Result<(), PathValidationError> { - let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); - - if segments.is_empty() { - return Err(PathValidationError::MissingServiceName); - } - - // First segment must be service name (not a version) - let service_name = segments[0]; - if is_valid_version(service_name) { - return Err(PathValidationError::MissingServiceName); - } - - if !is_valid_kebab_case(service_name) { - return Err(PathValidationError::InvalidServiceName( - service_name.to_string(), - )); - } - - // Second segment must be version - if segments.len() < 2 { - return Err(PathValidationError::MissingVersion); - } - let version = segments[1]; - if !is_valid_version(version) { - return Err(PathValidationError::InvalidVersionFormat( - version.to_string(), - )); - } - - // Must have at least one resource after version - if segments.len() < 3 { - return Err(PathValidationError::MissingResource); - } - - // Validate all remaining segments (resources and sub-resources) - for segment in &segments[2..] { - if is_path_param(segment) { - continue; - } - if !is_valid_kebab_case(segment) { - return Err(PathValidationError::InvalidResourceName( - (*segment).to_string(), - )); - } - } - - Ok(()) -} - -/// HTTP method names that OperationBuilder uses -const HTTP_METHODS: &[&str] = &["get", "post", "put", "delete", "patch"]; - -/// Recursively check if a type contains "OperationBuilder" -fn type_contains_operation_builder(ty: &rustc_hir::Ty<'_>) -> bool { - match &ty.kind { - rustc_hir::TyKind::Path(qpath) => match qpath { - rustc_hir::QPath::Resolved(_, path) => path - .segments - .iter() - .any(|seg| seg.ident.name.as_str() == "OperationBuilder"), - rustc_hir::QPath::TypeRelative(inner_ty, segment) => { - segment.ident.name.as_str() == "OperationBuilder" - || type_contains_operation_builder(inner_ty) - } - }, - _ => false, - } -} - -fn check_path_argument<'tcx>(cx: &LateContext<'tcx>, path_arg: &'tcx Expr<'tcx>) { - if let ExprKind::Lit(lit) = &path_arg.kind - && let rustc_ast::ast::LitKind::Str(sym, _) = lit.node - { - let path = sym.as_str(); - - if let Err(err) = validate_api_path(path) { - let (message, help, note) = match err { - PathValidationError::MissingServiceName => ( - format!( - "API endpoint `{}` is missing a service name before version (DE0801)", - path - ), - "use format: /{service-name}/v{N}/{resource}".to_string(), - "service name must come before version segment".to_string(), - ), - PathValidationError::InvalidServiceName(name) => ( - format!( - "API endpoint `{}` has invalid service name `{}` (DE0801)", - path, name - ), - "service name must be kebab-case (lowercase letters, numbers, dashes)" - .to_string(), - "service name must not start or end with a dash".to_string(), - ), - PathValidationError::MissingVersion => ( - format!( - "API endpoint `{}` is missing a version segment (DE0801)", - path - ), - "add version as second segment: /{service-name}/v{N}/{resource}".to_string(), - "version must be v1, v2, etc.".to_string(), - ), - PathValidationError::InvalidVersionFormat(ver) => ( - format!( - "API endpoint `{}` has invalid version format `{}` (DE0801)", - path, ver - ), - "version must be lowercase 'v' followed by digits (v1, v2, v10)".to_string(), - "semver (v1.0) and uppercase (V1) are not allowed".to_string(), - ), - PathValidationError::MissingResource => ( - format!( - "API endpoint `{}` is missing a resource after version (DE0801)", - path - ), - "add resource: /{service-name}/v{N}/{resource}".to_string(), - "at least one resource segment is required after version".to_string(), - ), - PathValidationError::InvalidResourceName(name) => ( - format!( - "API endpoint `{}` has invalid resource name `{}` (DE0801)", - path, name - ), - "resource names must be kebab-case (lowercase letters, numbers, dashes)" - .to_string(), - "resource names must not start or end with a dash".to_string(), - ), - }; - - span_lint_and_then( - cx, - DE0801_API_ENDPOINT_VERSION, - path_arg.span, - message, - |diag| { - diag.help(help); - diag.note(note); - }, - ); - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0801", - "API endpoint version", - ); - } -} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/invalid_endpoints.rs b/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/invalid_endpoints.rs deleted file mode 100644 index 5ad02876e..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/invalid_endpoints.rs +++ /dev/null @@ -1,91 +0,0 @@ -#![allow(dead_code)] - -pub struct OperationBuilder; - -impl OperationBuilder { - pub fn get(_path: &str) -> Self { - Self - } - pub fn post(_path: &str) -> Self { - Self - } - pub fn put(_path: &str) -> Self { - Self - } - pub fn delete(_path: &str) -> Self { - Self - } - pub fn patch(_path: &str) -> Self { - Self - } - pub fn handler(self, _handler: F) -> Self { - self - } - pub fn build(self) -> Self { - self - } -} - -fn dummy_handler() {} - -pub fn define_endpoints() { - // Missing service name and version - // Should trigger DE0801 - API endpoint version - OperationBuilder::get("/users"); - - // Missing service name (looks like version but list is not valid version) - // Should trigger DE0801 - API endpoint version - OperationBuilder::get("/users/list").handler(dummy_handler); - - // Second segment not a valid version - // Should trigger DE0801 - API endpoint version - OperationBuilder::post("/api/users") - .handler(dummy_handler) - .build(); - - // Invalid version format - // Should trigger DE0801 - API endpoint version - OperationBuilder::put("/version1/users"); - - // Missing service name (version first) - // Should trigger DE0801 - API endpoint version - OperationBuilder::delete("/v1/products").handler(dummy_handler); - - // Service name with underscore (not kebab-case) - // Should trigger DE0801 - API endpoint version - OperationBuilder::patch("/some_service/v1/products"); - - // Service name with capital letters - // Should trigger DE0801 - API endpoint version - OperationBuilder::get("/SomeService/v1/products") - .handler(dummy_handler) - .build(); - - // Uppercase version - // Should trigger DE0801 - API endpoint version - OperationBuilder::post("/some-service/V1/products"); - - // Capital letter in resource name - // Should trigger DE0801 - API endpoint version - OperationBuilder::put("/some-service/v1/Products").handler(dummy_handler); - - // Leading dash in service name - // Should trigger DE0801 - API endpoint version - OperationBuilder::get("/-some-service/v1/products"); - - // Leading dash in resource name - // Should trigger DE0801 - API endpoint version - OperationBuilder::delete("/some-service/v1/-products").handler(dummy_handler); - - // Leading dash in sub-resource - // Should trigger DE0801 - API endpoint version - OperationBuilder::patch("/some-service/v1/products/-abc"); - - // Missing resource after version - // Should trigger DE0801 - API endpoint version - OperationBuilder::post("/my-service/v1") - .handler(dummy_handler) - .build(); -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/invalid_endpoints.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/invalid_endpoints.stderr deleted file mode 100644 index e4dbe4a35..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/invalid_endpoints.stderr +++ /dev/null @@ -1,120 +0,0 @@ -error: API endpoint `/users` is missing a version segment (DE0801) - --> $DIR/invalid_endpoints.rs:34:27 - | -LL | OperationBuilder::get("/users"); - | ^^^^^^^^ - | - = help: add version as second segment: /{service-name}/v{N}/{resource} - = note: version must be v1, v2, etc. - = note: `#[deny(de0801_api_endpoint_version)]` on by default - -error: API endpoint `/users/list` has invalid version format `list` (DE0801) - --> $DIR/invalid_endpoints.rs:38:27 - | -LL | OperationBuilder::get("/users/list").handler(dummy_handler); - | ^^^^^^^^^^^^^ - | - = help: version must be lowercase 'v' followed by digits (v1, v2, v10) - = note: semver (v1.0) and uppercase (V1) are not allowed - -error: API endpoint `/api/users` has invalid version format `users` (DE0801) - --> $DIR/invalid_endpoints.rs:42:28 - | -LL | OperationBuilder::post("/api/users") - | ^^^^^^^^^^^^ - | - = help: version must be lowercase 'v' followed by digits (v1, v2, v10) - = note: semver (v1.0) and uppercase (V1) are not allowed - -error: API endpoint `/version1/users` has invalid version format `users` (DE0801) - --> $DIR/invalid_endpoints.rs:48:27 - | -LL | OperationBuilder::put("/version1/users"); - | ^^^^^^^^^^^^^^^^^ - | - = help: version must be lowercase 'v' followed by digits (v1, v2, v10) - = note: semver (v1.0) and uppercase (V1) are not allowed - -error: API endpoint `/v1/products` is missing a service name before version (DE0801) - --> $DIR/invalid_endpoints.rs:52:30 - | -LL | OperationBuilder::delete("/v1/products").handler(dummy_handler); - | ^^^^^^^^^^^^^^ - | - = help: use format: /{service-name}/v{N}/{resource} - = note: service name must come before version segment - -error: API endpoint `/some_service/v1/products` has invalid service name `some_service` (DE0801) - --> $DIR/invalid_endpoints.rs:56:29 - | -LL | OperationBuilder::patch("/some_service/v1/products"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: service name must be kebab-case (lowercase letters, numbers, dashes) - = note: service name must not start or end with a dash - -error: API endpoint `/SomeService/v1/products` has invalid service name `SomeService` (DE0801) - --> $DIR/invalid_endpoints.rs:60:27 - | -LL | OperationBuilder::get("/SomeService/v1/products") - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: service name must be kebab-case (lowercase letters, numbers, dashes) - = note: service name must not start or end with a dash - -error: API endpoint `/some-service/V1/products` has invalid version format `V1` (DE0801) - --> $DIR/invalid_endpoints.rs:66:28 - | -LL | OperationBuilder::post("/some-service/V1/products"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: version must be lowercase 'v' followed by digits (v1, v2, v10) - = note: semver (v1.0) and uppercase (V1) are not allowed - -error: API endpoint `/some-service/v1/Products` has invalid resource name `Products` (DE0801) - --> $DIR/invalid_endpoints.rs:70:27 - | -LL | OperationBuilder::put("/some-service/v1/Products").handler(dummy_handler); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: resource names must be kebab-case (lowercase letters, numbers, dashes) - = note: resource names must not start or end with a dash - -error: API endpoint `/-some-service/v1/products` has invalid service name `-some-service` (DE0801) - --> $DIR/invalid_endpoints.rs:74:27 - | -LL | OperationBuilder::get("/-some-service/v1/products"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: service name must be kebab-case (lowercase letters, numbers, dashes) - = note: service name must not start or end with a dash - -error: API endpoint `/some-service/v1/-products` has invalid resource name `-products` (DE0801) - --> $DIR/invalid_endpoints.rs:78:30 - | -LL | OperationBuilder::delete("/some-service/v1/-products").handler(dummy_handler); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: resource names must be kebab-case (lowercase letters, numbers, dashes) - = note: resource names must not start or end with a dash - -error: API endpoint `/some-service/v1/products/-abc` has invalid resource name `-abc` (DE0801) - --> $DIR/invalid_endpoints.rs:82:29 - | -LL | OperationBuilder::patch("/some-service/v1/products/-abc"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: resource names must be kebab-case (lowercase letters, numbers, dashes) - = note: resource names must not start or end with a dash - -error: API endpoint `/my-service/v1` is missing a resource after version (DE0801) - --> $DIR/invalid_endpoints.rs:86:28 - | -LL | OperationBuilder::post("/my-service/v1") - | ^^^^^^^^^^^^^^^^ - | - = help: add resource: /{service-name}/v{N}/{resource} - = note: at least one resource segment is required after version - -error: aborting due to 13 previous errors - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/valid_endpoints.rs b/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/valid_endpoints.rs deleted file mode 100644 index 08cd96ffb..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/valid_endpoints.rs +++ /dev/null @@ -1,71 +0,0 @@ -#![allow(dead_code)] - -pub struct OperationBuilder; - -impl OperationBuilder { - pub fn get(_path: &str) -> Self { - Self - } - pub fn post(_path: &str) -> Self { - Self - } - pub fn put(_path: &str) -> Self { - Self - } - pub fn delete(_path: &str) -> Self { - Self - } - pub fn patch(_path: &str) -> Self { - Self - } - pub fn handler(self, _handler: F) -> Self { - self - } - pub fn build(self) -> Self { - self - } -} - -fn list_users() {} -fn get_user() {} -fn create_order() {} -fn update_product() {} -fn delete_resource() {} - -pub fn define_endpoints() { - // Valid patterns: /{service-name}/v{N}/{resource} - - // Should not trigger DE0801 - API endpoint version - // Simple GET with handler - OperationBuilder::get("/tests/v1/users") - .handler(list_users) - .build(); - - // Should not trigger DE0801 - API endpoint version - // POST with multiple methods - OperationBuilder::post("/abc/v2/products").handler(create_order); - - // Should not trigger DE0801 - API endpoint version - // Various HTTP methods - OperationBuilder::post("/a-b-c/v1/orders"); - // Should not trigger DE0801 - API endpoint version - OperationBuilder::put("/tests/v1/users/{id}").handler(update_product); - // Should not trigger DE0801 - API endpoint version - OperationBuilder::delete("/tests/v2/users/{id}/profile"); - // Should not trigger DE0801 - API endpoint version - OperationBuilder::patch("/tests/v3/products/{id}"); - - // Should not trigger DE0801 - API endpoint version - // Different service names and version numbers - OperationBuilder::get("/my-service/v10/resources") - .handler(get_user) - .build(); - // Should not trigger DE0801 - API endpoint version - OperationBuilder::post("/service1/v1/items/{id}/details"); - - // Should not trigger DE0801 - API endpoint version - // Path parameters in various positions - OperationBuilder::get("/api-service/v5/users/{user-id}/orders/{order-id}").handler(list_users); -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/valid_endpoints.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0801_api_endpoint_version/ui/valid_endpoints.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/Cargo.toml b/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/Cargo.toml deleted file mode 100644 index 7a7ba601d..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "de0802_use_odata_ext" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "DE0802: Use OperationBuilderODataExt methods instead of .query_param() for OData parameters" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "valid_odata" -path = "ui/valid_odata.rs" - -[[example]] -name = "invalid_odata" -path = "ui/invalid_odata.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/src/lib.rs b/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/src/lib.rs deleted file mode 100644 index 68a95446c..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/src/lib.rs +++ /dev/null @@ -1,168 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; -extern crate rustc_hir; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_hir::{Expr, ExprKind}; -use rustc_lint::{LateContext, LateLintPass}; - -dylint_linting::declare_late_lint! { - /// ### What it does - /// - /// Checks that OData query parameters (`$filter`, `$orderby`, `$select`, `$top`, `$skip`) - /// are registered using `OperationBuilderODataExt` methods instead of manual `.query_param()` calls. - /// - /// ### Why is this bad? - /// - /// Using `.query_param("$filter", ...)` bypasses the type-safe OData system: - /// - No compile-time validation of filterable/orderable fields - /// - No automatic OpenAPI schema generation for allowed fields - /// - Inconsistent API documentation - /// - Harder to maintain as DTO fields change - /// - /// ### Example - /// - /// ```rust,ignore - /// // Bad - manual OData parameter registration - /// OperationBuilder::get("/users-info/v1/users") - /// .query_param("$filter", false, "OData filter") - /// .query_param("$orderby", false, "OData ordering") - /// .query_param("$select", false, "OData field selection") - /// ``` - /// - /// Use instead: - /// - /// ```rust,ignore - /// // Good - type-safe OData registration - /// OperationBuilder::get("/users-info/v1/users") - /// .with_odata_filter::() - /// .with_odata_orderby::() - /// .with_odata_select() - /// ``` - pub DE0802_USE_ODATA_EXT, - Deny, - "use OperationBuilderODataExt methods instead of .query_param() for OData parameters (DE0802)" -} - -/// OData query parameter names that should use the type-safe extension methods -const ODATA_PARAMS: &[&str] = &["$filter", "$orderby", "$select", "$top", "$skip", "$count"]; - -/// Mapping from OData parameter to the recommended method -fn get_recommended_method(param: &str) -> &'static str { - match param { - "$filter" => ".with_odata_filter::()", - "$orderby" => ".with_odata_orderby::()", - "$select" => ".with_odata_select()", - "$top" | "$skip" | "$count" => ".query_param_typed() with proper OData extractor", - _ => "the appropriate OperationBuilderODataExt method", - } -} - -impl<'tcx> LateLintPass<'tcx> for De0802UseOdataExt { - fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { - // Look for method calls like .query_param(...) or .query_param_typed(...) - if let ExprKind::MethodCall(method_segment, receiver, args, _span) = &expr.kind { - let method_name = method_segment.ident.name.as_str(); - - // Check if this is a query_param or query_param_typed call - if method_name != "query_param" && method_name != "query_param_typed" { - return; - } - - // Check if the receiver chain contains OperationBuilder - if !is_operation_builder_chain(receiver) { - return; - } - - // Check the first argument (parameter name) - if let Some(first_arg) = args.first() { - check_odata_param(cx, first_arg, method_name); - } - } - } -} - -/// Check if an expression is part of an OperationBuilder method chain -fn is_operation_builder_chain(expr: &Expr<'_>) -> bool { - match &expr.kind { - // Direct call like OperationBuilder::get(...) - ExprKind::Call(func, _) => { - if let ExprKind::Path(qpath) = &func.kind { - return path_contains_operation_builder(qpath); - } - false - } - // Method chain like builder.something().query_param(...) - ExprKind::MethodCall(_, receiver, _, _) => is_operation_builder_chain(receiver), - // Path expression - ExprKind::Path(qpath) => path_contains_operation_builder(qpath), - _ => false, - } -} - -/// Check if a QPath contains "OperationBuilder" -fn path_contains_operation_builder(qpath: &rustc_hir::QPath<'_>) -> bool { - match qpath { - rustc_hir::QPath::Resolved(_, path) => path - .segments - .iter() - .any(|seg| seg.ident.name.as_str() == "OperationBuilder"), - rustc_hir::QPath::TypeRelative(ty, segment) => { - segment.ident.name.as_str() == "OperationBuilder" || type_contains_operation_builder(ty) - } - } -} - -/// Recursively check if a type contains "OperationBuilder" -fn type_contains_operation_builder(ty: &rustc_hir::Ty<'_>) -> bool { - match &ty.kind { - rustc_hir::TyKind::Path(qpath) => path_contains_operation_builder(qpath), - _ => false, - } -} - -/// Check if the first argument is an OData parameter and emit lint if so -fn check_odata_param<'tcx>(cx: &LateContext<'tcx>, arg: &'tcx Expr<'tcx>, method_name: &str) { - if let ExprKind::Lit(lit) = &arg.kind - && let rustc_ast::ast::LitKind::Str(sym, _) = lit.node - { - let param_name = sym.as_str(); - - // Check if this is an OData parameter - if ODATA_PARAMS.contains(¶m_name) { - let recommended = get_recommended_method(param_name); - - span_lint_and_then( - cx, - DE0802_USE_ODATA_EXT, - arg.span, - format!( - "use OperationBuilderODataExt instead of .{}() for OData parameter `{}` (DE0802)", - method_name, param_name - ), - |diag| { - diag.help(format!("use {} instead", recommended)); - diag.note( - "type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation", - ); - }, - ); - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE0802", "use OData ext"); - } -} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/invalid_odata.rs b/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/invalid_odata.rs deleted file mode 100644 index 2d0e78e9c..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/invalid_odata.rs +++ /dev/null @@ -1,73 +0,0 @@ -#![allow(dead_code)] - -pub struct OperationBuilder; - -impl OperationBuilder { - pub fn get(_path: &str) -> Self { - Self - } - pub fn post(_path: &str) -> Self { - Self - } - pub fn query_param(self, _name: &str, _required: bool, _desc: &str) -> Self { - self - } - pub fn query_param_typed(self, _name: &str, _required: bool, _desc: &str, _type: &str) -> Self { - self - } - pub fn handler(self, _handler: F) -> Self { - self - } - pub fn register(self) -> Self { - self - } -} - -fn dummy_handler() {} - -pub fn define_endpoints() { - // Using query_param for $filter - should use with_odata_filter - OperationBuilder::get("/users-info/v1/users") - // Should trigger DE0802 - use OData ext - .query_param("$filter", false, "OData filter expression"); - - // Using query_param for $orderby - should use with_odata_orderby - OperationBuilder::get("/users-info/v1/users") - // Should trigger DE0802 - use OData ext - .query_param("$orderby", false, "OData ordering"); - - // Using query_param for $select - should use with_odata_select - OperationBuilder::get("/users-info/v1/users") - // Should trigger DE0802 - use OData ext - .query_param("$select", false, "OData field selection"); - - // Using query_param_typed for $filter - OperationBuilder::post("/users-info/v1/users") - // Should trigger DE0802 - use OData ext - .query_param_typed("$filter", false, "OData filter", "string"); - - // Using query_param for $top - OperationBuilder::get("/users-info/v1/users") - // Should trigger DE0802 - use OData ext - .query_param("$top", false, "Maximum number of results"); - - // Using query_param for $skip - OperationBuilder::get("/users-info/v1/users") - // Should trigger DE0802 - use OData ext - .query_param("$skip", false, "Number of results to skip"); - - // Using query_param for $count - OperationBuilder::get("/users-info/v1/users") - // Should trigger DE0802 - use OData ext - .query_param("$count", false, "Include total count"); - - // Multiple OData params in chain - OperationBuilder::get("/users-info/v1/users") - // Should trigger DE0802 - use OData ext - .query_param("$filter", false, "Filter") - // Should trigger DE0802 - use OData ext - .query_param("$orderby", false, "Order") - .handler(dummy_handler) - .register(); -} -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/invalid_odata.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/invalid_odata.stderr deleted file mode 100644 index 5e3770065..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/invalid_odata.stderr +++ /dev/null @@ -1,84 +0,0 @@ -error: use OperationBuilderODataExt instead of .query_param() for OData parameter `$filter` (DE0802) - --> $DIR/invalid_odata.rs:32:22 - | -LL | .query_param("$filter", false, "OData filter expression"); - | ^^^^^^^^^ - | - = help: use .with_odata_filter::() instead - = note: type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation - = note: `#[deny(de0802_use_odata_ext)]` on by default - -error: use OperationBuilderODataExt instead of .query_param() for OData parameter `$orderby` (DE0802) - --> $DIR/invalid_odata.rs:37:22 - | -LL | .query_param("$orderby", false, "OData ordering"); - | ^^^^^^^^^^ - | - = help: use .with_odata_orderby::() instead - = note: type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation - -error: use OperationBuilderODataExt instead of .query_param() for OData parameter `$select` (DE0802) - --> $DIR/invalid_odata.rs:42:22 - | -LL | .query_param("$select", false, "OData field selection"); - | ^^^^^^^^^ - | - = help: use .with_odata_select() instead - = note: type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation - -error: use OperationBuilderODataExt instead of .query_param_typed() for OData parameter `$filter` (DE0802) - --> $DIR/invalid_odata.rs:47:28 - | -LL | .query_param_typed("$filter", false, "OData filter", "string"); - | ^^^^^^^^^ - | - = help: use .with_odata_filter::() instead - = note: type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation - -error: use OperationBuilderODataExt instead of .query_param() for OData parameter `$top` (DE0802) - --> $DIR/invalid_odata.rs:52:22 - | -LL | .query_param("$top", false, "Maximum number of results"); - | ^^^^^^ - | - = help: use .query_param_typed() with proper OData extractor instead - = note: type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation - -error: use OperationBuilderODataExt instead of .query_param() for OData parameter `$skip` (DE0802) - --> $DIR/invalid_odata.rs:57:22 - | -LL | .query_param("$skip", false, "Number of results to skip"); - | ^^^^^^^ - | - = help: use .query_param_typed() with proper OData extractor instead - = note: type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation - -error: use OperationBuilderODataExt instead of .query_param() for OData parameter `$count` (DE0802) - --> $DIR/invalid_odata.rs:62:22 - | -LL | .query_param("$count", false, "Include total count"); - | ^^^^^^^^ - | - = help: use .query_param_typed() with proper OData extractor instead - = note: type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation - -error: use OperationBuilderODataExt instead of .query_param() for OData parameter `$orderby` (DE0802) - --> $DIR/invalid_odata.rs:69:22 - | -LL | .query_param("$orderby", false, "Order") - | ^^^^^^^^^^ - | - = help: use .with_odata_orderby::() instead - = note: type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation - -error: use OperationBuilderODataExt instead of .query_param() for OData parameter `$filter` (DE0802) - --> $DIR/invalid_odata.rs:67:22 - | -LL | .query_param("$filter", false, "Filter") - | ^^^^^^^^^ - | - = help: use .with_odata_filter::() instead - = note: type-safe OData methods provide compile-time validation and automatic OpenAPI schema generation - -error: aborting due to 9 previous errors - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/valid_odata.rs b/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/valid_odata.rs deleted file mode 100644 index 3c4bc90a8..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/valid_odata.rs +++ /dev/null @@ -1,74 +0,0 @@ -#![allow(dead_code)] - -pub struct OperationBuilder; - -impl OperationBuilder { - pub fn get(_path: &str) -> Self { - Self - } - pub fn post(_path: &str) -> Self { - Self - } - pub fn query_param(self, _name: &str, _required: bool, _desc: &str) -> Self { - self - } - pub fn query_param_typed(self, _name: &str, _required: bool, _desc: &str, _type: &str) -> Self { - self - } - pub fn with_odata_filter(self) -> Self { - let _ = std::any::type_name::(); - self - } - pub fn with_odata_orderby(self) -> Self { - let _ = std::any::type_name::(); - self - } - pub fn with_odata_select(self) -> Self { - self - } - pub fn handler(self, _handler: F) -> Self { - self - } - pub fn register(self) -> Self { - self - } -} - -struct UserDtoFilterField; - -fn dummy_handler() {} - -pub fn define_endpoints() { - // Should not trigger DE0802 - use OData ext (using proper OData extension methods) - OperationBuilder::get("/users-info/v1/users") - .with_odata_filter::() - .with_odata_orderby::() - .with_odata_select() - .handler(dummy_handler) - .register(); - - // Should not trigger DE0802 - use OData ext (non-OData query params are fine) - OperationBuilder::get("/users-info/v1/users") - .query_param("limit", false, "Maximum number of results") - .query_param("cursor", false, "Pagination cursor") - .query_param("search", false, "Search term") - .handler(dummy_handler) - .register(); - - // Should not trigger DE0802 - use OData ext (typed non-OData params are fine) - OperationBuilder::post("/users-info/v1/users") - .query_param_typed("limit", false, "Max results", "integer") - .query_param_typed("offset", false, "Offset", "integer") - .handler(dummy_handler) - .register(); - - // Should not trigger DE0802 - use OData ext (mixed valid usage) - OperationBuilder::get("/users-info/v1/users") - .with_odata_filter::() - .with_odata_select() - .query_param("include_deleted", false, "Include soft-deleted records") - .handler(dummy_handler) - .register(); -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/valid_odata.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0802_use_odata_ext/ui/valid_odata.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/Cargo.toml b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/Cargo.toml deleted file mode 100644 index 0763ed6e3..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/Cargo.toml +++ /dev/null @@ -1,126 +0,0 @@ -[package] -name = "de0803_api_snake_case" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "DTOs must not use CamelCase or PascalCase in serde rename_all, only snake_case is allowed" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -serde.workspace = true - -[[example]] -name = "pass" -path = "ui/pass.rs" - -[[example]] -name = "fail_camel_case" -path = "ui/fail_camel_case.rs" - -[[example]] -name = "fail_lowercase" -path = "ui/fail_lowercase.rs" - -[[example]] -name = "fail_pascal_case" -path = "ui/fail_pascal_case.rs" - -[[example]] -name = "fail_uppercase" -path = "ui/fail_uppercase.rs" - -[[example]] -name = "fail_screaming_snake_case" -path = "ui/fail_screaming_snake_case.rs" - -[[example]] -name = "fail_kebab_case" -path = "ui/fail_kebab_case.rs" - -[[example]] -name = "fail_screaming_kebab_case" -path = "ui/fail_screaming_kebab_case.rs" - -[[example]] -name = "pass_outside_api" -path = "ui/pass_outside_api.rs" - -[[example]] -name = "fail_field_camel_case" -path = "ui/fail_field_camel_case.rs" - -[[example]] -name = "fail_field_pascal_case" -path = "ui/fail_field_pascal_case.rs" - -[[example]] -name = "fail_field_kebab_case" -path = "ui/fail_field_kebab_case.rs" - -[[example]] -name = "fail_field_uppercase" -path = "ui/fail_field_uppercase.rs" - -[[example]] -name = "fail_field_screaming_snake_case" -path = "ui/fail_field_screaming_snake_case.rs" - -[[example]] -name = "fail_field_screaming_kebab_case" -path = "ui/fail_field_screaming_kebab_case.rs" - -[[example]] -name = "pass_field_snake_case" -path = "ui/pass_field_snake_case.rs" - -[[example]] -name = "pass_field_lowercase" -path = "ui/pass_field_lowercase.rs" - -[[example]] -name = "fail_field_name_camel_case" -path = "ui/fail_field_name_camel_case.rs" - -[[example]] -name = "pass_field_name_with_snake_rename" -path = "ui/pass_field_name_with_snake_rename.rs" - -[[example]] -name = "fail_rename_all_nested_camel_case" -path = "ui/fail_rename_all_nested_camel_case.rs" - -[[example]] -name = "pass_rename_all_nested_snake_case" -path = "ui/pass_rename_all_nested_snake_case.rs" - -[[example]] -name = "fail_field_rename_nested_camel_case" -path = "ui/fail_field_rename_nested_camel_case.rs" - -[[example]] -name = "pass_field_rename_nested_snake_case" -path = "ui/pass_field_rename_nested_snake_case.rs" - -[[example]] -name = "fail_enum_variant_rename_camel_case" -path = "ui/fail_enum_variant_rename_camel_case.rs" - -[[example]] -name = "pass_enum_variant_rename_snake_case" -path = "ui/pass_enum_variant_rename_snake_case.rs" - -[[example]] -name = "fail_enum_variant_rename_nested_pascal_case" -path = "ui/fail_enum_variant_rename_nested_pascal_case.rs" - -[package.metadata.rust-analyzer] -rustc_private = true \ No newline at end of file diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/README.md b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/README.md deleted file mode 100644 index 82fb7990f..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# de0803_api_snake_case - -## What it does -Checks that DTOs (structs and enums) defined in the `api/rest` directory use `snake_case` for serde renaming configurations. Specifically: -1. `#[serde(rename_all = "...")]` on structs/enums must be "snake_case". -2. `#[serde(rename = "...")]` on fields must be in snake_case. - -## Why is this bad? -The API standard requires all JSON properties to be in `snake_case`. Using other casing styles (like `camelCase`, `PascalCase`, etc.) leads to inconsistent API responses and violations of the project's API guidelines. - -## Example - -```rust -// Bad: using camelCase for rename_all -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MyDto { - pub my_field: String, -} - -// Bad: using camelCase for field rename -#[derive(Serialize, Deserialize)] -pub struct AnotherDto { - #[serde(rename = "myField")] - pub my_field: String, -} -``` - -Use instead: - -```rust -// Good: using snake_case for rename_all (or omitting it if fields are already snake_case) -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct MyDto { - pub my_field: String, -} - -// Good: using snake_case for field rename -#[derive(Serialize, Deserialize)] -pub struct AnotherDto { - #[serde(rename = "my_field")] - pub my_field: String, -} -``` diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/src/lib.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/src/lib.rs deleted file mode 100644 index 9c3b0a54a..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/src/lib.rs +++ /dev/null @@ -1,258 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; -extern crate rustc_span; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{Attribute, FieldDef, Item, ItemKind, VariantData}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; - -use lint_utils::is_in_api_rest_folder; - -dylint_linting::declare_pre_expansion_lint! { - /// DE0803: API DTOs Must Use Snake Case in Serde Attributes - /// - /// DTOs must use snake_case in serde rename_all and rename attributes. - /// This lint checks both: - /// - Type-level `#[serde(rename_all = "...")]` attributes - /// - Field-level `#[serde(rename = "...")]` attributes - /// - /// Only snake_case is allowed for API consistency per DNA guidelines. - pub DE0803_API_SNAKE_CASE, - Deny, - "API DTOs must use snake_case in serde rename attributes (DE0803)" -} - -impl EarlyLintPass for De0803ApiSnakeCase { - /// Checks structs and enums in api/rest folders for snake_case compliance. - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - if !is_in_api_rest_folder(cx.sess().source_map(), item.span) { - return; - } - - match &item.kind { - ItemKind::Struct(_, _, variant_data) => { - check_type_rename_all(cx, &item.attrs); - check_fields(cx, variant_data); - } - ItemKind::Enum(_, _, enum_def) => { - check_type_rename_all(cx, &item.attrs); - for variant in &enum_def.variants { - check_variant_rename(cx, &variant.attrs); - check_fields(cx, &variant.data); - } - } - _ => {} - } - } -} - -/// Extracts values from serde attributes matching the given name. -/// -/// Handles both direct forms like `rename = "value"` and nested forms like -/// `rename(serialize = "value1", deserialize = "value2")`. -/// -/// Returns spans and string values for all matching attributes. -fn find_serde_attribute_value( - attrs: &[Attribute], - attribute_name: &str, -) -> Vec<(rustc_span::Span, String)> { - let mut results = Vec::new(); - - for attr in attrs { - if !attr.has_name(rustc_span::Symbol::intern("serde")) { - continue; - } - - let Some(list) = attr.meta_item_list() else { - continue; - }; - - for nested in list { - let Some(meta_item) = nested.meta_item() else { - continue; - }; - - if !meta_item.has_name(rustc_span::Symbol::intern(attribute_name)) { - continue; - } - - // Try to get direct value: rename = "value" - if let Some(value) = meta_item.value_str() { - results.push((meta_item.span, value.as_str().to_string())); - } - - // Try to get nested list values: rename(serialize = "value1", deserialize = "value2") - if let Some(inner_list) = meta_item.meta_item_list() { - for inner_nested in inner_list { - let Some(inner_meta_item) = inner_nested.meta_item() else { - continue; - }; - - if let Some(inner_value) = inner_meta_item.value_str() { - results.push((inner_meta_item.span, inner_value.as_str().to_string())); - } - } - } - } - } - - results -} - -/// Validates that `rename_all` attributes use the literal "snake_case" value. -fn check_type_rename_all(cx: &EarlyContext<'_>, attrs: &[Attribute]) { - for (span, value) in find_serde_attribute_value(attrs, "rename_all") { - if value != "snake_case" { - span_lint_and_then( - cx, - DE0803_API_SNAKE_CASE, - span, - "DTOs must not use non-snake_case in serde rename_all (DE0803)", - |diag| { - diag.help( - "DTOs in api/rest must use snake_case (or default) to match API standards", - ); - }, - ); - } - } -} - -/// Validates that enum variant `rename` attributes use snake_case values. -fn check_variant_rename(cx: &EarlyContext<'_>, attrs: &[Attribute]) { - for (span, value) in find_serde_attribute_value(attrs, "rename") { - if !is_snake_case(&value) { - span_lint_and_then( - cx, - DE0803_API_SNAKE_CASE, - span, - "Enum variants must not use non-snake_case in serde rename (DE0803)", - |diag| { - diag.help( - "Enum variants in api/rest must use snake_case to match API standards", - ); - }, - ); - } - } -} - -/// Validates that fields use snake_case names or have a serde rename to snake_case. -fn check_fields(cx: &EarlyContext<'_>, variant_data: &VariantData) { - for field in variant_data.fields() { - check_field_snake_case(cx, field); - } -} - -/// Checks a single field for snake_case compliance. -/// -/// A field is valid if: -/// 1. The field name is snake_case, OR -/// 2. The field has a `#[serde(rename = "snake_case_value")]` attribute -/// -/// A field is invalid if: -/// 1. The field name is not snake_case AND has no serde rename, OR -/// 2. The field has a serde rename to a non-snake_case value -fn check_field_snake_case(cx: &EarlyContext<'_>, field: &FieldDef) { - let field_name = match &field.ident { - Some(ident) => ident.name.as_str().to_string(), - None => return, // Tuple struct fields have no name - }; - - let rename_values = find_serde_attribute_value(&field.attrs, "rename"); - - if rename_values.is_empty() { - // No field-level serde rename - field name must be snake_case - if !is_snake_case(&field_name) { - span_lint_and_then( - cx, - DE0803_API_SNAKE_CASE, - field.ident.unwrap().span, - "DTO field name must be snake_case or have a serde rename to snake_case (DE0803)", - |diag| { - diag.help(format!( - "rename field to snake_case or add #[serde(rename = \"{}\")]", - to_snake_case(&field_name) - )); - }, - ); - } - } else { - // Has field-level serde rename - the rename value must be snake_case - for (span, value) in rename_values { - if !is_snake_case(&value) { - span_lint_and_then( - cx, - DE0803_API_SNAKE_CASE, - span, - "DTO fields must not use non-snake_case in serde rename (DE0803)", - |diag| { - diag.help( - "DTO fields in api/rest must use snake_case to match API standards", - ); - }, - ); - } - } - } -} - -/// Checks if a string is valid snake_case. -/// -/// Snake case: lowercase letters, digits, and underscores only. -/// Examples: "my_field", "user_id", "field_123" -fn is_snake_case(s: &str) -> bool { - if s.is_empty() { - return false; - } - - // Must not start or end with underscore - if s.starts_with('_') || s.ends_with('_') { - return false; - } - - // Must not have consecutive underscores - if s.contains("__") { - return false; - } - - // All characters must be lowercase, digits, or underscore - s.chars() - .all(|c| c.is_lowercase() || c.is_ascii_digit() || c == '_') -} - -/// Converts a string to snake_case. -fn to_snake_case(s: &str) -> String { - let mut result = String::with_capacity(s.len()); - for (i, c) in s.chars().enumerate() { - if c.is_uppercase() { - if i > 0 { - result.push('_'); - } - result.push(c.to_lowercase().next().unwrap()); - } else { - result.push(c); - } - } - result -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0803", - "DTO fields must not use non-snake_case in serde rename/rename_all", - ); - } -} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_camel_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_camel_case.rs deleted file mode 100644 index 4daab46fe..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_camel_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all -#[serde(rename_all = "camelCase")] -pub struct BadCamelCaseDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_camel_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_camel_case.stderr deleted file mode 100644 index 736787500..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_camel_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTOs must not use non-snake_case in serde rename_all (DE0803) - --> $DIR/fail_camel_case.rs:6:9 - | -LL | #[serde(rename_all = "camelCase")] - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs in api/rest must use snake_case (or default) to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_camel_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_camel_case.rs deleted file mode 100644 index 458b1aaaa..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_camel_case.rs +++ /dev/null @@ -1,14 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub enum BadEnumVariantRenameDto { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "firstVariant")] - FirstVariant, - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "secondVariant")] - SecondVariant, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_camel_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_camel_case.stderr deleted file mode 100644 index 9bc21e851..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_camel_case.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: Enum variants must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_enum_variant_rename_camel_case.rs:7:13 - | -LL | #[serde(rename = "firstVariant")] - | ^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: Enum variants in api/rest must use snake_case to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: Enum variants must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_enum_variant_rename_camel_case.rs:10:13 - | -LL | #[serde(rename = "secondVariant")] - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: Enum variants in api/rest must use snake_case to match API standards - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_nested_pascal_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_nested_pascal_case.rs deleted file mode 100644 index cee94baad..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_nested_pascal_case.rs +++ /dev/null @@ -1,14 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub enum BadNestedVariantRenameEnum { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename(serialize = "FirstVariant"))] - FirstVariantSerialize, - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename(deserialize = "FirstVariant"))] - FirstVariantDeserialize, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_nested_pascal_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_nested_pascal_case.stderr deleted file mode 100644 index 229d15aa2..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_enum_variant_rename_nested_pascal_case.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: Enum variants must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_enum_variant_rename_nested_pascal_case.rs:7:20 - | -LL | #[serde(rename(serialize = "FirstVariant"))] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: Enum variants in api/rest must use snake_case to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: Enum variants must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_enum_variant_rename_nested_pascal_case.rs:10:20 - | -LL | #[serde(rename(deserialize = "FirstVariant"))] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: Enum variants in api/rest must use snake_case to match API standards - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_camel_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_camel_case.rs deleted file mode 100644 index 3d3eb8b44..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_camel_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct BadFieldCamelCaseDto { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "camelCaseField")] - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_camel_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_camel_case.stderr deleted file mode 100644 index 38a30680e..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_camel_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTO fields must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_field_camel_case.rs:7:13 - | -LL | #[serde(rename = "camelCaseField")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTO fields in api/rest must use snake_case to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_kebab_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_kebab_case.rs deleted file mode 100644 index 3ea9d8dc2..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_kebab_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct BadFieldKebabCaseDto { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "kebab-case-field")] - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_kebab_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_kebab_case.stderr deleted file mode 100644 index debfd848a..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_kebab_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTO fields must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_field_kebab_case.rs:7:13 - | -LL | #[serde(rename = "kebab-case-field")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTO fields in api/rest must use snake_case to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_name_camel_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_name_camel_case.rs deleted file mode 100644 index 83ff3fda1..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_name_camel_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -#![allow(non_snake_case)] -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct BadFieldNameCamelCaseDto { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - pub camelCaseField: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_name_camel_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_name_camel_case.stderr deleted file mode 100644 index 685d4a412..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_name_camel_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTO field name must be snake_case or have a serde rename to snake_case (DE0803) - --> $DIR/fail_field_name_camel_case.rs:8:9 - | -LL | pub camelCaseField: String, - | ^^^^^^^^^^^^^^ - | - = help: rename field to snake_case or add #[serde(rename = "camel_case_field")] - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_pascal_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_pascal_case.rs deleted file mode 100644 index 67fac8e42..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_pascal_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct BadFieldPascalCaseDto { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "PascalCaseField")] - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_pascal_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_pascal_case.stderr deleted file mode 100644 index 450c98b0d..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_pascal_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTO fields must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_field_pascal_case.rs:7:13 - | -LL | #[serde(rename = "PascalCaseField")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTO fields in api/rest must use snake_case to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_rename_nested_camel_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_rename_nested_camel_case.rs deleted file mode 100644 index d22316e5b..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_rename_nested_camel_case.rs +++ /dev/null @@ -1,14 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct BadNestedFieldRenameDto { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename(serialize = "userName"))] - pub user_name_serialize: String, - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename(deserialize = "userName"))] - pub user_name_deserialize: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_rename_nested_camel_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_rename_nested_camel_case.stderr deleted file mode 100644 index 164ffd016..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_rename_nested_camel_case.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: DTO fields must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_field_rename_nested_camel_case.rs:7:20 - | -LL | #[serde(rename(serialize = "userName"))] - | ^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTO fields in api/rest must use snake_case to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: DTO fields must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_field_rename_nested_camel_case.rs:10:20 - | -LL | #[serde(rename(deserialize = "userName"))] - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTO fields in api/rest must use snake_case to match API standards - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_kebab_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_kebab_case.rs deleted file mode 100644 index 7585a55e0..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_kebab_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct BadFieldScreamingKebabCaseDto { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "SCREAMING-KEBAB-FIELD")] - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_kebab_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_kebab_case.stderr deleted file mode 100644 index 25ff2d25f..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_kebab_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTO fields must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_field_screaming_kebab_case.rs:7:13 - | -LL | #[serde(rename = "SCREAMING-KEBAB-FIELD")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTO fields in api/rest must use snake_case to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_snake_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_snake_case.rs deleted file mode 100644 index 4ebdeef4d..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_snake_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct BadFieldScreamingSnakeCaseDto { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "SCREAMING_SNAKE_FIELD")] - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_snake_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_snake_case.stderr deleted file mode 100644 index cda156d25..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_screaming_snake_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTO fields must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_field_screaming_snake_case.rs:7:13 - | -LL | #[serde(rename = "SCREAMING_SNAKE_FIELD")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTO fields in api/rest must use snake_case to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_uppercase.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_uppercase.rs deleted file mode 100644 index c7133860a..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_uppercase.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct BadFieldUppercaseDto { - // Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "UPPERCASE_FIELD")] - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_uppercase.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_uppercase.stderr deleted file mode 100644 index 91c1e9b10..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_field_uppercase.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTO fields must not use non-snake_case in serde rename (DE0803) - --> $DIR/fail_field_uppercase.rs:7:13 - | -LL | #[serde(rename = "UPPERCASE_FIELD")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTO fields in api/rest must use snake_case to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_kebab_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_kebab_case.rs deleted file mode 100644 index edbbaa729..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_kebab_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all -#[serde(rename_all = "kebab-case")] -pub struct BadKebabCaseDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_kebab_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_kebab_case.stderr deleted file mode 100644 index d970cc62e..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_kebab_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTOs must not use non-snake_case in serde rename_all (DE0803) - --> $DIR/fail_kebab_case.rs:6:9 - | -LL | #[serde(rename_all = "kebab-case")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs in api/rest must use snake_case (or default) to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_lowercase.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_lowercase.rs deleted file mode 100644 index db8ff3137..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_lowercase.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all -#[serde(rename_all = "lowercase")] -pub struct BadLowercaseDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_lowercase.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_lowercase.stderr deleted file mode 100644 index 3a06aafeb..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_lowercase.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTOs must not use non-snake_case in serde rename_all (DE0803) - --> $DIR/fail_lowercase.rs:6:9 - | -LL | #[serde(rename_all = "lowercase")] - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs in api/rest must use snake_case (or default) to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_pascal_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_pascal_case.rs deleted file mode 100644 index 996256929..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_pascal_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all -#[serde(rename_all = "PascalCase")] -pub struct BadPascalCaseDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_pascal_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_pascal_case.stderr deleted file mode 100644 index 70eeb513c..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_pascal_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTOs must not use non-snake_case in serde rename_all (DE0803) - --> $DIR/fail_pascal_case.rs:6:9 - | -LL | #[serde(rename_all = "PascalCase")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs in api/rest must use snake_case (or default) to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_rename_all_nested_camel_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_rename_all_nested_camel_case.rs deleted file mode 100644 index c235b8a2d..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_rename_all_nested_camel_case.rs +++ /dev/null @@ -1,18 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all -#[serde(rename_all(serialize = "camelCase"))] -pub struct BadNestedRenameAllSerializeDto { - pub id: String, -} - -#[derive(Serialize, Deserialize)] -// Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all -#[serde(rename_all(deserialize = "camelCase"))] -pub struct BadNestedRenameAllDeserializeDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_rename_all_nested_camel_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_rename_all_nested_camel_case.stderr deleted file mode 100644 index 5841627b4..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_rename_all_nested_camel_case.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: DTOs must not use non-snake_case in serde rename_all (DE0803) - --> $DIR/fail_rename_all_nested_camel_case.rs:6:20 - | -LL | #[serde(rename_all(serialize = "camelCase"))] - | ^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs in api/rest must use snake_case (or default) to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: DTOs must not use non-snake_case in serde rename_all (DE0803) - --> $DIR/fail_rename_all_nested_camel_case.rs:13:20 - | -LL | #[serde(rename_all(deserialize = "camelCase"))] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs in api/rest must use snake_case (or default) to match API standards - -error: aborting due to 2 previous errors - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_kebab_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_kebab_case.rs deleted file mode 100644 index a429e1c52..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_kebab_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all -#[serde(rename_all = "SCREAMING-KEBAB-CASE")] -pub struct BadScreamingKebabCaseDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_kebab_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_kebab_case.stderr deleted file mode 100644 index f4cba6eef..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_kebab_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTOs must not use non-snake_case in serde rename_all (DE0803) - --> $DIR/fail_screaming_kebab_case.rs:6:9 - | -LL | #[serde(rename_all = "SCREAMING-KEBAB-CASE")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs in api/rest must use snake_case (or default) to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_snake_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_snake_case.rs deleted file mode 100644 index 1f9cbbb44..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_snake_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub struct BadScreamingSnakeCaseDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_snake_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_snake_case.stderr deleted file mode 100644 index f86315c15..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_screaming_snake_case.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTOs must not use non-snake_case in serde rename_all (DE0803) - --> $DIR/fail_screaming_snake_case.rs:6:9 - | -LL | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs in api/rest must use snake_case (or default) to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_uppercase.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_uppercase.rs deleted file mode 100644 index ce4c9d363..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_uppercase.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all -#[serde(rename_all = "UPPERCASE")] -pub struct BadUppercaseDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_uppercase.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_uppercase.stderr deleted file mode 100644 index ecd2c64b7..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/fail_uppercase.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: DTOs must not use non-snake_case in serde rename_all (DE0803) - --> $DIR/fail_uppercase.rs:6:9 - | -LL | #[serde(rename_all = "UPPERCASE")] - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: DTOs in api/rest must use snake_case (or default) to match API standards - = note: `#[deny(de0803_api_snake_case)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass.rs deleted file mode 100644 index 565a809dc..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass.rs +++ /dev/null @@ -1,15 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct GoodDto { - pub id: String, -} - -#[derive(Serialize, Deserialize)] -pub struct DefaultDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_enum_variant_rename_snake_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_enum_variant_rename_snake_case.rs deleted file mode 100644 index c68ce5b03..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_enum_variant_rename_snake_case.rs +++ /dev/null @@ -1,14 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub enum GoodEnumVariantRenameDto { - // Should not trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "first_variant")] - FirstVariant, - // Should not trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "second_variant")] - SecondVariant, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_lowercase.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_lowercase.rs deleted file mode 100644 index 332a44753..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_lowercase.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct GoodFieldLowercaseDto { - // Should not trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "lowercase")] - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_lowercase.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_lowercase.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_name_with_snake_rename.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_name_with_snake_rename.rs deleted file mode 100644 index 9375f69f8..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_name_with_snake_rename.rs +++ /dev/null @@ -1,12 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -#![allow(non_snake_case)] -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct GoodFieldNameWithSnakeRenameDto { - // Should not trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "camel_case_field")] - pub camelCaseField: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_name_with_snake_rename.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_name_with_snake_rename.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_rename_nested_snake_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_rename_nested_snake_case.rs deleted file mode 100644 index 08c7603ad..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_rename_nested_snake_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct GoodNestedFieldRenameDto { - // Should not trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename(serialize = "user_name", deserialize = "user_name"))] - pub user_name: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_snake_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_snake_case.rs deleted file mode 100644 index cb094aa77..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_snake_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct GoodFieldSnakeCaseDto { - // Should not trigger DE0803 - DTO fields must not use non-snake_case in serde rename/rename_all - #[serde(rename = "snake_case_field")] - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_snake_case.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_field_snake_case.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_outside_api.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_outside_api.rs deleted file mode 100644 index daed29b7f..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_outside_api.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/other/structs.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should not trigger DE0803 - DTOs must not use non-snake_case in serde rename_all (DE0803) -#[serde(rename_all = "PascalCase")] -pub struct OutsideApiDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_outside_api.stderr b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_outside_api.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_rename_all_nested_snake_case.rs b/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_rename_all_nested_snake_case.rs deleted file mode 100644 index ea350f161..000000000 --- a/tools/dylint_lints/de08_rest_api_conventions/de0803_api_snake_case/ui/pass_rename_all_nested_snake_case.rs +++ /dev/null @@ -1,11 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/api/rest/dto.rs -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -// Should not trigger DE0803 - DTOs must not use non-snake_case in serde rename/rename_all -#[serde(rename_all(serialize = "snake_case", deserialize = "snake_case"))] -pub struct GoodNestedRenameAllDto { - pub id: String, -} - -fn main() {} diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/.gitignore b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/Cargo.toml b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/Cargo.toml deleted file mode 100644 index 13b34b0fc..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "de0901_gts_string_pattern" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Validates GTS string patterns in functions and annotations" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "valid_use_cases" -path = "ui/valid_use_cases.rs" - -[[example]] -name = "invalid_cases" -path = "ui/invalid_cases.rs" - -[[example]] -name = "permission_strings" -path = "ui/permission_strings.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true -gts.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -gts-macros.workspace = true -serde.workspace = true -serde_json.workspace = true -schemars.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/README.md b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/README.md deleted file mode 100644 index 3e8bdfc96..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# DE0901 – GTS string pattern validator - -## What it does - -`DE0901_GTS_STRING_PATTERN` validates every string literal that looks like a -Global Type Schema (GTS) identifier. It ensures that: - -1. `schema_id = "..."` inside `#[struct_to_gts_schema]` attributes is a valid - **type schema** (must end with `~`, no wildcards). -2. Arguments passed to `gts_make_instance_id("...")` are valid **instance - segment identifiers** (single segment, no wildcards, no `:` or `~`). -3. Any other string literal that starts with `gts.` or appears inside a - colon-separated permission string contains a valid schema/instance chain. -4. `const`/`static` items holding GTS wildcard strings (`*`) **must** have - names ending with `_WILDCARD` — otherwise the lint reports an error. - -Wildcards (`*`) are only allowed in contexts where they are used as patterns: -permission strings, `resource_pattern(...)`, `with_pattern(...)`, -`resolve_to_uuids(...)`, `GtsWildcard::new(...)`, and `str.starts_with(...)`. -Everywhere else the lint rejects wildcard tokens. - -Use `#[allow(de0901_gts_string_pattern)]` to suppress the lint: -```rust -#[allow(unknown_lints)] -#[allow(de0901_gts_string_pattern)] -let schema = "gts.acme.core.events.*"; -``` - -## Why is this bad? - -* Invalid identifiers break contract generation, registry lookups, or instance - resolution at runtime. -* Wildcards in schema identifiers create ambiguous or insecure behavior, e.g., - allowing access to whole type families. -* Providing schemas to APIs that expect instance segments (or vice versa) leads - to confusing errors buried deep inside infrastructure crates. - -By catching the issues early, the lint prevents accidental schema typos and -protects security-critical permission checks. - -## Known exceptions - -* Permission strings (anything containing `:`) allow wildcards in the GTS - segment, but the lint still validates each GTS component. -* `resource_pattern("...")`, `with_pattern("...")`, and - `resolve_to_uuids(&["..."])` calls also allow wildcards, since they represent - pattern matching or resolution contexts. -* `GtsWildcard::new("...")` — arguments are allowed to contain wildcards, since - `GtsWildcard` is explicitly typed to hold pattern values. -* `const`/`static` items whose names end with `_WILDCARD` may hold GTS wildcard - strings (they are allowed and marked as intentional wildcard constants). -* Strings passed to `str.starts_with("gts.")` are ignored. -* Inline suppressions are supported through `#[allow(de0901_gts_string_pattern)]` - on a binding or expression when a wildcard must be hard-coded outside the - recognised helper APIs. - -## `_WILDCARD` naming convention for constants - -When a wildcard GTS pattern must be stored in a `const` or `static`, the item -name **must** end with `_WILDCARD`: - -```rust -// ✅ Allowed — name ends with _WILDCARD -const SRR_WILDCARD: &str = "gts.cf.core.srr.resource.v1~*"; -GtsWildcard::new(SRR_WILDCARD).unwrap(); - -// ❌ DE0901: name does not end with _WILDCARD -const SRR_PATTERN: &str = "gts.cf.core.srr.resource.v1~*"; -// → rename to `SRR_PATTERN_WILDCARD` or use a non-wildcard value -``` - -## Example - -```rust -// ❌ Triggers DE0901: wildcard inside a plain schema string -let schema = "gts.acme.core.events.*"; - -// ❌ Triggers DE0901: schema (with `~`) used in gts_make_instance_id -let _id = Product::gts_make_instance_id("vendor.package.sku.some.v1~"); - -// ❌ Triggers DE0901: const named without _WILDCARD suffix holds a wildcard -const BAD_PATTERN: &str = "gts.cf.core.srr.resource.v1~*"; -``` - -Use instead: - -```rust -// ✅ Explicit type schema -let schema = "gts.acme.core.events.type.v1~"; - -// ✅ Instance id segment -let _id = Product::gts_make_instance_id("vendor.package.sku.some.v1"); - -// ✅ Wildcard allowed inside permission/resource patterns -let pattern = Permission::builder() - .resource_pattern("gts.acme.core.events.topic.v1~vendor.*") - .action("publish") - .build() - .unwrap(); - -// ✅ Wildcard constant with _WILDCARD suffix -const ALL_SRR_WILDCARD: &str = "gts.cf.core.srr.resource.v1~*"; -let wc = GtsWildcard::new(ALL_SRR_WILDCARD).unwrap(); - -// ✅ Inline wildcard passed directly to GtsWildcard::new() -let wc = GtsWildcard::new("gts.cf.core.srr.resource.v1~*").unwrap(); -``` diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/src/lib.rs b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/src/lib.rs deleted file mode 100644 index d1e4a0e05..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/src/lib.rs +++ /dev/null @@ -1,742 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; -extern crate rustc_span; - -use clippy_utils::diagnostics::span_lint_and_then; -use gts::{GtsIdSegment, GtsOps}; -use lint_utils::{filename_str, is_temp_path}; -use rustc_ast::token::LitKind; -use rustc_ast::{AttrKind, Attribute, Expr, ExprKind, Item, ItemKind}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; -use rustc_span::Span; -use std::cell::RefCell; -use std::collections::HashSet; - -// Thread-local storage for spans to skip (inside starts_with calls) -thread_local! { - static SKIP_SPANS: RefCell> = RefCell::new(HashSet::new()); - static IN_TEST_DEPTH: RefCell = const { RefCell::new(0) }; -} - -const CODE_ALLOWED_VENDORS: &[&str] = &["cf"]; -const TEST_ALLOWED_VENDORS: &[&str] = &[ - "cf", "vendor", "example", "fabrikam", "contoso", "acme", "globex", -]; - -dylint_linting::declare_pre_expansion_lint! { - /// ### What it does - /// - /// Validates GTS schema identifiers used by `gts-macros`. - /// - /// Checks: - /// 1. `schema_id = "..."` in `#[struct_to_gts_schema(...)]` - must be valid GTS type schema - /// 2. `gts_make_instance_id("...")` - must be valid GTS instance segment id - /// 3. GTS-looking string literals - must be valid GTS entity id - /// - /// Uses `GtsOps::parse_id()` from the GTS library for validation. - pub DE0901_GTS_STRING_PATTERN, - Deny, - "invalid GTS string pattern (DE0901)" -} - -impl EarlyLintPass for De0901GtsStringPattern { - fn check_crate_post(&mut self, _cx: &EarlyContext<'_>, _krate: &rustc_ast::Crate) { - SKIP_SPANS.with(|s| s.borrow_mut().clear()); - IN_TEST_DEPTH.with(|d| *d.borrow_mut() = 0); - } - - fn check_attribute(&mut self, cx: &EarlyContext<'_>, attr: &Attribute) { - self.check_struct_to_gts_schema_attr(cx, attr); - } - - /// Enforce naming convention for `const`/`static` items holding GTS wildcard strings. - /// - /// A wildcard GTS string (contains `*`) stored in a `const` or `static` item - /// **must** have a name ending with `_WILDCARD`. This makes wildcard constants - /// explicitly opt-in and easy to audit. - /// - /// | Item name | Value | Result | - /// |-------------------|-----------------------------------|---------| - /// | `SRR_WILDCARD` | `"gts.cf.core.srr.resource.v1~*"` | ✅ allowed — name ends with `_WILDCARD` | - /// | `SRR_PATTERN` | `"gts.cf.core.srr.resource.v1~*"` | ❌ flagged — name must end with `_WILDCARD` | - /// - /// Items with a compliant name are added to the skip set so their value span - /// is not re-checked by `check_expr`. - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - if is_test_item(item) { - IN_TEST_DEPTH.with(|d| *d.borrow_mut() += 1); - } - - // Extract both the item name and the initializer expression from const/static items. - // Note: `Item` has no top-level `ident`; it lives inside `ConstItem` / `StaticItem`. - let (item_name, init_expr): (&str, Option<&Expr>) = match &item.kind { - ItemKind::Const(ci) => (ci.ident.name.as_str(), ci.rhs_kind.expr()), - ItemKind::Static(si) => (si.ident.name.as_str(), si.expr.as_deref()), - _ => return, - }; - let Some(init) = init_expr else { return }; - - // Only act on GTS wildcard string values (starts with "gts." and contains '*'). - let Some(s) = Self::string_lit_value(init) else { - return; - }; - if !s.starts_with("gts.") || !s.contains('*') { - return; - } - - // Validate the wildcard GTS pattern itself before skip-listing. - let result = GtsOps::parse_id(s); - if !result.ok { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - item.span, - format!("invalid GTS wildcard pattern in `{item_name}`: '{s}' (DE0901)"), - |diag| { - diag.note(result.error); - diag.help("Example: gts.cf.core.srr.resource.v1~*"); - }, - ); - // Still skip-list so check_expr doesn't double-report the literal. - SKIP_SPANS.with(|spans| { - collect_nested_spans(init, &mut spans.borrow_mut()); - }); - return; - } - - self.check_vendors_in_parse_result(cx, item.span, s, &result); - - if !item_name.ends_with("_WILDCARD") { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - item.span, - format!( - "GTS wildcard string in `const`/`static` `{item_name}` must have a name ending with `_WILDCARD` (DE0901)" - ), - |diag| { - diag.note(format!( - "found wildcard GTS pattern `{s}` stored in `{item_name}`" - )); - diag.help(format!( - "rename to `{item_name}_WILDCARD` or use a non-wildcard value" - )); - }, - ); - } - - // Skip-list the span so check_expr doesn't re-flag (or double-report) the literal. - SKIP_SPANS.with(|spans| { - collect_nested_spans(init, &mut spans.borrow_mut()); - }); - } - - fn check_item_post(&mut self, _cx: &EarlyContext<'_>, item: &Item) { - if is_test_item(item) { - IN_TEST_DEPTH.with(|d| *d.borrow_mut() -= 1); - } - } - - fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { - // ── Phase 1: collect spans to skip ──────────────────────────────────── - if let ExprKind::MethodCall(method_call) = &expr.kind { - let method_name = method_call.seg.ident.name.as_str(); - if method_name == "starts_with" { - // Add the receiver and all arguments to skip list - SKIP_SPANS.with(|spans| { - let mut spans = spans.borrow_mut(); - spans.insert(method_call.receiver.span); - for arg in &method_call.args { - spans.insert(arg.span); - } - }); - // Don't check anything in starts_with calls - return; - } - if method_name == "resource_pattern" - || method_name == "with_pattern" - || method_name == "resolve_to_uuids" - { - // Validate nested string literals BEFORE skip-listing so that - // deeply nested GTS strings (e.g. inside &["gts...".to_owned()]) - // are checked rather than silently escaping validation. - for arg in &method_call.args { - self.validate_nested_gts_strings(cx, arg, true); - } - // Then add all nested sub-expression spans to the skip list - // to prevent duplicate reports from check_expr. - SKIP_SPANS.with(|spans| { - let mut spans = spans.borrow_mut(); - for arg in &method_call.args { - collect_nested_spans(arg, &mut spans); - } - }); - } - } - - // Detect free-function calls: `GtsWildcard::new("...")` or `SomeType::new(...)` where - // the path contains "GtsWildcard". Arguments are allowed to contain wildcards. - if let ExprKind::Call(func, args) = &expr.kind - && is_gts_wildcard_new_call(func) - { - SKIP_SPANS.with(|spans| { - let mut spans = spans.borrow_mut(); - for arg in args { - collect_nested_spans(arg, &mut spans); - } - }); - // Validate args (including nested literals) as wildcard-allowed - // patterns and return early. - for arg in args { - self.validate_nested_gts_strings(cx, arg, true); - } - return; - } - - // ── Phase 2: skip if this expression was marked ──────────────────────── - let should_skip = SKIP_SPANS.with(|spans| spans.borrow().contains(&expr.span)); - if should_skip { - return; - } - - self.check_gts_make_instance_id_call(cx, expr); - - // Check if this is a method call - handle resource_pattern and with_pattern specially - if let ExprKind::MethodCall(method_call) = &expr.kind { - let method_name = method_call.seg.ident.name.as_str(); - // Already validated in Phase 1 via validate_nested_gts_strings - if method_name == "resource_pattern" - || method_name == "with_pattern" - || method_name == "resolve_to_uuids" - { - return; - } - - // Check arguments of other method calls normally - for arg in &method_call.args { - self.check_gts_string_literal(cx, arg); - } - return; - } - - // For non-method-call expressions, check normally - self.check_gts_string_literal(cx, expr); - } -} - -/// Recursively collect spans from all sub-expressions so that deeply nested -/// string literals (e.g. inside `&["gts...".to_owned()]`) are included in the -/// skip set. -fn collect_nested_spans(expr: &Expr, spans: &mut HashSet) { - spans.insert(expr.span); - match &expr.kind { - ExprKind::MethodCall(mc) => { - collect_nested_spans(&mc.receiver, spans); - for arg in &mc.args { - collect_nested_spans(arg, spans); - } - } - ExprKind::AddrOf(_, _, inner) => { - collect_nested_spans(inner, spans); - } - ExprKind::Array(elements) => { - for elem in elements { - collect_nested_spans(elem, spans); - } - } - ExprKind::Call(func, args) => { - collect_nested_spans(func, spans); - for arg in args { - collect_nested_spans(arg, spans); - } - } - ExprKind::Tup(elements) => { - for elem in elements { - collect_nested_spans(elem, spans); - } - } - ExprKind::Paren(inner) => { - collect_nested_spans(inner, spans); - } - _ => {} - } -} - -/// Returns `true` if `func_expr` is a path call of the form `GtsWildcard::new` -/// (or `gts::GtsWildcard::new`, `::GtsWildcard::new`, etc.). -/// -/// We check that: -/// 1. The expression is a `Path` with at least two segments. -/// 2. The last segment is named `new`. -/// 3. At least one other segment is named `GtsWildcard`. -fn is_gts_wildcard_new_call(func_expr: &Expr) -> bool { - let ExprKind::Path(_, path) = &func_expr.kind else { - return false; - }; - let segments = &path.segments; - if segments.len() < 2 { - return false; - } - let last = segments.last().unwrap(); - if last.ident.name.as_str() != "new" { - return false; - } - segments - .iter() - .any(|seg| seg.ident.name.as_str() == "GtsWildcard") -} - -fn is_in_test() -> bool { - IN_TEST_DEPTH.with(|d| *d.borrow() > 0) -} - -fn is_ui_test(cx: &EarlyContext<'_>, span: Span) -> bool { - let Some(file_path) = filename_str(cx.sess().source_map(), span) else { - return false; - }; - is_temp_path(&file_path) -} - -fn allowed_vendors(cx: &EarlyContext<'_>, span: Span) -> &'static [&'static str] { - if is_in_test() || is_ui_test(cx, span) { - TEST_ALLOWED_VENDORS - } else { - CODE_ALLOWED_VENDORS - } -} - -/// Returns `true` if the item is annotated with `#[cfg(test)]` or `#[test]`. -fn is_test_item(item: &Item) -> bool { - item.attrs.iter().any(|attr| { - let AttrKind::Normal(normal) = &attr.kind else { - return false; - }; - let segments = &normal.item.path.segments; - if segments.len() != 1 { - return false; - } - let name = segments[0].ident.name.as_str(); - if name == "test" { - return true; - } - if name == "cfg" - && let Some(items) = normal.item.meta_item_list() - { - return items.iter().any(|nested| { - nested.meta_item().is_some_and(|mi| { - mi.path.segments.len() == 1 && mi.path.segments[0].ident.name.as_str() == "test" - }) - }); - } - false - }) -} - -impl De0901GtsStringPattern { - fn check_gts_make_instance_id_call(&self, cx: &EarlyContext<'_>, expr: &Expr) { - let ExprKind::Call(func, args) = &expr.kind else { - return; - }; - - if args.len() != 1 { - return; - } - - let Some(arg0) = args.first() else { - return; - }; - - let Some(arg_str) = Self::string_lit_value(arg0) else { - return; - }; - - // Detect `...::gts_make_instance_id("...")` - let ExprKind::Path(_, path) = &func.kind else { - return; - }; - - let Some(last) = path.segments.last() else { - return; - }; - - if last.ident.name.as_str() != "gts_make_instance_id" { - return; - } - - self.validate_instance_id_segment(cx, expr.span, arg_str); - } - - /// Recursively traverse an expression tree and validate any GTS string - /// literals found within. Mirrors the structure of `collect_nested_spans` - /// so that every string literal that would be skip-listed is also validated. - fn validate_nested_gts_strings( - &self, - cx: &EarlyContext<'_>, - expr: &Expr, - allow_wildcards: bool, - ) { - // If this is a string literal, validate it directly - if Self::string_lit_value(expr).is_some() { - self.check_gts_string_literal_with_wildcard_flag(cx, expr, allow_wildcards); - return; - } - // Otherwise, recurse into sub-expressions - match &expr.kind { - ExprKind::MethodCall(mc) => { - self.validate_nested_gts_strings(cx, &mc.receiver, allow_wildcards); - for arg in &mc.args { - self.validate_nested_gts_strings(cx, arg, allow_wildcards); - } - } - ExprKind::AddrOf(_, _, inner) => { - self.validate_nested_gts_strings(cx, inner, allow_wildcards); - } - ExprKind::Array(elements) => { - for elem in elements { - self.validate_nested_gts_strings(cx, elem, allow_wildcards); - } - } - ExprKind::Call(_, args) => { - for arg in args { - self.validate_nested_gts_strings(cx, arg, allow_wildcards); - } - } - ExprKind::Tup(elements) => { - for elem in elements { - self.validate_nested_gts_strings(cx, elem, allow_wildcards); - } - } - ExprKind::Paren(inner) => { - self.validate_nested_gts_strings(cx, inner, allow_wildcards); - } - _ => {} - } - } - - fn check_gts_string_literal(&self, cx: &EarlyContext<'_>, expr: &Expr) { - self.check_gts_string_literal_with_wildcard_flag(cx, expr, false); - } - - fn check_gts_string_literal_with_wildcard_flag( - &self, - cx: &EarlyContext<'_>, - expr: &Expr, - allow_wildcards: bool, - ) { - if let Some(s) = Self::string_lit_value(expr) { - let s = s.trim(); - - // Option 1: String starts with "gts." - validate directly - if s.starts_with("gts.") { - if allow_wildcards { - self.validate_any_gts_id_allow_wildcards(cx, expr.span, s); - } else { - self.validate_any_gts_id(cx, expr.span, s); - } - return; - } - - // Option 2: String contains ":" - this is a permission string format - // Permission strings ALWAYS allow wildcards in their GTS parts - if s.contains(':') { - for part in s.split(':') { - if part.trim().starts_with("gts.") { - self.validate_any_gts_id_allow_wildcards(cx, expr.span, part.trim()); - break; // Only validate the first GTS part found - } - } - } - } - } - - fn string_lit_value(expr: &Expr) -> Option<&str> { - match &expr.kind { - ExprKind::Lit(lit) => match lit.kind { - LitKind::Str | LitKind::StrRaw(_) => Some(lit.symbol.as_str()), - _ => None, - }, - _ => None, - } - } - - fn check_struct_to_gts_schema_attr(&self, cx: &EarlyContext<'_>, attr: &Attribute) { - let AttrKind::Normal(normal_attr) = &attr.kind else { - return; - }; - - // We only care about #[struct_to_gts_schema(...)] - if normal_attr.item.path.segments.len() != 1 - || normal_attr.item.path.segments[0].ident.name.as_str() != "struct_to_gts_schema" - { - return; - } - - let Some(items) = normal_attr.item.meta_item_list() else { - return; - }; - - for nested in items { - let Some(mi) = nested.meta_item() else { - continue; - }; - - // gts 0.10.0 renamed the attribute `schema_id` -> `type_id` - // (the old name is still accepted by upstream as a deprecated - // alias, so validate either form). - if mi.path.segments.len() != 1 - || !matches!( - mi.path.segments[0].ident.name.as_str(), - "type_id" | "schema_id" - ) - { - continue; - } - - let Some(val) = mi.value_str() else { - continue; - }; - - self.validate_schema_id(cx, mi.span, val.as_str()); - } - } - - /// Validate a GTS schema_id using GtsOps::parse_id() - /// schema_id must be a valid GTS type schema (ending with ~) - fn validate_schema_id(&self, cx: &EarlyContext<'_>, span: rustc_span::Span, s: &str) { - let s = s.trim(); - - // Wildcards are NOT allowed in schema_id - if s.contains('*') { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!("wildcards are not allowed in schema_id: '{}' (DE0901)", s), - |diag| { - diag.note("Wildcards (*) are only allowed in permission strings, not in schema_id attributes"); - diag.help("Use concrete type names in schema_id"); - }, - ); - return; - } - - // Use GtsOps::parse_id() for validation - it gives us parsed segments - let result = GtsOps::parse_id(s); - - if !result.ok { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!("invalid GTS schema_id: '{}' (DE0901)", s), - |diag| { - diag.note(result.error); - diag.help("Example: gts.cf.core.events.type.v1~"); - }, - ); - return; - } - - // Ensure it's actually a schema (type), not an instance - if result.is_type != Some(true) { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!( - "schema_id must be a type schema, not an instance: '{}' (DE0901)", - s - ), - |diag| { - diag.note("schema_id must end with '~' to indicate it's a type schema"); - diag.help("Example: gts.cf.core.events.type.v1~"); - }, - ); - } else { - self.check_vendors_in_parse_result(cx, span, s, &result); - } - } - - fn validate_instance_id_segment(&self, cx: &EarlyContext<'_>, span: rustc_span::Span, s: &str) { - let s = s.trim(); - - // `gts_make_instance_id` accepts a single *segment id* (no `gts.` prefix), - // so we must not validate it as a full GTS ID. - // If the input contains delimiters for chained ids / permission strings, - // it is not a single segment. - if s.contains('~') || s.contains(':') { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!( - "gts_make_instance_id expects a single GTS segment, got: '{}' (DE0901)", - s - ), - |diag| { - diag.help("Example: vendor.package.sku.abc.v1"); - }, - ); - return; - } - - if s.contains('*') { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!( - "wildcards are not allowed in instance id segments: '{}' (DE0901)", - s - ), - |diag| { - diag.help("Example: vendor.package.sku.abc.v1"); - }, - ); - return; - } - - match GtsIdSegment::new(0, 0, s) { - Err(e) => { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!("invalid GTS segment: '{}' (DE0901)", s), - |diag| { - diag.note(e.to_string()); - diag.help("Example: vendor.package.sku.abc.v1"); - }, - ); - } - Ok(seg) if !allowed_vendors(cx, span).contains(&seg.vendor.as_str()) => { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!("invalid GTS vendor in segment: '{}' (DE0901)", s), - |diag| { - diag.note(format!( - "found vendor '{}', allowed vendors are 'cf' and 'example'", - seg.vendor - )); - }, - ); - } - Ok(_) => {} - } - } - - fn check_vendors_in_parse_result( - &self, - cx: &EarlyContext<'_>, - span: rustc_span::Span, - s: &str, - result: >s::ops::GtsIdParseResult, - ) { - let vendors = allowed_vendors(cx, span); - for (idx, seg) in result.segments.iter().enumerate() { - if seg.vendor.is_empty() || seg.vendor == "*" || vendors.contains(&seg.vendor.as_str()) - { - continue; - } - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!("invalid GTS vendor in segment #{idx}: '{}' (DE0901)", s), - |diag| { - diag.note(format!( - "found vendor '{}', allowed vendors are 'cf' and 'example'", - seg.vendor - )); - }, - ); - break; - } - } - - fn validate_any_gts_id(&self, cx: &EarlyContext<'_>, span: rustc_span::Span, s: &str) { - let s = s.trim(); - - // Wildcards are NOT allowed in regular GTS strings (only in permission strings) - if s.contains('*') { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!( - "invalid GTS string (wildcards not allowed): '{}' (DE0901)", - s - ), - |diag| { - diag.note("Wildcards (*) are only allowed in permission strings, not in regular GTS identifiers"); - diag.help("Use concrete type names"); - }, - ); - return; - } - - let result = GtsOps::parse_id(s); - - if !result.ok { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!("invalid GTS string: '{}' (DE0901)", s), - |diag| { - diag.note(result.error); - }, - ); - } else { - self.check_vendors_in_parse_result(cx, span, s, &result); - } - } - - fn validate_any_gts_id_allow_wildcards( - &self, - cx: &EarlyContext<'_>, - span: rustc_span::Span, - s: &str, - ) { - let s = s.trim(); - - // For resource_pattern calls, we allow wildcards but still validate the GTS structure - let result = GtsOps::parse_id(s); - - if !result.ok { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!("invalid GTS string: '{}' (DE0901)", s), - |diag| { - diag.note(result.error); - }, - ); - } else { - self.check_vendors_in_parse_result(cx, span, s, &result); - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0901", - "invalid GTS", // Matches both "invalid GTS string" and "invalid GTS schema_id string" - ); - } -} diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.rs b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.rs deleted file mode 100644 index 0e823f8b4..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Test file for invalid schema_id in struct_to_gts_schema attributes - -use gts::GtsInstanceId; -use gts_macros::struct_to_gts_schema; - -#[derive(Debug)] -#[struct_to_gts_schema( - dir_path = "schemas", - base = true, - // Should NOT trigger - valid GTS schema_id string - type_id = "gts.vendor.test.entities.product.v1~", - description = "Product entity", - properties = "id" -)] -pub struct ProductV1 { - pub id: GtsInstanceId, - pub properties: P, -} - -// NOTE: Structs with invalid schema_ids (missing tilde, hyphen, wildcard) were removed. -// gts 0.8.0's struct_to_gts_schema macro now validates schema_ids at compile time, -// rejecting them before the lint can run. String literal checks in main() still -// cover these scenarios at the lint level. - -fn main() { - // Error 1: Incomplete chained segments (missing type component) - // Should trigger DE0901 - invalid GTS string - let _id1 = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.abc.v1~a.b.c"); - - // Error 2: Incomplete segment (missing type component) - // Should trigger DE0901 - invalid GTS format - let _id2 = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.v1"); - - // Error 3: Type schema (ends with ~) - gts_make_instance_id must not accept schemas - // Should trigger DE0901 - invalid GTS entity type (schema instead of instance) - let _id3 = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.abc.v1~"); - - // Error 4: Wildcard - gts_make_instance_id must not accept wildcards - // Should trigger DE0901 - invalid GTS format - let _id4 = ProductV1::<()>::gts_make_instance_id("vendor.package.*.abc.v1"); - - // Error 5: Multiple segments (contains ~) - gts_make_instance_id must accept only ONE instance segment - // Should trigger DE0901 - invalid GTS string - let _id1 = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.abc.v1~a.b.c.d.v1"); - - // Error 6: invalid GTS segment - // Should trigger DE0901 - invalid GTS segment - let _s = "gts.vendor.core.lic.feat.v1~cf.core.global.base"; - - // Error 7: Invalid GTS identifier (no trailing type segment) - // Should trigger DE0901 - invalid GTS indentifier - let _s = "gts.vendor.core.events.type.v1"; - - // Error 8: GTS wildcard is not allowed in regular strings - // Should trigger DE0901 - invalid GTS - let _s = "gts.vendor.core.events.type.*"; - - // Error 9: disallowed vendor in full GTS string - // Should trigger DE0901 - invalid GTS vendor - let _s = "gts.badvendor.core.events.type.v1~"; - - // Error 10: disallowed vendor in instance segment - // Should trigger DE0901 - invalid GTS vendor - let _id_vendor = ProductV1::<()>::gts_make_instance_id("badvendor.package.sku.abc.v1"); - - // Valid case for comparison - // Should NOT trigger - valid GTS instance segment - let _id_valid = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.abc.v1"); - - // Use the bad pattern to suppress unused warning - _use_bad_pattern(); -} - -// Error 11: GTS wildcard in const without _WILDCARD suffix -// Should trigger DE0901 - invalid GTS wildcard const name (must end with _WILDCARD) -const BAD_PATTERN: &str = "gts.vendor.core.srr.resource.v1~*"; - -fn _use_bad_pattern() { - let _ = BAD_PATTERN; -} diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.stderr b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.stderr deleted file mode 100644 index d34f12e47..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.stderr +++ /dev/null @@ -1,94 +0,0 @@ -error: gts_make_instance_id expects a single GTS segment, got: 'vendor.package.sku.abc.v1~a.b.c' (DE0901) - --> $DIR/invalid_cases.rs:28:16 - | -LL | let _id1 = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.abc.v1~a.b.c"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: Example: vendor.package.sku.abc.v1 - = note: `#[deny(de0901_gts_string_pattern)]` on by default - -error: invalid GTS segment: 'vendor.package.sku.v1' (DE0901) - --> $DIR/invalid_cases.rs:32:16 - | -LL | let _id2 = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.v1"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Invalid GTS segment #0 @ offset 0: 'vendor.package.sku.v1': Too few tokens (got 4, min 5). Expected format: vendor.package.namespace.type.vMAJOR[.MINOR] - = help: Example: vendor.package.sku.abc.v1 - -error: gts_make_instance_id expects a single GTS segment, got: 'vendor.package.sku.abc.v1~' (DE0901) - --> $DIR/invalid_cases.rs:36:16 - | -LL | let _id3 = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.abc.v1~"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: Example: vendor.package.sku.abc.v1 - -error: wildcards are not allowed in instance id segments: 'vendor.package.*.abc.v1' (DE0901) - --> $DIR/invalid_cases.rs:40:16 - | -LL | let _id4 = ProductV1::<()>::gts_make_instance_id("vendor.package.*.abc.v1"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: Example: vendor.package.sku.abc.v1 - -error: gts_make_instance_id expects a single GTS segment, got: 'vendor.package.sku.abc.v1~a.b.c.d.v1' (DE0901) - --> $DIR/invalid_cases.rs:44:16 - | -LL | let _id1 = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.abc.v1~a.b.c.d.v1"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: Example: vendor.package.sku.abc.v1 - -error: invalid GTS string: 'gts.vendor.core.lic.feat.v1~cf.core.global.base' (DE0901) - --> $DIR/invalid_cases.rs:48:14 - | -LL | let _s = "gts.vendor.core.lic.feat.v1~cf.core.global.base"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Invalid GTS segment #2 @ offset 28: 'cf.core.global.base': Too few tokens (got 4, min 5). Expected format: vendor.package.namespace.type.vMAJOR[.MINOR] - -error: invalid GTS string: 'gts.vendor.core.events.type.v1' (DE0901) - --> $DIR/invalid_cases.rs:52:14 - | -LL | let _s = "gts.vendor.core.events.type.v1"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Invalid GTS identifier: gts.vendor.core.events.type.v1: Single-segment instance IDs are prohibited. Instance IDs must be chained with at least one type segment (e.g., 'type~instance') - -error: invalid GTS string (wildcards not allowed): 'gts.vendor.core.events.type.*' (DE0901) - --> $DIR/invalid_cases.rs:56:14 - | -LL | let _s = "gts.vendor.core.events.type.*"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Wildcards (*) are only allowed in permission strings, not in regular GTS identifiers - = help: Use concrete type names - -error: invalid GTS vendor in segment #0: 'gts.badvendor.core.events.type.v1~' (DE0901) - --> $DIR/invalid_cases.rs:60:14 - | -LL | let _s = "gts.badvendor.core.events.type.v1~"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: found vendor 'badvendor', allowed vendors are 'cf' and 'example' - -error: invalid GTS vendor in segment: 'badvendor.package.sku.abc.v1' (DE0901) - --> $DIR/invalid_cases.rs:64:22 - | -LL | let _id_vendor = ProductV1::<()>::gts_make_instance_id("badvendor.package.sku.abc.v1"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: found vendor 'badvendor', allowed vendors are 'cf' and 'example' - -error: GTS wildcard string in `const`/`static` `BAD_PATTERN` must have a name ending with `_WILDCARD` (DE0901) - --> $DIR/invalid_cases.rs:76:1 - | -LL | const BAD_PATTERN: &str = "gts.vendor.core.srr.resource.v1~*"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: found wildcard GTS pattern `gts.vendor.core.srr.resource.v1~*` stored in `BAD_PATTERN` - = help: rename to `BAD_PATTERN_WILDCARD` or use a non-wildcard value - -error: aborting due to 11 previous errors - diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.rs b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.rs deleted file mode 100644 index b679b3a13..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Test file for permission strings with colon-separated parts -//! Security claims can contain any GTS format: type schemas, instance segments, or wildcards -//! These should NOT trigger the lint because they are in permission string context - -fn main() { - // Test 1: Permission string with GTS type schema (ending with ~) - // Should NOT trigger DE0901 - type schemas are allowed in permission strings - let _perm1 = "550e8400-e29b-41d4-a716-446655440000:gts.cf.core.events.topic.v1~:*:publish"; - - // Test 2: Permission string with GTS instance segment (not ending with ~) - // Should NOT trigger DE0901 - instance segments are allowed in permission strings - let _perm2 = "550e8400-e29b-41d4-a716-446655440000:gts.cf.core.events.tenant.v1~cf.core.example.tenant.v1:660e8400-e29b-41d4-a716-446655440002:edit"; - - // Test 3: Permission string with GTS wildcard pattern - // Should trigger DE0901 - invalid GTS - let _perm3 = "resource-id:gts.cf.*.events.*.v1~:action:scope"; - - // Test 4: Permission string with GTS wildcard pattern - // Should trigger DE0901 - invalid GTS format - let _perm3 = "resource-id:gts.cf.core.events.event.v1~a.b.c~:action:scope"; - - // Test 5: Invalid GTS instance segment - // Should trigger DE0901 - invalid GTS - let _perm2 = "550e8400-e29b-41d4-a716-446655440000:gts.cf.events.tenant.v1:660e8400-e29b-41d4-a716-446655440002:edit"; - - // Test 6: Invalid GTS identifier (not leading type segment) - // Should trigger DE0901 - invalid GTS - let _perm5 = "uuid:gts.vendor.pkg.ns.type.v1:action"; - - // Test 7: Disallowed vendor in permission string - // Should trigger DE0901 - invalid GTS vendor - let _perm6 = "uuid:gts.badvendor.pkg.ns.type.v1~cf.pkg.ns.derived.v1~:action"; - - let _perm7 = MockPermissionBuilder::default() - // Should trigger DE0901 - invalid GTS - .resource_pattern("gts.cf.core.events.type.v*") - .build(); - - // Additional valid cases - - // Should NOT trigger DE0901 - typical permission string - let _perm4 = "uuid:gts.cf.pkg.ns.type.v1~:action"; - - // Should NOT trigger DE0901 - wildcards are allowed in resource_pattern() calls - let _perm = MockPermissionBuilder::default() - .resource_pattern("gts.cf.core.events.topic.v1~example.*") - .build(); - - // Should NOT trigger DE0901 - wildcards are allowed in resource_pattern() calls - let _perm8 = MockPermissionBuilder::default() - .resource_pattern("gts.cf.core.events.type.v1~*") - .build(); - - // Should NOT trigger DE0901 - wildcards are allowed in resolve_to_uuids() calls - let _resolver = MockResolver; - _resolver.resolve_to_uuids(&["gts.example.core.events.*".to_owned()]); -} - -#[derive(Default)] -struct MockResolver; - -impl MockResolver { - fn resolve_to_uuids(&self, _patterns: &[String]) -> Vec { - vec![] - } -} - -#[derive(Default)] -struct MockPermissionBuilder { - resource_pattern: Option, -} - -impl MockPermissionBuilder { - fn resource_pattern(mut self, pattern: &str) -> Self { - self.resource_pattern = Some(pattern.to_owned()); - self - } - - fn build(self) -> String { - self.resource_pattern.unwrap_or_default() - } -} diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.stderr b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.stderr deleted file mode 100644 index 60f9de049..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.stderr +++ /dev/null @@ -1,51 +0,0 @@ -error: invalid GTS string: 'gts.cf.*.events.*.v1~' (DE0901) - --> $DIR/permission_strings.rs:16:18 - | -LL | let _perm3 = "resource-id:gts.cf.*.events.*.v1~:action:scope"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Invalid GTS wildcard pattern: gts.cf.*.events.*.v1~: The wildcard '*' token is allowed only once - = note: `#[deny(de0901_gts_string_pattern)]` on by default - -error: invalid GTS string: 'gts.cf.core.events.event.v1~a.b.c~' (DE0901) - --> $DIR/permission_strings.rs:20:18 - | -LL | let _perm3 = "resource-id:gts.cf.core.events.event.v1~a.b.c~:action:scope"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Invalid GTS segment #2 @ offset 28: 'a.b.c~': Too few tokens (got 3, min 5). Expected format: vendor.package.namespace.type.vMAJOR[.MINOR] - -error: invalid GTS string: 'gts.cf.events.tenant.v1' (DE0901) - --> $DIR/permission_strings.rs:24:18 - | -LL | let _perm2 = "550e8400-e29b-41d4-a716-446655440000:gts.cf.events.tenant.v1:660e8400-e29b-41d4-a716-446655440002:edit"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Invalid GTS segment #1 @ offset 4: 'cf.events.tenant.v1': Too few tokens (got 4, min 5). Expected format: gts.vendor.package.namespace.type.vMAJOR[.MINOR] - -error: invalid GTS string: 'gts.vendor.pkg.ns.type.v1' (DE0901) - --> $DIR/permission_strings.rs:28:18 - | -LL | let _perm5 = "uuid:gts.vendor.pkg.ns.type.v1:action"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Invalid GTS identifier: gts.vendor.pkg.ns.type.v1: Single-segment instance IDs are prohibited. Instance IDs must be chained with at least one type segment (e.g., 'type~instance') - -error: invalid GTS vendor in segment #0: 'gts.badvendor.pkg.ns.type.v1~cf.pkg.ns.derived.v1~' (DE0901) - --> $DIR/permission_strings.rs:32:18 - | -LL | let _perm6 = "uuid:gts.badvendor.pkg.ns.type.v1~cf.pkg.ns.derived.v1~:action"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: found vendor 'badvendor', allowed vendors are 'cf' and 'example' - -error: invalid GTS string: 'gts.cf.core.events.type.v*' (DE0901) - --> $DIR/permission_strings.rs:36:27 - | -LL | .resource_pattern("gts.cf.core.events.type.v*") - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Invalid GTS wildcard pattern: gts.cf.core.events.type.v*: The wildcard '*' token is allowed only at the end of the pattern - -error: aborting due to 6 previous errors - diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/valid_use_cases.rs b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/valid_use_cases.rs deleted file mode 100644 index 299bcb0ae..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/valid_use_cases.rs +++ /dev/null @@ -1,86 +0,0 @@ -// Test file for valid GTS strings and gts-macros annotations - should not trigger DE0901 - -use gts::{GtsInstanceId, GtsWildcard}; -use gts_macros::struct_to_gts_schema; - -#[derive(Debug)] -#[struct_to_gts_schema( - dir_path = "schemas", - base = true, - // Should NOT trigger DE0901 - valid GTS schema_id string - type_id = "gts.example.core.events.topic.v1~", - description = "Event Topic definition", - properties = "id,name" -)] -pub struct EventTopicV1 { - pub id: GtsInstanceId, - pub name: String, - pub properties: T, -} - -#[derive(Debug)] -#[struct_to_gts_schema( - dir_path = "schemas", - base = true, - // Should NOT trigger DE0901- valid GTS schema_id string - type_id = "gts.example.core.events.type.v1~", - description = "Base event type definition", - properties = "id" -)] -pub struct BaseEventTypeV1 { - pub id: GtsInstanceId, - pub properties: P, -} - -#[derive(Debug)] -#[struct_to_gts_schema( - dir_path = "schemas", - base = BaseEventTypeV1, - // Should NOT trigger DE0901 - valid GTS schema_id string with inheritance - type_id = "gts.example.core.events.type.v1~cf.core.audit.event.v1~", - description = "Audit event", - properties = "user_id" -)] -pub struct AuditEventV1 { - pub user_id: String, -} - -// Should NOT trigger DE0901 - wildcard const has _WILDCARD suffix -const SRR_WILDCARD: &str = "gts.example.core.srr.resource.v1~*"; - -fn main() { - // Should NOT trigger DE0901 - valid GTS instance segment - let _id = EventTopicV1::<()>::gts_make_instance_id("example.commerce.orders.orders.v1.0"); - - // Should NOT trigger DE0901 - valid GTS type schema string - let _s1 = "gts.example.core.events.type.v1~"; - - // Should NOT trigger DE0901 - valid GTS type schema string with inheritance - let _s2 = "gts.example.core.events.type.v1~cf.core.audit.event.v1~"; - - // Should NOT trigger DE0901 - strings inside starts_with() should be ignored - let _check = "some.invalid.gts.string".starts_with("gts."); - // Should NOT trigger DE0901 - strings inside starts_with() should be ignored - let _check2 = "another.invalid.gts.string".starts_with("gts.example.core."); - - // Should NOT trigger DE0901 - GtsWildcard::new() accepts wildcard patterns - let _wc1 = GtsWildcard::new("gts.example.core.srr.resource.v1~*"); - - // Should NOT trigger DE0901 - GtsWildcard::new() accepts wildcard with sub-prefix - let _wc2 = GtsWildcard::new("gts.example.core.srr.resource.v1~example.*"); - - // Should NOT trigger DE0901 - gts::GtsWildcard::new() qualified path form - let _wc3 = gts::GtsWildcard::new("gts.example.core.events.type.v1~*"); - - // Should NOT trigger DE0901 - const holding wildcard used with GtsWildcard::new() - let _wc4 = GtsWildcard::new(SRR_WILDCARD); -} - -// Vendor checks are skipped in #[cfg(test)] modules - any vendor is allowed -#[cfg(test)] -mod test_vendors_allowed { - // Should NOT trigger DE0901 - vendor checks are skipped in test code - fn _test_acme_vendor() { - let _s = "gts.acme.core.events.type.v1~"; - } -} diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/valid_use_cases.stderr b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/valid_use_cases.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/Cargo.toml b/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/Cargo.toml deleted file mode 100644 index 20f7fe3ae..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "de0902_no_schema_for_on_gts_structs" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Prohibit using schemars::schema_for!() on GTS-wrapped structs" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "gts_struct_schema_for" -path = "ui/gts_struct_schema_for.rs" - -[[example]] -name = "non_gts_struct_schema_for" -path = "ui/non_gts_struct_schema_for.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -gts-macros.workspace = true -gts.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/README.md b/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/README.md deleted file mode 100644 index 1c40c9f81..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# DE0110: No `schema_for!` on GTS Structs - -## What it does - -Detects usage of `schemars::schema_for!()` macro on GTS-wrapped structs (those using `#[struct_to_gts_schema]`). - -## Why is this bad? - -GTS-wrapped structs **must** use `gts_schema_with_refs_as_string()` for schema generation because: - -1. **Correct `$id`**: It automatically sets the correct `$id` field, no need to do it manually -2. **Proper `$ref`s**: It generates proper schema with `$ref` references, while `schema_for!` inlines everything - -## Example - -```rust -// BAD - uses schemars::schema_for!() on a GTS struct -use schemars::schema_for; - -#[struct_to_gts_schema(...)] -pub struct MyPluginSpec { ... } - -let schema = schema_for!(MyPluginSpec); // ❌ Will trigger DE0110 -``` - -Use instead: - -```rust -// GOOD - uses GTS-provided method -#[struct_to_gts_schema(...)] -pub struct MyPluginSpec { ... } - -let schema = MyPluginSpec::gts_schema_with_refs_as_string(); // ✅ Correct -``` - -## Detection - -The lint detects `schema_for!` macro invocations where the type argument has the `#[struct_to_gts_schema]` attribute. Types with this attribute implement the `gts::GtsSchema` trait and have a `GTS_SCHEMA_ID` constant. diff --git a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/src/lib.rs b/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/src/lib.rs deleted file mode 100644 index d35d36af3..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/src/lib.rs +++ /dev/null @@ -1,157 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_hir; -extern crate rustc_middle; -extern crate rustc_span; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_hir::{Expr, ExprKind, GenericArg}; -use rustc_lint::{LateContext, LateLintPass, LintContext}; -use rustc_middle::ty::{self, Ty}; -use rustc_span::Symbol; - -dylint_linting::declare_late_lint! { - /// ### What it does - /// - /// Detects usage of `schemars::schema_for!()` macro on GTS-wrapped structs. - /// - /// ### Why is this bad? - /// - /// GTS-wrapped structs (those using `#[struct_to_gts_schema]`) must use - /// `gts_schema_with_refs_as_string()` for schema generation because: - /// - /// 1. **Performance**: It is static (computed at compile time), so it's faster - /// 2. **Correct `$id`**: It automatically sets the correct `$id` field - /// 3. **Proper `$ref`s**: It generates proper schema with `$ref` references, - /// while `schema_for!` inlines everything - /// - /// ### Example - /// - /// ```rust - /// // Bad - uses schema_for! on a GTS struct - /// #[struct_to_gts_schema(...)] - /// pub struct MyPluginSpec { ... } - /// - /// let schema = schemars::schema_for!(MyPluginSpec); - /// ``` - /// - /// Use instead: - /// - /// ```rust - /// // Good - uses GTS-provided method - /// #[struct_to_gts_schema(...)] - /// pub struct MyPluginSpec { ... } - /// - /// let schema = MyPluginSpec::gts_schema_with_refs_as_string(); - /// ``` - pub DE0902_NO_SCHEMA_FOR_ON_GTS_STRUCTS, - Deny, - "GTS structs must use gts_schema_with_refs_as_string() instead of schema_for!() (DE0902)" -} - -/// Check if a type has the gts_schema_with_refs_as_string method. -/// GTS types generated by `#[struct_to_gts_schema]` have this method. -fn is_gts_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { - // Get the ADT (struct/enum) definition if this is one - if let ty::Adt(adt_def, _) = ty.kind() { - let def_id = adt_def.did(); - - // Check if this type has a method named gts_schema_with_refs_as_string - let gts_method = Symbol::intern("gts_schema_with_refs_as_string"); - - for item in cx.tcx.inherent_impls(def_id).iter() { - for &assoc_item_def_id in cx.tcx.associated_item_def_ids(*item) { - let assoc_item = cx.tcx.associated_item(assoc_item_def_id); - if assoc_item.name() == gts_method { - return true; - } - } - } - } - false -} - -/// Extract type name for error message -fn get_type_name<'tcx>(ty: Ty<'tcx>) -> String { - if let ty::Adt(adt_def, _) = ty.kind() { - adt_def.variant(0u32.into()).name.to_string() - } else { - ty.to_string() - } -} - -impl<'tcx> LateLintPass<'tcx> for De0902NoSchemaForOnGtsStructs { - fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { - // The schema_for! macro expands to: - // schemars::gen::SchemaGenerator::default().into_root_schema_for::() - // - // We look for method calls to `into_root_schema_for` with a GTS type as type parameter. - - if let ExprKind::MethodCall(segment, _receiver, _args, _span) = expr.kind { - let method_name = segment.ident.name.as_str(); - - // Check for into_root_schema_for (from schema_for! macro expansion) - if method_name == "into_root_schema_for" { - // Only report if this comes from a schema_for! macro invocation, - // not from derive macro expansions - let callsite_span = expr.span.source_callsite(); - - // Check if the callsite is a schema_for! macro call by looking at the source - let source_map = cx.sess().source_map(); - let snippet = source_map - .span_to_snippet(callsite_span) - .unwrap_or_default(); - if !snippet.contains("schema_for!") { - return; - } - - // Check the generic type argument - if let Some(args) = segment.args { - for arg in args.args { - if let GenericArg::Type(hir_ty) = arg - && let Some(ty) = cx.typeck_results().node_type_opt(hir_ty.hir_id) - && is_gts_type(cx, ty) - { - let type_name = get_type_name(ty); - span_lint_and_then( - cx, - DE0902_NO_SCHEMA_FOR_ON_GTS_STRUCTS, - callsite_span, - format!( - "do not use `schema_for!({})` on GTS-wrapped struct (DE0902)", - type_name - ), - |diag| { - diag.help(format!( - "use `{}::gts_schema_with_refs_as_string()` instead for proper `$id` and `$ref` handling", - type_name - )); - }, - ); - return; - } - } - } - } - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE0902", - "schema_for on GTS struct", - ); - } -} diff --git a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs b/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs deleted file mode 100644 index 8106f06b9..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Test case: schema_for! on GTS-wrapped struct should trigger DE0902 - -use gts_macros::struct_to_gts_schema; - -/// A GTS-wrapped struct (has struct_to_gts_schema attribute) -#[derive(Debug, Clone)] -#[struct_to_gts_schema( - dir_path = "schemas", - base = true, - type_id = "gts.cf.core.test.plugin.v1~", - description = "Test plugin specification", - properties = "id,vendor" -)] -pub struct MyGtsPluginSpecV1 { - pub id: gts::GtsInstanceId, - pub vendor: String, -} - -fn main() { - // Should trigger DE0902 - schema_for on GTS struct - let _schema = schemars::schema_for!(MyGtsPluginSpecV1); -} diff --git a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr b/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr deleted file mode 100644 index a7433cefe..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: do not use `schema_for!(MyGtsPluginSpecV1)` on GTS-wrapped struct (DE0902) - --> $DIR/gts_struct_schema_for.rs:21:19 - | -LL | let _schema = schemars::schema_for!(MyGtsPluginSpecV1); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: use `MyGtsPluginSpecV1::gts_schema_with_refs_as_string()` instead for proper `$id` and `$ref` handling - = note: `#[deny(de0902_no_schema_for_on_gts_structs)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.rs b/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.rs deleted file mode 100644 index 9ac0c8100..000000000 --- a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Test case: schema_for! on regular (non-GTS) struct should NOT trigger DE0110 - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// A regular struct (NOT GTS-wrapped, no struct_to_gts_schema attribute) -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct RegularDto { - pub id: String, - pub name: String, -} - -fn main() { - // Should not trigger DE0110 - schema_for on non-GTS struct - let _schema = schemars::schema_for!(RegularDto); -} diff --git a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.stderr b/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/non_gts_struct_schema_for.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/AGENTS.md b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/AGENTS.md deleted file mode 100644 index e3d8d95e7..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/AGENTS.md +++ /dev/null @@ -1,58 +0,0 @@ - -# DE1101: Enabling the lint for a new gear - -## Quick start - -Three steps to enable DE1101 for a module that's currently excluded: - -### 1. Remove the module from `dylint.toml` exclusions - -Open `dylint.toml` in the workspace root and delete the gear's line from `[de1101_tests_in_separate_files].excluded_paths`: - -```toml -[de1101_tests_in_separate_files] -excluded_paths = [ - # ... - # "gears/my-gear", ← delete this line - # ... -] -``` - -### 2. Extract inline tests - -Run the extraction script from the workspace root: - -```sh -python3 dylint_lints/de11_testing/de1101_tests_in_separate_files/extract_tests.py . -``` - -The script will: -- Find all `#[cfg(test)] mod tests { ... }` inline blocks in `.rs` files -- Extract each test body into a `_tests.rs` file next to the source -- Replace the inline block with `#[cfg(test)] #[path = "_tests.rs"] mod _tests;` -- Print `WARN` for `#[cfg(test)]` on individual items (structs, impls) — fix those manually -- Skip `tests/`, `ui/`, `target/`, `.git/` directories automatically - -### 3. Format and verify - -```sh -cargo fmt --all -make check -``` - -`make check` runs fmt, clippy, dylint, and all tests. If it passes, you're done. - -## Common issues after extraction - -| Problem | Cause | Fix | -|---------|-------|-----| -| `unused import: super::*` | Test doesn't use parent module items | Remove `use super::*;` line | -| Clippy lint fires in test file | Test code was hidden behind `#[cfg(test)]`, now visible to clippy | Add `#![allow(clippy::the_lint)]` at top of test file | -| `#[cfg(test)]` on individual items | Script only extracts `mod` blocks, not standalone items | Move manually or leave as-is (these don't trigger DE1101) | - -## What the lint enforces - -For any `.rs` file in scope: -- No inline `#[test]` or `#[cfg(test)] mod ... { }` blocks -- Test module name must be `{source_stem}_tests` (e.g. `handler.rs` → `mod handler_tests;`) -- If `#[path = "..."]` is used, it must point to `{source_stem}_tests.rs` diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/Cargo.toml b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/Cargo.toml deleted file mode 100644 index 18944d56c..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -# Created: 2026-04-07 by Constructor Tech -[package] -name = "de1101_tests_in_separate_files" -version = "0.1.0" -authors.workspace = true -edition.workspace = true -license.workspace = true -description = "Tests must live in separate files for LOC filtering, easier navigation for humans and LLMs, and strict separation so tests never contain production logic (DE1101)" -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "resource_group_inline_case" -path = "ui/resource_group_inline_case.rs" - -[[example]] -name = "resource_group_out_of_line_test_gear" -path = "ui/resource_group_out_of_line_test_gear.rs" - -[[example]] -name = "resource_group_separate_test_file_tests" -path = "ui/resource_group_separate_test_file_tests.rs" - -[[example]] -name = "other_gear_inline_case" -path = "ui/other_gear_inline_case.rs" - -[[example]] -name = "resource_group_cfg_not_test" -path = "ui/resource_group_cfg_not_test.rs" - -[[example]] -name = "naming_correct_case" -path = "ui/naming_correct_case.rs" - -[[example]] -name = "naming_wrong_module" -path = "ui/naming_wrong_gear.rs" - -[[example]] -name = "naming_path_forbidden" -path = "ui/naming_path_forbidden.rs" - -[[example]] -name = "naming_path_correct" -path = "ui/naming_path_correct.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true -serde.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/README.md b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/README.md deleted file mode 100644 index 68a3663fb..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/README.md +++ /dev/null @@ -1,122 +0,0 @@ - -# DE1101: Tests Must Be In Separate Files - -## What it does - -This lint forbids inline test code inside production Rust files. It scans production `.rs` files (not in `tests/`, not `*_tests.rs`) and reports violations. - -## Why - -Keeping tests in separate files makes it easier to: - -- filter test files out when counting lines of code -- navigate the codebase for both humans and LLMs because files stay smaller -- keep production logic and test code separated by file type - -## Triggers (error) - -### Inline `#[test]` in a production file - -```rust -// handler.rs -fn handle() {} - -#[test] // ❌ DE1101: test code must be moved to a separate test file -fn test_handle() {} -``` - -### Inline `#[cfg(test)] mod ... { }` block - -```rust -// handler.rs -#[cfg(test)] // ❌ DE1101: test code must be moved to a separate test file -mod tests { - #[test] - fn test_handle() {} -} -``` - -### `#[path]` pointing to wrong file - -```rust -// handler.rs -#[cfg(test)] -#[path = "dto_tests.rs"] // ❌ DE1101: must reference handler_tests.rs -mod tests; -``` - -## Does not trigger (ok) - -### Out-of-line mod — any name, no `#[path]` - -```rust -// handler.rs -#[cfg(test)] -mod handler_tests; // ✅ ok -``` - -```rust -// handler.rs -#[cfg(test)] -mod tests; // ✅ ok — without #[path], any module name is accepted -``` - -### `#[path]` pointing to correct file - -```rust -// handler.rs -#[cfg(test)] -#[path = "handler_tests.rs"] -mod tests; // ✅ ok -``` - -### Test files — not scanned - -```rust -// handler_tests.rs — test file, lint does not scan it -#[test] -fn test_handle() {} // ✅ ok — *_tests.rs files are skipped -``` - -```rust -// tests/integration.rs — integration test, lint does not scan it -#[test] -fn e2e() {} // ✅ ok — files in tests/ are skipped -``` - -### `#[cfg(test)]` on items (not modules) - -```rust -// handler.rs -#[cfg(test)] -impl Handler { // ✅ does not trigger (this is not #[test] and not an inline mod) - fn test_helper() {} -} -``` - -## Not scanned at all - -- Files ending with `_tests.rs` -- Files under `tests/` directories -- Gears listed in `excluded_paths` in `dylint.toml` - -## Configuration - -Exclusions are configured in `dylint.toml` at the workspace root: - -```toml -[de1101_tests_in_separate_files] -excluded_paths = [ - "libs/toolkit", - "gears/mini-chat", - # ... -] -``` - -Each entry is a module path prefix (e.g. `libs/toolkit`, `gears/system/oagw`). Remove entries one by one as gears are migrated. - -## Relation to Rust Book Guidance - -The Rust Book recommends keeping `#[cfg(test)] mod tests { ... }` inline in the same file as the production code ([ch11-03](https://doc.rust-lang.org/book/ch11-03-test-organization.html)). That is valid and idiomatic Rust. - -This lint intentionally adopts a stricter repository-level policy. The cost is deviation from the most common Rust convention. The benefit is stronger separation between production and test code, easier LOC filtering, smaller production files, and simpler navigation for both humans and LLMs. diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/extract_tests.py b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/extract_tests.py deleted file mode 100644 index 3ccb0b432..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/extract_tests.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 -# Created: 2026-04-07 by Constructor Tech -"""Extract inline #[cfg(test)] mod tests { ... } blocks into separate *_tests.rs files. - -Usage: - python3 scripts/extract_tests.py - -For each .rs file in (recursive) that contains an inline test module, -the script: - 1. Extracts the test body into _tests.rs - 2. Replaces the inline block with an out-of-line #[path = "..."] reference - 3. Preserves #[cfg_attr(coverage_nightly, coverage(off))] if present -""" - -import os -import sys - - -def find_inline_test_block(lines): - """Find the first inline #[cfg(test)] mod ... { } block. Returns (start_idx, end_idx) or None.""" - for i, line in enumerate(lines): - stripped = line.strip() - if stripped.startswith("//"): - continue - compact = stripped.replace(" ", "") - if compact != "#[cfg(test)]": - continue - # Look ahead for "mod {" - for j in range(i + 1, min(i + 6, len(lines))): - s = lines[j].strip() - if s.startswith("//") or s == "" or s.startswith("#["): - continue - if s.startswith("mod ") and "{" in s: - # Found inline test module, now find closing brace - brace_depth = 0 - mod_end = None - for k in range(i, len(lines)): - for ch in lines[k]: - if ch == "{": - brace_depth += 1 - elif ch == "}": - brace_depth -= 1 - if brace_depth == 0: - mod_end = k - break - if mod_end is not None: - break - if mod_end is not None: - return (i, mod_end) - break - return None - - -def extract_test_body(lines, start, end): - """Extract the body of a mod block (everything between { and }).""" - body_lines = [] - inside = False - for i in range(start, end + 1): - line = lines[i] - if not inside: - if "{" in line: - inside = True - after = line[line.index("{") + 1 :] - if after.strip(): - body_lines.append(after) - continue - if i == end: - before = line[: line.rindex("}")] - if before.strip(): - body_lines.append(before) - else: - body_lines.append(line) - - # Dedent - min_indent = 999 - for tl in body_lines: - if tl.strip(): - min_indent = min(min_indent, len(tl) - len(tl.lstrip())) - if min_indent == 999: - min_indent = 0 - body_lines = [tl[min_indent:] if len(tl) >= min_indent else tl for tl in body_lines] - return "\n".join(body_lines).strip() + "\n" - - -def has_coverage_attr(lines, start, end): - for i in range(start, min(start + 4, end + 1)): - if "coverage" in lines[i]: - return True - return False - - -def has_super_import(text): - for line in text.split("\n"): - stripped = line.strip() - if stripped.startswith("use super::"): - return True - return False - - -def process_file(fpath): - with open(fpath, encoding="utf-8") as f: - content = f.read() - lines = content.split("\n") - - block = find_inline_test_block(lines) - if block is None: - return False - - start, end = block - test_body = extract_test_body(lines, start, end) - coverage = has_coverage_attr(lines, start, end) - - # Build test file content - if not has_super_import(test_body): - test_content = "#[allow(unused_imports)]\nuse super::*;\n\n" + test_body - else: - test_content = test_body - - # Build replacement - stem = os.path.basename(fpath).replace(".rs", "") - test_filename = f"{stem}_tests.rs" - replacement = ["#[cfg(test)]"] - if coverage: - replacement.append("#[cfg_attr(coverage_nightly, coverage(off))]") - replacement.append(f'#[path = "{test_filename}"]') - replacement.append(f"mod {stem}_tests;") - - # Write source — preserve any code after the test block - new_lines = lines[:start] + replacement + [""] + lines[end + 1:] - with open(fpath, "w", encoding="utf-8") as f: - f.write("\n".join(new_lines)) - - # Write test file — refuse to overwrite an existing companion file - test_filepath = os.path.join(os.path.dirname(fpath), test_filename) - if os.path.exists(test_filepath): - print(f" SKIP {fpath}: {test_filename} already exists, not overwriting") - return False - with open(test_filepath, "x", encoding="utf-8") as f: - f.write(test_content) - - print(f" {fpath} -> {test_filename}") - return True - - -def find_cfg_test_items(fpath): - """Find #[cfg(test)] on individual items (not modules) — these also trigger DE1101.""" - with open(fpath, encoding="utf-8") as f: - content = f.read() - lines = content.split("\n") - items = [] - for i, line in enumerate(lines): - stripped = line.strip() - compact = stripped.replace(" ", "") - if compact != "#[cfg(test)]": - continue - # Check what follows — if it's NOT a mod declaration, it's an item - for j in range(i + 1, min(i + 6, len(lines))): - s = lines[j].strip() - if s.startswith("//") or s == "" or s.startswith("#["): - continue - if not s.startswith("mod "): - items.append((i, s[:60])) - break - return items - - -## Directories that must be skipped entirely. -## - `tests/` contains integration tests (separate crates, `super` is invalid) -## - `ui/` contains dylint UI-example fixtures that must keep inline tests -SKIP_DIRS = {"tests", "ui", "target", ".git"} - - -def should_skip(root): - """Return True if *root* is inside a directory that must not be touched.""" - parts = root.replace("\\", "/").split("/") - return bool(SKIP_DIRS.intersection(parts)) - - -def main(): - if len(sys.argv) < 2: - print(f"Usage: {sys.argv[0]} ") - sys.exit(1) - - target_dir = sys.argv[1] - count = 0 - - for root, dirs, files in os.walk(target_dir): - dirs[:] = [d for d in dirs if d not in SKIP_DIRS] - - for fname in sorted(files): - if not fname.endswith(".rs"): - continue - if fname.endswith("_tests.rs") or fname.endswith("_test.rs"): - continue - fpath = os.path.join(root, fname) - if process_file(fpath): - count += 1 - - print(f"\nExtracted {count} inline test modules.") - - # Report remaining #[cfg(test)] on individual items - print("\nChecking for #[cfg(test)] on individual items (may need manual fix)...") - warn_count = 0 - for root, dirs, files in os.walk(target_dir): - dirs[:] = [d for d in dirs if d not in SKIP_DIRS] - for fname in sorted(files): - if not fname.endswith(".rs") or fname.endswith("_tests.rs") or fname.endswith("_test.rs"): - continue - fpath = os.path.join(root, fname) - items = find_cfg_test_items(fpath) - for line_num, preview in items: - print(f" WARN: {fpath}:{line_num + 1} — #[cfg(test)] on item: {preview}") - warn_count += 1 - if warn_count == 0: - print(" None found.") - else: - print(f"\n {warn_count} items with #[cfg(test)] — these may trigger DE1101.") - - -if __name__ == "__main__": - main() diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/src/lib.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/src/lib.rs deleted file mode 100644 index dd86714f3..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/src/lib.rs +++ /dev/null @@ -1,723 +0,0 @@ -// Created: 2026-04-07 by Constructor Tech -// Updated: 2026-04-14 by Constructor Tech -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::Item; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; -use std::cell::RefCell; -use std::collections::HashSet; - -thread_local! { - static SCANNED_FILES: RefCell> = RefCell::new(HashSet::new()); -} - -/// Known path prefixes for module directories, longest-first -/// so that `gears/system/` matches before `gears/`. -/// Top-level dirs (`examples/`, `apps/`, `plugins/`) are included so they -/// go through the same segment-boundary check and config-driven exclusion. -const MODULE_PREFIXES: &[&str] = &[ - "libs/", - "gears/system/", - "gears/", - "examples/", - "apps/", - "plugins/", -]; - -const DEFAULT_MAX_INLINE_TEST_LINES: usize = 100; - -#[derive(Default, serde::Deserialize)] -struct Config { - #[serde(default)] - excluded_paths: Vec, - /// Maximum number of test lines allowed inline before the linter enforces - /// moving them to a separate file. Set to 0 to always require separation. - /// Default: 100. - #[serde(default)] - max_inline_test_lines: Option, -} - -struct De1101TestsInSeparateFiles { - excluded_set: HashSet, - max_inline_test_lines: usize, -} - -impl De1101TestsInSeparateFiles { - pub fn new() -> Self { - let config: Config = dylint_linting::config_or_default(env!("CARGO_PKG_NAME")); - Self { - excluded_set: config.excluded_paths.into_iter().collect(), - max_inline_test_lines: config - .max_inline_test_lines - .unwrap_or(DEFAULT_MAX_INLINE_TEST_LINES), - } - } - - fn is_in_scope(&self, normalized_path: &str) -> bool { - // Try to extract a module key (e.g. "libs/toolkit", "gears/system/oagw", - // "examples/oop-gears"). - for prefix in MODULE_PREFIXES { - if let Some(pos) = normalized_path.find(prefix) { - // Ensure match is at a path segment boundary, not inside - // a compound directory name like "oop-gears/". - if pos > 0 && normalized_path.as_bytes()[pos - 1] != b'/' { - continue; - } - let rest = &normalized_path[pos + prefix.len()..]; - let seg_end = rest.find('/').unwrap_or(rest.len()); - let key = &normalized_path[pos..pos + prefix.len() + seg_end]; - return !self.excluded_set.contains(key); - } - } - - true - } -} - -dylint_linting::impl_pre_expansion_lint! { - /// DE1101: Tests must be in separate files - /// - /// ### Why - /// - /// Keeping tests in separate files makes it easier to: - /// - filter test files out when counting lines of code - /// - navigate the codebase for both humans and LLMs because files stay smaller - /// - keep production logic and test code separated by file type - /// - /// Test files should never be the place where production logic lives. - /// - /// Test code is allowed in: - /// - integration tests under `tests/` - /// - dedicated unit-test files named `{source_stem}_tests.rs` - /// - /// Test code is forbidden inline inside production source files when: - /// - the inline test block exceeds `max_inline_test_lines` (default: 100), OR - /// - a companion `{source_stem}_tests.rs` file already exists (tests must not - /// be split across two files) - /// - /// Additionally: - /// - test module reference must resolve to `{source_stem}_tests.rs` - /// - if `#[path = "..."]` is used, its value must be `{source_stem}_tests.rs` - /// - if no `#[path]`, the module name must be `{source_stem}_tests` - pub DE1101_TESTS_IN_SEPARATE_FILES, - Deny, - "tests must live in separate files, not inline in production files (DE1101)", - De1101TestsInSeparateFiles::new() -} - -/// The kind of test-declaration violation found in a source file. -enum TestViolation { - /// Inline test code (`#[test]` or `#[cfg(test)] mod tests { ... }`) in a production file. - InlineTestCode, - /// Inline test code when a companion `_tests.rs` file already exists — always denied. - InlineTestCodeWithCompanion, - /// `#[path = "..."]` value does not match `{source_stem}_tests.rs`. - WrongPathAttr { expected: String, actual: String }, -} - -impl EarlyLintPass for De1101TestsInSeparateFiles { - fn check_crate_post(&mut self, _cx: &EarlyContext<'_>, _krate: &rustc_ast::Crate) { - SCANNED_FILES.with(|files| files.borrow_mut().clear()); - } - - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - let Some(path) = lint_utils::filename_str(cx.sess().source_map(), item.span) else { - return; - }; - - let normalized = path.replace('\\', "/"); - - if !self.is_in_scope(&normalized) { - return; - } - - let should_scan = SCANNED_FILES.with(|files| files.borrow_mut().insert(normalized.clone())); - if !should_scan { - return; - } - - let Ok(source) = std::fs::read_to_string(&path) else { - return; - }; - - if is_allowed_test_file(&normalized) { - return; - } - - let source_stem = file_stem(&normalized); - let has_companion = has_companion_test_file(&path, source_stem.as_deref()); - let violations = find_test_violations( - &source, - source_stem.as_deref(), - has_companion, - self.max_inline_test_lines, - ); - - for violation in violations { - match violation { - TestViolation::InlineTestCode => { - span_lint_and_then( - cx, - DE1101_TESTS_IN_SEPARATE_FILES, - item.span, - "test code must be moved to a separate test file (DE1101)", - |diag| { - diag.help(format!( - "move the test into `tests/*.rs` or an out-of-line `*_tests.rs` module (inline test block exceeds {} lines)", - self.max_inline_test_lines, - )); - }, - ); - } - TestViolation::InlineTestCodeWithCompanion => { - span_lint_and_then( - cx, - DE1101_TESTS_IN_SEPARATE_FILES, - item.span, - "test code must not be added back to a file that already has a companion test file (DE1101)", - |diag| { - diag.help( - "a `*_tests.rs` companion file already exists; add tests there instead", - ); - }, - ); - } - TestViolation::WrongPathAttr { expected, actual } => { - span_lint_and_then( - cx, - DE1101_TESTS_IN_SEPARATE_FILES, - item.span, - format!( - "test module path `{actual}.rs` must reference `{expected}.rs` to match the source file (DE1101)", - ), - |diag| { - diag.help(format!( - "use `#[path = \"{expected}.rs\"]` or remove `#[path]`" - )); - }, - ); - } - } - } - } -} - -fn is_allowed_test_file(path: &str) -> bool { - let file_name = path.rsplit('/').next().unwrap_or(path); - - path.contains("/tests/") || file_name.ends_with("_tests.rs") -} - -/// Extract the file stem from a normalized path. -/// `"/foo/bar/handler.rs"` → `Some("handler")` -/// -/// Returns `None` for special entry-point files (`lib.rs`, `main.rs`, `mod.rs`) -/// where enforcing `{stem}_tests` naming would be meaningless. -fn file_stem(path: &str) -> Option { - let file_name = path.rsplit('/').next().unwrap_or(path); - let stem = file_name.strip_suffix(".rs")?; - match stem { - "lib" | "main" | "mod" | "tests" | "test" => None, - _ => Some(stem.to_string()), - } -} - -/// Check whether a companion `{stem}_tests.rs` file exists next to the source file. -fn has_companion_test_file(source_path: &str, source_stem: Option<&str>) -> bool { - let Some(stem) = source_stem else { - return false; - }; - let parent = match source_path.rfind('/').or_else(|| source_path.rfind('\\')) { - Some(pos) => &source_path[..=pos], - None => "", - }; - let companion = format!("{parent}{stem}_tests.rs"); - std::path::Path::new(&companion).exists() -} - -/// Count the number of lines in an inline `#[cfg(test)] mod ... { ... }` block, -/// starting from the line containing the opening `{`. -fn count_inline_test_block_lines(lines: &[&str], open_brace_line: usize) -> usize { - let mut depth = 0usize; - let mut count = 0usize; - - for line in &lines[open_brace_line..] { - count += 1; - for ch in line.chars() { - if ch == '{' { - depth += 1; - } else if ch == '}' { - depth = depth.saturating_sub(1); - if depth == 0 { - return count; - } - } - } - } - - count -} - -/// Scan source text for test-declaration violations. -/// -/// Returns all violations found (inline code, wrong test file name). -/// -/// - `has_companion`: whether a `{stem}_tests.rs` file exists alongside this file. -/// If true, any inline test code is unconditionally denied. -/// - `max_inline_lines`: the threshold below which inline test blocks are tolerated. -fn find_test_violations( - source: &str, - source_stem: Option<&str>, - has_companion: bool, - max_inline_lines: usize, -) -> Vec { - let lines: Vec<&str> = source.lines().collect(); - let mut violations = Vec::new(); - let mut reported_inline = false; - let mut reported_naming = false; - - for (index, line) in lines.iter().enumerate() { - if is_comment_or_blank_line(line) { - continue; - } - - let compact_line = compact(line); - - // A bare `#[test]` / `#[tokio::test]` in a production file. - if !reported_inline && is_direct_test_attr(&compact_line) { - if has_companion { - violations.push(TestViolation::InlineTestCodeWithCompanion); - } else { - violations.push(TestViolation::InlineTestCode); - } - reported_inline = true; - continue; - } - - if !is_cfg_test_attr(&compact_line) { - continue; - } - - // Found `#[cfg(test)]` — scan ahead for the declaration that follows. - let mut next = index + 1; - let mut path_attr_value: Option = None; - - while let Some(candidate) = lines.get(next) { - let trimmed = candidate.trim(); - let candidate_compact = compact(candidate); - - if is_comment_or_blank_line(candidate) { - next += 1; - continue; - } - - // Collect attributes between `#[cfg(test)]` and the item. - if candidate_compact.starts_with("#[") { - if is_path_attr(&candidate_compact) { - path_attr_value = extract_path_attr_value(trimmed); - } - next += 1; - continue; - } - - // Out-of-line mod declaration (e.g. `mod foo_tests;`). - // Without #[path]: any module name is accepted. - // With #[path]: value must be `{stem}_tests.rs` or `{stem}_test.rs`. - if is_out_of_line_mod_decl(trimmed) { - if let (Some(stem), Some(pv)) = (source_stem, &path_attr_value) - && !reported_naming - { - let expected = format!("{stem}_tests"); - let filename = pv.rsplit('/').next().unwrap_or(pv); - let actual = filename.strip_suffix(".rs").unwrap_or(filename); - - if actual != expected { - violations.push(TestViolation::WrongPathAttr { - expected, - actual: actual.to_string(), - }); - reported_naming = true; - } - } - break; - } - - // `extern crate` alias after `#[cfg(test)]` is allowed. - if is_extern_crate_alias(trimmed) { - break; - } - - // Anything else is inline test code — check threshold. - if !reported_inline { - if has_companion { - violations.push(TestViolation::InlineTestCodeWithCompanion); - reported_inline = true; - } else { - // Count the lines in the inline test block. - let block_lines = count_inline_test_block_lines(&lines, next); - // Include the #[cfg(test)] line and any attributes above the block. - let total_test_lines = (next - index) + block_lines; - - if total_test_lines > max_inline_lines { - violations.push(TestViolation::InlineTestCode); - } - // Mark as reported either way to avoid re-triggering on - // bare `#[test]` lines inside this allowed inline block. - reported_inline = true; - } - } - break; - } - } - - violations -} - -fn is_comment_or_blank_line(line: &str) -> bool { - let trimmed = line.trim(); - trimmed.is_empty() || trimmed.starts_with("//") -} - -fn compact(line: &str) -> String { - // Strip trailing line comments before removing whitespace, so that - // `#[cfg(test)] // comment` compacts to `#[cfg(test)]` and is detected. - let without_comment = match line.find("//") { - Some(pos) => &line[..pos], - None => line, - }; - without_comment - .chars() - .filter(|ch| !ch.is_whitespace()) - .collect() -} - -fn is_direct_test_attr(line: &str) -> bool { - let trimmed = line.trim_start(); - let is_attr = trimmed.starts_with("#["); - - trimmed.starts_with("#[test") - || trimmed.starts_with("#[tokio::test") - || (is_attr && trimmed.contains("::test]")) - || (is_attr && trimmed.contains("::test(")) -} - -/// Returns true for `#[cfg(test)]`, `#[cfg(test, ...)]`, `#[cfg(any(test, ...))]`, -/// `#[cfg(all(test, ...))]`. -/// Does NOT match `#[cfg(not(test))]` or feature names containing "test". -fn is_cfg_test_attr(line: &str) -> bool { - let Some(inner) = line - .strip_prefix("#[cfg(") - .and_then(|rest| rest.strip_suffix(")]")) - else { - return false; - }; - - contains_test_cfg_operand(inner) -} - -fn contains_test_cfg_operand(input: &str) -> bool { - split_top_level_args(input).into_iter().any(|arg| { - if arg == "test" { - return true; - } - - if let Some(inner) = arg - .strip_prefix("all(") - .and_then(|rest| rest.strip_suffix(')')) - { - return contains_test_cfg_operand(inner); - } - - if let Some(inner) = arg - .strip_prefix("any(") - .and_then(|rest| rest.strip_suffix(')')) - { - return contains_test_cfg_operand(inner); - } - - false - }) -} - -fn split_top_level_args(input: &str) -> Vec<&str> { - let mut args = Vec::new(); - let mut depth = 0usize; - let mut start = 0usize; - - for (index, ch) in input.char_indices() { - match ch { - '(' => depth += 1, - ')' => depth = depth.saturating_sub(1), - ',' if depth == 0 => { - args.push(input[start..index].trim()); - start = index + ch.len_utf8(); - } - _ => {} - } - } - - args.push(input[start..].trim()); - args -} - -/// Returns `true` when the compacted line is a `#[path = "..."]` attribute. -fn is_path_attr(compact_line: &str) -> bool { - compact_line.starts_with("#[path=") -} - -/// Extract the string value from a `#[path = "..."]` attribute. -/// Works on the original (non-compacted) line to preserve the value intact. -fn extract_path_attr_value(line: &str) -> Option { - let trimmed = line.trim(); - let rest = trimmed.strip_prefix("#[path")?; - let rest = rest.trim_start(); - let rest = rest.strip_prefix('=')?; - let rest = rest.trim_start(); - let rest = rest.strip_prefix('"')?; - let end = rest.find('"')?; - Some(rest[..end].to_string()) -} - -/// Strip a leading visibility qualifier from a line, including `pub(in path)`. -fn strip_visibility(line: &str) -> &str { - if let Some(rest) = line.strip_prefix("pub(in ") - // Find the closing ')' and skip past it plus any trailing space. - && let Some(close) = rest.find(')') - { - let after = &rest[close + 1..]; - return after.strip_prefix(' ').unwrap_or(after); - } - line.strip_prefix("pub(crate) ") - .or_else(|| line.strip_prefix("pub(super) ")) - .or_else(|| line.strip_prefix("pub(self) ")) - .or_else(|| line.strip_prefix("pub ")) - .unwrap_or(line) -} - -/// Extract the module name from an out-of-line mod declaration. -/// `"mod foo_tests;"` → `Some("foo_tests")` -/// `"pub(crate) mod foo_tests;"` → `Some("foo_tests")` -#[cfg(test)] -fn extract_mod_name(line: &str) -> Option<&str> { - if !line.ends_with(';') { - return None; - } - let name_with_semi = strip_visibility(line).strip_prefix("mod ")?; - Some(name_with_semi.trim_end_matches(';').trim()) -} - -fn is_out_of_line_mod_decl(line: &str) -> bool { - line.ends_with(';') && strip_visibility(line).starts_with("mod ") -} - -fn is_extern_crate_alias(line: &str) -> bool { - line.starts_with("extern crate ") && line.ends_with(';') -} - -#[cfg(test)] -mod tests { - use super::{ - count_inline_test_block_lines, extract_mod_name, extract_path_attr_value, - find_test_violations, is_cfg_test_attr, is_path_attr, - }; - - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr( - &ui_dir, - "DE1101", - "tests must be in separate files", - ); - } - - #[test] - fn test_is_cfg_test_attr_matches_supported_forms() { - assert!(is_cfg_test_attr("#[cfg(test)]")); - assert!(is_cfg_test_attr("#[cfg(test,feature=\"foo\")]")); - assert!(is_cfg_test_attr("#[cfg(all(test,feature=\"foo\"))]")); - assert!(is_cfg_test_attr("#[cfg(any(feature=\"foo\",test))]")); - } - - #[test] - fn test_is_cfg_test_attr_rejects_unsupported_forms() { - assert!(!is_cfg_test_attr("#[cfg(not(test))]")); - assert!(!is_cfg_test_attr("#[cfg(feature=\"test\")]")); - assert!(!is_cfg_test_attr("#[cfg(any(feature=\"test\",unix))]")); - } - - #[test] - fn test_is_path_attr() { - assert!(is_path_attr("#[path=\"foo.rs\"]")); - assert!(is_path_attr("#[path=\"some/path.rs\"]")); - assert!(!is_path_attr("#[cfg(test)]")); - assert!(!is_path_attr("#[derive(Debug)]")); - } - - #[test] - fn test_extract_path_attr_value() { - assert_eq!( - extract_path_attr_value(r#"#[path = "foo_tests.rs"]"#), - Some("foo_tests.rs".to_string()) - ); - assert_eq!( - extract_path_attr_value(r#"#[path="bar.rs"]"#), - Some("bar.rs".to_string()) - ); - assert_eq!(extract_path_attr_value(r#"#[cfg(test)]"#), None); - } - - #[test] - fn test_extract_mod_name() { - assert_eq!(extract_mod_name("mod foo_tests;"), Some("foo_tests")); - assert_eq!(extract_mod_name("pub mod foo_tests;"), Some("foo_tests")); - assert_eq!( - extract_mod_name("pub(crate) mod foo_tests;"), - Some("foo_tests") - ); - assert_eq!(extract_mod_name("mod tests;"), Some("tests")); - assert_eq!(extract_mod_name("mod tests { }"), None); - assert_eq!(extract_mod_name("fn main() {}"), None); - } - - #[test] - fn test_find_violations_correct_name_no_issues() { - let source = r#" -#[cfg(test)] -mod handler_tests; - -fn main() {} -"#; - let violations = find_test_violations(source, Some("handler"), false, 100); - assert!(violations.is_empty(), "expected no violations"); - } - - #[test] - fn test_find_violations_any_mod_name_without_path_ok() { - // Without #[path], any module name is accepted. - let source = r#" -#[cfg(test)] -mod tests; - -fn main() {} -"#; - let violations = find_test_violations(source, Some("handler"), false, 100); - assert!( - violations.is_empty(), - "any mod name should be accepted without #[path]" - ); - } - - #[test] - fn test_find_violations_path_attr_wrong_value() { - let source = r#" -#[cfg(test)] -#[path = "dto_tests.rs"] -mod tests; - -fn main() {} -"#; - let violations = find_test_violations(source, Some("handler"), false, 100); - assert_eq!(violations.len(), 1); - assert!(matches!( - &violations[0], - super::TestViolation::WrongPathAttr { expected, actual } - if expected == "handler_tests" && actual == "dto_tests" - )); - } - - #[test] - fn test_find_violations_path_attr_correct_value() { - let source = r#" -#[cfg(test)] -#[path = "handler_tests.rs"] -mod tests; - -fn main() {} -"#; - let violations = find_test_violations(source, Some("handler"), false, 100); - assert!(violations.is_empty(), "expected no violations"); - } - - #[test] - fn test_find_violations_inline_code_over_threshold() { - let source = r#" -#[cfg(test)] -mod tests { - #[test] - fn foo() {} -} - -fn main() {} -"#; - // Threshold 3: the test block is 4 lines (mod tests { ... }), trigger. - let violations = find_test_violations(source, Some("handler"), false, 3); - assert!( - violations - .iter() - .any(|v| matches!(v, super::TestViolation::InlineTestCode)) - ); - } - - #[test] - fn test_find_violations_inline_code_under_threshold() { - let source = r#" -#[cfg(test)] -mod tests { - #[test] - fn foo() {} -} - -fn main() {} -"#; - // Threshold 100: the test block is ~6 lines total, allow. - let violations = find_test_violations(source, Some("handler"), false, 100); - assert!( - violations.is_empty(), - "expected no violations under threshold" - ); - } - - #[test] - fn test_find_violations_inline_code_with_companion() { - let source = r#" -#[cfg(test)] -mod tests { - #[test] - fn foo() {} -} - -fn main() {} -"#; - // Even tiny inline tests are denied when companion exists. - let violations = find_test_violations(source, Some("handler"), true, 100); - assert!( - violations - .iter() - .any(|v| matches!(v, super::TestViolation::InlineTestCodeWithCompanion)) - ); - } - - #[test] - fn test_count_inline_test_block_lines() { - let source = "mod tests {\n #[test]\n fn foo() {}\n}\n"; - let lines: Vec<&str> = source.lines().collect(); - assert_eq!(count_inline_test_block_lines(&lines, 0), 4); - } - - #[test] - fn test_count_inline_test_block_lines_nested() { - let source = "mod tests {\n fn foo() {\n if true {\n }\n }\n}\n"; - let lines: Vec<&str> = source.lines().collect(); - assert_eq!(count_inline_test_block_lines(&lines, 0), 6); - } -} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/dto_tests.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/dto_tests.rs deleted file mode 100644 index b93a03446..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/dto_tests.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[test] -fn out_of_line_test_gear_is_allowed() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/large_inline_test_block.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/large_inline_test_block.rs deleted file mode 100644 index 692259046..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/large_inline_test_block.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Created: 2026-04-14 by Constructor Tech -// simulated_dir=/workspace/gears/system/resource-group/resource-group/src/api/rest/ -#[cfg(test)] -// Should trigger DE1101 - tests must be in separate files -mod tests { - #[test] - fn test_0() {} - #[test] - fn test_1() {} - #[test] - fn test_2() {} - #[test] - fn test_3() {} - #[test] - fn test_4() {} - #[test] - fn test_5() {} - #[test] - fn test_6() {} - #[test] - fn test_7() {} - #[test] - fn test_8() {} - #[test] - fn test_9() {} - #[test] - fn test_10() {} - #[test] - fn test_11() {} - #[test] - fn test_12() {} - #[test] - fn test_13() {} - #[test] - fn test_14() {} - #[test] - fn test_15() {} - #[test] - fn test_16() {} - #[test] - fn test_17() {} - #[test] - fn test_18() {} - #[test] - fn test_19() {} - #[test] - fn test_20() {} - #[test] - fn test_21() {} - #[test] - fn test_22() {} - #[test] - fn test_23() {} - #[test] - fn test_24() {} - #[test] - fn test_25() {} - #[test] - fn test_26() {} - #[test] - fn test_27() {} - #[test] - fn test_28() {} - #[test] - fn test_29() {} - #[test] - fn test_30() {} - #[test] - fn test_31() {} - #[test] - fn test_32() {} - #[test] - fn test_33() {} - #[test] - fn test_34() {} - #[test] - fn test_35() {} - #[test] - fn test_36() {} - #[test] - fn test_37() {} - #[test] - fn test_38() {} - #[test] - fn test_39() {} - #[test] - fn test_40() {} - #[test] - fn test_41() {} - #[test] - fn test_42() {} - #[test] - fn test_43() {} - #[test] - fn test_44() {} - #[test] - fn test_45() {} - #[test] - fn test_46() {} - #[test] - fn test_47() {} - #[test] - fn test_48() {} - #[test] - fn test_49() {} -} - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/large_inline_test_block.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/large_inline_test_block.stderr deleted file mode 100644 index 0be60a84a..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/large_inline_test_block.stderr +++ /dev/null @@ -1,16 +0,0 @@ -error: test code must be moved to a separate test file (DE1101) - --> $DIR/large_inline_test_block.rs:5:1 - | -LL | / mod tests { -LL | | #[test] -LL | | fn test_0() {} -LL | | #[test] -... | -LL | | fn test_49() {} -LL | | } - | |_^ - | - = help: move the test into `tests/*.rs` or an out-of-line `*_tests.rs` module (inline test block exceeds 100 lines) - = note: `#[deny(de1101_tests_in_separate_files)]` on by default - -error: aborting due to 1 previous error diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_correct_case.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_correct_case.rs deleted file mode 100644 index 0de3fa233..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_correct_case.rs +++ /dev/null @@ -1,6 +0,0 @@ -// simulated_dir=/workspace/gears/system/resource-group/resource-group/src/api/rest/ -#[cfg(test)] -// Should not trigger DE1101 - tests must be in separate files -mod naming_correct_case_tests; - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_correct_case.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_correct_case.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_correct_case_tests.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_correct_case_tests.rs deleted file mode 100644 index 8b1378917..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_correct_case_tests.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_correct.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_correct.rs deleted file mode 100644 index d4b7c494c..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_correct.rs +++ /dev/null @@ -1,7 +0,0 @@ -// simulated_dir=/workspace/gears/system/resource-group/resource-group/src/api/rest/ -#[cfg(test)] -#[path = "naming_path_correct_tests.rs"] -// Should not trigger DE1101 - tests must be in separate files -mod tests; - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_correct.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_correct.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_correct_tests.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_correct_tests.rs deleted file mode 100644 index 8b1378917..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_correct_tests.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_forbidden.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_forbidden.rs deleted file mode 100644 index b2bcdb2f6..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_forbidden.rs +++ /dev/null @@ -1,7 +0,0 @@ -// simulated_dir=/workspace/gears/system/resource-group/resource-group/src/api/rest/ -#[cfg(test)] -#[path = "dto_tests.rs"] -// Should trigger DE1101 - tests must be in separate files -mod tests; - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_forbidden.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_forbidden.stderr deleted file mode 100644 index 65e58859f..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_path_forbidden.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: test module path `dto_tests.rs` must reference `naming_path_forbidden_tests.rs` to match the source file (DE1101) - --> $DIR/naming_path_forbidden.rs:5:1 - | -LL | mod tests; - | ^^^^^^^^^^ - | - = help: use `#[path = "naming_path_forbidden_tests.rs"]` or remove `#[path]` - = note: `#[deny(de1101_tests_in_separate_files)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_wrong_gear.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_wrong_gear.rs deleted file mode 100644 index c339ade0c..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_wrong_gear.rs +++ /dev/null @@ -1,6 +0,0 @@ -// simulated_dir=/workspace/gears/system/resource-group/resource-group/src/api/rest/ -#[cfg(test)] -// Should not trigger DE1101 - tests must be in separate files -mod tests; - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_wrong_gear.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/naming_wrong_gear.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/other_gear_inline_case.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/other_gear_inline_case.rs deleted file mode 100644 index cabd37862..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/other_gear_inline_case.rs +++ /dev/null @@ -1,10 +0,0 @@ -// Created: 2026-04-14 by Constructor Tech -// simulated_dir=/workspace/gears/system/types-registry/types-registry/src/api/rest/ -#[cfg(test)] -// Small inline test block (under threshold) — should NOT trigger DE1101 -mod tests { - #[test] - fn inline_test() {} -} - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/other_gear_inline_case.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/other_gear_inline_case.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_cfg_not_test.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_cfg_not_test.rs deleted file mode 100644 index 7949aa5a9..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_cfg_not_test.rs +++ /dev/null @@ -1,9 +0,0 @@ -// simulated_dir=/workspace/gears/system/resource-group/resource-group/src/api/rest/ -// Should not trigger DE1101 - #[cfg(not(test))] is production-only code, not a test module -#[cfg(not(test))] -mod production_diagnostics { - #[allow(dead_code)] - pub fn init() {} -} - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_cfg_not_test.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_cfg_not_test.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_inline_case.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_inline_case.rs deleted file mode 100644 index 9f6194ff6..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_inline_case.rs +++ /dev/null @@ -1,10 +0,0 @@ -// Created: 2026-04-14 by Constructor Tech -// simulated_dir=/workspace/gears/system/resource-group/resource-group/src/api/rest/ -#[cfg(test)] -// Small inline test block (under threshold) — should NOT trigger DE1101 -mod tests { - #[test] - fn inline_test() {} -} - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_inline_case.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_inline_case.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_out_of_line_test_gear.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_out_of_line_test_gear.rs deleted file mode 100644 index b2bcdb2f6..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_out_of_line_test_gear.rs +++ /dev/null @@ -1,7 +0,0 @@ -// simulated_dir=/workspace/gears/system/resource-group/resource-group/src/api/rest/ -#[cfg(test)] -#[path = "dto_tests.rs"] -// Should trigger DE1101 - tests must be in separate files -mod tests; - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_out_of_line_test_gear.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_out_of_line_test_gear.stderr deleted file mode 100644 index f0512cfbf..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_out_of_line_test_gear.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: test module path `dto_tests.rs` must reference `resource_group_out_of_line_test_gear_tests.rs` to match the source file (DE1101) - --> $DIR/resource_group_out_of_line_test_gear.rs:5:1 - | -LL | mod tests; - | ^^^^^^^^^^ - | - = help: use `#[path = "resource_group_out_of_line_test_gear_tests.rs"]` or remove `#[path]` - = note: `#[deny(de1101_tests_in_separate_files)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_separate_test_file_tests.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_separate_test_file_tests.rs deleted file mode 100644 index d8c8ee3fa..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_separate_test_file_tests.rs +++ /dev/null @@ -1,6 +0,0 @@ -// simulated_dir=/workspace/gears/system/resource-group/resource-group/src/api/rest/ -// Should not trigger DE1101 - tests must be in separate files -#[test] -fn separate_test_file_is_allowed() {} - -fn main() {} diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_separate_test_file_tests.stderr b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/resource_group_separate_test_file_tests.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/tests.rs b/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/tests.rs deleted file mode 100644 index 8b1378917..000000000 --- a/tools/dylint_lints/de11_testing/de1101_tests_in_separate_files/ui/tests.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/Cargo.toml b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/Cargo.toml deleted file mode 100644 index bf3fe9cc7..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "de1201_docs_rs_all_features" -version = "0.1.0" -authors.workspace = true -edition.workspace = true -license.workspace = true -description = "Publishable crates must enable docs.rs all-features builds (DE1201)" -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies] -cargo_metadata = "0.23" -dylint_linting.workspace = true -serde.workspace = true -serde_json.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/build.rs b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/build.rs deleted file mode 100644 index f8d7f0373..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -const ENV_EXCLUDED_CRATES: &str = "DE1201_DOCS_RS_ALL_FEATURES_EXCLUDED_CRATES"; - -fn main() { - println!("cargo:rerun-if-env-changed={ENV_EXCLUDED_CRATES}"); - - if let Ok(value) = std::env::var(ENV_EXCLUDED_CRATES) { - println!("cargo:rustc-env={ENV_EXCLUDED_CRATES}={value}"); - } -} diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/src/lib.rs b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/src/lib.rs deleted file mode 100644 index e1b089404..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/src/lib.rs +++ /dev/null @@ -1,381 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_errors; -extern crate rustc_span; - -use cargo_metadata::{Metadata, MetadataCommand, Package}; -use rustc_errors::DiagDecorator; -use rustc_lint::{LateContext, LateLintPass, LintContext}; -use rustc_span::DUMMY_SP; -use serde_json::Value; -use std::collections::HashSet; -use std::path::{Path, PathBuf}; - -const ENV_EXCLUDED_CRATES: &str = "DE1201_DOCS_RS_ALL_FEATURES_EXCLUDED_CRATES"; - -#[derive(Default, serde::Deserialize)] -struct Config { - #[serde(default)] - excluded_crates: Vec, -} - -struct De1201DocsRsAllFeatures { - excluded_crates: HashSet, -} - -impl De1201DocsRsAllFeatures { - pub fn new() -> Self { - let config: Config = dylint_linting::config_or_default(env!("CARGO_PKG_NAME")); - let mut excluded_crates: HashSet = config.excluded_crates.into_iter().collect(); - excluded_crates.extend(env_excluded_crates()); - - Self { excluded_crates } - } -} - -dylint_linting::impl_late_lint! { - /// DE1201: Publishable crates must enable docs.rs all-features builds - /// - /// ### What it does - /// - /// Checks publishable crates for: - /// - /// ```toml - /// [package.metadata.docs.rs] - /// all-features = true - /// ``` - /// - /// ### Why - /// - /// docs.rs builds each crate with a constrained feature set unless configured - /// otherwise. Enabling all features catches documentation failures for optional - /// feature combinations before publishing and keeps public API docs complete. - /// - /// ### Scope - /// - /// - Applies to crates where Cargo metadata says publishing is allowed. - /// - Skips crates with `publish = false`. - /// - Skips crate names listed in `[de1201_docs_rs_all_features].excluded_crates`. - /// - Skips crate names listed in `DE1201_DOCS_RS_ALL_FEATURES_EXCLUDED_CRATES`. - pub DE1201_DOCS_RS_ALL_FEATURES, - Warn, - "publishable crates must set package.metadata.docs.rs.all-features = true (DE1201)", - De1201DocsRsAllFeatures::new() -} - -impl LateLintPass<'_> for De1201DocsRsAllFeatures { - fn check_crate(&mut self, cx: &LateContext<'_>) { - let Ok(manifest_path) = current_manifest_path() else { - return; - }; - - let metadata = match MetadataCommand::new() - .manifest_path(&manifest_path) - .no_deps() - .exec() - { - Ok(metadata) => metadata, - Err(error) => { - cx.emit_span_lint( - DE1201_DOCS_RS_ALL_FEATURES, - DUMMY_SP, - DiagDecorator(|diag| { - diag.primary_message(format!( - "could not read Cargo metadata for docs.rs configuration check: {error}" - )); - }), - ); - return; - } - }; - - let Some(package) = find_current_package(&metadata, &manifest_path) else { - cx.emit_span_lint( - DE1201_DOCS_RS_ALL_FEATURES, - DUMMY_SP, - DiagDecorator(|diag| { - diag.primary_message(format!( - "could not find current package in Cargo metadata for `{}`", - manifest_path.display() - )); - }), - ); - return; - }; - - let Some(status) = docs_rs_all_features_violation( - package.name.as_ref(), - package.publish.as_deref(), - &package.metadata, - &self.excluded_crates, - ) else { - return; - }; - - cx.emit_span_lint( - DE1201_DOCS_RS_ALL_FEATURES, - DUMMY_SP, - DiagDecorator(|diag| { - diag.primary_message(format!( - "publishable crate `{}` must set `package.metadata.docs.rs.all-features = true` (DE1201)", - package.name - )); - diag.help(format!( - "{}; add `[package.metadata.docs.rs] all-features = true` to `{}` or add `{}` to `[de1201_docs_rs_all_features].excluded_crates` in `dylint.toml` or `{ENV_EXCLUDED_CRATES}`", - status.help_reason(), - package.manifest_path, - package.name, - )); - }), - ); - } -} - -fn current_manifest_path() -> Result { - std::env::var("CARGO_MANIFEST_DIR").map(|dir| PathBuf::from(dir).join("Cargo.toml")) -} - -fn env_excluded_crates() -> Vec { - let mut excluded_crates = Vec::new(); - - if let Some(value) = option_env!("DE1201_DOCS_RS_ALL_FEATURES_EXCLUDED_CRATES") { - excluded_crates.extend(parse_excluded_crates(value)); - } - - std::env::var(ENV_EXCLUDED_CRATES) - .map(|value| excluded_crates.extend(parse_excluded_crates(&value))) - .ok(); - - excluded_crates -} - -fn parse_excluded_crates(value: &str) -> Vec { - value - .split(|ch: char| ch == ',' || ch.is_ascii_whitespace()) - .map(str::trim) - .filter(|crate_name| !crate_name.is_empty()) - .map(ToOwned::to_owned) - .collect() -} - -fn find_current_package<'metadata>( - metadata: &'metadata Metadata, - manifest_path: &Path, -) -> Option<&'metadata Package> { - let expected = normalize_path(&manifest_path.to_string_lossy()); - metadata.packages.iter().find(|package| { - let actual = normalize_path(package.manifest_path.as_str()); - actual == expected - }) -} - -fn normalize_path(path: &str) -> String { - path.replace('\\', "/") -} - -fn docs_rs_all_features_violation( - package_name: &str, - publish: Option<&[String]>, - metadata: &Value, - excluded_crates: &HashSet, -) -> Option { - if excluded_crates.contains(package_name) || !is_publishable(publish) { - return None; - } - - match docs_rs_all_features_status(metadata) { - DocsRsAllFeaturesStatus::Enabled => None, - status => Some(status), - } -} - -fn is_publishable(publish: Option<&[String]>) -> bool { - publish.is_none_or(|registries| !registries.is_empty()) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum DocsRsAllFeaturesStatus { - Enabled, - MissingDocsRsTable, - MissingAllFeatures, - Disabled, - NonBoolean, -} - -impl DocsRsAllFeaturesStatus { - fn help_reason(self) -> &'static str { - match self { - Self::Enabled => "docs.rs all-features is enabled", - Self::MissingDocsRsTable => "`package.metadata.docs.rs` is missing", - Self::MissingAllFeatures => "`package.metadata.docs.rs.all-features` is missing", - Self::Disabled => "`package.metadata.docs.rs.all-features` is false", - Self::NonBoolean => "`package.metadata.docs.rs.all-features` is not a boolean", - } - } -} - -fn docs_rs_all_features_status(metadata: &Value) -> DocsRsAllFeaturesStatus { - let Some(docs_rs) = docs_rs_metadata(metadata) else { - return DocsRsAllFeaturesStatus::MissingDocsRsTable; - }; - - match docs_rs.get("all-features") { - Some(Value::Bool(true)) => DocsRsAllFeaturesStatus::Enabled, - Some(Value::Bool(false)) => DocsRsAllFeaturesStatus::Disabled, - Some(_) => DocsRsAllFeaturesStatus::NonBoolean, - None => DocsRsAllFeaturesStatus::MissingAllFeatures, - } -} - -fn docs_rs_metadata(metadata: &Value) -> Option<&Value> { - metadata - .get("docs") - .and_then(|docs| docs.get("rs")) - .or_else(|| metadata.get("docs.rs")) -} - -#[cfg(test)] -mod tests { - use super::{ - DocsRsAllFeaturesStatus, docs_rs_all_features_status, docs_rs_all_features_violation, - is_publishable, parse_excluded_crates, - }; - use serde_json::json; - use std::collections::HashSet; - - #[test] - fn publish_omitted_is_publishable() { - assert!(is_publishable(None)); - } - - #[test] - fn publish_empty_list_is_not_publishable() { - let publish = Vec::new(); - assert!(!is_publishable(Some(&publish))); - } - - #[test] - fn publish_non_empty_list_is_publishable() { - let publish = vec!["crates-io".to_string()]; - assert!(is_publishable(Some(&publish))); - } - - #[test] - fn missing_docs_rs_table_is_violation() { - assert_eq!( - docs_rs_all_features_status(&json!({})), - DocsRsAllFeaturesStatus::MissingDocsRsTable - ); - } - - #[test] - fn missing_all_features_is_violation() { - assert_eq!( - docs_rs_all_features_status(&json!({ - "docs": { - "rs": {} - } - })), - DocsRsAllFeaturesStatus::MissingAllFeatures - ); - } - - #[test] - fn false_all_features_is_violation() { - assert_eq!( - docs_rs_all_features_status(&json!({ - "docs": { - "rs": { - "all-features": false - } - } - })), - DocsRsAllFeaturesStatus::Disabled - ); - } - - #[test] - fn non_boolean_all_features_is_violation() { - assert_eq!( - docs_rs_all_features_status(&json!({ - "docs": { - "rs": { - "all-features": "true" - } - } - })), - DocsRsAllFeaturesStatus::NonBoolean - ); - } - - #[test] - fn true_all_features_is_allowed() { - assert_eq!( - docs_rs_all_features_status(&json!({ - "docs": { - "rs": { - "all-features": true - } - } - })), - DocsRsAllFeaturesStatus::Enabled - ); - } - - #[test] - fn quoted_docs_rs_table_is_allowed() { - assert_eq!( - docs_rs_all_features_status(&json!({ - "docs.rs": { - "all-features": true - } - })), - DocsRsAllFeaturesStatus::Enabled - ); - } - - #[test] - fn publish_false_skips_violation() { - let publish = Vec::new(); - let exclusions = HashSet::new(); - - assert_eq!( - docs_rs_all_features_violation( - "internal-crate", - Some(&publish), - &json!({}), - &exclusions - ), - None - ); - } - - #[test] - fn excluded_crate_skips_violation() { - let exclusions = HashSet::from(["excluded-crate".to_string()]); - - assert_eq!( - docs_rs_all_features_violation("excluded-crate", None, &json!({}), &exclusions), - None - ); - } - - #[test] - fn publishable_missing_metadata_reports_violation() { - let exclusions = HashSet::new(); - - assert_eq!( - docs_rs_all_features_violation("publishable-crate", None, &json!({}), &exclusions), - Some(DocsRsAllFeaturesStatus::MissingDocsRsTable) - ); - } - - #[test] - fn parses_env_excluded_crates() { - assert_eq!( - parse_excluded_crates("crate-one, crate-two\ncrate-three\tcrate-four"), - vec!["crate-one", "crate-two", "crate-three", "crate-four"] - ); - } -} diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/cargo_fixtures.rs b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/cargo_fixtures.rs deleted file mode 100644 index 3f768b16d..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/cargo_fixtures.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; - -const ENV_EXCLUDED_CRATES: &str = "DE1201_DOCS_RS_ALL_FEATURES_EXCLUDED_CRATES"; -const LINT_NAME: &str = "de1201_docs_rs_all_features"; - -#[test] -fn cargo_lint_fixtures_cover_manifest_cases() { - let missing_docs_rs = run_fixture("missing_docs_rs"); - assert_success("missing_docs_rs", &missing_docs_rs); - assert_contains( - &missing_docs_rs, - "publishable crate `de1201_missing_docs_rs` must set `package.metadata.docs.rs.all-features = true` (DE1201)", - ); - assert_contains(&missing_docs_rs, "`package.metadata.docs.rs` is missing"); - - let env_excluded = run_fixture_with_env( - "missing_docs_rs", - &[(ENV_EXCLUDED_CRATES, "de1201_missing_docs_rs")], - ); - assert_success("missing_docs_rs env exclusion", &env_excluded); - assert_not_contains(&env_excluded, "DE1201"); - - for fixture in ["all_features_true", "publish_false", "excluded_crate"] { - let output = run_fixture(fixture); - assert_success(fixture, &output); - assert_not_contains(&output, "DE1201"); - } -} - -fn run_fixture(name: &str) -> Output { - run_fixture_with_env(name, &[]) -} - -fn run_fixture_with_env(name: &str, extra_env: &[(&str, &str)]) -> Output { - let fixture = fixtures_dir().join(name); - let manifest_path = fixture.join("Cargo.toml"); - let lint_parent_dir = lint_parent_dir(); - - let mut command = Command::new("cargo"); - command - .arg("dylint") - .arg("--path") - .arg(&lint_parent_dir) - .arg("--pattern") - .arg(LINT_NAME) - .arg("--manifest-path") - .arg(&manifest_path) - .arg("--no-deps") - .env_remove("CARGO_TARGET_DIR") - .env_remove(ENV_EXCLUDED_CRATES) - .env_remove("DYLINT_RUSTFLAGS") - .env_remove("DYLINT_TOML") - .env_remove("RUSTFLAGS") - .current_dir(&fixture); - - for (key, value) in extra_env { - command.env(key, value); - } - - let output = command - .output() - .unwrap_or_else(|error| panic!("failed to run cargo dylint for fixture `{name}`: {error}")); - - remove_fixture_lockfile(&fixture); - output -} - -fn fixtures_dir() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("fixtures") -} - -fn lint_parent_dir() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("lint crate should have a parent directory") - .to_path_buf() -} - -fn remove_fixture_lockfile(fixture: &Path) { - match fs::remove_file(fixture.join("Cargo.lock")) { - Ok(()) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => panic!( - "failed to remove fixture Cargo.lock in `{}`: {error}", - fixture.display() - ), - } -} - -fn assert_success(name: &str, output: &Output) { - if output.status.success() { - return; - } - - let toolchain = std::env::var("RUSTUP_TOOLCHAIN").unwrap_or_else(|_| "".into()); - let has_dylint_link = std::env::var_os("PATH") - .map(|path| std::env::split_paths(&path).any(|dir| dir.join("dylint-link").exists())) - .unwrap_or(false); - - panic!( - "fixture `{name}` failed (exit code: {:?})\n\ - --- diagnostics ---\n\ - RUSTUP_TOOLCHAIN={toolchain}\n\ - dylint-link in PATH: {has_dylint_link}\n\ - --- stdout ---\n{}\n\ - --- stderr ---\n{}", - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); -} - -fn assert_contains(output: &Output, needle: &str) { - let combined = combined_output(output); - assert!( - combined.contains(needle), - "expected output to contain `{needle}`\noutput:\n{combined}" - ); -} - -fn assert_not_contains(output: &Output, needle: &str) { - let combined = combined_output(output); - assert!( - !combined.contains(needle), - "expected output not to contain `{needle}`\noutput:\n{combined}" - ); -} - -fn combined_output(output: &Output) -> String { - format!( - "{}{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ) -} diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.lock b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.lock deleted file mode 100644 index 7d1e23c15..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "de1201_all_features_true" -version = "0.1.0" diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.toml b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.toml deleted file mode 100644 index a85dc5384..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "de1201_all_features_true" -version = "0.1.0" -edition = "2024" -metadata.docs.rs.all-features = true - -[workspace] diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/src/lib.rs b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/src/lib.rs deleted file mode 100644 index ace84d377..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub fn fixture() {} diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.lock b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.lock deleted file mode 100644 index 2c0f2a82b..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "de1201_excluded_crate" -version = "0.1.0" diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.toml b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.toml deleted file mode 100644 index 1c8abbace..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "de1201_excluded_crate" -version = "0.1.0" -edition = "2024" - -[workspace] diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/dylint.toml b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/dylint.toml deleted file mode 100644 index 9422c6c82..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/dylint.toml +++ /dev/null @@ -1,2 +0,0 @@ -[de1201_docs_rs_all_features] -excluded_crates = ["de1201_excluded_crate"] diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/src/lib.rs b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/src/lib.rs deleted file mode 100644 index ace84d377..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub fn fixture() {} diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.lock b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.lock deleted file mode 100644 index 3daa52a41..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "de1201_missing_docs_rs" -version = "0.1.0" diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.toml b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.toml deleted file mode 100644 index ed9fbdeb6..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "de1201_missing_docs_rs" -version = "0.1.0" -edition = "2024" - -[workspace] diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/src/lib.rs b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/src/lib.rs deleted file mode 100644 index ace84d377..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub fn fixture() {} diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.lock b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.lock deleted file mode 100644 index b0286bc7b..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "de1201_publish_false" -version = "0.1.0" diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.toml b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.toml deleted file mode 100644 index b271f1541..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "de1201_publish_false" -version = "0.1.0" -edition = "2024" -publish = false - -[workspace] diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/src/lib.rs b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/src/lib.rs deleted file mode 100644 index ace84d377..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub fn fixture() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/Cargo.toml b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/Cargo.toml deleted file mode 100644 index 340109455..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/Cargo.toml +++ /dev/null @@ -1,66 +0,0 @@ -[package] -name = "de1301_no_print_macros" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Forbids println!/eprintln!/print!/eprint!/dbg! macros in production code (DE1301)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "forbidden_macros" -path = "ui/forbidden_macros.rs" -crate-type = ["lib"] - -[[example]] -name = "allowed" -path = "ui/allowed.rs" - -[[example]] -name = "allowed_in_apps" -path = "ui/allowed_in_apps.rs" - -[[example]] -name = "allowed_in_build_rs" -path = "ui/allowed_in_build_rs.rs" - -[[example]] -name = "allowed_in_proc_macro" -path = "ui/allowed_in_proc_macro.rs" -crate-type = ["proc-macro"] - -[[example]] -name = "allowed_in_proc_macro_impl_method" -path = "ui/allowed_in_proc_macro_impl_method.rs" -crate-type = ["proc-macro"] - -[[example]] -name = "forbidden_public_in_proc_macro" -path = "ui/forbidden_public_in_proc_macro.rs" -crate-type = ["proc-macro"] - -[[example]] -name = "allowed_in_main" -path = "ui/allowed_in_main.rs" - -[[example]] -name = "allowed_in_tests" -path = "ui/allowed_in_tests.rs" - -[[example]] -name = "allowed_in_cfg_test_impl" -path = "ui/allowed_in_cfg_test_impl.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -tokio = { workspace = true, features = ["macros", "rt"] } - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/README.md b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/README.md deleted file mode 100644 index ba6d4b32f..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# DE1301 — No Print/Debug Macros in Production Code - -## Rule - -This lint forbids using the following macros in production Rust gears code: - -- `println!` -- `eprintln!` -- `print!` -- `eprint!` -- `dbg!` - -These macros bypass the project’s structured logging/observability approach and are easy to leave behind accidentally. - -## Rationale - -- **Observability consistency**: prefer `tracing` (or the project’s logging facade) so logs are structured, filterable, and routable. -- **Noise control**: ad-hoc stdout/stderr prints introduce noisy output in services, CI, and integration tests. -- **Accidental leakage**: `dbg!` and print macros often ship unintentionally. - -## Allowed Exceptions - -This lint intentionally allows these macros in the following cases: - -### 1) `proc-macro` crates - -Procedural macro crates may emit warnings or diagnostics during compilation. - -This lint allows these macros: - -- Inside `#[proc_macro]` / `#[proc_macro_attribute]` / `#[proc_macro_derive]` entrypoints -- Inside private helper functions (`fn helper() { ... }`) - -But it still forbids these macros inside other public helper functions (`pub fn helper() { ... }`). - -### 2) Any `build.rs` - -`build.rs` scripts often need to print instructions to Cargo (e.g. `cargo:rerun-if-changed=...`) or debug build-time behavior. - -### 3) Anything under `apps/*` - -Application binaries may have legitimate reasons to print directly: - -- CLI-style UX output -- Early bootstrap diagnostics before logging is initialized -- Very small tools where stdout is the primary interface - -### 4) Binary crates (top-level functions) - -All top-level functions in binary crates are allowed to use print macros. -Binary crates are the application boundary — printing to stderr/stdout is -the boundary-level handling that the lint's guidance recommends: - -- CLI-style UX output -- Early bootstrap diagnostics before logging is initialized -- Fatal error reporting in xtask / build tooling - -Functions inside nested gears within binary crates are still checked. - -### 5) Tests (`#[test]` / `#[tokio::test]` / `#[cfg(test)]`) - -Unit tests and test-only gears may use these macros for debug output and quick feedback. - -## Examples - -### Forbidden (non-main functions) - -```rust -fn helper() { - println!("hello"); - dbg!(42); -} -``` - -### Allowed in `build.rs` - -```rust -// build.rs -fn main() { - println!("cargo:rerun-if-changed=src/schema.json"); - dbg!("build-time debug"); -} -``` - -### Allowed in `apps/*` - -```rust -// apps/my-tool/src/main.rs -fn main() { - println!("Usage: my-tool "); -} -``` - -## Guidance - -- Prefer `tracing::{info, warn, error, debug}` for runtime output. -- If you need temporary debugging in library/module code, use a proper logger at `debug` level. -- For code that runs **before tracing is initialized** (e.g. logging bootstrap), suppress the lint with a targeted allow and a comment explaining why: - -```rust -#[allow(unknown_lints, de1301_no_print_macros)] // runs before tracing subscriber is installed -fn init_logging(...) { - eprintln!("error during logging init"); -} -``` - -## UI Tests - -This lint includes UI tests covering: - -- Forbidden usage in normal code -- Allowed usage in `apps/*` -- Allowed usage in `build.rs` -- Allowed usage in `proc-macro` crates -- Allowed usage in binary crate top-level functions -- Allowed usage in tests (`#[test]`, `#[tokio::test]`, `#[cfg(test)]`) diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/src/lib.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/src/lib.rs deleted file mode 100644 index 5cf8c84ea..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/src/lib.rs +++ /dev/null @@ -1,263 +0,0 @@ -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; -extern crate rustc_span; - -use clippy_utils::diagnostics::span_lint_and_then; -use rustc_ast::{ - AttrKind, Attribute, ExprKind, Item, ItemKind, MacCall, VisibilityKind, visit, visit::Visitor, -}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; -use rustc_session::config::CrateType; -use rustc_span::{FileName, sym}; - -const FORBIDDEN_MACROS: &[&str] = &["println", "eprintln", "print", "eprint", "dbg"]; - -dylint_linting::declare_pre_expansion_lint! { - /// DE1301: Forbid print/debug macros in production code - /// - /// Disallows using the following macros: - /// - println! - /// - eprintln! - /// - print! - /// - eprint! - /// - dbg! - pub DE1301_NO_PRINT_MACROS, - Deny, - "print/debug macros are forbidden in production code (DE1301)" -} - -impl EarlyLintPass for De1301NoPrintMacros { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { - // In pre-expansion lints, rustc does not reliably walk into bodies for us. - // Walk the item ourselves and look for `MacCall` nodes. - let mut v = ForbiddenMacroVisitor { - cx, - in_proc_macro_crate: is_proc_macro_crate(cx), - is_bin_crate: is_bin_crate(cx), - allow_stack: Vec::new(), - }; - v.visit_item(item); - } -} - -fn is_allowed_location(cx: &EarlyContext<'_>, span: rustc_span::Span) -> bool { - let source_map = cx.sess().source_map(); - let file_name = source_map.span_to_filename(span); - - let Some(path_str) = (match file_name { - FileName::Real(real_name) => real_name - .local_path() - .map(|p| p.to_string_lossy().to_string()), - _ => None, - }) else { - return false; - }; - - // Support UI tests by allowing a first-line override of the logical path. - let effective_path = extract_simulated_path(&path_str).unwrap_or(path_str); - let effective_path = effective_path.replace('\\', "/"); - - // Exception 1: any build.rs - if effective_path.ends_with("/build.rs") { - return true; - } - - // Exception 2: anything under apps/* - // Accept both absolute paths ("/.../apps/..."), and repo-relative paths ("apps/..."). - if effective_path.starts_with("apps/") || effective_path.contains("/apps/") { - return true; - } - - false -} - -fn is_proc_macro_crate(cx: &EarlyContext<'_>) -> bool { - cx.sess().opts.crate_types.contains(&CrateType::ProcMacro) -} - -fn is_bin_crate(cx: &EarlyContext<'_>) -> bool { - cx.sess().opts.crate_types.contains(&CrateType::Executable) -} - -fn extract_simulated_path(path_str: &str) -> Option { - // Only check for simulated_dir in temporary paths (UI tests run in temp directories) - let is_temp = path_str.contains("/tmp/") - || path_str.contains("/var/folders/") - || path_str.contains("\\Temp\\") - || path_str.contains(".tmp"); - - if !is_temp { - return None; - } - - let contents = std::fs::read_to_string(std::path::PathBuf::from(path_str)).ok()?; - for line in contents.lines().take(1) { - let trimmed = line.trim(); - if trimmed.starts_with("// simulated_dir=") { - return Some(trimmed.trim_start_matches("// simulated_dir=").to_string()); - } - if !trimmed.is_empty() && !trimmed.starts_with("//") && !trimmed.starts_with("#!") { - break; - } - } - - None -} - -struct ForbiddenMacroVisitor<'a, 'cx> { - cx: &'a EarlyContext<'cx>, - in_proc_macro_crate: bool, - is_bin_crate: bool, - allow_stack: Vec, -} - -impl<'a, 'cx> ForbiddenMacroVisitor<'a, 'cx> { - fn lint_mac_call(&self, mac_call: &MacCall) { - let allowed_here = self.allow_stack.last().copied().unwrap_or(false); - if allowed_here { - return; - } - - if is_allowed_location(self.cx, mac_call.span()) { - return; - } - - let Some(last) = mac_call.path.segments.last() else { - return; - }; - - let name = last.ident.name.as_str(); - if !FORBIDDEN_MACROS.contains(&name) { - return; - } - - span_lint_and_then( - self.cx, - DE1301_NO_PRINT_MACROS, - mac_call.span(), - format!("macro `{name}!` is forbidden in production code (DE1301)"), - |diag| { - diag.help( - "use `tracing`/`log` for observability, or return the value and handle it at the boundary", - ); - }, - ); - } -} - -impl<'ast, 'a, 'cx> visit::Visitor<'ast> for ForbiddenMacroVisitor<'a, 'cx> { - fn visit_item(&mut self, item: &'ast Item) { - let parent_allow = self.allow_stack.last().copied().unwrap_or(false); - - match &item.kind { - ItemKind::Fn(_fn_item) => { - let is_binary_entry = self.allow_stack.is_empty() && self.is_bin_crate; - let is_private = matches!(item.vis.kind, VisibilityKind::Inherited); - let allow_here = parent_allow - || is_binary_entry - || is_test_item(&item.attrs) - || (self.in_proc_macro_crate - && (is_private || has_proc_macro_attr(&item.attrs))); - - self.allow_stack.push(allow_here); - visit::walk_item(self, item); - self.allow_stack.pop(); - } - ItemKind::Mod(..) => { - let allow_here = parent_allow || is_test_item(&item.attrs); - self.allow_stack.push(allow_here); - visit::walk_item(self, item); - self.allow_stack.pop(); - } - _ => { - let allow_here = parent_allow || is_test_item(&item.attrs); - self.allow_stack.push(allow_here); - visit::walk_item(self, item); - self.allow_stack.pop(); - } - } - } - - fn visit_assoc_item( - &mut self, - assoc_item: &'ast rustc_ast::Item, - ctxt: visit::AssocCtxt, - ) { - let parent_allow = self.allow_stack.last().copied().unwrap_or(false); - - let is_private = matches!(assoc_item.vis.kind, VisibilityKind::Inherited); - let allow_here = parent_allow - || is_test_item(&assoc_item.attrs) - || (self.in_proc_macro_crate && (is_private || has_proc_macro_attr(&assoc_item.attrs))); - - self.allow_stack.push(allow_here); - visit::walk_assoc_item(self, assoc_item, ctxt); - self.allow_stack.pop(); - } - - fn visit_expr(&mut self, expr: &'ast rustc_ast::Expr) { - if let ExprKind::MacCall(mac_call) = &expr.kind { - self.lint_mac_call(mac_call); - } - visit::walk_expr(self, expr); - } - - fn visit_mac_call(&mut self, mac_call: &'ast MacCall) { - self.lint_mac_call(mac_call); - } -} - -fn has_proc_macro_attr(attrs: &[rustc_ast::Attribute]) -> bool { - attrs.iter().any(|attr| { - let AttrKind::Normal(normal) = &attr.kind else { - return false; - }; - - let Some(last) = normal.item.path.segments.last() else { - return false; - }; - - matches!( - last.ident.name.as_str(), - "proc_macro" | "proc_macro_attribute" | "proc_macro_derive" - ) - }) -} - -fn is_test_item(attrs: &[Attribute]) -> bool { - attrs.iter().any(|attr| { - if attr.has_name(sym::test) { - return true; - } - - if let Some(ident) = attr.path().last() - && *ident == sym::test - { - return true; - } - - if attr.has_name(sym::cfg) - && let Some(list) = attr.meta_item_list() - { - return list.iter().any(|item| item.has_name(sym::test)); - } - - false - }) -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE1301", "Print macros"); - } -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed.rs deleted file mode 100644 index 356bcf91b..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - let x = 1 + 1; - let _ = x; -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed.stderr b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_apps.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_apps.rs deleted file mode 100644 index 79cefcd99..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_apps.rs +++ /dev/null @@ -1,9 +0,0 @@ -// simulated_dir=/cf-gears/apps/cf-gears-example-server/src/main.rs - -fn main() { - print!("allowed in apps"); - eprint!("allowed in apps"); - println!("allowed in apps"); - eprintln!("allowed in apps"); - dbg!(1); -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_apps.stderr b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_apps.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_build_rs.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_build_rs.rs deleted file mode 100644 index adf326cc8..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_build_rs.rs +++ /dev/null @@ -1,6 +0,0 @@ -// simulated_dir=/cf-gears/gears/file_parser/build.rs - -fn main() { - println!("allowed in build.rs"); - dbg!("allowed in build.rs"); -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_build_rs.stderr b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_build_rs.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_cfg_test_impl.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_cfg_test_impl.rs deleted file mode 100644 index 181b77bbd..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_cfg_test_impl.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[cfg(test)] -impl Helper { - fn call() { - // Should not trigger DE1301 - Print macros - println!("allowed inside cfg(test) impl"); - // Should not trigger DE1301 - Print macros - dbg!(42); - } -} - -struct Helper; - -fn _use_helper() { - let _ = Helper; -} - -fn main() { - // This call is not compiled, but keeps the impl reachable for the parser. - #[cfg(test)] - { - Helper::call(); - } - - // Use the helper to suppress unused warning - _use_helper(); -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_cfg_test_impl.stderr b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_cfg_test_impl.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_main.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_main.rs deleted file mode 100644 index b15eb368d..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_main.rs +++ /dev/null @@ -1,15 +0,0 @@ -// compile-flags: --crate-type=bin - -fn helper() { - println!("allowed in binary crate helper"); - eprintln!("allowed in binary crate helper"); -} - -fn main() { - helper(); - println!("hello"); - eprintln!("hello"); - print!("hello"); - eprint!("hello"); - dbg!(42); -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_main.stderr b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_main.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_proc_macro.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_proc_macro.rs deleted file mode 100644 index 8d3126ef2..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_proc_macro.rs +++ /dev/null @@ -1,21 +0,0 @@ -// compile-flags: --crate-type=proc-macro - -extern crate proc_macro; - -use proc_macro::TokenStream; - -#[proc_macro] -pub fn my_macro(_input: TokenStream) -> TokenStream { - eprintln!("warning from proc macro"); - TokenStream::new() -} - -#[proc_macro] -pub fn my_another_macro(_input: TokenStream) -> TokenStream { - nested_func(); - TokenStream::new() -} - -fn nested_func() { - println!("hello"); -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_proc_macro.stderr b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_proc_macro.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_proc_macro_impl_method.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_proc_macro_impl_method.rs deleted file mode 100644 index a38d1f51e..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_proc_macro_impl_method.rs +++ /dev/null @@ -1,20 +0,0 @@ -// compile-flags: --crate-type=proc-macro - -extern crate proc_macro; - -use proc_macro::TokenStream; - -#[proc_macro] -pub fn my_macro(_input: TokenStream) -> TokenStream { - Helper::private_method(); - TokenStream::new() -} - -struct Helper; - -impl Helper { - fn private_method() { - // Should not trigger DE1301 - Print macros - println!("allowed in private impl method"); - } -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_tests.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_tests.rs deleted file mode 100644 index fcae5a649..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_tests.rs +++ /dev/null @@ -1,26 +0,0 @@ -fn helper() {} - -fn main() { - helper(); -} - -#[test] -fn unit_test_allows_prints() { - println!("hello from test"); - eprintln!("hello from test"); - dbg!(42); -} - -#[tokio::test] -async fn tokio_test_allows_prints() { - println!("hello from tokio test"); -} - -#[cfg(test)] -mod nested_tests { - #[test] - fn nested_test_allows_prints() { - print!("nested test"); - eprint!("nested test"); - } -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_tests.stderr b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/allowed_in_tests.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_macros.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_macros.rs deleted file mode 100644 index 5b1f6665b..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_macros.rs +++ /dev/null @@ -1,18 +0,0 @@ -// compile-flags: --crate-type=lib - -pub fn not_main() { - // Should trigger DE1301 - Print macros - println!("hello"); - - // Should trigger DE1301 - Print macros - eprintln!("hello"); - - // Should trigger DE1301 - Print macros - print!("hello"); - - // Should trigger DE1301 - Print macros - eprint!("hello"); - - // Should trigger DE1301 - Print macros - dbg!(42); -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_macros.stderr b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_macros.stderr deleted file mode 100644 index 854992575..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_macros.stderr +++ /dev/null @@ -1,43 +0,0 @@ -error: macro `println!` is forbidden in production code (DE1301) - --> $DIR/forbidden_macros.rs:5:5 - | -LL | println!("hello"); - | ^^^^^^^^^^^^^^^^^ - | - = help: use `tracing`/`log` for observability, or return the value and handle it at the boundary - = note: `#[deny(de1301_no_print_macros)]` on by default - -error: macro `eprintln!` is forbidden in production code (DE1301) - --> $DIR/forbidden_macros.rs:8:5 - | -LL | eprintln!("hello"); - | ^^^^^^^^^^^^^^^^^^ - | - = help: use `tracing`/`log` for observability, or return the value and handle it at the boundary - -error: macro `print!` is forbidden in production code (DE1301) - --> $DIR/forbidden_macros.rs:11:5 - | -LL | print!("hello"); - | ^^^^^^^^^^^^^^^ - | - = help: use `tracing`/`log` for observability, or return the value and handle it at the boundary - -error: macro `eprint!` is forbidden in production code (DE1301) - --> $DIR/forbidden_macros.rs:14:5 - | -LL | eprint!("hello"); - | ^^^^^^^^^^^^^^^^ - | - = help: use `tracing`/`log` for observability, or return the value and handle it at the boundary - -error: macro `dbg!` is forbidden in production code (DE1301) - --> $DIR/forbidden_macros.rs:17:5 - | -LL | dbg!(42); - | ^^^^^^^^ - | - = help: use `tracing`/`log` for observability, or return the value and handle it at the boundary - -error: aborting due to 5 previous errors - diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_public_in_proc_macro.rs b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_public_in_proc_macro.rs deleted file mode 100644 index f13402878..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_public_in_proc_macro.rs +++ /dev/null @@ -1,21 +0,0 @@ -// compile-flags: --crate-type=proc-macro - -extern crate proc_macro; - -use proc_macro::TokenStream; - -#[proc_macro] -pub fn my_macro(_input: TokenStream) -> TokenStream { - private_helper(); - public_helper(); - TokenStream::new() -} - -pub(crate) fn public_helper() { - // Should trigger DE1301 - Print macros - eprintln!("not allowed in public helper"); -} - -fn private_helper() { - println!("allowed in private helper"); -} diff --git a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_public_in_proc_macro.stderr b/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_public_in_proc_macro.stderr deleted file mode 100644 index e3d79d4ef..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1301_no_print_macros/ui/forbidden_public_in_proc_macro.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: macro `eprintln!` is forbidden in production code (DE1301) - --> $DIR/forbidden_public_in_proc_macro.rs:16:5 - | -LL | eprintln!("not allowed in public helper"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: use `tracing`/`log` for observability, or return the value and handle it at the boundary - = note: `#[deny(de1301_no_print_macros)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/Cargo.toml b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/Cargo.toml deleted file mode 100644 index 071350808..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -# Created: 2026-03-13 by Constructor Tech -# Updated: 2026-03-16 by Constructor Tech -[package] -name = "de1302_error_from_to_string" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Calling .to_string() in From impls destroys the error chain (DE1302)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "bad_from_to_string" -path = "ui/bad_from_to_string.rs" - -[[example]] -name = "bad_ufcs_to_string" -path = "ui/bad_ufcs_to_string.rs" - -[[example]] -name = "bad_closure_to_string" -path = "ui/bad_closure_to_string.rs" - -[[example]] -name = "bad_tryfrom_to_string" -path = "ui/bad_tryfrom_to_string.rs" - -[[example]] -name = "bad_tryfrom_assoc_error" -path = "ui/bad_tryfrom_assoc_error.rs" - -[[example]] -name = "bad_macro_rules" -path = "ui/bad_macro_rules.rs" - -[[example]] -name = "good_from_preserve" -path = "ui/good_from_preserve.rs" - -[[example]] -name = "good_unrelated_error" -path = "ui/good_unrelated_error.rs" - -[[example]] -name = "good_from_u32" -path = "ui/good_from_u32.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -thiserror.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/README.md b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/README.md deleted file mode 100644 index 8d232fb80..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/README.md +++ /dev/null @@ -1,432 +0,0 @@ -# DE1302 — No `.to_string()` in Error Conversion Impls - -## TL;DR - -Calling `.to_string()` on an error inside `impl From for MyErr` (or -`TryFrom`) collapses a typed source into a string. After that, callers -can't `.source()` to the cause, can't `.downcast_ref::()` to recover -the type, and tracing/alerting tools see only the message — not the -chain. DE1302 fires when the receiver of `.to_string()` is the source -parameter itself (or a `TryFrom::Error` assoc type that implements -`Error`). Fix it by storing the source in the variant — usually with -thiserror's `#[from]` / `#[source]` / `#[error(transparent)]`, or via -`Box` for opaque buckets. See -**[Fixing a flagged site](#fixing-a-flagged-site)** for the full -decision flow. - -## Rule - -This lint flags `.to_string()` calls inside `fn from()` (or `fn try_from()`) -bodies when they appear in: - -- `impl From for Y` -- `impl TryFrom for Y` - -where the source type `X`, the target type `Y`, or the `TryFrom::Error` -associated type implements `std::error::Error`. Both syntactic forms are -caught: - -- Method-call form: `e.to_string()` -- UFCS form: `ToString::to_string(&e)` and `::to_string(&e)` - -Closure bodies are also walked (e.g. `.map(|e| e.to_string())` inside a From -body). - -## Rationale - -`From` impls on error types exist primarily to power the `?` operator: when -a function returns `Result` and the caller writes -`db_query()?`, Rust desugars that to `db_query().map_err(AppError::from)`. -Whatever your `From for AppError` does is what every `?` in -the codebase will do. - -Calling `e.to_string()` on an error inside that conversion converts the -error to a `String` and **discards the original**. The result is a new error -that: - -- Has no `.source()` — the chain is broken; callers can't follow back to the - root cause. -- Cannot be `.downcast_ref::()` to recover the underlying type. -- Loses structured metadata (error codes, fields, retry hints, etc.). -- Is missing the information `tracing`, alerting, and bug-report tooling - rely on. - -For most conversions, you have better options that preserve the chain: - -- **`thiserror`'s `#[from]`** auto-derives a `From` impl that stores the - source error directly. The variant's first field becomes the source. -- **`#[error(transparent)]`** delegates `Display` / `source()` to a wrapped - inner error — the variant disappears from messages but is still reachable - via `.source()`. -- **`#[source]`** marks a field as the chain source without auto-generating - the `From` impl. Use this when you want a custom variant constructor - (`Internal { msg: String, #[source] source: SomeError }`) but still want - `.source()` to work. -- **Box the source**: `Internal(Box)` - with a manual `From` that calls `.into()` (no stringification). The - `Send + Sync + 'static` bound is what async runtimes (tokio, async-std) - and error-reporting libraries need to move errors across tasks. -- **Match-and-forward**: pattern-match the source variants and map them to - shape-preserving target variants. - -## Fixing a flagged site - -When DE1302 fires, work through these in order — the first match is -almost always the right answer. - -1. **Can the target variant hold the source directly (one field, exact - type)?** Use thiserror's `#[from]` and delete the manual `From` impl. - ```rust - #[derive(thiserror::Error, Debug)] - enum AppError { - #[error("db: {0}")] - Database(#[from] DatabaseError), - } - ``` - -2. **Is the variant a pure forward (no extra fields, Display delegates)?** - Add `#[error(transparent)]` to make it invisible in messages while - keeping the chain via `.source()`. - ```rust - #[derive(thiserror::Error, Debug)] - enum AppError { - #[error(transparent)] - Database(#[from] DatabaseError), - } - ``` - -3. **Need a custom variant shape (extra fields like `msg`, `code`, - `retry_after`) but still want `.source()` to work?** Use `#[source]` - on the source field and write the `From` impl manually. - ```rust - #[derive(thiserror::Error, Debug)] - enum AppError { - #[error("db op '{op}' failed: {source}")] - Database { op: String, #[source] source: DatabaseError }, - } - ``` - -4. **One `Internal` bucket that needs to absorb many source types?** - Replace `Internal(String)` with - `Internal(Box)` and use `.into()` - in each From impl — never `.to_string()`. - ```rust - #[derive(thiserror::Error, Debug)] - enum AppError { - #[error(transparent)] - Internal(Box), - } - - impl From for AppError { - fn from(e: anyhow::Error) -> Self { AppError::Internal(e.into()) } - } - ``` - -5. **Is the source already an enum whose variants align with the target - enum's variants?** Match-and-forward — explicit but preserves shape. - ```rust - impl From for AppError { - fn from(e: DbError) -> Self { - match e { - DbError::NotFound(id) => AppError::NotFound(id), - DbError::Conflict(c) => AppError::Conflict(c), - other => AppError::Internal(Box::new(other)), - } - } - } - ``` - -6. **None of the above fit** (e.g. an SDK boundary that exposes only an - opaque `Internal(String)` and changing the SDK is out of scope). - Silence at the impl with a TODO so the debt is grep-able. See - **[Configuration](#configuration)** for the exact pattern. - -If the answer is "none of these fit because the source isn't actually -an `Error`-implementing type" — DE1302 won't fire. The receiver-tightening -gate skips non-Error sources. See `good_from_u32.rs` in the UI tests. - -## Gating - -The lint is type-driven, not name-based. It only walks a body when **at -least one of**: - -- `source_ty` implements `std::error::Error`, **or** -- `target_ty` implements `std::error::Error`, **or** -- (TryFrom only) the `type Error` associated type implements - `std::error::Error`. - -Inside the body, `.to_string()` is only flagged when: - -- The receiver type equals the source parameter type **and** the source type - implements `Error`, **or** -- The receiver type equals the `TryFrom::Error` associated type. - -Any other receiver — `&str`, `String`, `Uuid`, an unrelated error fetched for -logging — is left alone. This prevents false positives like `impl From -for MyErr` flagging `n.to_string()`. - -## Examples - -### Bad — chain destroyed - -```rust -impl From for AppError { - fn from(e: DatabaseError) -> Self { - AppError::Internal(e.to_string()) // chain lost - } -} -``` - -```rust -impl TryFrom for AppError { - type Error = ConversionError; - - fn try_from(e: DatabaseError) -> Result { - Ok(AppError::Internal(e.to_string())) // chain lost - } -} -``` - -```rust -// UFCS form — same problem. -impl From for AppError { - fn from(e: DatabaseError) -> Self { - AppError::Internal(ToString::to_string(&e)) - } -} -``` - -```rust -// Inside a closure inside a From body — also caught. -impl From for AppError { - fn from(e: DatabaseError) -> Self { - let render = |x: &DatabaseError| x.to_string(); - AppError::Internal(render(&e)) - } -} -``` - -### Good — chain preserved - -```rust -// thiserror #[from] — the cleanest path. -#[derive(thiserror::Error, Debug)] -enum AppError { - #[error(transparent)] - Database(#[from] DatabaseError), - #[error("internal: {0}")] - Internal(String), -} -``` - -```rust -// Manual From that stores the source directly. -impl From for AppError { - fn from(e: DatabaseError) -> Self { - AppError::Database(e) // chain preserved via #[error(transparent)] / source() - } -} -``` - -```rust -// Custom variant shape with `#[source]` — when `#[from]` doesn't fit but you -// still want `.source()` to walk into the underlying error. -#[derive(thiserror::Error, Debug)] -enum AppError { - #[error("internal: {msg}")] - Internal { - msg: String, - #[source] - source: DatabaseError, - }, -} - -impl From for AppError { - fn from(e: DatabaseError) -> Self { - AppError::Internal { - msg: "database operation failed".into(), - source: e, - } - } -} -``` - -```rust -// Boxed source variant — keeps a single Internal bucket while preserving -// `.source()`. anyhow::Error already implements Into>. -#[derive(thiserror::Error, Debug)] -enum AppError { - #[error(transparent)] - Unexpected(Box), -} - -impl From for AppError { - fn from(e: anyhow::Error) -> Self { - AppError::Unexpected(e.into()) // no .to_string(), chain preserved - } -} -``` - -### Not flagged (intentional) - -```rust -// Source type is not an Error — stringifying a u32 has no chain to lose. -impl From for AppError { - fn from(n: u32) -> Self { - AppError::Internal(n.to_string()) // OK - } -} -``` - -```rust -// Stringifying an unrelated error inside a From body for logging context. -// The returned error preserves the actual source. -impl From for AppError { - fn from(e: DatabaseError) -> Self { - let other_err = build_some_unrelated_error(); - AppError { - context: other_err.to_string(), // OK — recv is not the source type - source: e, - } - } -} -``` - -```rust -// Format-arg macros (format!, write!, panic!, tracing::*) are NOT caught — -// they construct strings via Display::fmt, not ToString::to_string. See the -// "Known gaps" section. -impl From for AppError { - fn from(e: DatabaseError) -> Self { - AppError::Internal(format!("db: {e}")) // NOT flagged today - } -} -``` - -## Macro behavior - -| Source | Treatment | -| -------------------------------- | ------------------------ | -| `macro_rules!` / bang proc-macro | **Checked.** A macro that expands to `.to_string()` on the source error is just as much a chain-loss pattern as inline code. | -| Attribute macros (`#[attr]`) | Skipped — assumed to be third-party codegen the user can't easily change. | -| Derive macros | Skipped — same reason. | -| Compiler desugarings (`?`, etc.) | Skipped — synthetic, not user intent. | - -## Known gaps - -- **`format!("...{err}")` / `write!` / `panic!`** — these macros destroy the - chain identically (they go through `Display::fmt` rather than - `ToString::to_string`), but DE1302 doesn't see them. Catching this needs - `format_args!`-level inspection; tracked as a follow-up. -- **Logging-only stringification** — `tracing::error!(error = %err)` inside a - conversion body is not flagged; the receiver-type tightening rules out - side-channel `.to_string()` calls. - -## Configuration - -The lint level is **deny** by default. To silence a known site, prefer fixing -the conversion to preserve the chain. When the underlying error type's shape -truly forbids that (e.g. an SDK boundary that exposes only opaque -`Internal(String)`), silence the impl explicitly. - -### Preferred: `#[expect]` with `reason` - -`#[expect(lint, reason = "...")]` (Rust 1.81+) is a stricter form of -`#[allow]`: if the underlying violation ever stops firing — for example, -because someone refactors `Internal(String)` into -`Internal(Box)` — the compiler **warns** that the -expectation didn't fire. The silence ages out automatically as soon as the -real fix lands. - -```rust -#[expect( - unknown_lints, - de1302_error_from_to_string, - reason = "Internal only carries a String; extend to hold \ - Box so .source() returns \ - the original error, then remove this expect." -)] -impl From for MyError { - fn from(e: SomeError) -> Self { - Self::Internal(e.to_string()) - } -} -``` - -If several adjacent conversions share the same rationale, group them in a -small inner module so the attribute appears once: - -```rust -#![expect( - unknown_lints, - de1302_error_from_to_string, - reason = "MyError::Internal collapses many sources into a String. \ - Extend to a boxed-source variant in a follow-up PR, then \ - drop this expect." -)] -mod error_froms { - use super::MyError; - - impl From for MyError { fn from(e: A) -> Self { Self::Internal(e.to_string()) } } - impl From for MyError { fn from(e: B) -> Self { Self::Internal(e.to_string()) } } - impl From for MyError { fn from(e: C) -> Self { Self::Internal(e.to_string()) } } -} -``` - -The `reason` field is also machine-readable — `cargo clippy --message-format=json` -exposes it, so a debt-audit script can list every silenced site with its -explanation in one pass. - -### Acceptable: `#[allow]` with a `TODO(DE1302)` comment - -For sites that pre-date the `#[expect]` recommendation, or when you -specifically don't want the auto-warn behaviour, the older pattern still -works: - -```rust -// TODO(DE1302): `Internal` only carries a String; extend to hold a boxed -// source so `.source()` returns the original error, then remove this allow. -#[allow(unknown_lints, de1302_error_from_to_string)] -impl From for MyError { - fn from(e: SomeError) -> Self { - Self::Internal(e.to_string()) - } -} -``` - -The trade-off: an `#[allow]` doesn't notice when the underlying fix lands, so -it can rot in the codebase indefinitely. Migrate to `#[expect]` whenever you -touch a silenced site. - -### Why `unknown_lints` is in the list - -The dylint driver is only loaded by `make dylint`. Plain `cargo check` / -`cargo clippy` doesn't know about `de1302_error_from_to_string` and would -otherwise reject the attribute as an unknown lint name. Adding -`unknown_lints` to the same `#[expect]` / `#[allow]` makes the attribute -parse cleanly under both toolchains. - -## UI Tests - -This lint includes UI tests covering: - -- Method-call positive case (`bad_from_to_string.rs`) -- UFCS positive case (`bad_ufcs_to_string.rs`) -- Closure body recursion (`bad_closure_to_string.rs`) -- `TryFrom` with Error source (`bad_tryfrom_to_string.rs`) -- `TryFrom` whose only Error is the assoc type (`bad_tryfrom_assoc_error.rs`) -- `macro_rules!` expansion still flagged (`bad_macro_rules.rs`) -- `From` with non-Error source — not flagged (`good_from_u32.rs`) -- Stringifying an unrelated error inside a From body — not flagged - (`good_unrelated_error.rs`) -- `#[from]`-style and source-preserving conversions — not flagged - (`good_from_preserve.rs`) - -## See Also - -- [thiserror](https://crates.io/crates/thiserror) — derive macro for typed - errors with `#[from]` / `#[source]` / `#[error(transparent)]`. -- [anyhow](https://crates.io/crates/anyhow) — opaque error type that - preserves chain via `.source()` and `Into>`. -- [`std::error::Error::source`](https://doc.rust-lang.org/std/error/trait.Error.html#method.source) — the chain navigation method. -- [Error handling in Rust](https://doc.rust-lang.org/book/ch09-00-error-handling.html) diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/src/lib.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/src/lib.rs deleted file mode 100644 index 5041ea7f1..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/src/lib.rs +++ /dev/null @@ -1,357 +0,0 @@ -// Created: 2026-03-13 by Constructor Tech -// Updated: 2026-04-22 by Constructor Tech -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_hir; -extern crate rustc_middle; -extern crate rustc_span; - -use clippy_utils::diagnostics::span_lint_and_then; -use clippy_utils::ty::implements_trait; -use rustc_hir::def::{DefKind, Res}; -use rustc_hir::def_id::DefId; -use rustc_hir::{self as hir, Expr, ExprKind, ImplItemKind, ItemKind, QPath}; -use rustc_lint::{LateContext, LateLintPass}; -use rustc_middle::ty::{Ty, TypeckResults}; -use rustc_span::hygiene::{ExpnKind, MacroKind}; - -dylint_linting::declare_late_lint! { - /// ### What it does - /// - /// Detects `.to_string()` calls inside `fn from()` (or `fn try_from()`) - /// bodies within `impl From for Y` and `impl TryFrom for Y` blocks - /// where X or Y implements `std::error::Error`, which silently destroys - /// the error chain. Catches both method-call syntax (`e.to_string()`) and - /// UFCS form (`ToString::to_string(&e)`), and recurses into closure bodies. - /// - /// ### Why is this bad? - /// - /// When you call `e.to_string()` inside a `From` or `TryFrom` impl, you - /// convert the original error to a string and discard it. The resulting - /// error: - /// - Has no `.source()` (error chain is broken) - /// - Cannot be matched or downcast by callers - /// - Loses structured metadata (error codes, fields, etc.) - /// - /// Tools like `anyhow`, `thiserror`'s `#[from]`, or storing the error directly - /// preserve the chain without any extra effort. - /// - /// Unlike the early-pass version, this lint gates on whether the source or target - /// type actually implements `std::error::Error` (and, for `TryFrom`, also the - /// associated `Error` type), eliminating false positives from name-based - /// heuristics. Inside the matched body, `.to_string()` is only flagged when the - /// receiver type is the source parameter type itself (or the `TryFrom::Error` - /// assoc type) *and* that type implements `Error` — `.to_string()` on unrelated - /// error values used for logging, or on plain non-error source parameters (e.g. - /// `impl From`), is left alone. - /// - /// ### Known gaps - /// - /// Attribute macros, derive macros, and compiler desugarings are skipped so - /// the lint doesn't flag synthesized `.to_string()` calls. `macro_rules!` and - /// bang proc-macro expansions are still checked — `render!(err)` that expands - /// to `err.to_string()` is flagged like hand-written code. - /// - /// **`format!("{}", err)`, `write!(buf, "{}", err)`, and similar macros are - /// NOT caught.** They destroy the chain through `Display::fmt` rather than - /// `ToString::to_string`, so this lint never sees them. If you rely on DE1302 - /// for enforcement, you also need a sibling check on format-arg macros. - /// - /// ### Example - /// - /// ```rust,ignore - /// // Bad - DatabaseError is swallowed; callers can't inspect the root cause - /// impl From for AppError { - /// fn from(e: DatabaseError) -> Self { - /// AppError::Internal(e.to_string()) // chain lost! - /// } - /// } - /// ``` - /// - /// Use instead: - /// - /// ```rust,ignore - /// // Good - store the source error; chain preserved - /// #[derive(thiserror::Error, Debug)] - /// enum AppError { - /// #[error(transparent)] - /// Database(#[from] DatabaseError), - /// } - /// ``` - pub DE1302_ERROR_FROM_TO_STRING, - Deny, - "calling .to_string() in From impl destroys the error chain (DE1302)" -} - -/// Returns true if `ty` implements `std::error::Error`. -/// -/// Uses the `rustc_diagnostic_item = "Error"` marker and `clippy_utils::ty::implements_trait` -/// for proper trait resolution. Handles ADTs, type aliases, and generic params with bounds. -fn implements_error<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { - let Some(error_did) = cx.tcx.get_diagnostic_item(clippy_utils::sym::Error) else { - return false; - }; - implements_trait(cx, ty, error_did, &[]) -} - -struct ToStringVisitor<'tcx, 'cx> { - cx: &'cx LateContext<'tcx>, - /// Typeck results for the body currently being walked. Swapped when we - /// descend into a closure, which has its own typeck tables. - typeck: &'tcx TypeckResults<'tcx>, - /// The source parameter type of the `From` / `TryFrom` impl (`X` in - /// `impl From for Y`). We only flag `.to_string()` when the receiver - /// type equals this, which eliminates false positives from stringifying - /// unrelated error values for logging etc. - source_ty: Ty<'tcx>, - /// For `TryFrom` impls, the `type Error = ...` associated type if it - /// implements `std::error::Error`. Also a valid receiver match — this - /// catches patterns like stringifying a locally constructed error of the - /// associated type while building the `Err(..)` branch. - error_assoc_ty: Option>, -} - -impl<'tcx> ToStringVisitor<'tcx, '_> { - /// Emit the DE1302 diagnostic at `span`. - fn emit(&self, span: rustc_span::Span) { - span_lint_and_then( - self.cx, - DE1302_ERROR_FROM_TO_STRING, - span, - "`.to_string()` in `From`/`TryFrom` impl destroys the error chain (DE1302)", - |diag| { - diag.help( - "store the source error directly, use an enum variant, or use `#[from]` with thiserror", - ); - diag.note( - "`.to_string()` discards the original error type: `.source()` returns None and the error cannot be downcast", - ); - }, - ); - } - - /// Returns true if `ty` (after peeling references) is a type whose - /// stringification inside this impl would destroy an error chain: - /// - The `TryFrom::Error` associated type (if present); or - /// - The source parameter type *and* that source type implements `Error`. - /// - /// The `implements_error` re-check on `source_ty` is important — the - /// impl-level gate accepts an impl when *either* source or target is an - /// Error, so without this check `impl From for MyErr` would flag - /// `n.to_string()` even though `u32` has no chain to lose. - fn is_relevant_receiver(&self, ty: Ty<'tcx>) -> bool { - let inner = ty.peel_refs(); - if let Some(e) = self.error_assoc_ty - && inner == e - { - return true; - } - inner == self.source_ty && implements_error(self.cx, inner) - } - - /// Walk a closure's body with the closure's own typeck results installed. - /// Uses `std::mem::replace` for the swap so the restore is a single - /// unambiguous assignment. Panic-safety is intentionally not provided — - /// if `walk_expr` panics, rustc is already unwinding out of a lint pass - /// and per-visitor state is moot. - fn visit_closure_body(&mut self, closure: &'tcx hir::Closure<'tcx>) { - let body = self.cx.tcx.hir_body(closure.body); - let prev = std::mem::replace(&mut self.typeck, self.cx.tcx.typeck(closure.def_id)); - hir::intravisit::walk_expr(self, body.value); - self.typeck = prev; - } -} - -/// Returns true if `def_id` is `core::string::ToString::to_string`. -/// -/// Walks up from the associated fn to its containing trait and compares to -/// the `ToString` diagnostic item. Shared by the MethodCall and UFCS arms so -/// both paths verify they're actually hitting the trait method, not a bare -/// inherent method named `to_string`. -fn is_to_string_def<'tcx>(cx: &LateContext<'tcx>, def_id: DefId) -> bool { - let Some(to_string_trait) = cx.tcx.get_diagnostic_item(clippy_utils::sym::ToString) else { - return false; - }; - cx.tcx.trait_of_assoc(def_id) == Some(to_string_trait) -} - -/// Returns true if the outer expansion of `span` is one we want to silently -/// skip — specifically attribute macros, derive macros, and compiler -/// desugarings. `macro_rules!` and bang proc-macro expansions are NOT -/// skipped: if a user-defined macro expands to `.to_string()` on a source -/// error, the chain is just as lost as if they had written it inline. -fn is_hidden_expansion(span: rustc_span::Span) -> bool { - matches!( - span.ctxt().outer_expn_data().kind, - ExpnKind::Macro(MacroKind::Attr | MacroKind::Derive, _) | ExpnKind::Desugaring(_) - ) -} - -impl<'tcx> hir::intravisit::Visitor<'tcx> for ToStringVisitor<'tcx, '_> { - fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { - // Skip expansions we can't meaningfully attribute to user intent - // (attr/derive macros, compiler desugarings). We still descend into - // children in case the expansion wraps user-written subexpressions - // whose spans are attributed to the caller. - let hidden = is_hidden_expansion(expr.span); - - match expr.kind { - // Method call form: `e.to_string()`. Resolve the method's DefId - // through typeck and verify it lives in `core::string::ToString` - // — a bare inherent `fn to_string` shouldn't be flagged. - ExprKind::MethodCall(seg, recv, args, _) if !hidden => { - if seg.ident.name.as_str() == "to_string" - && args.is_empty() - && let Some(def_id) = self.typeck.type_dependent_def_id(expr.hir_id) - && is_to_string_def(self.cx, def_id) - { - let recv_ty = self.typeck.expr_ty(recv); - if self.is_relevant_receiver(recv_ty) { - self.emit(expr.span); - } - } - } - // UFCS form: `ToString::to_string(&e)` or `::to_string(&e)`. - ExprKind::Call(callee, [arg]) if !hidden && is_to_string_path(self.cx, callee) => { - let arg_ty = self.typeck.expr_ty(arg); - if self.is_relevant_receiver(arg_ty) { - self.emit(expr.span); - } - } - // Closures have their own typeck tables; delegate to a helper - // that swaps `self.typeck`, walks the body, and restores the - // outer tables before returning. - ExprKind::Closure(closure) => { - self.visit_closure_body(closure); - return; - } - _ => {} - } - hir::intravisit::walk_expr(self, expr); - } -} - -/// Returns true if `callee` resolves to `core::string::ToString::to_string`. -/// Handles both `ToString::to_string(&e)` and `::to_string(&e)` forms. -fn is_to_string_path<'tcx>(cx: &LateContext<'tcx>, callee: &Expr<'tcx>) -> bool { - let ExprKind::Path(qpath) = &callee.kind else { - return false; - }; - // `QPath::LangItem` was removed in nightly-2026-01-22 (lang-item paths are - // expressed via `QPath::Resolved` with `Res::Def(DefKind::*, _)`). - let res = match qpath { - QPath::Resolved(_, path) => path.res, - QPath::TypeRelative(..) => cx.qpath_res(qpath, callee.hir_id), - }; - let Res::Def(DefKind::AssocFn, def_id) = res else { - return false; - }; - is_to_string_def(cx, def_id) -} - -impl<'tcx> LateLintPass<'tcx> for De1302ErrorFromToString { - fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'tcx>) { - let ItemKind::Impl(impl_block) = item.kind else { - return; - }; - - // Only examine `impl From for Y` and `impl TryFrom for Y` blocks. - let Some(trait_ref) = impl_block.of_trait else { - return; - }; - let Some(last_seg) = trait_ref.trait_ref.path.segments.last() else { - return; - }; - let conversion_method = match last_seg.ident.name.as_str() { - "From" => "from", - "TryFrom" => "try_from", - _ => return, - }; - - // Resolve the actual types from the type system. - // For `impl From for Y`: args[0] = Y (Self), args[1] = X (source type). - // `TryFrom` shares the same arg layout (the associated `Error` type lives in - // the impl, not in the trait substs). - let impl_def_id = item.owner_id.def_id; - // `tcx.impl_trait_ref` returns `EarlyBinder<...>` directly in - // nightly-2026-01-22 (no longer wrapped in `Option`). - let impl_trait_ref = cx.tcx.impl_trait_ref(impl_def_id).instantiate_identity(); - let source_ty = impl_trait_ref.args.type_at(1); // X - let target_ty = impl_trait_ref.args.type_at(0); // Y = Self - - // For `TryFrom`, extract `type Error = ...` — used both to extend the - // impl-level gate (so bodies that only touch Error via the assoc type - // still get checked) and to widen the tightened receiver check. - let error_assoc_ty: Option> = if conversion_method == "try_from" { - impl_block.items.iter().find_map(|item_ref| { - let node = cx.tcx.hir_node_by_def_id(item_ref.owner_id.def_id); - let hir::Node::ImplItem(impl_item) = node else { - return None; - }; - if impl_item.ident.name.as_str() != "Error" { - return None; - } - if !matches!(impl_item.kind, ImplItemKind::Type(..)) { - return None; - } - let ty = cx - .tcx - .type_of(item_ref.owner_id.def_id) - .instantiate_identity(); - implements_error(cx, ty).then_some(ty) - }) - } else { - None - }; - - // Gate: at least one of source, target, or (for TryFrom) the Error - // associated type must actually implement std::error::Error. This - // replaces name heuristics, eliminating false positives like - // `impl From for ParseError` where String is not an Error. - if !implements_error(cx, source_ty) - && !implements_error(cx, target_ty) - && error_assoc_ty.is_none() - { - return; - } - - // Walk the `from` / `try_from` body looking for .to_string() calls. - // tcx.hir() was removed in nightly-2025-09-18; use hir_node_by_def_id instead. - for item_ref in impl_block.items { - let node = cx.tcx.hir_node_by_def_id(item_ref.owner_id.def_id); - let hir::Node::ImplItem(impl_item) = node else { - continue; - }; - if impl_item.ident.name.as_str() != conversion_method { - continue; - } - let ImplItemKind::Fn(_, body_id) = impl_item.kind else { - continue; - }; - let body = cx.tcx.hir_body(body_id); - let typeck = cx.tcx.typeck(item_ref.owner_id.def_id); - let mut visitor = ToStringVisitor { - cx, - typeck, - source_ty, - error_assoc_ty, - }; - hir::intravisit::walk_expr(&mut visitor, body.value); - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE1302", "to_string"); - } -} diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_closure_to_string.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_closure_to_string.rs deleted file mode 100644 index 41902285e..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_closure_to_string.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Created: 2026-04-21 by Constructor Tech -// Updated: 2026-04-21 by Constructor Tech -#![allow(dead_code)] - -use std::fmt; - -#[derive(Debug)] -struct DatabaseError(String); - -impl fmt::Display for DatabaseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for DatabaseError {} - -#[derive(Debug)] -struct AppError(String); - -impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for AppError {} - -// Closure inside `fn from` body — the lint must descend into the closure -// using its own typeck context. -impl From for AppError { - fn from(e: DatabaseError) -> Self { - let render = |err: &DatabaseError| { - // Should trigger DE1302 - to_string - err.to_string() - }; - AppError(render(&e)) - } -} - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_closure_to_string.stderr b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_closure_to_string.stderr deleted file mode 100644 index 1f1d34dc2..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_closure_to_string.stderr +++ /dev/null @@ -1,12 +0,0 @@ -error: `.to_string()` in `From`/`TryFrom` impl destroys the error chain (DE1302) - --> $DIR/bad_closure_to_string.rs:35:13 - | -LL | err.to_string() - | ^^^^^^^^^^^^^^^ - | - = help: store the source error directly, use an enum variant, or use `#[from]` with thiserror - = note: `.to_string()` discards the original error type: `.source()` returns None and the error cannot be downcast - = note: `#[deny(de1302_error_from_to_string)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_from_to_string.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_from_to_string.rs deleted file mode 100644 index 660215b07..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_from_to_string.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Created: 2026-03-13 by Constructor Tech -// Updated: 2026-03-13 by Constructor Tech -#![allow(dead_code)] - -use std::fmt; - -#[derive(Debug)] -struct DatabaseError(String); - -impl fmt::Display for DatabaseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for DatabaseError {} - -#[derive(Debug)] -struct AppError(String); - -impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for AppError { - fn from(e: DatabaseError) -> Self { - // Should trigger DE1302 - to_string loses error chain - AppError(e.to_string()) - } -} - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_from_to_string.stderr b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_from_to_string.stderr deleted file mode 100644 index a089c25ed..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_from_to_string.stderr +++ /dev/null @@ -1,12 +0,0 @@ -error: `.to_string()` in `From`/`TryFrom` impl destroys the error chain (DE1302) - --> $DIR/bad_from_to_string.rs:30:18 - | -LL | AppError(e.to_string()) - | ^^^^^^^^^^^^^ - | - = help: store the source error directly, use an enum variant, or use `#[from]` with thiserror - = note: `.to_string()` discards the original error type: `.source()` returns None and the error cannot be downcast - = note: `#[deny(de1302_error_from_to_string)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_macro_rules.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_macro_rules.rs deleted file mode 100644 index 89f133ebd..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_macro_rules.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Created: 2026-04-22 by Constructor Tech -// Updated: 2026-04-22 by Constructor Tech -#![allow(dead_code)] - -use std::fmt; - -// Positive case: `macro_rules!` expansions that contain `.to_string()` on -// the source error are NOT silenced. A user-defined macro that stringifies -// an error is just as much a chain-loss pattern as hand-written code, so -// the lint still fires through the expansion. - -#[derive(Debug)] -struct DatabaseError(String); - -impl fmt::Display for DatabaseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for DatabaseError {} - -#[derive(Debug)] -struct AppError(String); - -impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for AppError {} - -macro_rules! render_err { - ($e:expr) => { - // Should trigger DE1302 - to_string - $e.to_string() - }; -} - -impl From for AppError { - fn from(e: DatabaseError) -> Self { - AppError(render_err!(e)) - } -} - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_macro_rules.stderr b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_macro_rules.stderr deleted file mode 100644 index 0f3573b1b..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_macro_rules.stderr +++ /dev/null @@ -1,16 +0,0 @@ -error: `.to_string()` in `From`/`TryFrom` impl destroys the error chain (DE1302) - --> $DIR/bad_macro_rules.rs:37:9 - | -LL | $e.to_string() - | ^^^^^^^^^^^^^^ -... -LL | AppError(render_err!(e)) - | -------------- in this macro invocation - | - = help: store the source error directly, use an enum variant, or use `#[from]` with thiserror - = note: `.to_string()` discards the original error type: `.source()` returns None and the error cannot be downcast - = note: `#[deny(de1302_error_from_to_string)]` on by default - = note: this error originates in the macro `render_err` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_assoc_error.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_assoc_error.rs deleted file mode 100644 index c75b03c3e..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_assoc_error.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Created: 2026-04-22 by Constructor Tech -// Updated: 2026-04-22 by Constructor Tech -#![allow(dead_code)] - -use std::fmt; - -// Positive case for the TryFrom::Error gate + tightened receiver check: -// source and target are both plain non-error types, but the associated -// `type Error = MyErr` implements `std::error::Error`. The body takes a -// pre-existing `MyErr` whose `.source()` carries a real `ParseIntError` -// chain, then stringifies it while building a new `MyErr` — dropping the -// `ParseIntError` cause. - -#[derive(Debug)] -struct MyErr { - msg: String, - source: Option>, -} - -impl fmt::Display for MyErr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.msg) - } -} - -impl std::error::Error for MyErr { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - self.source.as_deref().map(|s| s as _) - } -} - -fn parse_strict(s: &str) -> Result { - s.parse::().map_err(|e| MyErr { - msg: format!("parse failed for {s:?}"), - source: Some(Box::new(e)), - }) -} - -struct PlainData(u32); -struct PlainOutput(u32); - -impl TryFrom for PlainOutput { - type Error = MyErr; - - fn try_from(value: PlainData) -> Result { - if value.0 == 0 { - // `inner` carries a real `.source()` chain (ParseIntError). - let inner = parse_strict("not a number").unwrap_err(); - return Err(MyErr { - // Should trigger DE1302 - to_string - msg: inner.to_string(), - source: None, - }); - } - Ok(PlainOutput(value.0)) - } -} - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_assoc_error.stderr b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_assoc_error.stderr deleted file mode 100644 index 576d8d025..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_assoc_error.stderr +++ /dev/null @@ -1,12 +0,0 @@ -error: `.to_string()` in `From`/`TryFrom` impl destroys the error chain (DE1302) - --> $DIR/bad_tryfrom_assoc_error.rs:51:22 - | -LL | msg: inner.to_string(), - | ^^^^^^^^^^^^^^^^^ - | - = help: store the source error directly, use an enum variant, or use `#[from]` with thiserror - = note: `.to_string()` discards the original error type: `.source()` returns None and the error cannot be downcast - = note: `#[deny(de1302_error_from_to_string)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_to_string.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_to_string.rs deleted file mode 100644 index bd5668e57..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_to_string.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Created: 2026-04-21 by Constructor Tech -// Updated: 2026-04-21 by Constructor Tech -#![allow(dead_code)] - -use std::fmt; - -#[derive(Debug)] -struct DatabaseError(String); - -impl fmt::Display for DatabaseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for DatabaseError {} - -#[derive(Debug)] -struct AppError(String); - -impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for AppError {} - -// Intentionally NOT `impl Error for ConversionRejected` — this test exercises -// the path where the source (`DatabaseError`) is the Error-implementing gate -// trigger, not the associated `Error` type. That way the lint's firing here -// depends only on the source-type match, not on the assoc-type gate. -#[derive(Debug)] -struct ConversionRejected; - -// `TryFrom` impls are also covered. -impl TryFrom for AppError { - type Error = ConversionRejected; - - fn try_from(e: DatabaseError) -> Result { - // Should trigger DE1302 - to_string - Ok(AppError(e.to_string())) - } -} - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_to_string.stderr b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_to_string.stderr deleted file mode 100644 index d8ca3acfc..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_tryfrom_to_string.stderr +++ /dev/null @@ -1,12 +0,0 @@ -error: `.to_string()` in `From`/`TryFrom` impl destroys the error chain (DE1302) - --> $DIR/bad_tryfrom_to_string.rs:42:21 - | -LL | Ok(AppError(e.to_string())) - | ^^^^^^^^^^^^^ - | - = help: store the source error directly, use an enum variant, or use `#[from]` with thiserror - = note: `.to_string()` discards the original error type: `.source()` returns None and the error cannot be downcast - = note: `#[deny(de1302_error_from_to_string)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_ufcs_to_string.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_ufcs_to_string.rs deleted file mode 100644 index be3da150f..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_ufcs_to_string.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Created: 2026-04-20 by Constructor Tech -// Updated: 2026-04-20 by Constructor Tech -#![allow(dead_code)] - -use std::fmt; - -#[derive(Debug)] -struct DatabaseError(String); - -impl fmt::Display for DatabaseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for DatabaseError {} - -#[derive(Debug)] -struct AppError(String); - -impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for AppError { - fn from(e: DatabaseError) -> Self { - // Should trigger DE1302 - to_string - AppError(ToString::to_string(&e)) - } -} - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_ufcs_to_string.stderr b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_ufcs_to_string.stderr deleted file mode 100644 index a139f932e..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/bad_ufcs_to_string.stderr +++ /dev/null @@ -1,12 +0,0 @@ -error: `.to_string()` in `From`/`TryFrom` impl destroys the error chain (DE1302) - --> $DIR/bad_ufcs_to_string.rs:30:18 - | -LL | AppError(ToString::to_string(&e)) - | ^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: store the source error directly, use an enum variant, or use `#[from]` with thiserror - = note: `.to_string()` discards the original error type: `.source()` returns None and the error cannot be downcast - = note: `#[deny(de1302_error_from_to_string)]` on by default - -error: aborting due to 1 previous error - diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_from_preserve.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_from_preserve.rs deleted file mode 100644 index ee14e09ad..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_from_preserve.rs +++ /dev/null @@ -1,106 +0,0 @@ -// Created: 2026-03-13 by Constructor Tech -// Updated: 2026-03-13 by Constructor Tech -#![allow(dead_code)] - -use std::fmt; - -#[derive(Debug)] -struct DatabaseError(String); - -impl fmt::Display for DatabaseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for DatabaseError {} - -// Good - store the source error directly (preserves chain) -#[derive(Debug)] -enum AppError { - Database(DatabaseError), - Other(String), -} - -impl From for AppError { - fn from(e: DatabaseError) -> Self { - AppError::Database(e) // error chain preserved - } -} - -// Good - From for error types does not involve an Error source -#[derive(Debug)] -struct ParseError(String); - -impl From for ParseError { - fn from(s: String) -> Self { - ParseError(s.to_string()) // not From, not flagged - } -} - -// Good - to_string() in a non-From method is fine -impl AppError { - fn message(&self) -> String { - format!("{:?}", self) - } -} - -// Good - to_string() on a non-Error receiver inside From body is NOT flagged. -// The receiver "database layer" is &str, which does not implement Error. -#[derive(Debug)] -struct ContextError { - source: DatabaseError, - context: String, -} - -impl fmt::Display for ContextError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {}", self.context, self.source) - } -} - -impl std::error::Error for ContextError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.source) - } -} - -impl From for ContextError { - fn from(e: DatabaseError) -> Self { - ContextError { - source: e, - context: "database layer".to_string(), // receiver is &str, not Error — not flagged - } - } -} - -// Good - UFCS ToString::to_string on a non-Error value must not be flagged. -#[derive(Debug)] -struct KeyedError { - key: String, - source: DatabaseError, -} - -impl fmt::Display for KeyedError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {}", self.key, self.source) - } -} - -impl std::error::Error for KeyedError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.source) - } -} - -impl From for KeyedError { - fn from(e: DatabaseError) -> Self { - let label: &str = "db"; - KeyedError { - key: ToString::to_string(label), // UFCS on &str — not flagged - source: e, - } - } -} - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_from_preserve.stderr b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_from_preserve.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_from_u32.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_from_u32.rs deleted file mode 100644 index bb6f31a75..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_from_u32.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Created: 2026-04-22 by Constructor Tech -// Updated: 2026-04-22 by Constructor Tech -#![allow(dead_code)] - -use std::fmt; - -// Negative case: the tightened receiver check re-verifies that the source -// parameter type itself implements `Error`. The impl-level gate passes -// because the target `MyError` implements `Error`, but stringifying a plain -// `u32` source doesn't destroy any chain — nothing to flag. - -#[derive(Debug)] -struct MyError(String); - -impl fmt::Display for MyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for MyError {} - -impl From for MyError { - fn from(n: u32) -> Self { - // Should not trigger DE1302 - to_string - MyError(n.to_string()) - } -} - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_unrelated_error.rs b/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_unrelated_error.rs deleted file mode 100644 index d79e7d947..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1302_error_from_to_string/ui/good_unrelated_error.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Created: 2026-04-22 by Constructor Tech -// Updated: 2026-04-22 by Constructor Tech -#![allow(dead_code)] - -use std::fmt; - -// Negative case: the tightened receiver check only flags `.to_string()` when -// the receiver is the source parameter type itself (or the `TryFrom::Error` -// assoc type). Stringifying an *unrelated* error inside a From body — e.g. -// for logging or to include a sibling error's message in a constructed -// variant — is intentionally not flagged. - -#[derive(Debug)] -struct DatabaseError(String); - -impl fmt::Display for DatabaseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for DatabaseError {} - -#[derive(Debug)] -struct OtherError(String); - -impl fmt::Display for OtherError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for OtherError {} - -#[derive(Debug)] -struct AppError { - source: DatabaseError, - context: String, -} - -impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {}", self.context, self.source) - } -} - -impl std::error::Error for AppError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.source) - } -} - -// The From body stringifies `other`, which is an Error but NOT the source -// type (`DatabaseError`). The real source `e` is preserved in `source`, so -// the chain is intact. The tightened lint does not flag this. -fn build_other() -> OtherError { - OtherError("sibling".into()) -} - -impl From for AppError { - fn from(e: DatabaseError) -> Self { - let other = build_other(); - AppError { - context: other.to_string(), // Should not trigger DE1302 - to_string - source: e, - } - } -} - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/Cargo.toml b/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/Cargo.toml deleted file mode 100644 index 736e2973e..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -# Created: 2026-03-13 by Constructor Tech -# Updated: 2026-03-17 by Constructor Tech -[package] -name = "de1303_no_primitive_type_alias" -version = "0.1.0" -authors = ["Constructor Fabric"] -description = "Disallow pub type X = primitive; use newtype for type safety (DE1303)" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib"] - -[[example]] -name = "bad_uuid_type_alias" -path = "ui/bad_uuid_type_alias.rs" - -[[example]] -name = "good_uuid_newtype" -path = "ui/good_uuid_newtype.rs" - -[dependencies] -clippy_utils.workspace = true -dylint_linting.workspace = true -lint_utils.workspace = true - -[dev-dependencies] -dylint_testing.workspace = true -serde_json.workspace = true -uuid.workspace = true - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/README.md b/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/README.md deleted file mode 100644 index fdc51b2fa..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/README.md +++ /dev/null @@ -1,63 +0,0 @@ -Created: 2026-03-18 by Constructor Tech -Updated: 2026-03-18 by Constructor Tech - -# DE1303: No Primitive Type Aliases in Contract - -## What it does - -Detects `pub type X = Y` aliases in **contract gears** where `Y` is a primitive-like type (Uuid, String, integers, etc.). Such aliases provide zero compile-time type safety and should be newtypes instead. - -## Why is this bad? - -A bare type alias is fully transparent: `TenantId` and `UserId` both resolve to `Uuid`, so the compiler accepts one where the other is expected. A newtype (`pub struct TenantId(Uuid)`) makes such confusion a hard compile error. - -Type aliases are useful for generics or shortening complex types, not for wrapping a single primitive. - -## Scope - -This lint **only** enforces in **contract gears** (paths containing `contract/`). SDK and contract boundaries are where transparent primitive aliases cause API type-safety problems. - -## Example - -### Bad - -```rust -// In contract/ -pub type TenantId = Uuid; -pub type GtsId = String; -pub type Port = u16; -``` - -### Good - -```rust -// In contract/ -pub struct TenantId(pub Uuid); -pub struct GtsId(String); -pub struct Port(u16); -``` - -### Excluded (not flagged) - -```rust -pub type Wrapper = Vec; // Generic alias -pub type JsonValue = serde_json::Value; // Complex type, not primitive -``` - -## Primitive types flagged - -- UUID/ID: `Uuid`, `Ulid` -- String: `String` -- Integers: `u8`, `u16`, `u32`, `u64`, `u128`, `usize`, `i8`, `i16`, `i32`, `i64`, `i128`, `isize` -- Floats: `f32`, `f64` -- Other: `bool`, `char` - -## Configuration - -This lint is configured to **deny** by default. - -## See Also - -- [Newtype pattern](https://doc.rust-lang.org/rust-by-example/generics/new_types.html) -- [DE0309](../../de03_domain_layer/de0309_must_have_domain_model) - Must Have Domain Model -- [Gear layout and SDK pattern](../../../../docs/toolkit_unified_system/02_gear_layout_and_sdk_pattern.md) diff --git a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/src/lib.rs b/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/src/lib.rs deleted file mode 100644 index 56e298ad9..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/src/lib.rs +++ /dev/null @@ -1,136 +0,0 @@ -// Created: 2026-03-13 by Constructor Tech -// Updated: 2026-03-17 by Constructor Tech -#![feature(rustc_private)] -#![warn(unused_extern_crates)] - -extern crate rustc_ast; - -use clippy_utils::diagnostics::span_lint_and_then; -use lint_utils::is_in_contract_module_ast; -use rustc_ast::{Item, ItemKind, TyKind, VisibilityKind}; -use rustc_lint::EarlyLintPass; - -dylint_linting::declare_early_lint! { - /// ### What it does - /// - /// Detects `pub type X = Y` aliases where Y is a primitive-like type (Uuid, String, - /// integer types). Such aliases provide zero compile-time type safety and should - /// be newtypes instead. - /// - /// ### Why is this bad? - /// - /// A bare type alias is fully transparent: `TenantId` and `UserId` both resolve - /// to `Uuid`, so the compiler accepts one where the other is expected. A newtype - /// (`pub struct TenantId(Uuid)`) makes such confusion a hard compile error. - /// Type aliases are useful for generics or shortening complex types, not for - /// wrapping a single primitive. - /// - /// ### Known Exclusions - /// - /// Generic type aliases (e.g., `pub type BoxedId = ...`) are not flagged. - /// Aliases of complex types (e.g., `pub type JsonValue = serde_json::Value`) are - /// not flagged — only primitive-like backing types are reported. - /// - /// ### Example - /// - /// ```rust,ignore - /// // Bad - transparent alias; no type safety - /// pub type TenantId = Uuid; - /// pub type GtsId = String; - /// pub type Port = u16; - /// ``` - /// - /// Use instead: - /// - /// ```rust,ignore - /// // Good - newtypes provide compile-time separation - /// pub struct TenantId(pub Uuid); - /// pub struct GtsId(String); - /// pub struct Port(u16); - /// ``` - pub DE1303_NO_PRIMITIVE_TYPE_ALIAS, - Deny, - "pub type X = primitive is a transparent alias; use a newtype for type safety (DE1303)" -} - -impl EarlyLintPass for De1303NoPrimitiveTypeAlias { - fn check_item(&mut self, cx: &rustc_lint::EarlyContext<'_>, item: &Item) { - // Only enforce in contract gears — SDK/contract boundaries are where - // transparent primitive aliases cause API type-safety problems. - if !is_in_contract_module_ast(cx, item) { - return; - } - - let ItemKind::TyAlias(ty_alias) = &item.kind else { - return; - }; - - // Only flag public aliases — private helpers are internal details - if !matches!(item.vis.kind, VisibilityKind::Public) { - return; - } - - let name = ty_alias.ident.name.as_str(); - - // Skip generic aliases like `pub type WrappedId = ...` - if !ty_alias.generics.params.is_empty() { - return; - } - - // RHS must be a bare path whose last segment is a primitive-like backing type. - // Covers UUID types, String, and all built-in primitive types. Qualified paths - // like `uuid::Uuid` work because we only inspect the last path segment. - const PRIMITIVE_BACKING_TYPES: &[&str] = &[ - // UUID / identifier types - "Uuid", "Ulid", // String - "String", // Unsigned integers - "u8", "u16", "u32", "u64", "u128", "usize", // Signed integers - "i8", "i16", "i32", "i64", "i128", "isize", // Floating point - "f32", "f64", // Other primitives - "bool", "char", - ]; - - let Some(ty) = &ty_alias.ty else { - return; - }; - let TyKind::Path(None, path) = &ty.kind else { - return; - }; - let Some(last_seg) = path.segments.last() else { - return; - }; - let backing = last_seg.ident.name.as_str(); - if !PRIMITIVE_BACKING_TYPES.contains(&backing) { - return; - } - - span_lint_and_then( - cx, - DE1303_NO_PRIMITIVE_TYPE_ALIAS, - item.span, - format!( - "`pub type {name} = {backing}` is a transparent alias with no type safety (DE1303)" - ), - |diag| { - diag.help(format!( - "wrap {backing} in a newtype: `pub struct {name}(pub {backing});` or `pub struct {name}({backing});`" - )); - diag.note("transparent aliases provide no compile-time separation; use a newtype for distinct semantic types"); - }, - ); - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_examples() { - dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME")); - } - - #[test] - fn test_comment_annotations_match_stderr() { - let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui"); - lint_utils::test_comment_annotations_match_stderr(&ui_dir, "DE1303", "transparent alias"); - } -} diff --git a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/bad_uuid_type_alias.rs b/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/bad_uuid_type_alias.rs deleted file mode 100644 index dce406216..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/bad_uuid_type_alias.rs +++ /dev/null @@ -1,24 +0,0 @@ -// simulated_dir=/cf-gears/gears/some_gear/contract/ -#![allow(dead_code)] - -use uuid::Uuid; - -// Should trigger DE1303 - transparent alias of primitive -pub type TenantId = Uuid; - -// Should trigger DE1303 - transparent alias of primitive -pub type UserId = Uuid; - -// Should trigger DE1303 - transparent alias of String -pub type GtsId = String; - -// Should trigger DE1303 - transparent alias of primitive -pub type Port = u16; - -// Should trigger DE1303 - transparent alias (qualified path, last segment matches) -pub type CorrelationId = uuid::Uuid; - -// Should trigger DE1303 - transparent alias (i32 backing type) -pub type Count = i32; - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/bad_uuid_type_alias.stderr b/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/bad_uuid_type_alias.stderr deleted file mode 100644 index 4e0b46386..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/bad_uuid_type_alias.stderr +++ /dev/null @@ -1,57 +0,0 @@ -error: `pub type TenantId = Uuid` is a transparent alias with no type safety (DE1303) - --> $DIR/bad_uuid_type_alias.rs:7:1 - | -LL | pub type TenantId = Uuid; - | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: wrap Uuid in a newtype: `pub struct TenantId(pub Uuid);` or `pub struct TenantId(Uuid);` - = note: transparent aliases provide no compile-time separation; use a newtype for distinct semantic types - = note: `#[deny(de1303_no_primitive_type_alias)]` on by default - -error: `pub type UserId = Uuid` is a transparent alias with no type safety (DE1303) - --> $DIR/bad_uuid_type_alias.rs:10:1 - | -LL | pub type UserId = Uuid; - | ^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: wrap Uuid in a newtype: `pub struct UserId(pub Uuid);` or `pub struct UserId(Uuid);` - = note: transparent aliases provide no compile-time separation; use a newtype for distinct semantic types - -error: `pub type GtsId = String` is a transparent alias with no type safety (DE1303) - --> $DIR/bad_uuid_type_alias.rs:13:1 - | -LL | pub type GtsId = String; - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: wrap String in a newtype: `pub struct GtsId(pub String);` or `pub struct GtsId(String);` - = note: transparent aliases provide no compile-time separation; use a newtype for distinct semantic types - -error: `pub type Port = u16` is a transparent alias with no type safety (DE1303) - --> $DIR/bad_uuid_type_alias.rs:16:1 - | -LL | pub type Port = u16; - | ^^^^^^^^^^^^^^^^^^^^ - | - = help: wrap u16 in a newtype: `pub struct Port(pub u16);` or `pub struct Port(u16);` - = note: transparent aliases provide no compile-time separation; use a newtype for distinct semantic types - -error: `pub type CorrelationId = Uuid` is a transparent alias with no type safety (DE1303) - --> $DIR/bad_uuid_type_alias.rs:19:1 - | -LL | pub type CorrelationId = uuid::Uuid; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: wrap Uuid in a newtype: `pub struct CorrelationId(pub Uuid);` or `pub struct CorrelationId(Uuid);` - = note: transparent aliases provide no compile-time separation; use a newtype for distinct semantic types - -error: `pub type Count = i32` is a transparent alias with no type safety (DE1303) - --> $DIR/bad_uuid_type_alias.rs:22:1 - | -LL | pub type Count = i32; - | ^^^^^^^^^^^^^^^^^^^^^ - | - = help: wrap i32 in a newtype: `pub struct Count(pub i32);` or `pub struct Count(i32);` - = note: transparent aliases provide no compile-time separation; use a newtype for distinct semantic types - -error: aborting due to 6 previous errors - diff --git a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/good_uuid_newtype.rs b/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/good_uuid_newtype.rs deleted file mode 100644 index cc8846e79..000000000 --- a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/good_uuid_newtype.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![allow(dead_code)] - -use uuid::Uuid; - -// Good - newtypes provide compile-time type safety -pub struct TenantId(pub Uuid); -pub struct UserId(Uuid); -pub struct GtsId(String); -pub struct Port(u16); - -// Good - generic alias (excluded by design) -pub type Wrapper = Vec; - -// Good - alias of complex type, not a primitive -pub type JsonValue = serde_json::Value; - -// Good - pub(crate) visibility is not flagged (lint only targets fully public aliases) -pub(crate) type InternalId = Uuid; - -fn main() {} diff --git a/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/good_uuid_newtype.stderr b/tools/dylint_lints/de13_common_patterns/de1303_no_primitive_type_alias/ui/good_uuid_newtype.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/tools/dylint_lints/lint_utils/Cargo.toml b/tools/dylint_lints/lint_utils/Cargo.toml deleted file mode 100644 index edfeeb85a..000000000 --- a/tools/dylint_lints/lint_utils/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "lint_utils" -version = "0.1.0" -edition.workspace = true -publish = false - -[lib] -test = false - -[package.metadata.rust-analyzer] -rustc_private = true diff --git a/tools/dylint_lints/lint_utils/src/lib.rs b/tools/dylint_lints/lint_utils/src/lib.rs deleted file mode 100644 index 4a3a73426..000000000 --- a/tools/dylint_lints/lint_utils/src/lib.rs +++ /dev/null @@ -1,706 +0,0 @@ -#![feature(rustc_private)] - -extern crate rustc_ast; -extern crate rustc_driver; -extern crate rustc_hir; -extern crate rustc_lint; -extern crate rustc_session; -extern crate rustc_span; - -use rustc_lint::LintContext; - -use rustc_ast::{UseTree, UseTreeKind}; - -use rustc_span::source_map::SourceMap; -use rustc_span::{FileName, RemapPathScopeComponents, Span}; -use std::collections::HashSet; - -const ALLOWED_FLAGS: &[&str] = &["request", "response"]; - -pub fn is_in_domain_path(source_map: &SourceMap, span: Span) -> bool { - check_span_path(source_map, span, "/domain/") -} - -pub fn is_in_infra_path(source_map: &SourceMap, span: Span) -> bool { - check_span_path(source_map, span, "/infra/") -} - -pub fn is_in_contract_path(source_map: &SourceMap, span: Span) -> bool { - check_span_path(source_map, span, "/contract/") -} - -/// AST-based helper to check if an item is in a contract module. -/// This works with EarlyLintPass and checks both file paths and simulated_dir comments. -pub fn is_in_contract_module_ast( - cx: &rustc_lint::EarlyContext<'_>, - item: &rustc_ast::Item, -) -> bool { - is_in_contract_path(cx.sess().source_map(), item.span) -} - -pub fn is_in_api_rest_folder(source_map: &SourceMap, span: Span) -> bool { - check_span_path(source_map, span, "/api/rest/") -} - -pub fn is_in_module_folder(source_map: &SourceMap, span: Span) -> bool { - check_span_path(source_map, span, "/gears/") -} - -/// Extract the filename string from a span. -/// Handles local paths and remapped paths with virtual name fallback. -pub fn filename_str(source_map: &SourceMap, span: Span) -> Option { - let file_name = source_map.span_to_filename(span); - match &file_name { - FileName::Real(real) => { - if let Some(local) = real.local_path() { - Some(local.to_string_lossy().to_string()) - } else { - Some( - real.path(RemapPathScopeComponents::DIAGNOSTICS) - .to_string_lossy() - .to_string(), - ) - } - } - _ => None, - } -} - -/// Check if a file path is in a temporary directory (used by test infrastructure). -pub fn is_temp_path(path: &str) -> bool { - // Primary check: compare against the actual system temp directory - let temp_dir = std::env::temp_dir(); - if let Some(temp_str) = temp_dir.to_str() - && path.starts_with(temp_str) - { - return true; - } - // Fallback patterns for known temp directory locations - path.contains("/tmp/") || path.contains("/var/folders/") || path.contains("\\Temp\\") -} - -/// Result of parsing a version suffix from a name like `FooClientV1` or `FooClient2`. -pub struct VersionParts<'a> { - /// Base name without version suffix or trailing digits (e.g., `FooClient`) - pub base: &'a str, - /// Valid version suffix like `V1`, `V2`, or empty string if none - pub version_suffix: &'a str, - /// Trailing digits without V prefix (e.g., `2` from `FooClient2`), or empty string - pub malformed_digits: &'a str, -} - -impl VersionParts<'_> { - /// Returns true if a valid version suffix (V + digits) was found. - pub fn has_valid_version(&self) -> bool { - !self.version_suffix.is_empty() - } - - /// Returns true if there are trailing digits but no V prefix (malformed version). - pub fn has_malformed_version(&self) -> bool { - !self.malformed_digits.is_empty() && self.version_suffix.is_empty() - } -} - -/// Parse version suffix from a trait/type name. -/// -/// - `FooClientV1` -> base=`FooClient`, version_suffix=`V1`, malformed_digits=`` -/// - `FooClientV10` -> base=`FooClient`, version_suffix=`V10`, malformed_digits=`` -/// - `FooClient2` -> base=`FooClient`, version_suffix=``, malformed_digits=`2` -/// - `FooClient` -> base=`FooClient`, version_suffix=``, malformed_digits=`` -/// - `FooClientV` -> base=`FooClient`, version_suffix=``, malformed_digits=`` (bare V stripped) -/// - `FooClientV0` -> base=`FooClient`, version_suffix=``, malformed_digits=`` (V0 rejected) -/// - `FooClientV01` -> base=`FooClient`, version_suffix=``, malformed_digits=`` (leading zero rejected) -pub fn parse_version_suffix(name: &str) -> VersionParts<'_> { - if name.is_empty() { - return VersionParts { - base: name, - version_suffix: "", - malformed_digits: "", - }; - } - - let bytes = name.as_bytes(); - let len = bytes.len(); - - let mut digit_count = 0; - for &b in bytes.iter().rev() { - if b.is_ascii_digit() { - digit_count += 1; - } else { - break; - } - } - - if digit_count == 0 { - // No trailing digits — check for bare trailing V (e.g., `FooClientV`) - if len > 1 && bytes[len - 1] == b'V' { - return VersionParts { - base: &name[..len - 1], - version_suffix: "", - malformed_digits: "", - }; - } - return VersionParts { - base: name, - version_suffix: "", - malformed_digits: "", - }; - } - - let digits_start = len - digit_count; - - if digits_start > 0 && bytes[digits_start - 1] == b'V' { - let v_pos = digits_start - 1; - let digit_str = &name[digits_start..]; - - // Valid version: V followed by non-zero number without leading zeros (V1, V2, V10) - // Invalid: V0, V00, V01 (leading zeros or zero version) - if !digit_str.starts_with('0') { - VersionParts { - base: &name[..v_pos], - version_suffix: &name[v_pos..], - malformed_digits: "", - } - } else { - // V0, V00, V01 — strip the invalid V-prefix version from base - VersionParts { - base: &name[..v_pos], - version_suffix: "", - malformed_digits: "", - } - } - } else { - VersionParts { - base: &name[..digits_start], - version_suffix: "", - malformed_digits: &name[digits_start..], - } - } -} - -/// Check if the current compilation target is an SDK crate (by crate name or file path). -/// -/// Also returns true for files in temporary directories — this is required because -/// `dylint_testing::ui_test_examples()` compiles UI test files from temp dirs without -/// passing `--crate-name`, so the crate name check alone doesn't work for UI tests. -pub fn is_in_sdk_crate(cx: &rustc_lint::EarlyContext<'_>, span: Span) -> bool { - if let Some(crate_name) = cx.sess().opts.crate_name.as_deref() - // Cargo normalizes dashes to underscores for `--crate-name`. - && (crate_name.ends_with("-sdk") || crate_name.ends_with("_sdk")) - { - return true; - } - - let Some(file_path) = filename_str(cx.sess().source_map(), span) else { - return false; - }; - - file_path.contains("-sdk/") || file_path.contains("-sdk\\") || is_temp_path(&file_path) -} - -/// Check if span is within the allow-list for non-FIPS hasher imports (DE0708). -/// -/// Entries are confined `sha2` call sites approved per `SECURITY.md §9`: -/// - `gears/file-storage/file-storage/src/infra/content/hash.rs` — the single -/// SHA-256 site in file-storage, used for content addressing/integrity -/// (`expected_hash`, version identity) and the opaque ETag, **not** for -/// signatures. The signed-URL signing primitive lives behind its own -/// provider abstraction (ADR-0004), not here. -/// -/// Add entries here only for legitimate non-cryptographic or FIPS-validated -/// usage, with a corresponding `SECURITY.md §9` disclaimer. -pub fn is_in_hasher_allow_list(source_map: &SourceMap, span: Span) -> bool { - check_span_path( - source_map, - span, - "gears/file-storage/file-storage/src/infra/content/hash.rs", - ) || check_span_path(source_map, span, "file-storage/src/infra/content/hash.rs") -} - -/// Check if span is within libs/toolkit-db/ - the internal sqlx wrapper library -/// This path is excluded from sqlx restrictions as it provides the abstraction layer -pub fn is_in_toolkit_db_path(source_map: &SourceMap, span: Span) -> bool { - // Multiple checks handle different path contexts: - // - "/libs/toolkit-db/" - absolute path from workspace root - // - "libs/toolkit-db/" - relative path in some contexts - // - "toolkit-db/src/" - simulated_dir paths in tests - check_span_path(source_map, span, "/libs/toolkit-db/") - || check_span_path(source_map, span, "libs/toolkit-db/") - || check_span_path(source_map, span, "toolkit-db/src/") -} - -/// Check if span is within apps/cf-gears-example-server - the main server binary -/// This path is excluded from sqlx restrictions as it needs driver linkage workaround -pub fn is_in_cf_gears_server_path(source_map: &SourceMap, span: Span) -> bool { - // Multiple checks handle different path contexts: - // - "/apps/cf-gears-example-server/" - absolute path from workspace root - // - "apps/cf-gears-example-server/" - relative path in some contexts - // - "cf-gears-example-server/src/" - simulated_dir paths in tests - check_span_path(source_map, span, "/apps/cf-gears-example-server/") - || check_span_path(source_map, span, "apps/cf-gears-example-server/") - || check_span_path(source_map, span, "cf-gears-example-server/src/") -} - -pub fn check_derive_attrs(item: &rustc_ast::Item, mut f: F) -where - F: FnMut(&rustc_ast::MetaItem, &rustc_ast::Attribute), -{ - for attr in &item.attrs { - if !attr.has_name(rustc_span::symbol::sym::derive) { - continue; - } - - // Parse the derive attribute meta list - if let rustc_ast::AttrKind::Normal(attr_item) = &attr.kind - && let Some(meta_items) = attr_item.item.meta_item_list() - { - for nested_meta in meta_items { - if let Some(meta_item) = nested_meta.meta_item() { - f(meta_item, attr) - } - } - } - } -} - -pub fn get_derive_path_segments(meta_item: &rustc_ast::MetaItem) -> Vec<&str> { - let path = &meta_item.path; - path.segments - .iter() - .map(|s| s.ident.name.as_str()) - .collect() -} - -/// Check if path segments represent a serde trait (Serialize or Deserialize) -/// -/// Handles various forms: -/// - Bare: `Serialize`, `Deserialize` -/// - Qualified: `serde::Serialize`, `serde::Deserialize` -/// - Fully qualified: `::serde::Serialize` -/// ``` -pub fn is_serde_trait(segments: &[&str], trait_name: &str) -> bool { - if segments.is_empty() { - return false; - } - - if segments.last() != Some(&trait_name) { - return false; - } - - // If it's a qualified path, ensure it contains "serde" - // Accept: serde::Serialize, ::serde::Serialize - // Reject: other_crate::Serialize - if segments.len() >= 2 { - segments.contains(&"serde") - } else { - // Bare identifier: Serialize or Deserialize - // We accept this as it's commonly used with `use serde::{Serialize, Deserialize}` - true - } -} - -/// Check if an item has the `#[toolkit_macros::api_dto(...)]` attribute. -/// -/// The `api_dto` macro automatically adds: -/// - `#[derive(serde::Serialize)]` (if `response` is specified) -/// - `#[derive(serde::Deserialize)]` (if `request` is specified) -/// - `#[derive(utoipa::ToSchema)]` (always) -/// - `#[serde(rename_all = "snake_case")]` (if `request` or `response` are specified) -/// -/// Lints checking for these derives/attributes should skip items with this attribute. -pub fn has_api_dto_attribute(item: &rustc_ast::Item) -> bool { - for attr in &item.attrs { - // Check for toolkit_macros::api_dto or just api_dto - if let rustc_ast::AttrKind::Normal(attr_item) = &attr.kind { - let path = &attr_item.item.path; - let segments: Vec<&str> = path - .segments - .iter() - .map(|s| s.ident.name.as_str()) - .collect(); - - // Match: api_dto, toolkit_macros::api_dto - if segments.last() == Some(&"api_dto") { - return true; - } - } - } - false -} - -/// Returns the api_dto arguments (request, response) if present and valid. -/// Returns None if the attribute is not present OR if it contains invalid flags. -/// Returns Some with flags indicating which modes are enabled. -/// -/// # Validation -/// -/// This function validates the attribute arguments to match the proc-macro's behavior: -/// - Only "request" and "response" flags are allowed -/// - Duplicate flags are rejected -/// - Unknown flags are rejected -/// - At least one of "request" or "response" must be present -/// -/// If any validation fails, this function returns `None`, treating the invalid -/// attribute the same as an absent attribute. This ensures lint behavior stays -/// in sync with the proc-macro, which would reject these attributes at compile time. -pub fn get_api_dto_args(item: &rustc_ast::Item) -> Option { - for attr in &item.attrs { - if let rustc_ast::AttrKind::Normal(attr_item) = &attr.kind { - let path = &attr_item.item.path; - let segments: Vec<&str> = path - .segments - .iter() - .map(|s| s.ident.name.as_str()) - .collect(); - - if segments.last() != Some(&"api_dto") { - continue; - } - - // Parse and validate the arguments - let mut has_request = false; - let mut has_response = false; - let mut seen_flags = HashSet::new(); - let mut has_invalid = false; - - if let Some(args) = attr_item.item.meta_item_list() { - for arg in args { - if let Some(ident) = arg.ident() { - let flag_str = ident.name.as_str(); - - // Check if flag is allowed - if !ALLOWED_FLAGS.contains(&flag_str) { - has_invalid = true; - break; - } - - // Check for duplicates (convert to String for storage) - if !seen_flags.insert(flag_str.to_string()) { - has_invalid = true; - break; - } - - match flag_str { - "request" => has_request = true, - "response" => has_response = true, - _ => unreachable!(), - } - } - } - } - - // Reject invalid attributes by returning None - if has_invalid { - return None; - } - - // Reject empty attributes (no request or response) - if !has_request && !has_response { - return None; - } - - return Some(ApiDtoArgs { - has_request, - has_response, - }); - } - } - None -} - -/// Arguments parsed from a valid `#[api_dto(request, response)]` attribute. -/// -/// # Validity -/// -/// This struct is only returned by `get_api_dto_args()` for valid attributes. -/// Invalid attributes (unknown flags, duplicates, or empty) cause `get_api_dto_args()` -/// to return `None` instead. -/// -/// A valid `api_dto` attribute has: -/// - At least one of `request` or `response` -/// - Only "request" and "response" flags (no unknown flags) -/// - No duplicate flags -#[derive(Debug, Clone, Copy)] -pub struct ApiDtoArgs { - pub has_request: bool, - pub has_response: bool, -} - -impl ApiDtoArgs { - /// Returns true if the macro will add Serialize derive (response mode) - pub fn adds_serialize(&self) -> bool { - self.has_response - } - - /// Returns true if the macro will add Deserialize derive (request mode) - pub fn adds_deserialize(&self) -> bool { - self.has_request - } - - /// Returns true if the macro will add ToSchema derive. - /// Always returns true because `ApiDtoArgs` only exists for valid attributes. - pub fn adds_toschema(&self) -> bool { - true - } - - /// Returns true if the macro will add serde(rename_all = "snake_case"). - /// This is added when serde derives are present (i.e., at least one mode is enabled). - /// Always returns true for valid `ApiDtoArgs` since validation requires at least one mode. - pub fn adds_snake_case_rename(&self) -> bool { - // Matches proc-macro logic: has_serde = serialize || deserialize - self.has_request || self.has_response - } -} - -// Check if path segments represent a utoipa trait -// Examples: ["ToSchema"], ["utoipa", "ToSchema"], ["utoipa", "ToSchema"] -pub fn is_utoipa_trait(segments: &[&str], trait_name: &str) -> bool { - if segments.is_empty() { - return false; - } - - if segments.last() != Some(&trait_name) { - return false; - } - - // If it's a qualified path, ensure it contains "utoipa" - // Accept: utoipa::ToSchema, ::utoipa::ToSchema - // Reject: other_crate::ToSchema - if segments.len() >= 2 { - segments.contains(&"utoipa") - } else { - // Bare identifier: ToSchema - // We accept this as it's commonly used with `use utoipa::ToSchema` - true - } -} - -/// Converts a UseTree to a vector of fully qualified path strings. -/// Handles Simple, Glob, and Nested use tree kinds. -/// -/// Examples: -/// - `use foo::bar` -> `["foo::bar"]` -/// - `use foo::{bar, baz}` -> `["foo::bar", "foo::baz"]` -/// - `use foo::*` -> `["foo"]` -pub fn use_tree_to_strings(tree: &UseTree) -> Vec { - match &tree.kind { - UseTreeKind::Simple(..) | UseTreeKind::Glob(_) => { - vec![ - tree.prefix - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"), - ] - } - UseTreeKind::Nested { items, .. } => { - let prefix = tree - .prefix - .segments - .iter() - .map(|seg| seg.ident.name.as_str()) - .collect::>() - .join("::"); - - let mut paths = Vec::new(); - for (nested_tree, _) in items { - for nested_str in use_tree_to_strings(nested_tree) { - if nested_str.is_empty() { - paths.push(prefix.clone()); - } else if prefix.is_empty() { - paths.push(nested_str); - } else { - paths.push(format!("{}::{}", prefix, nested_str)); - } - } - } - if paths.is_empty() { - vec![prefix] - } else { - paths - } - } - } -} - -fn check_span_path(source_map: &SourceMap, span: Span, pattern: &str) -> bool { - let pattern_windows = pattern.replace('/', "\\"); - let Some(path_str) = get_path_str_from_session(source_map, span) else { - // If we can't get the path (e.g., synthetic/virtual files), assume not matching - return false; - }; - - // Check for simulated directory in test files first - if let Some(simulated) = extract_simulated_dir(&path_str) { - return simulated.contains(pattern) || simulated.contains(&pattern_windows); - } - - path_str.contains(pattern) || path_str.contains(&pattern_windows) -} - -fn get_path_str_from_session(source_map: &SourceMap, span: Span) -> Option { - let file_name = source_map.span_to_filename(span); - - match file_name { - FileName::Real(ref real_name) => real_name - .local_path() - .map(|local| local.to_string_lossy().to_string()), - _ => None, - } -} - -/// Extract simulated directory path from a comment at the start of a file. -/// Looks for a comment like: `// simulated_dir=/cf-gears/gears/some_gear/contract/` -/// Returns None if no such comment is found. -/// -/// Only checks files in temporary directories to avoid unnecessary file I/O in production. -fn extract_simulated_dir(path_str: &str) -> Option { - // Only check for simulated_dir in temporary paths (tests run in temp directories) - let is_temp = path_str.contains("/tmp/") - || path_str.contains("/var/folders/") // macOS temp - || path_str.contains("\\Temp\\") // Windows temp - || path_str.contains(".tmp"); // dylint test temp dirs - - if !is_temp { - return None; - } - - // Read the first few lines of the file to check for simulated_dir comment - let contents = std::fs::read_to_string(std::path::PathBuf::from(path_str)).ok()?; - - for line in contents.lines().take(1) { - let trimmed = line.trim(); - if trimmed.starts_with("// simulated_dir=") { - return Some(trimmed.trim_start_matches("// simulated_dir=").to_string()); - } - if !trimmed.is_empty() && !trimmed.starts_with("//") && !trimmed.starts_with("#!") { - break; - } - } - - None -} - -/// Test helper function to validate that comment annotations in UI test files match the stderr outputs. -/// -/// This function scans all `.rs` files in the specified UI test directory and verifies that: -/// - Lines with a "Should trigger" comment have corresponding errors in the `.stderr` file -/// - Lines with a "Should not trigger" comment do NOT have errors in the `.stderr` file -/// - All errors in `.stderr` files are properly annotated with "Should trigger" comments -/// -/// # Arguments -/// * `ui_dir` - Path to the directory containing UI test files -/// * `lint_code` - The lint code to check for in comments (e.g., "DE0101") -/// * `comment_pattern` - The pattern to match in comments (e.g., "Serde in contract") -pub fn test_comment_annotations_match_stderr( - ui_dir: &std::path::Path, - lint_code: &str, - comment_pattern: &str, -) { - use std::collections::{HashMap, HashSet}; - use std::fs; - - let trigger_comment = format!("// Should trigger {} - {}", lint_code, comment_pattern); - let not_trigger_comment = format!("// Should not trigger {} - {}", lint_code, comment_pattern); - - // Find all .rs files in ui directory - let rs_files: Vec<_> = fs::read_dir(ui_dir) - .expect("Failed to read ui directory") - .filter_map(|entry| { - let entry = entry.ok()?; - let path = entry.path(); - if path.extension()? == "rs" { - Some(path) - } else { - None - } - }) - .collect(); - - assert!( - !rs_files.is_empty(), - "No .rs test files found in ui directory" - ); - - for rs_file in rs_files { - let stderr_file = rs_file.with_extension("stderr"); - - // Read the .rs file - let rs_content = - fs::read_to_string(&rs_file).unwrap_or_else(|_| panic!("Failed to read {:?}", rs_file)); - - // Read the .stderr file (if it exists) - let stderr_content = fs::read_to_string(&stderr_file).unwrap_or_default(); - - // Parse lines from .rs file - let rs_lines: Vec<&str> = rs_content.lines().collect(); - - // Find all lines with "Should trigger" or "Should not trigger" comments - let mut should_trigger_lines = HashMap::new(); - let mut should_not_trigger_lines = HashMap::new(); - - for (idx, line) in rs_lines.iter().enumerate() { - let comment_line_num = idx + 1; - let expected_error_line_num = idx + 2; - - if line.contains(&trigger_comment) { - // The next line should have an error (idx + 1 is the next line, +1 again for 1-indexed) - should_trigger_lines.insert(expected_error_line_num, comment_line_num); - } else if line.contains(¬_trigger_comment) { - // The next line should NOT have an error - should_not_trigger_lines.insert(expected_error_line_num, comment_line_num); - } - } - - // Parse stderr file to find which lines have errors - let mut error_lines = HashSet::new(); - for line in stderr_content.lines() { - // Look for lines like " --> $DIR/file.rs:5:1" - if line.contains("-->") - && line.contains(".rs:") - && let Some(pos) = line.rfind(".rs:") - { - let rest = &line[pos + 4..]; - if let Some(colon_pos) = rest.find(':') - && let Ok(line_num) = rest[..colon_pos].parse::() - { - error_lines.insert(line_num); - } - } - } - - // Validate that should_trigger_lines match error_lines - for (line_num, comment_line_num) in &should_trigger_lines { - assert!( - error_lines.contains(line_num), - "In {:?}: Line {} has '{}' comment but no corresponding error in .stderr file", - rs_file.file_name().unwrap(), - comment_line_num, - trigger_comment - ); - } - - // Validate that should_not_trigger_lines do NOT appear in error_lines - for (line_num, comment_line_num) in &should_not_trigger_lines { - assert!( - !error_lines.contains(line_num), - "In {:?}: Line {} has '{}' comment but has an error in .stderr file", - rs_file.file_name().unwrap(), - comment_line_num, - not_trigger_comment - ); - } - - // Also verify that all error_lines are marked with should_trigger comments - for line_num in &error_lines { - assert!( - should_trigger_lines.contains_key(line_num), - "In {:?}: Line {} has an error in .stderr file but no '{}' comment", - rs_file.file_name().unwrap(), - line_num, - trigger_comment - ); - } - } -} diff --git a/tools/dylint_lints/run_dylint_tests.py b/tools/dylint_lints/run_dylint_tests.py deleted file mode 100644 index a9b3549be..000000000 --- a/tools/dylint_lints/run_dylint_tests.py +++ /dev/null @@ -1,452 +0,0 @@ -#!/usr/bin/env python3 -""" -Dylint Test Runner with Enhanced Formatting - -This script runs cargo test for all dylint lints and provides a detailed, -formatted output showing individual test cases, violations, and summaries. - -Note: This script focuses on UI test results and ignores test_comment_annotations_match_stderr -tests, which are developer-facing validation tests to ensure test annotations match stderr files. -""" - -import subprocess -import re -import sys -from pathlib import Path -from dataclasses import dataclass, field -from typing import List, Dict, Tuple - - -@dataclass -class Violation: - """Represents a single lint violation""" - file: str - line: int - message: str - lint_code: str - - -@dataclass -class TestCase: - """Represents a single test case (UI file)""" - name: str - file_path: Path - lint_code: str - lint_name: str - violations: List[Violation] = field(default_factory=list) - passed: bool = True - - -@dataclass -class LintPackage: - """Represents a dylint lint package""" - name: str - path: Path - lint_code: str - lint_description: str - test_cases: List[TestCase] = field(default_factory=list) - - -def parse_toml_simple(content: str, key_path: List[str]) -> List[str]: - """Simple TOML parser for specific keys""" - lines = content.split('\n') - result = [] - in_section = False - current_section = [] - - for line in lines: - line = line.strip() - - # Check for section header - if line.startswith('[') and line.endswith(']'): - section = line[1:-1] - current_section = section.split('.') - in_section = current_section == key_path - continue - - # Parse key-value in the right section - if in_section and '=' in line: - continue - - # Parse array values in the right section - if in_section and line.startswith('"') and line.endswith('",'): - value = line.strip('"').rstrip(',') - result.append(value) - elif in_section and line.startswith('"') and line.endswith('"'): - value = line.strip('"') - result.append(value) - - return result - - -def get_package_name_from_cargo(cargo_path: Path) -> str: - """Extract package name from Cargo.toml""" - content = cargo_path.read_text() - match = re.search(r'name\s*=\s*"([^"]+)"', content) - return match.group(1) if match else cargo_path.parent.name - - -def get_lint_packages(workspace_root: Path) -> List[LintPackage]: - """Discover all lint packages in the workspace""" - packages = [] - - cargo_toml = workspace_root / "Cargo.toml" - content = cargo_toml.read_text() - - # Parse members array manually - members = parse_toml_simple(content, ['workspace']) - if not members: - # Fallback: parse members manually with regex - match = re.search(r'\[workspace\].*?members\s*=\s*\[(.*?)\]', content, re.DOTALL) - if match: - members_str = match.group(1) - members = [m.strip().strip('"').strip(',') for m in members_str.split('\n') if m.strip()] - - # Discover packages by scanning directories - for lint_category in ['de01_contract_layer', 'de02_api_layer', 'de08_rest_api_conventions']: - category_dir = workspace_root / lint_category - if not category_dir.exists(): - continue - - for package_dir in sorted(category_dir.iterdir()): - if not package_dir.is_dir(): - continue - - package_cargo = package_dir / "Cargo.toml" - if not package_cargo.exists(): - continue - - package_name = get_package_name_from_cargo(package_cargo) - - # Extract lint code from package name (e.g., "de0101_no_serde_in_contract" -> "DE0101") - match = re.match(r'(de\d{4})', package_name) - lint_code = match.group(1).upper() if match else "UNKNOWN" - - # Try to get description from src/lib.rs - lib_rs = package_dir / "src" / "lib.rs" - lint_description = get_lint_description(lib_rs) - - packages.append(LintPackage( - name=package_name, - path=package_dir, - lint_code=lint_code, - lint_description=lint_description - )) - - return sorted(packages, key=lambda p: p.name) - - -def get_lint_description(lib_rs: Path) -> str: - """Extract lint description from lib.rs file""" - if not lib_rs.exists(): - return "Unknown" - - with open(lib_rs) as f: - content = f.read() - - # Look for the lint declaration and extract description - patterns = [ - r'pub\s+([A-Z0-9_]+),\s*\n\s*Deny,\s*\n\s*"([^"]+)"', - r'/// ### What it does\s*\n\s*///\s*\n\s*/// ([^\n]+)', - ] - - for pattern in patterns: - match = re.search(pattern, content) - if match: - desc = match.group(1) if len(match.groups()) == 1 else match.group(2) - return desc.strip() - - return "Unknown" - - -def parse_stderr_file(stderr_path: Path) -> List[Violation]: - """Parse a .stderr file to extract expected violations""" - violations = [] - - if not stderr_path.exists(): - return violations - - content = stderr_path.read_text() - - # Parse error messages - # Format: error: - # --> $DIR/.rs:: - error_pattern = r'error:\s*([^\n]+)\n\s*-->\s*\$DIR/([^:]+):(\d+):' - - for match in re.finditer(error_pattern, content): - message = match.group(1).strip() - file = match.group(2) - line = int(match.group(3)) - - # Extract lint code from message - lint_code_match = re.search(r'\(([A-Z]+\d+)\)', message) - lint_code = lint_code_match.group(1) if lint_code_match else "UNKNOWN" - - violations.append(Violation( - file=file, - line=line, - message=message, - lint_code=lint_code - )) - - return violations - - -def discover_test_cases(package: LintPackage) -> List[TestCase]: - """Discover all test cases for a lint package""" - test_cases = [] - ui_dir = package.path / "ui" - - if not ui_dir.exists(): - return test_cases - - # Find all .rs files in ui/ - for rs_file in sorted(ui_dir.glob("*.rs")): - stderr_file = rs_file.with_suffix(".stderr") - - violations = parse_stderr_file(stderr_file) - - test_case = TestCase( - name=rs_file.stem, - file_path=rs_file, - lint_code=package.lint_code, - lint_name=package.lint_description, - violations=violations - ) - - test_cases.append(test_case) - - return test_cases - - -def run_cargo_test(workspace_root: Path) -> Tuple[bool, str]: - """Run cargo test for all lint packages""" - print("Building dylint lints...\n") - - try: - result = subprocess.run( - ["cargo", "test", "--no-fail-fast"], - cwd=workspace_root, - capture_output=True, - text=True, - timeout=300 - ) - - output = result.stdout + result.stderr - - # Check if UI tests passed (ignore test_comment_annotations_match_stderr tests) - # We look for failures in ui_examples tests specifically - success = result.returncode == 0 or not has_ui_test_failures(output) - - return success, output - except subprocess.TimeoutExpired: - return False, "Test execution timed out" - except Exception as e: - return False, f"Failed to run tests: {e}" - - -def has_ui_test_failures(cargo_output: str) -> bool: - """Check if there are any UI test failures (excluding comment annotation tests)""" - lines = cargo_output.splitlines() - - for line in lines: - # Check for failed UI tests - if re.search(r"^test \[ui\] .*?\.rs \.\.\.\s+FAILED", line): - return True - # Check for failed ui_examples tests - if re.search(r"^test tests::ui_examples \.\.\.\s+FAILED", line): - return True - - return False - - -def parse_ui_test_statuses(cargo_output_stdout: str, cargo_output_stderr: str) -> Dict[Tuple[str, str], bool]: - """Parse UI test statuses from cargo output - - Returns only UI test results, excluding test_comment_annotations_match_stderr tests. - """ - statuses: Dict[Tuple[str, str], bool] = {} - - # Parse all test results from stdout - for line in cargo_output_stdout.splitlines(): - # Look for individual UI test results - m = re.search(r"^test \[ui\] .*?/([^/\s]+)\.rs \.\.\.\s+(ok|FAILED)", line) - if m: - test_stem = m.group(1) - result = m.group(2) - # Store without crate name for now - we'll match it later - statuses[test_stem] = result == "ok" - - return statuses - - -def print_test_header(): - """Print the test header""" - print("\nTesting Dylint Lints on UI Test Crate") - print("=" * 70) - print("\nCompiling with dylint (nightly)...\n") - - -def print_test_case_results(test_cases: List[TestCase], all_passed: bool): - """Print formatted results for all test cases""" - # Group test cases by lint code - grouped = {} - for tc in test_cases: - lint_key = f"{tc.lint_code}: {tc.lint_name}" - if lint_key not in grouped: - grouped[lint_key] = [] - grouped[lint_key].append(tc) - - total_lints = len(grouped) - total_tests = len(test_cases) - print(f"Testing {total_lints} lint(s) with {total_tests} test file(s)\n") - - for lint_key in sorted(grouped.keys()): - test_group = grouped[lint_key] - first_tc = test_group[0] - - print(f"→ {lint_key}") - print(" " + "─" * 66) - - all_group_passed = all(tc.passed for tc in test_group) - status = "✓ PASS" if all_group_passed else "✗ FAIL" - print(f" {status}") - - # Print test results grouped by test file - for tc in sorted(test_group, key=lambda x: x.name): - expected_label = "Expected: Triggered" if tc.violations else "Expected: Success" - symbol = "✓" if tc.passed else "✗" - print(f" {symbol} {tc.name}.rs: {expected_label}") - - if tc.violations: - for v in tc.violations: - print(f" - line {v.line}: {v.message}") - - print() - - - - -def print_violations_by_lint(packages: List[LintPackage]): - """Print all violations grouped by lint code""" - print("=" * 70) - print("\nAll Violations by Lint:\n") - - # Collect all violations by lint code - violations_by_lint: Dict[str, List[Tuple[str, Violation]]] = {} - - for package in packages: - for test_case in package.test_cases: - for violation in test_case.violations: - lint_code = violation.lint_code - if lint_code not in violations_by_lint: - violations_by_lint[lint_code] = [] - violations_by_lint[lint_code].append((test_case.name, violation)) - - # Print violations by lint - for lint_code in sorted(violations_by_lint.keys()): - violations = violations_by_lint[lint_code] - count = len(violations) - - print(f" {lint_code} ({count} violation{'s' if count != 1 else ''}):") - - for test_name, violation in sorted(violations, key=lambda x: (x[0], x[1].line)): - # Clean up the message - clean_message = violation.message - print(f" {test_name}.rs:{violation.line}: {clean_message}") - - print() - - -def print_summary(packages: List[LintPackage], all_tests_passed: bool, total_violations: int): - """Print test summary""" - print("=" * 70) - print("\nSummary:") - - passed = sum(1 for p in packages for tc in p.test_cases if tc.passed) - failed = sum(1 for p in packages for tc in p.test_cases if not tc.passed) - total_tests = passed + failed - - # Count expected violations - expected_violations = sum(len(tc.violations) for p in packages for tc in p.test_cases) - - print(f" Tests: {passed} passed, {failed} failed, {total_tests} total") - - if all_tests_passed: - percentage = 100 - print(f" Total violations detected: {expected_violations} out of {expected_violations} ({percentage}%). OK") - print("\n✓ All tests passed!") - else: - # When tests fail, we can't accurately count detected violations from the output - # The test framework only tells us which tests failed, not how many violations were found - print(f" Expected violations: {expected_violations}") - print("\n✗ Some tests failed!") - - -def main(): - """Main entry point""" - workspace_root = Path(__file__).parent - - # Discover all lint packages - packages = get_lint_packages(workspace_root) - - # Discover test cases for each package - for package in packages: - package.test_cases = discover_test_cases(package) - - # Run cargo test - cargo_tests_passed, output = run_cargo_test(workspace_root) - - # Parse UI test statuses from cargo output - try: - result = subprocess.run( - ["cargo", "test", "--no-fail-fast"], - cwd=workspace_root, - capture_output=True, - text=True, - timeout=300 - ) - ui_statuses_by_name = parse_ui_test_statuses(result.stdout, result.stderr) - except Exception as e: - print(f"Warning: Failed to parse test statuses: {e}") - ui_statuses_by_name = {} - - # Update test case pass/fail status based on parsed output - # Match test names to packages - for package in packages: - for tc in package.test_cases: - # Look up test status by name - if tc.name in ui_statuses_by_name: - tc.passed = ui_statuses_by_name[tc.name] - else: - # If test wasn't found in output, mark as failed if overall cargo test failed - tc.passed = cargo_tests_passed - - # Print formatted output - print_test_header() - - # Collect all test cases - all_test_cases = [] - for package in packages: - all_test_cases.extend(package.test_cases) - - print_test_case_results(all_test_cases, cargo_tests_passed) - print_violations_by_lint(packages) - - # Count expected violations from .stderr files - expected_violations = sum(len(tc.violations) for p in packages for tc in p.test_cases) - - # Determine overall success: - # 1. All UI test cases must have passed (tc.passed == True) - # 2. Cargo test must have passed (cargo_tests_passed) - # Note: We ignore test_comment_annotations_match_stderr tests in success determination - # as those are developer-facing validation tests, not lint behavior tests - all_tests_passed = cargo_tests_passed and all(tc.passed for p in packages for tc in p.test_cases) - - print_summary(packages, all_tests_passed, expected_violations) - - sys.exit(0 if all_tests_passed else 1) - - -if __name__ == "__main__": - main() diff --git a/tools/dylint_lints/rust-analyzer.toml b/tools/dylint_lints/rust-analyzer.toml deleted file mode 100644 index 0ab7b7426..000000000 --- a/tools/dylint_lints/rust-analyzer.toml +++ /dev/null @@ -1,5 +0,0 @@ -[rustc] -source = "discover" - -[cargo] -sysroot = "discover" \ No newline at end of file diff --git a/tools/dylint_lints/rust-toolchain.toml b/tools/dylint_lints/rust-toolchain.toml deleted file mode 100644 index 992f96651..000000000 --- a/tools/dylint_lints/rust-toolchain.toml +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "nightly-2026-04-16" -components = ["llvm-tools-preview", "rustc-dev"] diff --git a/tools/scripts/ci.py b/tools/scripts/ci.py index 559043959..e07c4448a 100644 --- a/tools/scripts/ci.py +++ b/tools/scripts/ci.py @@ -173,8 +173,6 @@ def cmd_check(args): cmd_cfs_validate(args) cmd_clippy(args) cmd_test(args) - cmd_dylint_test(args) - cmd_dylint(args) cmd_gts_docs(args) cmd_security(args) print("All checks passed") @@ -554,104 +552,6 @@ def cmd_e2e_docker(args): cmd_e2e(args) -def cmd_dylint(_args): - step("Building dylint lints") - dylint_dir = os.path.join(PROJECT_ROOT, "tools/dylint_lints") - run_cmd(["cargo", "build", "--release"], cwd=dylint_dir) - # Copy toolchain-suffixed names similar to Makefile - rustc_host = ( - subprocess.check_output(["rustc", "--version", "--verbose"]) - .decode() - .splitlines() - ) - host = next((line.split()[-1] for line in rustc_host if line.startswith("host:")), "") - toolchain = "nightly" - rust_toolchain_path = os.path.join(dylint_dir, "rust-toolchain.toml") - if os.path.isfile(rust_toolchain_path): - with open(rust_toolchain_path, "r", encoding="utf-8") as f: - for line in f: - if "channel" in line: - toolchain = line.split('"')[1] - break - target_release = os.path.join(dylint_dir, "target", "release") - for fname in os.listdir(target_release): - if not fname.startswith("libde") and not fname.startswith("de"): - continue - if "@" in fname: - continue - if fname.endswith(".dylib"): - ext = ".dylib" - elif fname.endswith(".so"): - ext = ".so" - elif fname.endswith(".dll"): - ext = ".dll" - else: - continue - base = fname[: -len(ext)] - target = f"{base}@{toolchain}-{host}{ext}" - src = os.path.join(target_release, fname) - dst = os.path.join(target_release, target) - try: - shutil.copyfile(src, dst) - except OSError: - pass - dylint_libs = sorted( - [ - os.path.join(target_release, f) - for f in os.listdir(target_release) - if (f.startswith("libde") or f.startswith("de")) - and ("@" in f) - and ( - f.endswith(".dylib") - or f.endswith(".so") - or f.endswith(".dll") - ) - ] - ) - if not dylint_libs: - print("ERROR: No dylint libraries found after build.") - sys.exit(1) - lib_args = [] - for lib in dylint_libs: - lib_args.extend(["--lib-path", lib]) - run_cmd( - ["cargo", f"+{toolchain}", "dylint", *lib_args, "--workspace"], - cwd=PROJECT_ROOT, - ) - print("Dylint checks passed") - - -def cmd_dylint_test(_args): - step("Running dylint tests") - dylint_dir = os.path.join(PROJECT_ROOT, "tools/dylint_lints") - run_cmd(["cargo", "test"], cwd=dylint_dir) - print("Dylint tests passed") - - -def cmd_dylint_list(_args): - step("Listing dylint lints") - dylint_dir = os.path.join(PROJECT_ROOT, "tools/dylint_lints") - target_release = os.path.join(dylint_dir, "target", "release") - dylint_libs = sorted( - [ - os.path.join(target_release, f) - for f in os.listdir(target_release) - if (f.startswith("libde") or f.startswith("de")) - and ( - f.endswith(".dylib") - or f.endswith(".so") - or f.endswith(".dll") - ) - ] - ) - if not dylint_libs: - print("ERROR: No dylint libraries found. Run 'python scripts/ci.py dylint' first.") - sys.exit(1) - for lib in dylint_libs: - print(f"=== {lib} ===") - run_cmd(["cargo", "dylint", "list", "--lib-path", lib], cwd=PROJECT_ROOT) - - def ensure_nightly_toolchain(): """Ensure Rust nightly toolchain is installed.""" result = run_cmd_allow_fail(["rustup", "run", "nightly", "rustc", "--version"]) @@ -904,18 +804,6 @@ def build_parser(): ) p_e2e_docker.set_defaults(func=cmd_e2e_docker) - # dylint - p_dylint = subparsers.add_parser("dylint", help="Build and run dylint lints") - p_dylint.set_defaults(func=cmd_dylint) - - # dylint-test - p_dylint_test = subparsers.add_parser("dylint-test", help="Run dylint UI tests") - p_dylint_test.set_defaults(func=cmd_dylint_test) - - # dylint-list - p_dylint_list = subparsers.add_parser("dylint-list", help="List available dylint lints") - p_dylint_list.set_defaults(func=cmd_dylint_list) - # fuzz-build p_fuzz_build = subparsers.add_parser("fuzz-build", help="Build all fuzz targets") p_fuzz_build.set_defaults(func=cmd_fuzz_build) diff --git a/tools/scripts/coverage.py b/tools/scripts/coverage.py index e6c09198e..e87855198 100644 --- a/tools/scripts/coverage.py +++ b/tools/scripts/coverage.py @@ -140,8 +140,7 @@ def ensure_coverage_disk_space(): "Coverage builds large instrumented test binaries. Free space under the " "workspace volume, then retry. Common cleanup commands:\n" " cargo llvm-cov clean --workspace\n" - " cargo clean\n" - " rm -rf tools/dylint_lints/target", + " cargo clean", file=sys.stderr, ) sys.exit(1) diff --git a/tools/scripts/md-fabric.toml b/tools/scripts/md-fabric.toml index fb5fc86ef..783e8577f 100644 --- a/tools/scripts/md-fabric.toml +++ b/tools/scripts/md-fabric.toml @@ -118,10 +118,6 @@ pattern = "tools/**/*.md" id = "libs" pattern = "libs/**/*.md" -[[categories.buckets]] -id = "tools/dylint_lints" -pattern = "tools/dylint_lints/**/*.md" - # ── Gears ─────────────────────────────────────────────────────────────────── [[categories]]