Skip to content

dylint: code-quality gotcha sweep (#826) — 5 new lints + websocket hardening#833

Merged
zackees merged 4 commits into
mainfrom
feat/dylint-gotcha-sweep-826
Jun 29, 2026
Merged

dylint: code-quality gotcha sweep (#826) — 5 new lints + websocket hardening#833
zackees merged 4 commits into
mainfrom
feat/dylint-gotcha-sweep-826

Conversation

@zackees

@zackees zackees commented Jun 29, 2026

Copy link
Copy Markdown
Member

Summary

Implements the code-quality dylint sweep from #826. Lands 5 new dylint crates + a small websockets error-reply hardening as one PR (4 commits).

What's in this PR

Two direct ports from zccache (Tier 1):

Lint Diagnostic Allowlist policy
ban_std_pathbuf "use fbuild_core::path::NormalizedPath instead of std::path::PathBuf" 193 legacy callers grandfathered; migrate over time
ban_unrooted_tempdir "specify a parent dir explicitly — TempDir::new()/tempdir() land in $TMPDIR which on Windows hits MAX_PATH and on CI may be tmpfs/RAM-disk" 92 legacy callers; tests/benches blanket-allowed

Three net-new fbuild-specific lints (Tier 2):

Lint Diagnostic Allowlist
ban_direct_serialport "direct `serialport::*` reference outside fbuild-serial — route serial access through `fbuild_serial::manager` so the Windows USB-CDC contract is applied" 11 legitimate diagnostic call sites (port-scan, serial-probe, deploy bootloader paths)
ban_file_based_locks "`OpenOptions::create_new(true)` is the rename-or-fail lock-file pattern; fbuild has no file-based locks (see CLAUDE.md Key Constraints)" empty — locks in current clean state
ban_deploy_tool_direct_invocation "`Command::new("esptool")` outside fbuild-deploy bypasses the `fbuild deploy` contract (no post-deploy serial recovery, no emulator routing)" empty — locks in current clean state

Tier 3 fix (partial):

  • crates/fbuild-daemon/src/handlers/websockets.rs lines 119/131/140/173/187 — serde_json::to_string(&err_msg).unwrap() in error-reply paths replaced with a serialize_or_fallback() helper that returns a hardcoded JSON error frame on serialization failure. Prevents a malformed message struct from crashing the WebSocket session (and potentially the daemon) inside the path that's supposed to report errors.

Build glue:

  • Cargo.toml workspace exclude extended for the 5 new lint crates (cargo metadata refuses to operate otherwise).
  • ci/hooks/crate_guard.py allowlist expanded for the new dylint Cargo.toml files.

What's NOT in this PR (deferred to follow-ups)

From #826:

Test plan

  • `soldr cargo check --workspace --all-targets` — clean
  • `soldr cargo clippy --workspace --all-targets -- -D warnings` — clean
  • `soldr cargo test --workspace --no-fail-fast` — all tests pass on Windows host
  • `cargo dylint --all -- --workspace --all-targets` — blocked locally on this Windows host by a Chocolatey-rust-shadows-rustup PATH issue (filed as zackees/soldr#1059). The lints themselves are sound — the lint binaries build cleanly when invoked from inside the lint crate dir; the failure is in cargo-dylint's internal `cargo build` subprocess finding a Chocolatey standalone cargo on PATH instead of rustup's proxy. CI runs on a clean Ubuntu runner without the Chocolatey shadow and validates the lints end-to-end.

Rollout

🤖 Generated with Claude Code

zackees and others added 4 commits June 29, 2026 10:10
Adopt zccache's two PathBuf/tempdir lints into fbuild with allowlists
matching the fbuild source tree:

- ban_std_pathbuf: 193-entry allowlist of legacy PathBuf call sites;
  steers new code at fbuild_core::path::NormalizedPath.
- ban_unrooted_tempdir: 92-entry allowlist of legacy unrooted tempdir
  call sites; steers new code at fbuild_paths::get_cache_root() and
  the tempfile::*_in(...) variants.

Both lints are ON and deny-by-default for new code. Target state is
zero allowlist entries; the lists shrink as migrations land.

Add the dylint crate dirs to the crate_guard allowlist so future
edits to those Cargo.tomls aren't blocked by the monocrate hook.

Refs #826, #436, #437, #282.
Add three lints that enforce fbuild's own architectural rules:

- ban_direct_serialport: bans `serialport::*` references outside
  crates/fbuild-serial/ so the Windows USB-CDC contract stays in one
  place. Legacy allowlist covers fbuild-cli diagnostics, the daemon
  device manager, and the fbuild-deploy bootloader paths.

- ban_file_based_locks: bans `OpenOptions::create_new(true)`, the
  `fs2::FileExt` API, and `libc::flock` / `nix::flock`. Allowlist is
  empty — fbuild has no file-based locks per CLAUDE.md "Key
  Constraints"; this locks the invariant in.

- ban_deploy_tool_direct_invocation: bans `Command::new("esptool" |
  "esptool.py" | "avrdude" | "picotool" | "dfu-util" | "pyocd")`
  outside crates/fbuild-deploy/. Pattern-matches the binary-name
  string literal; complements ban_raw_subprocess (which scopes by
  spawn shape, not binary).

All three follow the ban_raw_subprocess skeleton: pinned nightly,
file-path scope (crates/*/src/), file-level allowlist via include_str!,
forward-slash normalized. Each carries unit tests for the scope/dir
helpers.

Refs #826, #694.
The WebSocket error-reply paths in `handle_serial_ws` previously panicked
via `serde_json::to_string(&err_msg).unwrap()` if the JSON serializer
ever failed. While `SerialServerMessage` always serializes cleanly today,
a panic in the cold error path tears down the entire WebSocket — exactly
the wrong failure mode for a code path whose only job is to surface an
error to the client.

Add a small `serialize_or_fallback` helper that returns a hardcoded
JSON error frame on serializer failure, and route the 5 affected
`Message::Text(...)` constructions through it (open_port failure,
unexpected-message-shape, parse-error, attach_reader=None,
attached-confirmation send).

The 3 remaining `to_string(...).unwrap()` calls at lines ~441/450/467
are on normal data-forwarding paths (not error-reply paths) and are
out of scope for this fix; #826 only flagged the
error-reply ones as a daemon-stability hazard.

Refs #826.
cargo metadata refuses to operate on dylint sub-crates that are
neither members nor excluded; add the 5 new lint crates from #826
to the existing exclude list (alongside ban_raw_subprocess).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@zackees, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec01767d-b51e-4076-8acb-4cb7b1b61385

📥 Commits

Reviewing files that changed from the base of the PR and between d3e9658 and 8a79b2e.

⛔ Files ignored due to path filters (1)
  • dylints/ban_std_pathbuf/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (37)
  • Cargo.toml
  • ci/hooks/crate_guard.py
  • crates/fbuild-daemon/src/handlers/websockets.rs
  • dylints/README.md
  • dylints/ban_deploy_tool_direct_invocation/Cargo.toml
  • dylints/ban_deploy_tool_direct_invocation/README.md
  • dylints/ban_deploy_tool_direct_invocation/rust-toolchain.toml
  • dylints/ban_deploy_tool_direct_invocation/src/allowlist.txt
  • dylints/ban_deploy_tool_direct_invocation/src/lib.rs
  • dylints/ban_direct_serialport/Cargo.toml
  • dylints/ban_direct_serialport/README.md
  • dylints/ban_direct_serialport/rust-toolchain.toml
  • dylints/ban_direct_serialport/src/allowlist.txt
  • dylints/ban_direct_serialport/src/lib.rs
  • dylints/ban_file_based_locks/Cargo.toml
  • dylints/ban_file_based_locks/README.md
  • dylints/ban_file_based_locks/rust-toolchain.toml
  • dylints/ban_file_based_locks/src/allowlist.txt
  • dylints/ban_file_based_locks/src/lib.rs
  • dylints/ban_std_pathbuf/.cargo/config.toml
  • dylints/ban_std_pathbuf/Cargo.toml
  • dylints/ban_std_pathbuf/README.md
  • dylints/ban_std_pathbuf/rust-toolchain.toml
  • dylints/ban_std_pathbuf/src/README.md
  • dylints/ban_std_pathbuf/src/allowlist.txt
  • dylints/ban_std_pathbuf/src/lib.rs
  • dylints/ban_std_pathbuf/ui/disallowed.rs
  • dylints/ban_std_pathbuf/ui/disallowed.stderr
  • dylints/ban_unrooted_tempdir/Cargo.toml
  • dylints/ban_unrooted_tempdir/README.md
  • dylints/ban_unrooted_tempdir/rust-toolchain.toml
  • dylints/ban_unrooted_tempdir/src/README.md
  • dylints/ban_unrooted_tempdir/src/allowlist.txt
  • dylints/ban_unrooted_tempdir/src/lib.rs
  • dylints/ban_unrooted_tempdir/ui/README.md
  • dylints/ban_unrooted_tempdir/ui/allowed.rs
  • dylints/ban_unrooted_tempdir/ui/disallowed.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dylint-gotcha-sweep-826

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zackees zackees merged commit 3b22be9 into main Jun 29, 2026
24 of 91 checks passed
@zackees zackees deleted the feat/dylint-gotcha-sweep-826 branch June 29, 2026 18:13
zackees added a commit that referenced this pull request Jun 29, 2026
Adds five new dylints, completing the #826 audit:

- ban_process_exit_outside_main — `std::process::exit` only in
  `crates/*/src/main.rs` and `crates/*/src/bin/*.rs`. Skipping
  destructors leaks temp files, kills containment guards, and
  truncates in-flight HTTP/WS responses. Legacy CLI subcommand
  dispatchers (build/deploy/dispatch/lnk/pio/purge/reset/serial_probe/
  show) exempted via src/allowlist.txt.
- ban_unwrap_in_daemon_handlers — `.unwrap()` banned in
  `crates/fbuild-daemon/src/handlers/**`. Test files (*tests*.rs) and
  `#[cfg(test)]` modules are exempt; lint walks owner chain to detect
  the cfg(test) ancestor. Three legacy production sites
  (websockets.rs, operations/build.rs, operations/deploy.rs) are
  file-level allowlisted with follow-up notes.
- cli_no_build_deploy_direct_use — `fbuild-cli` is a thin HTTP client
  to the daemon; importing `fbuild_build::*` / `fbuild_deploy::*` is
  reserved for diagnostic subcommands (args/bloat_lookup/graph_cmd/
  symbols_cmd/compile_many/reset).
- require_multi_thread_flavor_when_spawning — `#[tokio::test]` on an
  async fn that calls `tokio::spawn` in its body must specify
  `flavor = "multi_thread"`, otherwise spawned tasks deadlock under
  the default `current_thread` flavor. Two legacy files
  (handlers/websockets_tests.rs, packages/install_lock.rs)
  allowlisted for follow-up migration.
- ban_std_sync_mutex_in_async — `std::sync::Mutex` /
  `std::sync::RwLock` banned in `crates/fbuild-daemon/src/**` and
  `crates/fbuild-serial/src/**`. Holding the guard across `.await`
  starves the Tokio worker; a panic poisons the lock for every
  subsequent caller. Test files and `#[cfg(test)]` modules exempt;
  existing synchronous-discipline uses (context.rs, device_manager.rs,
  status_manager.rs, handlers/operations/common.rs, manager.rs)
  allowlisted with inline justification.

All five lints follow the existing pattern from PR #833:

- Own crate under dylints/, own rust-toolchain.toml pin
  (nightly-2026-03-26), trailofbits/dylint at git rev 4bd91ce…
- Workspace excludes the crate so the stable workspace build is
  unaffected
- File-path-scoped detection via cx.sess().source_map() with
  slash-normalized matching
- File-level allowlist.txt with inline justifications
- Tests covering scope detection, allowlist suffix matching, and
  entry-point exemptions

Three additional items from #826's Tier 4 are not lints — they're
audit / process work tracked as separate follow-up issues:

- Mocks-as-primary-test-surface audit (recommend CodeRabbit rules,
  not dylint)
- Ignored-test bitrot policy (recommend weekly grep-count to
  tracking issue)
- Initializer-order lints (ban_env_var_set_after_import,
  require_oncelock_install_before_use) — need global flow analysis
  out of scope for this PR

Test plan:
- soldr cargo check --workspace --all-targets ✓
- soldr cargo clippy --workspace --all-targets -- -D warnings ✓
- soldr cargo test --workspace --no-fail-fast ✓ (all pass)
- `cargo dylint --all` validation deferred to CI on Ubuntu — the
  Windows host's Chocolatey rust shadow blocks local invocation
  (filed as zackees/soldr#1059)

See #826 for the original gotcha-sweep tracking issue
and PR #833 for the first five lints this builds on.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant