candor-scan
A stable-Rust effect scanner for Rust code. It maps which functions perform side effects — filesystem, network, subprocess, database, clock, env, randomness, IPC — and how those effects propagate transitively across the call graph, including the blast radius of editing any function.
No nightly, no rustc_private, no dylint. It parses your .rs files with syn
and runs anywhere cargo does — CI, a locked-down box, or cargo install. It does not build your
crate, so it can even analyze a dependency's source without compiling it.
The report is the same JSON the full candor nightly lint
produces, so the candor CLI's read-only queries (show/where/callers/map) read it identically.
What it does well, and where it stops
candor-scan is syntactic — it sees what's written, not what the compiler resolves. It is
calibrated to never fabricate an effect (validated across 1294 real crates: zero false positives on
a curated-pure set). When it can't see an effect, it stays silent rather than guessing — an honest
under-report, never a wrong label.
- Catches: path-qualified effect calls (
std::fs::read,Command::new,reqwest::Client::execute),use-aliases, transitive propagation, calls hidden in macros (try_call!,println!), four C-library FFI tiers (libc, libsqlite3, libgit2, libssl), and method dispatch via light local type inference (struct fields, params, constructors, factory return types). - Honest
Unknown: when the body invokes a callable the scan can't see through — a closure or fn-pointer held in a field, dispatch table, or index ((self.handler)(),arr[i]()) — it marks the functionUnknownrather than silently certifying it pure, and propagates that like any effect (so the receipt's unresolved count is truthful, not a hardcoded 0). A LOCAL closure whose body IS visible (let f = |..| ..; f()) is NOT flagged — its effects were already walked lexically. - Misses (silently): effects reached only through trait-object (
dyn) dispatch on an uninferrable receiver, overloaded operators /?/.awaitdesugars, RAII drops (an effectfulDrop::drophas no call expression — it runs at scope end), a customIterator::nextreached only via afor-loop desugar, and cross-crate propagation by stable identity. These need the semantic resolution only the nightly lint has (the soundness fuzzer locks all of them against the lint: formsop_add/index/deref/try_from/await_poll/drop/iterator/eq/add_assign).
For the soundness contract (Unknown over-approximation), conformance checking, and the policy/guard
CI gates, use the full nightly candor lint. The two share one
classifier, so they never disagree on what counts as an effect.
Usage
candor-scan [<dir>] [--out <prefix>] [--json] [--include-tests]
<dir>— crate root to scan (default.).--out <prefix>— report path prefix (default<dir>/.candor/report); writes<prefix>.<crate>.scan.jsonplus a call-graph sidecar.--json— print the report to stdout instead of writing files.--include-tests— also scantests//benches//examples/and#[cfg(test)]modules (off by default, so the report describes the crate, not its test harness).
Requires a reasonably recent stable Rust (edition 2024 → Rust 1.85+).
Licensed under MIT OR Apache-2.0. Part of candor; see the repo
for the calibration (eval/calibration/) and the full effect model.