Skip to content

Commit 075c1d4

Browse files
committed
Add cfg-gate lint check for macOS-only Rust crates
- Go check that parses Cargo.toml for macOS-only deps, scans .rs files for ungated `use` statements, and respects module-level cfg gating - 42 unit tests covering crate extraction, module gating, and scanning - Registered as `cfg-gate` in check runner (no deps, runs in ~40ms) - Added to CI
1 parent 741ed75 commit 075c1d4

8 files changed

Lines changed: 1422 additions & 38 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ jobs:
134134
- name: Run jscpd
135135
run: ./scripts/check/check --check desktop-rust-jscpd --ci
136136

137+
- name: Check cfg-gate
138+
run: ./scripts/check/check --check desktop-rust-cfg-gate --ci
139+
137140
- name: Run Rust tests
138141
run: ./scripts/check/check --check desktop-rust-tests --ci
139142

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Run the smallest set of checks possible for efficiency while maintaining confide
5050
- Running all Rust/Svelte tests: `./scripts/check.sh --rust` or `--svelte`
5151
- Running specific checks `/scripts/check.sh --check {desktop-svelte-prettier|desktop-svelte-eslint|stylelint|css-unused
5252
|svelte-check|knip|type-drift|svelte-tests|desktop-e2e|e2e-linux-typecheck|desktop-e2e-linux|rustfmt|clippy
53-
|cargo-audit|cargo-deny|cargo-udeps|jscpd-rust|rust-tests|rust-tests-linux (slow)|license-server-prettier
53+
|cargo-audit|cargo-deny|cargo-udeps|jscpd-rust|cfg-gate|rust-tests|rust-tests-linux (slow)|license-server-prettier
5454
|license-server-eslint|license-server-typecheck|license-server-tests|gofmt|go-vet|staticcheck|ineffassign|misspell
5555
|gocyclo|nilaway|govulncheck|deadcode|go-tests|website-prettier|website-eslint|website-typecheck|website-build
5656
|website-e2e|pnpm-audit}` (can use multiple `--check` flags or even a comma-separated list)

docs/specs/cfg-gate-lint-plan.md

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,42 @@ on Linux so it catches this, but only after a push. We want to catch it locally,
1010

1111
Add a new check to the Go-based check runner (`scripts/check/`) that:
1212

13-
1. **Parses `Cargo.toml`** to auto-derive the list of macOS-only crate names from the
14-
`[target.'cfg(target_os = "macos")'.dependencies]` section. Converts crate names to Rust module form (hyphens to
15-
underscores, for example `cmdr-fsevent-stream` becomes `cmdr_fsevent_stream`).
16-
2. **Scans all `.rs` files** under `apps/desktop/src-tauri/src/` for `use <crate>::` statements that reference any of
17-
those crates.
18-
3. **Verifies each match is gated.** For each `use` of a macOS-only crate, walks backwards from that line to check that
19-
either:
20-
- The line itself or a preceding non-empty line has `#[cfg(target_os = "macos")]`, or
21-
- The use is inside a block that's already gated (for example, a `mod` block or `#[cfg(test)]` + `target_os` combo).
22-
For simplicity, the initial version only checks the "preceding `#[cfg(...)]` attribute" pattern, which covers 100% of
23-
the current codebase's usage. The block-level gating can be added later if needed.
24-
4. **Reports violations** with file path and line number.
13+
1. **Parses `Cargo.toml`** using a TOML library (`github.com/BurntSushi/toml`) to extract macOS-only crate names from
14+
the `[target.'cfg(target_os = "macos")'.dependencies]` section. Converts crate names to Rust module form (hyphens to
15+
underscores, for example `cmdr-fsevent-stream` becomes `cmdr_fsevent_stream`). A proper TOML parser handles
16+
multi-line inline tables (like `objc2-foundation` with its multi-line features array) without fragile regex hacks.
17+
2. **Builds a set of "module-gated" files** by scanning `lib.rs` and `mod.rs` files for
18+
`#[cfg(target_os = "macos")] mod <name>;` patterns, resolving each to the corresponding `.rs` file (or
19+
`<name>/mod.rs`). Files inside a cfg-gated module are inherently safe — everything in them is already gated by the
20+
parent's `mod` declaration. These files are skipped during the `use` scan.
21+
3. **Scans remaining `.rs` files** under `apps/desktop/src-tauri/src/` for `use <crate>::` statements that reference any
22+
of those crates (including indented `use` inside function bodies and `pub use` re-exports).
23+
4. **Verifies each match is gated.** For each `use` of a macOS-only crate, walks backwards from that line to check that
24+
a preceding `#[...]` attribute contains `target_os = "macos"` (including compound forms like
25+
`cfg(all(test, target_os = "macos"))`).
26+
5. **Reports violations** with file path and line number.
27+
28+
## Why module-level gating matters
29+
30+
The codebase uses two gating patterns:
31+
32+
- **Per-line gating:** `#[cfg(target_os = "macos")]` before each `use` statement (for example, `watcher.rs`, `icons.rs`).
33+
- **Module-level gating:** `#[cfg(target_os = "macos")] mod foo;` in `lib.rs`/`mod.rs`, where everything inside `foo.rs`
34+
is inherently gated (for example, `drag_image_swap`, `accent_color`, `volumes`, `network`, `mtp`, `permissions`,
35+
`macos_icons`, `file_system/macos_metadata`, `file_system/volume/mtp`).
36+
37+
Module-level gating is actually the more common pattern. Without handling it, the checker would produce dozens of false
38+
positives.
2539

2640
## Scope
2741

2842
Only the `[target.'cfg(target_os = "macos")'.dependencies]` section. We could extend this to `cfg(unix)` later, but
2943
`libc` is the only unix-only dep and it's common enough to not be worth the noise.
3044

45+
Note: some cross-platform crates (`chrono`, `bytes`) live in the macOS-only section because they're only needed for
46+
macOS features. The checker correctly flags ungated uses of these too — if someone adds `use chrono::` in non-macOS
47+
code, it genuinely won't compile on Linux.
48+
3149
## Check runner integration
3250

3351
- **File:** `scripts/check/checks/desktop-rust-cfg-gate.go`
@@ -38,43 +56,57 @@ Only the `[target.'cfg(target_os = "macos")'.dependencies]` section. We could ex
3856
- **Slow:** No (pure text scanning, should run in milliseconds)
3957
- **Depends on:** Nothing (independent, can run in parallel with everything)
4058
- **Position in registry:** After `desktop-rust-jscpd`, before `desktop-rust-tests`
59+
- **New dependency:** `github.com/BurntSushi/toml` (MIT license — run `cargo deny`-equivalent check: it's fine)
4160

4261
## Algorithm
4362

4463
```
45-
1. Read Cargo.toml
46-
2. Find the [target.'cfg(target_os = "macos")'.dependencies] section
47-
3. Extract crate names, convert hyphens → underscores → build set
48-
4. Walk all .rs files in src-tauri/src/
49-
5. For each file, scan lines:
50-
a. If line matches `use <macos_crate>::` (ignoring leading whitespace and `pub`):
51-
- Walk backwards over blank lines and `#[...]` attribute lines
52-
- Look for `cfg(target_os = "macos")` in any of those attributes (including compound forms
53-
like `cfg(all(test, target_os = "macos"))`)
64+
1. Read and parse Cargo.toml with BurntSushi/toml
65+
2. Extract crate names from the [target.'cfg(target_os = "macos")'.dependencies] table
66+
3. Convert hyphens to underscores, build set of macOS-only crate module names
67+
4. Build set of module-gated files:
68+
a. Walk all lib.rs and mod.rs files in src-tauri/src/
69+
b. For each, find lines matching: #[cfg(target_os = "macos")] followed by mod <name>;
70+
(possibly with blank lines or other attributes in between)
71+
c. Resolve <name> to <dir>/<name>.rs or <dir>/<name>/mod.rs
72+
d. If the resolved file is a directory module (mod.rs), recursively add all .rs files under it
73+
e. Collect all resolved paths into a "skip set"
74+
5. Walk all .rs files in src-tauri/src/, skipping files in the skip set
75+
6. For each non-skipped file, scan lines:
76+
a. If line matches `use <macos_crate>::` (ignoring leading whitespace, optional `pub `):
77+
- Walk backwards over blank lines and #[...] attribute lines
78+
- Look for `target_os = "macos"` in any of those attributes
5479
- If not found, record a violation: {file, line number, crate name}
55-
6. If any violations, return error listing them all
56-
7. If none, return success with count of gated uses verified
80+
7. If any violations, return error listing them all
81+
8. If none, return success with count of gated uses verified + count of module-gated files skipped
5782
```
5883

5984
## Success message examples
6085

61-
- `23 gated uses of 8 macOS-only crates verified` (all good)
62-
- Error: `apps/desktop/src-tauri/src/indexing/watcher.rs:5: use of macOS-only crate 'cmdr_fsevent_stream' without #[cfg(target_os = "macos")]`
86+
- `23 gated uses of 8 macOS-only crates verified (12 files skipped via module-level gating)` (all good)
87+
- Error: `apps/desktop/src-tauri/src/indexing/watcher.rs:5: use of macOS-only crate 'cmdr_fsevent_stream' without
88+
#[cfg(target_os = "macos")]`
6389

6490
## Testing
6591

6692
Add a test in `scripts/check/checks/` that:
67-
- Constructs a minimal Cargo.toml snippet and a few `.rs` file contents (as strings/temp files)
68-
- Verifies that correctly gated uses pass
93+
- Constructs a minimal Cargo.toml and a few `.rs` files in a temp directory
94+
- Verifies that per-line gated uses pass
6995
- Verifies that ungated uses are caught
70-
- Verifies that crate name extraction handles hyphens, inline tables (`{ version = "..." }`), and git deps
96+
- Verifies that uses inside module-gated files are skipped (not flagged)
97+
- Verifies that crate name extraction handles hyphens, inline tables (`{ version = "..." }`), git deps, and multi-line
98+
feature arrays
99+
- Verifies the module-gated file resolver handles both `<name>.rs` and `<name>/mod.rs` layouts
71100

72101
## Task list
73102

74-
- [ ] Implement crate name extraction from Cargo.toml (parse the target section, convert hyphens to underscores)
75-
- [ ] Implement `.rs` file scanner (find `use <crate>::` lines, walk backwards for `cfg` attributes)
76-
- [ ] Wire up as `RunCfgGate` in `desktop-rust-cfg-gate.go`
77-
- [ ] Register in `registry.go` (after jscpd, before tests)
78-
- [ ] Add unit tests for extraction and scanning logic
79-
- [ ] Run `./scripts/check.sh --check cfg-gate` to verify it passes on the current codebase
80-
- [ ] Run `./scripts/check.sh --go` to verify Go checks pass (gofmt, vet, staticcheck, and the rest)
103+
- [x] Add `github.com/BurntSushi/toml` dependency to `scripts/check/go.mod`
104+
- [x] Implement crate name extraction from Cargo.toml (TOML parser, convert hyphens to underscores)
105+
- [x] Implement module-gated file detection (scan `lib.rs`/`mod.rs` for cfg-gated `mod` declarations, resolve to files)
106+
- [x] Implement `.rs` file scanner (find `use <crate>::` lines, walk backwards for `cfg` attributes, skip gated files)
107+
- [x] Wire up as `RunCfgGate` in `desktop-rust-cfg-gate.go`
108+
- [x] Register in `registry.go` (after jscpd, before tests)
109+
- [x] Add unit tests for crate extraction, module-gating detection, and use-line scanning
110+
- [x] Run `./scripts/check.sh --check cfg-gate` to verify it passes on the current codebase
111+
- [x] Run `./scripts/check.sh --go` to verify Go checks pass (gofmt, vet, staticcheck, and the rest)
112+
- [x] Add `desktop-rust-cfg-gate` to the `--check` list in `AGENTS.md`

0 commit comments

Comments
 (0)