candor-scan 0.3.2

candor's STABLE-Rust effect scanner — syntactic call-graph + effect report, no nightly.
candor-scan-0.3.2 is not a library.

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.

cargo install candor-scan
candor-scan path/to/crate              # writes <crate>/.candor/report.<crate>.scan.json
candor-scan . --json                   # print the report to stdout instead

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 function Unknown rather 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 / ? / .await desugars, RAII drops (an effectful Drop::drop has no call expression — it runs at scope end), a custom Iterator::next reached only via a for-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: forms op_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.json plus a call-graph sidecar.
  • --json — print the report to stdout instead of writing files.
  • --include-tests — also scan tests//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.