cargo-async-doctor
Spot common async Rust hazards. Get the fix.
cargo-async-doctor is a Cargo subcommand that detects high-signal async mistakes in Rust code, explains why they matter, and points to a practical fix. It is intentionally narrow: a small set of trustworthy checks, not a replacement for rustc, Clippy, or runtime documentation.
Status: v1.0.1 maintenance release. The shipped check set, CLI surface, and JSON contract are unchanged from v1.0.0; this release keeps the release and build documentation aligned with the stable production line. See CHANGELOG.md for release history.
Why cargo-async-doctor?
Async Rust has a class of bugs that compile fine, pass Clippy, and then deadlock or starve your runtime at 2 AM. The standard tooling doesn't catch them because they're semantically valid code doing the wrong thing in an async context. cargo-async-doctor exists to catch exactly those.
| Approach | Catches async hazards | Explains the fix | Machine-readable | Zero config |
|---|---|---|---|---|
rustc |
No | N/A | Yes | Yes |
| Clippy | Partial | Partial | Yes | Yes |
| Runtime docs | Sometimes | Yes | No | No |
| cargo-async-doctor | Yes | Yes | Yes | Yes |
Install
Quick start
Shipped checks
blocking-sleep-in-async
Detects direct std::thread::sleep(...) calls, plus module aliases imported from std::thread, inside async functions, async impl methods, nested async { ... } blocks, and async closures.
blocking-std-api-in-async
Detects direct blocking std::fs calls inside the same async contexts for this allowlist: canonicalize, copy, create_dir, create_dir_all, metadata, read, read_dir, read_link, read_to_string, remove_dir, remove_dir_all, remove_file, rename, symlink_metadata, and write. The same calls are also detected through module aliases imported from std::fs.
sync-async-bridge-hazard
Detects Handle::current().block_on(...) and Runtime::new().block_on(...) style Tokio bridges inside async contexts when the receiver is clearly tokio::runtime::Handle or tokio::runtime::Runtime, including imported type aliases and simple receiver wrappers such as unwrap, expect, clone, and references.
Architecture
┌──────────────────────────────────────────────┐
│ cargo-async-doctor CLI │
│ (clap: scan / explain / flags) │
└──────┬──────────────┬──────────────┬─────────┘
│ │ │
┌────▼────┐ ┌─────▼─────┐ ┌────▼──────┐
│ Scan │ │ Analysis │ │ Explain │
│ │ │ │ │ │
│ cargo │ │ syn AST │ │ check ID → │
│ metadata│ │ visitor │ │ content │
│ package │ │ detection │ │ │
│ resolve │ │ diagnostics│ │ │
└────┬────┘ └─────┬──────┘ └────┬──────┘
│ │ │
└──────────────▼──────────────┘
┌──────────┐
│ Render │
│ │
│ human / │
│ JSON │
└──────────┘
- Scan --- resolves package selection through
cargo metadata, discovers source files, walks target packages - Analysis --- parses Rust source with
syn, visits AST nodes, detects hazard patterns inside async contexts - Explain --- serves canonical explanation content by stable check ID
- Render --- formats diagnostics as human-readable text or structured JSON, separate from detection logic
What Ships Today
- Scan mode resolves package selection through
cargo metadata - Package manifests scan that package unless
--workspaceis set - Virtual workspace manifests scan default members by default and all members with
--workspace - Diagnostics include package context and workspace-relative paths when available
- Line/column data when a direct syntax span is available
- Explain mode serves canonical shipped-check content by stable check ID
- Rendering stays separate from detection and explain-content selection
Current limits
- Detection is syntax-driven, not semantic
- Does not resolve macros, re-exports, wildcard imports, function imports, or block-local
useitems - Sync/async bridge detection does not follow stored handles or runtimes
- Location data is best-effort, tied to direct syntax matches
- Explain mode covers shipped checks only
Development And Verification
From the repository root:
Full smoke test:
Repository Layout
.
├── BUILD.md # execution manual -- status, decisions, progress
├── README.md # public-facing project description
├── CHANGELOG.md # user-facing release history
├── LICENSE # MIT
├── Cargo.toml # published crate metadata and dependencies
├── .github/
│ └── workflows/
│ └── ci.yml # CI pipeline
├── docs/
│ └── release.md # release checklist and versioning policy
├── fixtures/
│ └── ... # positive/negative fixture truth for shipped checks
├── src/
│ ├── main.rs # binary entry point
│ ├── lib.rs # library root
│ ├── cli.rs # clap command definitions
│ ├── scan.rs # cargo metadata package resolution
│ ├── analysis.rs # syn AST visitor and hazard detection
│ ├── diagnostics.rs # diagnostic types and check IDs
│ ├── explain.rs # explain content by check ID
│ └── render.rs # human and JSON output rendering
└── tests/
├── fixtures.rs # fixture-backed integration tests
└── support/
└── mod.rs # test helpers
Roadmap
| Phase | Name | Status |
|---|---|---|
| 0 | Repo framing and public-surface definition | Done |
| 1 | First shipped checks and explain flow | Done |
| 2 | Package/workspace targeting fidelity | Done |
| 3 | Correctness hardening and structured output stability | Done |
| 4 | Future check expansion with strict false-positive control | Not started |
| 5 | Release polish and long-term maintenance discipline | In progress |
See BUILD.md for the full phase breakdown with goals, exit criteria, risks, and decisions.
Design principles
- Trust over breadth. A small set of reliable checks beats a large set of noisy ones. If a check cannot be made trustworthy, do not ship it.
- Explain what you find. Every shipped check has explanation content at least as strong as the detection itself. A warning without context is just noise.
- Stable public surfaces. Shipped check IDs and JSON output schemas are part of the contract. Breaking them costs downstream users.
- Syntax-driven but honest. The tool is explicit about what it can and cannot see. No false claims of semantic analysis.
- Conservative scope. This is a narrow tool on purpose. Scope creep toward a general linter is a bug, not a feature.
Relationship to rust-async-field-guide
cargo-async-doctor is the tool companion to rust-async-field-guide. The guide owns the longer teaching surface. This repo owns fast diagnosis and actionable warnings.
Contributing
The tool is published and in active use. Contributions are welcome for correctness improvements and false-positive fixes within the current shipped check set. New checks require a strong false-positive story before merging. Open an issue first.
License
MIT --- see LICENSE.