use super::*;
fn uses(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
fn empty_lazy() -> &'static std::collections::HashSet<String> {
static EMPTY: std::sync::OnceLock<std::collections::HashSet<String>> = std::sync::OnceLock::new();
EMPTY.get_or_init(std::collections::HashSet::new)
}
#[test]
fn expand_uses_the_use_map_and_strips_local_prefixes() {
let u = uses(&[("fs", "std::fs"), ("Command", "std::process::Command")]);
assert_eq!(expand("fs::read_to_string", &u), "std::fs::read_to_string");
assert_eq!(expand("Command::new", &u), "std::process::Command::new");
assert_eq!(expand("crate::pricing::priced", &u), "pricing::priced");
assert_eq!(expand("self::helper", &u), "helper");
assert_eq!(expand("foo::bar", &u), "foo::bar");
}
#[test]
fn collect_use_expands_groups_and_renames() {
let mut out = HashMap::new();
let tree: syn::UseTree = syn::parse_str("std::process::{Command, Stdio as Pipe}").unwrap();
collect_use(&tree, String::new(), &mut out);
assert_eq!(out.get("Command").map(String::as_str), Some("std::process::Command"));
assert_eq!(out.get("Pipe").map(String::as_str), Some("std::process::Stdio"));
let mut o2 = HashMap::new();
collect_use(&syn::parse_str("std::fs::{self, Metadata}").unwrap(), String::new(), &mut o2);
assert_eq!(o2.get("fs").map(String::as_str), Some("std::fs"));
assert_eq!(o2.get("Metadata").map(String::as_str), Some("std::fs::Metadata"));
assert_eq!(o2.get("self"), None); }
#[test]
fn module_path_mirrors_file_based_resolution() {
assert_eq!(module_path(Path::new("src/lib.rs")), "");
assert_eq!(module_path(Path::new("src/main.rs")), "");
assert_eq!(module_path(Path::new("src/pricing.rs")), "pricing");
assert_eq!(module_path(Path::new("src/billing/mod.rs")), "billing");
assert_eq!(module_path(Path::new("src/billing/tax.rs")), "billing::tax");
assert_eq!(
module_path(Path::new("src/generated/envoy.service.auth.v3.rs")),
"generated::envoy::service::auth::v3"
);
assert_eq!(module_path(Path::new("crates/cli/src/decompress.rs")), "decompress");
assert_eq!(module_path(Path::new("crates/ignore/src/walk.rs")), "walk");
assert_eq!(module_path(Path::new("crates/core/src/main.rs")), "");
}
#[test]
fn host_part_strips_scheme_path_and_userinfo() {
assert_eq!(host_part("https://api.stripe.com/v1/charges"), "api.stripe.com");
assert_eq!(host_part("user:pass@db.internal:5432"), "db.internal:5432");
assert_eq!(host_part("example.com"), "example.com");
}
#[test]
fn propagate_is_transitive_across_the_call_graph() {
let mut direct: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
direct.insert("leaf".into(), ["Fs"].into_iter().collect());
let mut calls: HashMap<String, BTreeSet<String>> = HashMap::new();
calls.insert("mid".into(), ["leaf".to_string()].into_iter().collect());
calls.insert("top".into(), ["mid".to_string()].into_iter().collect());
let all = vec!["leaf".to_string(), "mid".to_string(), "top".to_string(), "pure".to_string()];
let acc = propagate(&direct, &calls, &all);
assert!(acc["leaf"].contains("Fs"));
assert!(acc["mid"].contains("Fs"));
assert!(acc["top"].contains("Fs"));
assert!(acc["pure"].is_empty());
}
#[test]
fn tail2_keys_on_the_qualified_method() {
assert_eq!(tail2("a::b::RequestBuilder::new").as_deref(), Some("RequestBuilder::new"));
assert_eq!(tail2("pricing::compute_price").as_deref(), Some("pricing::compute_price"));
assert_eq!(tail2("send"), None); }
#[test]
fn qualified_tail_disambiguates_same_named_methods() {
let fns = ["http::RequestBuilder::new", "body::Body::new"];
let mut by_leaf: HashMap<String, Vec<String>> = HashMap::new();
let mut by_tail2: HashMap<String, Vec<String>> = HashMap::new();
for q in fns {
by_leaf.entry("new".into()).or_default().push(q.into());
by_tail2.entry(tail2(q).unwrap()).or_default().push(q.into());
}
assert_eq!(resolve_target("api::RequestBuilder::new", "new", false, &by_tail2, &by_leaf),
Some(&vec!["http::RequestBuilder::new".to_string()]));
assert_eq!(resolve_target("new", "new", true, &by_tail2, &by_leaf), None);
}
#[test]
fn macro_bodies_are_walked_for_hidden_calls() {
let uses = HashMap::new();
let fields = FieldIndex::new();
let block: syn::Block = syn::parse_str(
"{ try_call!(raw::git_remote_fetch(x)); println!(\"{}\", helper()); let _ = matches!(y, Some(_)); }",
)
.unwrap();
let returns = ReturnIndex::new();
let (ti, td, tf) = (TraitImplIndex::new(), HashMap::new(), TraitFieldIndex::new());
let (fe, ev) = (FieldElemIndex::new(), EnumVariantIndex::new());
let mut c = CallCollector {
uses: &uses,
vars: HashMap::new(),
trait_vars: HashMap::new(),
fields: &fields,
trait_fields: &tf,
trait_impls: &ti,
local_traits: &td,
returns: &returns,
field_elem: &fe,
enum_variants: &ev,
elem_of: HashMap::new(), tuple_of: HashMap::new(),
calls: Vec::new(),
closure_vars: std::collections::HashSet::new(),
fn_typed_vars: std::collections::HashSet::new(),
fn_alias: std::collections::HashMap::new(),
lazy_statics: empty_lazy(),
forced_lazies: std::collections::HashSet::new(),
unresolved: false,
err_ret_leaf: None,
};
for stmt in &block.stmts {
c.visit_stmt(stmt);
}
let leaves: Vec<&str> = c.calls.iter().map(|c| c.leaf.as_str()).collect();
assert!(leaves.contains(&"git_remote_fetch"), "call inside try_call! macro was missed: {leaves:?}");
assert!(leaves.contains(&"helper"), "call inside println! macro was missed: {leaves:?}");
}
#[test]
fn receiver_type_inference_resolves_method_dispatch() {
let uses = HashMap::new();
let mut fields = FieldIndex::new();
fields.entry("App".into()).or_default().insert("http".into(), "reqwest::Client".into());
let mut vars = HashMap::new();
vars.insert("client".to_string(), "reqwest::Client".to_string());
vars.insert("self".to_string(), "App".to_string());
let returns = ReturnIndex::new();
let (ti, td, tf) = (TraitImplIndex::new(), HashMap::new(), TraitFieldIndex::new());
let (fe, ev) = (FieldElemIndex::new(), EnumVariantIndex::new());
let block: syn::Block =
syn::parse_str("{ client.get(url).send(); self.http.execute(req); }").unwrap();
let mut c = CallCollector {
uses: &uses,
vars,
trait_vars: HashMap::new(),
fields: &fields,
trait_fields: &tf,
trait_impls: &ti,
local_traits: &td,
returns: &returns,
field_elem: &fe,
enum_variants: &ev,
elem_of: HashMap::new(), tuple_of: HashMap::new(),
calls: Vec::new(),
closure_vars: std::collections::HashSet::new(),
fn_typed_vars: std::collections::HashSet::new(),
fn_alias: std::collections::HashMap::new(),
lazy_statics: empty_lazy(),
forced_lazies: std::collections::HashSet::new(),
unresolved: false,
err_ret_leaf: None,
};
for stmt in &block.stmts {
c.visit_stmt(stmt);
}
let typed: Vec<&str> = c.calls.iter().map(|c| c.path.as_str()).collect();
assert!(typed.contains(&"reqwest::Client::send"), "chain not typed to base: {typed:?}");
assert!(typed.contains(&"reqwest::Client::execute"), "field recv not typed: {typed:?}");
assert_eq!(candor_classify::classify("reqwest", "reqwest::Client::send"), Some("Net"));
assert_eq!(candor_classify::classify("reqwest", "reqwest::Client::execute"), Some("Net"));
}
#[test]
fn cargo_toml_deps_handles_all_header_forms() {
let mut out = std::collections::HashSet::new();
let mut renames = HashMap::new();
cargo_toml_deps(
"[package]\nname = \"x\"\n\n[dependencies]\nserde_json = \"1\"\nleft-pad = \"1\"\n\n[dependencies.table-header]\nversion = \"1\"\n\n[target.'cfg(unix)'.dependencies.nix]\nversion = \"0.29\"\n\n[target.'cfg(windows)'.dependencies]\nwinapi = \"0.3\"\n\n[workspace.dependencies]\nshared-dep = \"2\"\n\n[dev-dependencies]\ncriterion = \"0.5\"\n\n[build-dependencies]\ncc = \"1\"\n\n[dev-dependencies.proptest]\nversion = \"1\"\n",
&mut out,
&mut renames,
);
for d in ["serde_json", "left_pad", "table_header", "nix", "winapi", "shared_dep"] {
assert!(out.contains(d), "missing {d}: {out:?}");
}
for d in ["criterion", "cc", "proptest", "x"] {
assert!(!out.contains(d), "harness/package dep leaked: {d}");
}
let mut out2 = std::collections::HashSet::new();
let mut ren2 = HashMap::new();
cargo_toml_deps(
"[dependencies]\ntui-common = { version = \"0.1\", package = \"tb-tui-common\" }\n\n[dependencies.short-name]\nversion = \"1\"\npackage = \"the-real-crate\"\n",
&mut out2,
&mut ren2,
);
assert!(out2.contains("tui_common") && out2.contains("short_name"), "{out2:?}");
assert_eq!(ren2.get("tui_common").map(String::as_str), Some("tb_tui_common"));
assert_eq!(ren2.get("short_name").map(String::as_str), Some("the_real_crate"));
let mut out3 = std::collections::HashSet::new();
let mut ren3 = HashMap::new();
cargo_toml_deps(
"[dependencies]\nmy-package = \"1.2\"\nfoo-package = { version = \"2\" }\npackage = \"0.1\"\nreal = { version = \"1\", package = \"renamed-crate\" }\n",
&mut out3,
&mut ren3,
);
assert!(out3.contains("my_package") && out3.contains("foo_package") && out3.contains("package"));
assert!(!ren3.contains_key("my_package"), "key-substring 'package' fabricated a rename: {ren3:?}");
assert!(!ren3.contains_key("foo_package"), "{ren3:?}");
assert!(!ren3.contains_key("package"), "a dep named `package` is not a rename: {ren3:?}");
assert_eq!(ren3.get("real").map(String::as_str), Some("renamed_crate"), "real rename lost: {ren3:?}");
}
#[test]
fn dep_report_chaining_joins_unambiguously_and_distrusts_stale_versions() {
let d = std::env::temp_dir().join(format!("candor-deps-test-{}", std::process::id()));
let _ = std::fs::create_dir_all(&d);
let me = format!("scan-{}", env!("CARGO_PKG_VERSION"));
std::fs::write(d.join("report.billing.scan.json"), format!(r#"{{
"candor": {{"version": "{me}", "toolchain": "stable", "spec": "0.3"}},
"functions": [
{{"fn": "ledger::Ledger::post", "inferred": ["Db"], "tables": ["ledger.entries"], "hash": "billing#ledger::Ledger::post"}},
{{"fn": "a::dup", "inferred": ["Net"], "hash": "billing#a::dup"}},
{{"fn": "b::dup", "inferred": ["Fs"], "hash": "billing#b::dup"}}
]}}"#)).unwrap();
std::fs::write(d.join("report.old_dep.scan.json"), r#"{
"candor": {"version": "scan-0.0.1", "toolchain": "stable", "spec": "0.3"},
"functions": [{"fn": "io::go", "inferred": ["Exec"], "hash": "old_dep#io::go"}]}"#).unwrap();
let idx = load_dep_reports(Some(d.to_str().unwrap()));
assert!(idx.crates.contains("billing") && idx.crates.contains("old_dep"));
let post = idx.by_key.get("billing#Ledger::post").expect("tail2 key");
assert_eq!(post.effects, vec!["Db"]);
assert_eq!(post.tables, vec!["ledger.entries"]);
assert!(idx.by_key.contains_key("billing#post"), "unambiguous leaf key");
assert!(!idx.by_key.contains_key("billing#dup"), "shared leaf must be dropped, never guessed");
assert!(idx.by_key.contains_key("billing#a::dup"), "tail2 still disambiguates the dups");
let old = idx.by_key.get("old_dep#go").expect("stale entry present");
assert_eq!(old.effects, vec!["Unknown"], "stale version must downgrade to Unknown");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn dep_report_package_field_registers_coverage_for_the_ledger_exemption() {
let d = std::env::temp_dir().join(format!("candor-deppkg-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::create_dir_all(&d);
let me = format!("scan-{}", env!("CARGO_PKG_VERSION"));
std::fs::write(d.join("purity-claim.json"), format!(r#"{{
"candor": {{"version": "{me}", "toolchain": "stable", "spec": "0.8"}},
"package": "dep-c",
"functions": []}}"#)).unwrap();
std::fs::write(d.join("multi.json"), format!(r#"{{
"candor": {{"version": "{me}", "toolchain": "stable", "spec": "0.8"}},
"packages": ["alpha", "beta"],
"functions": []}}"#)).unwrap();
let idx = load_dep_reports(Some(d.to_str().unwrap()));
assert!(idx.crates.contains("dep-c"), "the envelope `package` field registers coverage");
assert!(idx.crates.contains("dep_c"), "a hyphenated package also registers in Rust ident form");
assert!(idx.crates.contains("alpha") && idx.crates.contains("beta"),
"the JVM-shape `packages` array registers every covered package");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn dep_join_does_not_fabricate_onto_a_local_shadow() {
let dep = std::env::temp_dir().join(format!("candor-depjoin-rep-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dep);
let me = format!("scan-{}", env!("CARGO_PKG_VERSION"));
std::fs::write(dep.join("report.depb.scan.json"), format!(r#"{{
"candor": {{"version": "{me}", "toolchain": "stable", "spec": "0.7"}},
"functions": [{{"fn": "effectful_fn", "inferred": ["Net"], "hash": "depb#effectful_fn"}}]}}"#)).unwrap();
let idx = load_dep_reports(Some(dep.to_str().unwrap()));
assert!(idx.crates.contains("depb"));
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-depjoin-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let shadow = run("projsh", "mod depb { pub fn effectful_fn() -> i32 { 42 } }\npub fn uses_local() { let _ = depb::effectful_fn(); }");
assert!(eff(&shadow, "uses_local").is_empty(),
"dep-join FABRICATED the dep's effect onto a local shadow:\n{shadow}");
let genuine = run("projgen", "pub fn calls_dep() { depb::effectful_fn(); }");
assert!(eff(&genuine, "calls_dep").contains(&"Net".to_string()),
"a genuine dep call must still inherit the dep report's Net:\n{genuine}");
let _ = std::fs::remove_dir_all(&dep);
}
#[test]
fn smart_pointer_receiver_resolves_pointee_method_but_not_clone() {
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-smartptr-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let idx = load_dep_reports(None);
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v: serde_json::Value = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let src = r#"
use std::sync::Arc; use std::rc::Rc;
struct Inner;
impl Inner {
fn doit(&self) { std::process::Command::new("ls").status().unwrap(); }
fn clone(&self) -> Inner { std::fs::read("/x").unwrap(); Inner }
}
struct A(Arc<Inner>);
impl A { pub fn run(&self) { self.0.doit(); } pub fn dup(&self) -> Arc<Inner> { self.0.clone() } }
struct B(Box<Inner>); impl B { pub fn run(&self) { self.0.doit(); } }
struct R(Rc<Inner>); impl R { pub fn run(&self) { self.0.doit(); } }
"#;
let v = run("smartptr", src);
assert!(eff(&v, "A::run").contains(&"Exec".to_string()), "Arc deref lost Exec:\n{v}");
assert!(eff(&v, "B::run").contains(&"Exec".to_string()), "Box deref lost Exec:\n{v}");
assert!(eff(&v, "R::run").contains(&"Exec".to_string()), "Rc deref lost Exec:\n{v}");
assert!(eff(&v, "A::dup").is_empty(), "arc.clone() FABRICATED the pointee's clone effect:\n{v}");
}
#[test]
fn custom_deref_resolves_pointee_method() {
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-deref-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let idx = load_dep_reports(None);
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v: serde_json::Value = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let src = r#"
use std::ops::Deref;
struct Inner;
impl Inner {
fn doit(&self) { std::process::Command::new("ls").status().unwrap(); }
fn clone(&self) -> Inner { std::fs::read("/x").unwrap(); Inner }
}
struct W { inner: Inner }
impl Deref for W { type Target = Inner; fn deref(&self) -> &Inner { &self.inner } }
impl W { pub fn act(&self) { self.doit(); } pub fn dup(&self) { let _ = self.clone(); } }
"#;
let v = run("deref", src);
assert!(eff(&v, "W::act").contains(&"Exec".to_string()), "custom Deref lost the pointee Exec:\n{v}");
assert!(eff(&v, "W::dup").is_empty(), "custom-Deref clone FABRICATED the pointee's clone effect:\n{v}");
}
#[test]
fn implicit_iterator_force_charges_local_iter_next_but_not_generic() {
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-iternext-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let idx = load_dep_reports(None);
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, name: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str() == Some(name))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let src = r#"
struct LogTail { n: usize }
impl Iterator for LogTail {
type Item = String;
fn next(&mut self) -> Option<String> {
std::fs::write("/t", b"x").unwrap();
if self.n == 0 { None } else { self.n -= 1; Some(String::new()) }
}
}
fn tail(_p: &str) -> LogTail { LogTail { n: 1 } }
pub fn count_lines(p: &str) -> usize { tail(p).count() }
pub fn all_lines(p: &str) -> Vec<String> { tail(p).collect() }
pub fn process(p: &str) { for _l in tail(p) {} }
pub fn folded(p: &str) -> usize { tail(p).fold(0, |a, _| a + 1) }
pub fn explicit(p: &str) { let mut t = tail(p); let _ = t.next(); }
fn build() -> LogTail { LogTail { n: 1 } }
pub fn built_consumer() -> usize { build().count() }
struct PureIter { n: usize }
impl Iterator for PureIter {
type Item = u8;
fn next(&mut self) -> Option<u8> { if self.n == 0 { None } else { self.n -= 1; Some(1) } }
}
fn pure_src() -> PureIter { PureIter { n: 1 } }
pub fn pure_collect() -> Vec<u8> { pure_src().collect() }
pub fn pure_for() { for _ in pure_src() {} }
struct RowIter { n: usize }
impl Iterator for RowIter {
type Item = u8;
fn next(&mut self) -> Option<u8> {
std::fs::write("/db", b"q").unwrap();
if self.n == 0 { None } else { self.n -= 1; Some(0) }
}
}
pub fn generic_param(it: impl Iterator<Item = u8>) { for _ in it {} }
pub fn generic_bound<I: Iterator>(it: I) { let _ = it.count(); }
pub fn dyn_param(it: &mut dyn Iterator<Item = u8>) { let _ = it.count(); }
fn opaque() -> impl Iterator<Item = u8> { OpaqueSrc { n: 1 } }
struct OpaqueSrc { n: usize }
impl Iterator for OpaqueSrc {
type Item = u8;
fn next(&mut self) -> Option<u8> {
std::fs::write("/o", b"z").unwrap();
if self.n == 0 { None } else { self.n -= 1; Some(0) }
}
}
pub fn opaque_consumer() -> usize { opaque().count() }
"#;
let v = run("iternext", src);
for f in ["count_lines", "all_lines", "process", "folded", "explicit", "built_consumer"] {
assert!(eff(&v, f).contains(&"Fs".to_string()),
"implicit iterator force under-reported: {f} should be Fs but is {:?}\n{v}", eff(&v, f));
}
for f in ["pure_collect", "pure_for"] {
assert!(eff(&v, f).is_empty(), "pure custom iterator fabricated an effect at {f}: {:?}", eff(&v, f));
}
for f in ["generic_param", "generic_bound", "dyn_param"] {
assert!(eff(&v, f).is_empty(),
"RowIter guard breached: generic iterator consumer {f} was charged {:?}", eff(&v, f));
}
assert!(eff(&v, "opaque_consumer").is_empty(),
"opaque-return consumer should stay pure (no concrete type): {:?}", eff(&v, "opaque_consumer"));
}
#[test]
fn implicit_coercion_edges_charge_local_effectful_impls_but_never_std() {
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-coerce-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let idx = load_dep_reports(None);
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, name: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str() == Some(name))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let src = r#"
use std::fmt;
// #2 effectful Display via format! / println! / to_string
struct EffDisp;
impl fmt::Display for EffDisp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
std::fs::write("/t", b"x").unwrap(); write!(f, "e")
}
}
pub fn disp_format() -> String { let d = EffDisp; format!("{}", d) }
pub fn disp_println() { let d = EffDisp; println!("hi {}", d); }
pub fn disp_tostring() -> String { let d = EffDisp; d.to_string() }
// #2 effectful Debug via {:?}
struct EffDbg;
impl fmt::Debug for EffDbg {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
std::fs::write("/t", b"x").unwrap(); write!(f, "d")
}
}
pub fn dbg_format() -> String { let d = EffDbg; format!("{:?}", d) }
// control: PURE Display, and STD types (String/i32) → pure
struct PureDisp;
impl fmt::Display for PureDisp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "p") }
}
pub fn disp_pure() -> String { let d = PureDisp; format!("{}", d) }
pub fn fmt_std() -> String { let s = String::new(); let n = 3i32; format!("{} {}", s, n) }
// #1 effectful From via ?
struct MyErr;
impl From<std::io::Error> for MyErr {
fn from(_e: std::io::Error) -> MyErr { std::fs::write("/t", b"x").unwrap(); MyErr }
}
fn may_fail() -> Result<(), std::io::Error> { Ok(()) }
pub fn q_eff() -> Result<(), MyErr> { may_fail()?; Ok(()) }
// control: PURE From via ?
struct PureErr;
impl From<std::io::Error> for PureErr { fn from(_e: std::io::Error) -> PureErr { PureErr } }
fn may_fail2() -> Result<(), std::io::Error> { Ok(()) }
pub fn q_pure() -> Result<(), PureErr> { may_fail2()?; Ok(()) }
// #4 effectful operators
struct Acc;
impl std::ops::Add for Acc {
type Output = Acc;
fn add(self, _o: Acc) -> Acc { std::fs::write("/t", b"x").unwrap(); Acc }
}
pub fn op_add() -> Acc { let a = Acc; let b = Acc; a + b }
struct Cmp;
impl PartialEq for Cmp {
fn eq(&self, _o: &Cmp) -> bool { std::fs::write("/t", b"x").unwrap(); true }
}
pub fn op_eq() -> bool { let a = Cmp; let b = Cmp; a == b }
struct Ix;
impl std::ops::Index<usize> for Ix {
type Output = u8;
fn index(&self, _i: usize) -> &u8 { std::fs::write("/t", b"x").unwrap(); &0 }
}
pub fn op_index() -> u8 { let a = Ix; a[0] }
struct Ng;
impl std::ops::Neg for Ng {
type Output = Ng;
fn neg(self) -> Ng { std::fs::write("/t", b"x").unwrap(); Ng }
}
pub fn op_neg() -> Ng { let a = Ng; -a }
// control: PURE operator, and STD arithmetic → pure
struct PureAdd;
impl std::ops::Add for PureAdd { type Output = PureAdd; fn add(self, _o: PureAdd) -> PureAdd { PureAdd } }
pub fn op_pure() -> PureAdd { let a = PureAdd; let b = PureAdd; a + b }
pub fn op_std() -> i32 { let a = 1i32; let b = 2i32; a + b }
// #3 effectful Deref via *w
struct Wrap;
impl std::ops::Deref for Wrap {
type Target = u8;
fn deref(&self) -> &u8 { std::fs::write("/t", b"x").unwrap(); &0 }
}
pub fn deref_eff() -> u8 { let w = Wrap; *w }
// #5 effectful From via .into()
struct Tgt;
impl From<u8> for Tgt { fn from(_v: u8) -> Tgt { std::fs::write("/t", b"x").unwrap(); Tgt } }
pub fn into_eff() { let x: Tgt = 5u8.into(); let _ = x; }
// #6 struct-literal & unit Drop-glue
struct Guard { n: u8 }
impl Drop for Guard { fn drop(&mut self) { std::fs::write("/t", b"x").unwrap(); } }
pub fn drop_struct() { let _g = Guard { n: 1 }; }
struct UnitGuard;
impl Drop for UnitGuard { fn drop(&mut self) { std::fs::write("/t", b"x").unwrap(); } }
pub fn drop_unit() { let _g = UnitGuard; }
// control: PURE-Drop struct literal → pure
struct PureGuard { n: u8 }
impl Drop for PureGuard { fn drop(&mut self) {} }
pub fn drop_pure() { let _g = PureGuard { n: 1 }; }
"#;
let v = run("coerce", src);
for f in [
"disp_format", "disp_println", "disp_tostring", "dbg_format", "q_eff",
"op_add", "op_eq", "op_index", "op_neg", "deref_eff", "into_eff",
"drop_struct", "drop_unit",
] {
assert!(eff(&v, f).contains(&"Fs".to_string()),
"coercion under-reported: {f} should be Fs but is {:?}", eff(&v, f));
}
for f in ["disp_pure", "fmt_std", "q_pure", "op_pure", "op_std", "drop_pure"] {
assert!(eff(&v, f).is_empty(),
"coercion control fabricated an effect at {f}: {:?}", eff(&v, f));
}
}
#[test]
fn dispatch_typed_receivers_resolve_via_local_impls_or_read_unknown() {
let uses = HashMap::new();
let fields = FieldIndex::new();
let returns = ReturnIndex::new();
let mut ti = TraitImplIndex::new();
ti.insert("Store".into(), vec!["PgStore".into(), "MemStore".into()]);
let mut td: HashMap<String, LocalTrait> = HashMap::new();
td.insert("Store".into(), LocalTrait { count: 1, methods: ["save".to_string()].into_iter().collect() });
td.insert("Sink".into(), LocalTrait { count: 1, methods: ["flush".to_string()].into_iter().collect() }); let mut tf = TraitFieldIndex::new();
tf.entry("App".into()).or_default().insert("store".into(), vec!["Store".into()]);
let (fe, ev) = (FieldElemIndex::new(), EnumVariantIndex::new());
let run = |src: &str, sig: &str| {
let sig: syn::Signature = syn::parse_str(sig).unwrap();
let blk: syn::Block = syn::parse_str(src).unwrap();
let trait_vars = seed_trait_vars(&sig);
let mut vars = seed_vars(&sig, Some("App"), &uses);
for k in trait_vars.keys() {
vars.remove(k); }
vars.insert("self".to_string(), "App".to_string());
let mut c = CallCollector {
uses: &uses,
vars,
trait_vars,
fields: &fields,
trait_fields: &tf,
trait_impls: &ti,
local_traits: &td,
returns: &returns,
field_elem: &fe,
enum_variants: &ev,
elem_of: HashMap::new(), tuple_of: HashMap::new(),
calls: Vec::new(),
closure_vars: std::collections::HashSet::new(),
fn_typed_vars: std::collections::HashSet::new(),
fn_alias: std::collections::HashMap::new(),
lazy_statics: empty_lazy(),
forced_lazies: std::collections::HashSet::new(),
unresolved: false,
err_ret_leaf: None,
};
for stmt in &blk.stmts {
c.visit_stmt(stmt);
}
(c.calls.iter().map(|c| c.path.clone()).collect::<Vec<_>>(), c.unresolved)
};
let (paths, unres) = run("{ t.save(x); }", "fn f(t: &dyn Store)");
assert!(paths.contains(&"PgStore::save".to_string()), "dyn param not CHA-resolved: {paths:?}");
assert!(paths.contains(&"MemStore::save".to_string()), "dyn param missed an impl: {paths:?}");
assert!(!unres, "narrow local dispatch must not read Unknown");
let (paths, _) = run("{ s.save(x); }", "fn f(s: impl Store)");
assert!(paths.contains(&"PgStore::save".to_string()), "impl-Trait param not resolved: {paths:?}");
let (paths, _) = run("{ x.save(y); }", "fn f<X: Store>(x: X)");
assert!(paths.contains(&"PgStore::save".to_string()), "generic bound not resolved: {paths:?}");
let (paths, _) = run("{ self.store.save(x); }", "fn f(&self)");
assert!(paths.contains(&"PgStore::save".to_string()), "trait-typed field not resolved: {paths:?}");
let (_, unres) = run("{ k.flush(); }", "fn f(k: &dyn Sink)");
assert!(unres, "local trait without impls must read Unknown, not silent-pure");
let (paths, unres) = run("{ it.next(); }", "fn f(it: impl Iterator)");
assert!(!unres && !paths.iter().any(|p| p.contains("::next")), "external trait must stay out: {paths:?}");
{
let mut ti2 = TraitImplIndex::new();
ti2.insert("Iterator".into(), vec!["RowIter".into()]);
let sig: syn::Signature = syn::parse_str("fn f(it: impl Iterator)").unwrap();
let blk: syn::Block = syn::parse_str("{ it.next(); }").unwrap();
let mut c = CallCollector {
uses: &uses, vars: HashMap::new(), trait_vars: seed_trait_vars(&sig),
fields: &fields, trait_fields: &tf, trait_impls: &ti2, local_traits: &td,
returns: &returns, field_elem: &fe, enum_variants: &ev, elem_of: HashMap::new(), tuple_of: HashMap::new(),
calls: Vec::new(),
closure_vars: std::collections::HashSet::new(), fn_typed_vars: std::collections::HashSet::new(), fn_alias: std::collections::HashMap::new(), lazy_statics: empty_lazy(), forced_lazies: std::collections::HashSet::new(), unresolved: false, err_ret_leaf: None,
};
for stmt in &blk.stmts { c.visit_stmt(stmt); }
assert!(!c.calls.iter().any(|x| x.path == "RowIter::next"),
"external-trait local impl must not resolve (fabrication)");
assert!(!c.unresolved, "external trait must not flood Unknown either");
}
let (paths, unres) = run("{ t.clone(); }", "fn f(t: &dyn Store)");
assert!(!unres && !paths.iter().any(|p| p.ends_with("::clone")),
"undeclared method must neither edge nor flood: {paths:?}");
{
let wide = |n: usize, src: &str, sig: &str| {
let mut ti2 = TraitImplIndex::new();
ti2.insert("Store".into(), (0..n).map(|i| format!("S{i}")).collect());
let sig: syn::Signature = syn::parse_str(sig).unwrap();
let blk: syn::Block = syn::parse_str(src).unwrap();
let mut c = CallCollector {
uses: &uses, vars: HashMap::new(), trait_vars: seed_trait_vars(&sig),
fields: &fields, trait_fields: &tf, trait_impls: &ti2, local_traits: &td,
returns: &returns, field_elem: &fe, enum_variants: &ev, elem_of: HashMap::new(), tuple_of: HashMap::new(),
calls: Vec::new(),
closure_vars: std::collections::HashSet::new(), fn_typed_vars: std::collections::HashSet::new(), fn_alias: std::collections::HashMap::new(), lazy_statics: empty_lazy(), forced_lazies: std::collections::HashSet::new(), unresolved: false, err_ret_leaf: None,
};
for stmt in &blk.stmts { c.visit_stmt(stmt); }
(c.calls.iter().filter(|x| x.typed).count(), c.unresolved)
};
let (edges, unres) = wide(12, "{ t.save(x); }", "fn f(t: &dyn Store)");
assert!(edges == 12 && !unres, "12 impls must resolve (the shared bound)");
let (edges, unres) = wide(13, "{ t.save(x); }", "fn f(t: &dyn Store)");
assert!(edges == 0 && unres, "13 impls must read Unknown, not resolve");
}
}
#[test]
fn return_type_inference_flows_through_local_factories() {
let uses = HashMap::new();
let fields = FieldIndex::new();
let mut returns = ReturnIndex::new();
returns.insert("create_pool".to_string(), "sqlx::PgPool".to_string());
let (ti, td, tf) = (TraitImplIndex::new(), HashMap::new(), TraitFieldIndex::new());
let (fe, ev) = (FieldElemIndex::new(), EnumVariantIndex::new());
let block: syn::Block =
syn::parse_str("{ let p = create_pool()?; p.fetch_one(q); }").unwrap();
let mut c = CallCollector {
uses: &uses,
vars: HashMap::new(),
trait_vars: HashMap::new(),
fields: &fields,
trait_fields: &tf,
trait_impls: &ti,
local_traits: &td,
returns: &returns,
field_elem: &fe,
enum_variants: &ev,
elem_of: HashMap::new(), tuple_of: HashMap::new(),
calls: Vec::new(),
closure_vars: std::collections::HashSet::new(),
fn_typed_vars: std::collections::HashSet::new(),
fn_alias: std::collections::HashMap::new(),
lazy_statics: empty_lazy(),
forced_lazies: std::collections::HashSet::new(),
unresolved: false,
err_ret_leaf: None,
};
for stmt in &block.stmts {
c.visit_stmt(stmt);
}
let typed: Vec<&str> = c.calls.iter().map(|c| c.path.as_str()).collect();
assert!(typed.contains(&"sqlx::PgPool::fetch_one"), "return-typed recv not resolved: {typed:?}");
let mk = |src: &str| {
let blk: syn::Block = syn::parse_str(src).unwrap();
let mut cc = CallCollector {
uses: &uses,
vars: HashMap::new(),
trait_vars: HashMap::new(),
fields: &fields,
trait_fields: &tf,
trait_impls: &ti,
local_traits: &td,
returns: &returns,
field_elem: &fe,
enum_variants: &ev,
elem_of: HashMap::new(), tuple_of: HashMap::new(),
calls: Vec::new(),
closure_vars: std::collections::HashSet::new(),
fn_typed_vars: std::collections::HashSet::new(),
fn_alias: std::collections::HashMap::new(),
lazy_statics: empty_lazy(),
forced_lazies: std::collections::HashSet::new(),
unresolved: false,
err_ret_leaf: None,
};
for stmt in &blk.stmts {
cc.visit_stmt(stmt);
}
cc.unresolved
};
assert!(mk("{ (handlers[k])(); }"), "indexed callable must be unresolved");
assert!(mk("{ (self.cb)(); }"), "fn-pointer field call must be unresolved");
assert!(!mk("{ let g = |x: i32| x + 1; let _ = g(3); }"), "local closure body is visible — not unresolved");
assert!(!mk("{ helper(); other::thing(); }"), "ordinary path calls are not unresolved");
let r: syn::Type = syn::parse_str("std::io::Result<reqwest::Client>").unwrap();
assert_eq!(type_path(unwrap_result_option(&r), &uses).as_deref(), Some("reqwest::Client"));
let o: syn::Type = syn::parse_str("Option<PgPool>").unwrap();
assert_eq!(type_path(unwrap_result_option(&o), &uses).as_deref(), Some("PgPool"));
}
#[test]
fn test_file_stems_are_recognised() {
assert!(is_test_file_stem("tests")); assert!(is_test_file_stem("test"));
assert!(is_test_file_stem("decoder_tests")); assert!(is_test_file_stem("engine_test"));
assert!(!is_test_file_stem("latest")); assert!(!is_test_file_stem("request"));
assert!(!is_test_file_stem("contest"));
assert!(!is_test_file_stem("lib"));
}
#[test]
fn stable_policy_gate_evaluates_all_three_rule_kinds() {
let all = vec!["api::handle".to_string(), "db::run".to_string(), "ui::draw".to_string()];
let mut inferred: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
inferred.insert("api::handle".into(), ["Net"].into_iter().collect());
inferred.insert("db::run".into(), ["Db"].into_iter().collect());
let mut calls: HashMap<String, BTreeSet<String>> = HashMap::new();
calls.insert("ui::draw".into(), ["db::run".to_string()].into_iter().collect());
let mut hosts: HashMap<String, BTreeSet<String>> = HashMap::new();
hosts.insert("api::handle".into(), ["evil.example.com".to_string()].into_iter().collect());
let empty = HashMap::new();
let empty_inc: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
let mut tables: HashMap<String, BTreeSet<String>> = HashMap::new();
tables.insert("db::run".into(), ["audit.log".to_string()].into_iter().collect());
let v = policy_violations(
"deny Net api\nallow Net in api good.example.com\nforbid ui -> db\n",
&all, &inferred, &calls, &hosts, &empty, &empty, &tables, &empty_inc,
);
assert_eq!(v.len(), 3, "{}", v.iter().map(|x| x.detail.clone()).collect::<Vec<_>>().join(" | "));
assert!(v.iter().any(|g| g.rule == "AS-EFF-006" && g.func == "api::handle" && g.effects == ["Net"]));
assert!(v.iter().any(|g| g.rule == "AS-EFF-008" && g.detail.contains("evil.example.com") && g.effects == ["Net"]));
assert!(v.iter().any(|g| g.rule == "AS-EFF-009" && g.func == "ui::draw" && g.effects.is_empty()));
assert!(policy_violations("deny Exec\n", &all, &inferred, &calls, &hosts, &empty, &empty, &tables, &empty_inc).is_empty());
assert_eq!(policy_violations("pure db\n", &all, &inferred, &calls, &hosts, &empty, &empty, &tables, &empty_inc).len(), 1);
let bad = policy_violations("allow Db in db ledger.*\n", &all, &inferred, &calls, &hosts, &empty, &empty, &tables, &empty_inc);
assert_eq!(bad.len(), 1, "{}", bad.iter().map(|x| x.detail.clone()).collect::<Vec<_>>().join(" | "));
assert!(bad[0].detail.contains("audit.log"));
assert!(policy_violations("allow Db in db audit.*\n", &all, &inferred, &calls, &hosts, &empty, &empty, &tables, &empty_inc).is_empty());
}
#[test]
fn masking_incomplete_net_surface_not_certified() {
let all = vec!["a::mask".to_string(), "a::clean".to_string()];
let mut inferred: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
inferred.insert("a::mask".into(), ["Net"].into_iter().collect());
inferred.insert("a::clean".into(), ["Net"].into_iter().collect());
let calls: HashMap<String, BTreeSet<String>> = HashMap::new();
let mut hosts: HashMap<String, BTreeSet<String>> = HashMap::new();
hosts.insert("a::mask".into(), ["api.stripe.com".to_string()].into_iter().collect());
hosts.insert("a::clean".into(), ["api.stripe.com".to_string()].into_iter().collect());
let empty: HashMap<String, BTreeSet<String>> = HashMap::new();
let tables: HashMap<String, BTreeSet<String>> = HashMap::new();
let mut inc: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
inc.insert("a::mask".into(), ["Net"].into_iter().collect()); let v = policy_violations(
"allow Net api.stripe.com\n",
&all, &inferred, &calls, &hosts, &empty, &empty, &tables, &inc,
);
assert!(v.iter().any(|g| g.func == "a::mask" && g.detail.contains("cannot be certified")), "{:?}", v.iter().map(|x| x.detail.clone()).collect::<Vec<_>>());
assert!(!v.iter().any(|g| g.func == "a::clean"), "clean must certify: {:?}", v.iter().map(|x| x.detail.clone()).collect::<Vec<_>>());
}
#[test]
fn masking_fs_path_and_db_table_gate_fails_closed() {
let run = |name: &str, src: &str, policy: &str| -> i32 {
let d = std::env::temp_dir().join(format!("candor-mask-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let pp = d.join("candor.policy");
std::fs::write(&pp, policy).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let idx = load_dep_reports(None);
let (rc, _) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false,
policy: Some(pp.to_string_lossy().into_owned()), baseline: None, quiet: true, deps_idx: &idx,
});
let _ = std::fs::remove_dir_all(&d);
rc
};
let fs_mix = r#"
use std::fs;
pub fn go() {
let _ = fs::write("/var/app/x", b"x");
let p = format!("/etc/{}", "passwd");
let _ = fs::write(p, b"x");
}
"#;
assert_eq!(run("fsmix", fs_mix, "allow Fs /var/app\n"), 1, "masked Fs path must fail the gate");
let fs_ok = r#"
use std::fs;
pub fn go() { let _ = fs::write("/var/app/x", b"x"); }
"#;
assert_eq!(run("fsok", fs_ok, "allow Fs /var/app\n"), 0, "compliant Fs path must pass (no false positive)");
let fs_masked = r#"
use std::fs;
pub fn go() { let p = format!("/etc/{}", "passwd"); let _ = fs::write(p, b"x"); }
"#;
assert_eq!(run("fsmask", fs_masked, "allow Fs /var/app\n"), 1, "fully-masked Fs path must fail");
let db_mix = r#"
pub fn go(con: &rusqlite::Connection) {
let _ = con.execute("SELECT id FROM customers", []);
let q = format!("DELETE FROM {}", "secrets");
let _ = con.execute(&q, []);
}
"#;
assert_eq!(run("dbmix", db_mix, "allow Db customers\n"), 1, "masked Db table must fail the gate");
let db_ok = r#"
pub fn go(con: &rusqlite::Connection) {
let _ = con.execute("SELECT id FROM customers", []);
}
"#;
assert_eq!(run("dbok", db_ok, "allow Db customers\n"), 0, "compliant Db table must pass (no false positive)");
}
#[test]
fn gate_over_unparseable_source_fails_closed() {
let run = |name: &str, src: &str, with_policy: bool| -> i32 {
let d = std::env::temp_dir().join(format!("candor-parsefail-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let pp = d.join("candor.policy");
std::fs::write(&pp, "deny Exec\n").unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let idx = load_dep_reports(None);
let (rc, _) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false,
policy: if with_policy { Some(pp.to_string_lossy().into_owned()) } else { None },
baseline: None, quiet: true, deps_idx: &idx,
});
let _ = std::fs::remove_dir_all(&d);
rc
};
let broken = "pub fn ok() {}\nthis is not valid rust @@@\n";
assert_eq!(run("broken", broken, true), 2,
"a configured gate over an unparseable source file must FAIL exit 2, never green");
assert_eq!(run("brokennopol", broken, false), 0,
"without a policy there is no gate; a parse failure is disclosed, not an exit code");
assert_eq!(run("clean", "pub fn ok() {}\n", true), 0,
"a clean-parsing crate must still pass the gate (no blanket fail)");
}
#[test]
fn call_returning_a_callable_in_an_unannotated_local_reads_unknown() {
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-retcb-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let idx = load_dep_reports(None);
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v: serde_json::Value = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let boxed = r#"
pub fn make_cb() -> Box<dyn Fn()> { Box::new(|| {}) }
pub fn uses() { let g = make_cb(); g(); }
"#;
assert!(eff(&run("retbox", boxed), "uses").contains(&"Unknown".to_string()),
"a call returning Box<dyn Fn> bound to a local must read Unknown at the call site");
let bare = r#"
fn h() {}
pub fn make_cb() -> fn() { h }
pub fn uses() { let g = make_cb(); g(); }
"#;
assert!(eff(&run("retbare", bare), "uses").contains(&"Unknown".to_string()),
"a call returning fn() bound to a local must read Unknown at the call site");
let impl_fn = r#"
pub fn make_cb() -> impl Fn() { || {} }
pub fn uses() { let g = make_cb(); g(); }
"#;
assert!(eff(&run("retimpl", impl_fn), "uses").contains(&"Unknown".to_string()),
"a call returning impl Fn bound to a local must read Unknown at the call site");
let plain = r#"
pub fn make_v() -> i32 { 42 }
pub fn uses() { let v = make_v(); let _ = v; }
"#;
assert!(!eff(&run("retplain", plain), "uses").contains(&"Unknown".to_string()),
"a non-callable factory binding must stay pure (no fabricated Unknown)");
}
#[test]
fn ambiguous_same_name_local_bare_leaf_reads_unknown() {
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-amb-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let idx = load_dep_reports(None);
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v: serde_json::Value = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let ambiguous = r#"
pub fn process() {}
struct W;
impl W { fn process(&self) {} }
pub fn caller() { process(); }
"#;
assert!(eff(&run("ambig", ambiguous), "caller").contains(&"Unknown".to_string()),
"a bare leaf with two same-name local defs must read Unknown, not silent-pure");
let unique = r#"
pub fn solo() {}
pub fn caller() { solo(); }
"#;
assert!(!eff(&run("uniq", unique), "caller").contains(&"Unknown".to_string()),
"a unique same-name leaf must resolve, never read a spurious Unknown");
}
#[test]
fn only_root_build_rs_is_the_build_script() {
use std::path::Path;
assert!(is_build_script(Path::new("build.rs")));
assert!(!is_build_script(Path::new("src/build.rs")));
assert!(!is_build_script(Path::new("src/foo/build.rs")));
assert!(!is_build_script(Path::new("build/mod.rs"))); }
#[test]
fn cfg_test_modules_are_recognised() {
let yes1: syn::ItemMod = syn::parse_str("#[cfg(test)] mod tests {}").unwrap();
let yes2: syn::ItemMod =
syn::parse_str("#[cfg(any(test, feature = \"x\"))] mod tests {}").unwrap();
let no1: syn::ItemMod = syn::parse_str("#[cfg(feature = \"std\")] mod imp {}").unwrap();
let no2: syn::ItemMod = syn::parse_str("mod real {}").unwrap();
let yes3: syn::ItemMod =
syn::parse_str("#[cfg(any(all(test, unix), windows))] mod t {}").unwrap();
let prod1: syn::ItemMod = syn::parse_str("#[cfg(not(test))] mod prod {}").unwrap();
let prod2: syn::ItemMod = syn::parse_str("#[cfg(all(unix, not(test)))] mod prod {}").unwrap();
assert!(is_cfg_test(&yes1.attrs));
assert!(is_cfg_test(&yes2.attrs));
assert!(is_cfg_test(&yes3.attrs));
assert!(!is_cfg_test(&no1.attrs));
assert!(!is_cfg_test(&no2.attrs));
assert!(!is_cfg_test(&prod1.attrs), "cfg(not(test)) is production, not a test module");
assert!(!is_cfg_test(&prod2.attrs), "cfg(all(unix, not(test))) is production");
}
#[test]
fn expand_does_not_alias_a_crate_rooted_path() {
let u = uses(&[("config", "other::config")]);
assert_eq!(expand("crate::config::load", &u), "config::load");
assert_eq!(expand("self::config::load", &u), "config::load");
assert_eq!(expand("config::load", &u), "other::config::load");
}
#[test]
fn ctor_type_rejects_a_module_path_receiver() {
let u = HashMap::new();
let r = ReturnIndex::new();
let modcall: syn::Expr = syn::parse_str("serde_json::from_str(s)").unwrap();
assert_eq!(ctor_type(&modcall, &u, &r), None);
let typecall: syn::Expr = syn::parse_str("reqwest::Client::new()").unwrap();
assert_eq!(ctor_type(&typecall, &u, &r).as_deref(), Some("reqwest::Client"));
}
#[test]
fn struct_literal_bindings_infer_their_type() {
let u = HashMap::new();
let r = ReturnIndex::new();
let t = |src: &str| ctor_type(&syn::parse_str::<syn::Expr>(src).unwrap(), &u, &r);
assert_eq!(t("S").as_deref(), Some("S")); assert_eq!(t("S { a: 1 }").as_deref(), Some("S")); assert_eq!(t("m::S { a: 1 }").as_deref(), Some("m::S")); assert_eq!(t("Color::Red").as_deref(), Some("Color")); assert_eq!(t("Color::Red { x: 1 }").as_deref(), Some("Color")); assert_eq!(t("other_var"), None);
assert_eq!(t("MAX_SIZE"), None);
assert_eq!(t("config::MAX_SIZE"), None);
}
#[test]
fn self_returning_ctor_types_the_local_and_the_edge_survives() {
let src = r#"
pub struct Agent;
impl Agent {
pub fn new_with_defaults() -> Self { Agent }
pub fn run(&self) { let _ = std::fs::read("/tmp/x"); }
}
pub fn top() { let agent = Agent::new_with_defaults(); agent.run() }
"#;
let file: syn::File = syn::parse_str(src).unwrap();
let mut uses = HashMap::new();
let mut fields: FieldIndex = HashMap::new();
let mut rets: HashMap<String, Option<String>> = HashMap::new();
let (mut ti, mut td, mut tf) = (TraitImplIndex::new(), HashMap::new(), TraitFieldIndex::new());
let (mut fe, mut ev) = (FieldElemIndex::new(), HashMap::new());
collect_decls(&file.items, false, &mut uses, &mut fields, &mut fe, &mut rets, &mut ev, &mut ti, &mut td, &mut tf, &mut std::collections::HashSet::new(), &mut std::collections::HashSet::new(), &mut std::collections::HashSet::new(), &mut std::collections::HashMap::new(), &mut std::collections::HashSet::new());
assert_eq!(rets.get("new_with_defaults"), Some(&Some("Agent".to_string())),
"Self must resolve to the impl type, not the literal");
}
#[test]
fn local_method_named_like_a_crate_does_not_inherit_the_crate_effect() {
let d = std::env::temp_dir().join(format!("candor-scan-localcrate-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"localcrate\"\n").unwrap();
std::fs::write(
d.join("src/lib.rs"),
r#"
pub struct FastRand { one: u32 }
impl FastRand {
// a PURE local method merely NAMED like the `fastrand` crate (tokio's xorshift)
pub fn fastrand(&mut self) -> u32 { self.one ^= self.one << 1; self.one }
pub fn fastrand_n(&mut self, n: u32) -> u32 { self.fastrand() % n }
// a local method named like the `time`/`now` clock verb — also pure
pub fn time(&self) -> u32 { self.one }
}
pub fn uses_local(r: &mut FastRand) { let _ = r.fastrand_n(5); let _ = r.time(); }
// a REAL external dependency call — qualified, does NOT resolve locally → STILL Rand
pub fn uses_external() { let _ = fastrand::u32(0..10); }
"#,
)
.unwrap();
let idx = load_dep_reports(None);
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let body = body.expect("want_json returns the report body");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let effects_of = |needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>())
.collect()
};
for q in ["FastRand::fastrand", "FastRand::fastrand_n", "FastRand::time", "uses_local"] {
let eff = effects_of(q);
assert!(!eff.contains(&"Rand".to_string()) && !eff.contains(&"Clock".to_string()),
"local method named like a crate fabricated an effect on {q}: {eff:?}\n{body}");
}
assert!(effects_of("uses_external").contains(&"Rand".to_string()),
"a real external fastrand::u32 call must still report Rand:\n{body}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn local_fn_or_method_named_like_an_ffi_tier_does_not_fabricate() {
let d = std::env::temp_dir().join(format!("candor-scan-ffiname-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"ffiname\"\n").unwrap();
std::fs::write(
d.join("src/lib.rs"),
r#"
// PURE local free fns named like FFI tiers / whole-crate rules
pub fn sqlite3_step() -> i32 { 0 }
pub fn git_clone() {}
pub fn getrandom() -> u32 { 4 }
pub fn uses_sqlite() -> i32 { sqlite3_step() }
pub fn uses_git() { git_clone() }
pub fn uses_rand() -> u32 { getrandom() }
// a PURE local qualified Type::method named like the git_ tier
pub struct Repo;
impl Repo { pub fn git_remote_fetch(&self) {} }
pub fn uses_method(r: &Repo) { r.git_remote_fetch() }
// AMBIGUOUS bare leaf: a free fn AND a method share an FFI-named leaf, defeating
// resolve_target's uniqueness filter — the bare-leaf classifier must STILL be suppressed.
pub fn curl_easy_perform() {}
pub struct Conn;
impl Conn { pub fn curl_easy_perform(&self) {} }
pub fn uses_ambig() { curl_easy_perform() }
// a GENUINE FFI binding (extern decl, no Rust body) — must STILL classify Db
extern "C" { fn sqlite3_exec(p: *mut i8) -> i32; }
pub fn real_ffi() { unsafe { sqlite3_exec(std::ptr::null_mut()); } }
"#,
)
.unwrap();
let idx = load_dep_reports(None);
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let body = body.expect("want_json returns the report body");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let effects_of = |needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>())
.collect()
};
for q in ["sqlite3_step", "git_clone", "getrandom", "uses_sqlite", "uses_git", "uses_rand",
"git_remote_fetch", "uses_method", "curl_easy_perform", "uses_ambig"] {
assert!(effects_of(q).is_empty(),
"local fn/method named like an FFI tier FABRICATED an effect on {q}: {:?}\n{body}",
effects_of(q));
}
assert!(effects_of("real_ffi").contains(&"Db".to_string()),
"a real extern-C sqlite3_exec FFI call must still report Db:\n{body}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn ffi_safe_wrapper_of_an_unclassified_extern_fn_discloses_unknown() {
let d = std::env::temp_dir().join(format!("candor-scan-ffiwrap-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"ffiwrap\"\n").unwrap();
std::fs::write(
d.join("src/lib.rs"),
r#"
extern "C" {
fn system(cmd: *const i8) -> i32;
fn my_native_writer(p: *const u8, n: usize) -> i32;
fn sqlite3_exec(p: *mut i8) -> i32;
}
// safe wrappers over UNCLASSIFIED extern fns → must DISCLOSE Unknown
pub fn run_shell(cmd: *const i8) -> i32 { unsafe { system(cmd) } }
pub fn native_write(p: *const u8, n: usize) -> i32 { unsafe { my_native_writer(p, n) } }
// a transitive caller inherits the Unknown
pub fn does_native_io() -> i32 { native_write(std::ptr::null(), 0) }
// CONTROL (a): a genuinely-pure fn with NO extern call stays pure
pub fn pure_math(a: i32, b: i32) -> i32 { a + b }
// CONTROL (b): a wrapper of a CLASSIFIED extern leaf keeps the PRECISE effect (Db), not Unknown
pub fn run_query() -> i32 { unsafe { sqlite3_exec(std::ptr::null_mut()) } }
"#,
)
.unwrap();
let idx = load_dep_reports(None);
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let body = body.expect("want_json returns the report body");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let effects_of = |needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q == needle))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>())
.collect()
};
assert!(effects_of("run_shell").contains(&"Unknown".to_string()),
"FFI wrapper of `system` must disclose Unknown:\n{body}");
assert!(effects_of("native_write").contains(&"Unknown".to_string()),
"FFI wrapper of `my_native_writer` must disclose Unknown:\n{body}");
assert!(effects_of("does_native_io").contains(&"Unknown".to_string()),
"a caller of an FFI wrapper must inherit Unknown:\n{body}");
let why = v["functions"].as_array().into_iter().flatten()
.find(|f| f["fn"].as_str() == Some("run_shell"))
.and_then(|f| f.get("unknownWhy").or_else(|| f.get("unknown_why")))
.and_then(|w| w.as_array()).map(|a| a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>())
.unwrap_or_default();
assert!(why.iter().any(|r| r.starts_with("native:")), "unknownWhy must name the native/FFI boundary (canonical native:): {why:?}\n{body}");
assert!(effects_of("pure_math").is_empty(),
"a pure fn with no extern call must stay pure (no fabricated Unknown):\n{body}");
let rq = effects_of("run_query");
assert!(rq.contains(&"Db".to_string()), "classified extern (sqlite3_exec) must stay Db:\n{body}");
assert!(!rq.contains(&"Unknown".to_string()),
"a CLASSIFIED extern leaf must NOT be downgraded to Unknown:\n{body}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn drop_glue_of_a_local_effectful_drop_propagates_to_the_binder() {
let d = std::env::temp_dir().join(format!("candor-scan-dropglue-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"dropglue\"\n").unwrap();
std::fs::write(
d.join("src/lib.rs"),
r#"
// a LOCAL type whose Drop writes a file (effectful scope-exit)
pub struct FlushGuard { path: String }
impl FlushGuard { pub fn new(p: &str) -> Self { FlushGuard { path: p.to_string() } } }
impl Drop for FlushGuard {
fn drop(&mut self) { std::fs::write(&self.path, b"flush").unwrap(); }
}
// binds a FlushGuard → the implicit drop edge must give this fn Fs
pub fn does_work_with_guard() {
let _g = FlushGuard::new("/tmp/x");
let _ = 1 + 1;
}
// CONTROL (a): a LOCAL type with a PURE Drop — binding it adds no effect
pub struct PureGuard;
impl PureGuard { pub fn new() -> Self { PureGuard } }
impl Drop for PureGuard { fn drop(&mut self) { /* nothing */ } }
pub fn does_work_pure_guard() { let _g = PureGuard::new(); }
"#,
)
.unwrap();
let idx = load_dep_reports(None);
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let body = body.expect("want_json returns the report body");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let effects_of = |needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q == needle))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>())
.collect()
};
assert!(effects_of("does_work_with_guard").contains(&"Fs".to_string()),
"a fn binding a local guard with an effectful Drop must inherit Fs (drop glue):\n{body}");
assert!(effects_of("does_work_pure_guard").is_empty(),
"a local guard with a PURE Drop must add no effect:\n{body}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn drop_glue_never_fabricates_for_an_external_type() {
let d = std::env::temp_dir().join(format!("candor-scan-dropext-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"dropext\"\n").unwrap();
std::fs::write(
d.join("src/lib.rs"),
r#"
// receives an external File by value; it drops here, but its Drop is not a LOCAL impl → pure
pub fn consumes_a_file(f: std::fs::File) { let _f = f; }
"#,
)
.unwrap();
let idx = load_dep_reports(None);
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let body = body.expect("want_json returns the report body");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let effs: Vec<String> = v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str() == Some("consumes_a_file"))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>())
.collect();
assert!(effs.is_empty(),
"binding an EXTERNAL type must not fabricate a drop effect (only local Drop is modeled):\n{body}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn lazy_static_deferred_init_is_charged_to_the_forcing_site() {
let d = std::env::temp_dir().join(format!("candor-scan-lazystatic-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(
d.join("Cargo.toml"),
"[package]\nname = \"lazystatic\"\n[dependencies]\nonce_cell = \"1\"\nlazy_static = \"1\"\n",
)
.unwrap();
std::fs::write(
d.join("src/lib.rs"),
r#"
use once_cell::sync::Lazy;
use std::sync::LazyLock;
use std::fs;
use lazy_static::lazy_static;
// 1. once_cell Lazy, effectful init
pub static CFG: Lazy<String> = Lazy::new(|| fs::read_to_string("/etc/a").unwrap_or_default());
// 2. std LazyLock, effectful init
pub static CFG2: LazyLock<String> = LazyLock::new(|| fs::read_to_string("/etc/b").unwrap_or_default());
// 3. lazy_static!, effectful init
lazy_static! { pub static ref CFG3: String = fs::read_to_string("/etc/c").unwrap_or_default(); }
// 4. thread_local!, effectful init
thread_local! { pub static CFG4: String = fs::read_to_string("/etc/d").unwrap_or_default(); }
// NO-FABRICATION CONTROLS: pure inits contribute nothing
pub static PURE_NUM: Lazy<usize> = Lazy::new(|| 1 + 1);
// forcing sites — each names exactly ONE static
pub fn force1() -> bool { CFG.contains("x") }
pub fn force2() -> bool { CFG2.contains("x") }
pub fn force3() -> bool { CFG3.contains("x") }
pub fn force4() -> bool { CFG4.with(|c| c.contains("x")) }
// MULTI-STATIC SCOPING: this fn names only the PURE lazy — must stay pure even though
// effectful lazies live in the same module (static-scoped, not module-scoped).
pub fn force_pure() -> usize { *PURE_NUM + 5 }
"#,
)
.unwrap();
let idx = load_dep_reports(None);
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let body = body.expect("want_json returns the report body");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let effects_of = |needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str() == Some(needle))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>())
.collect()
};
for (f, idiom) in [("force1", "once_cell Lazy"), ("force2", "std LazyLock"),
("force3", "lazy_static!"), ("force4", "thread_local!")] {
assert!(effects_of(f).contains(&"Fs".to_string()),
"{idiom}: forcing site `{f}` must carry Fs (deferred init under-report):\n{body}");
}
assert!(effects_of("force_pure").is_empty(),
"a pure-init lazy's forcing site must stay pure (no fabrication / no module-bleed):\n{body}");
let pure_unit_effectful = v["functions"].as_array().into_iter().flatten().any(|f| {
f["fn"].as_str() == Some("<lazy>::PURE_NUM")
&& f["inferred"].as_array().is_some_and(|a| !a.is_empty())
});
assert!(!pure_unit_effectful, "the pure lazy's synthetic unit must carry no effect:\n{body}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn tuple_struct_fields_index_by_position() {
let src = r#"
pub struct Inner;
pub struct Outer(Inner);
pub struct Stack(Outer);
"#;
let file: syn::File = syn::parse_str(src).unwrap();
let mut uses = HashMap::new();
let mut fields: FieldIndex = HashMap::new();
let mut rets: HashMap<String, Option<String>> = HashMap::new();
let (mut ti, mut td, mut tf) = (TraitImplIndex::new(), HashMap::new(), TraitFieldIndex::new());
let (mut fe, mut ev) = (FieldElemIndex::new(), HashMap::new());
collect_decls(&file.items, false, &mut uses, &mut fields, &mut fe, &mut rets, &mut ev, &mut ti, &mut td, &mut tf, &mut std::collections::HashSet::new(), &mut std::collections::HashSet::new(), &mut std::collections::HashSet::new(), &mut std::collections::HashMap::new(), &mut std::collections::HashSet::new());
assert_eq!(fields["Outer"]["0"], "Inner");
assert_eq!(fields["Stack"]["0"], "Outer");
}
fn typed_calls_of(src: &str) -> HashMap<String, Vec<String>> {
let file: syn::File = syn::parse_str(src).unwrap();
let mut uses = HashMap::new();
let mut fields = FieldIndex::new();
let mut field_elem = FieldElemIndex::new();
let mut rets: HashMap<String, Option<String>> = HashMap::new();
let mut enum_tmp: HashMap<String, Option<String>> = HashMap::new();
let mut ti = TraitImplIndex::new();
let mut td: HashMap<String, LocalTrait> = HashMap::new();
let mut tf = TraitFieldIndex::new();
collect_decls(&file.items, false, &mut uses, &mut fields, &mut field_elem, &mut rets,
&mut enum_tmp, &mut ti, &mut td, &mut tf, &mut std::collections::HashSet::new(),
&mut std::collections::HashSet::new(), &mut std::collections::HashSet::new(), &mut std::collections::HashMap::new(), &mut std::collections::HashSet::new());
let returns: ReturnIndex = rets.into_iter().filter_map(|(k, v)| v.map(|t| (k, t))).collect();
let enum_variants: EnumVariantIndex =
enum_tmp.into_iter().filter_map(|(k, v)| v.map(|t| (k, t))).collect();
let traits = TraitIndexes { impls: &ti, decls: &td, fields: &tf };
let elems = ElemIndexes { field_elem: &field_elem, enum_variants: &enum_variants };
let mut fns: Vec<FnInfo> = Vec::new();
let mut us2 = HashMap::new();
let mut locs = Vec::new();
fn_locs(&file.items, "lib.rs", false, &mut locs);
let mut loc_idx = 0usize;
scan_items(&file.items, "", &locs, &mut loc_idx, false, &fields, &returns, traits, elems, &std::collections::HashSet::new(), &mut us2, &mut fns);
fns.into_iter()
.map(|f| (f.qual, f.calls.into_iter().filter(|c| c.typed).map(|c| c.path).collect()))
.collect()
}
fn unresolved_of(src: &str) -> HashMap<String, bool> {
let file: syn::File = syn::parse_str(src).unwrap();
let mut uses = HashMap::new();
let mut fields = FieldIndex::new();
let mut field_elem = FieldElemIndex::new();
let mut rets: HashMap<String, Option<String>> = HashMap::new();
let mut enum_tmp: HashMap<String, Option<String>> = HashMap::new();
let mut ti = TraitImplIndex::new();
let mut td: HashMap<String, LocalTrait> = HashMap::new();
let mut tf = TraitFieldIndex::new();
collect_decls(&file.items, false, &mut uses, &mut fields, &mut field_elem, &mut rets,
&mut enum_tmp, &mut ti, &mut td, &mut tf, &mut std::collections::HashSet::new(),
&mut std::collections::HashSet::new(), &mut std::collections::HashSet::new(), &mut std::collections::HashMap::new(), &mut std::collections::HashSet::new());
let returns: ReturnIndex = rets.into_iter().filter_map(|(k, v)| v.map(|t| (k, t))).collect();
let enum_variants: EnumVariantIndex =
enum_tmp.into_iter().filter_map(|(k, v)| v.map(|t| (k, t))).collect();
let traits = TraitIndexes { impls: &ti, decls: &td, fields: &tf };
let elems = ElemIndexes { field_elem: &field_elem, enum_variants: &enum_variants };
let mut fns: Vec<FnInfo> = Vec::new();
let mut us2 = HashMap::new();
let mut locs = Vec::new();
fn_locs(&file.items, "lib.rs", false, &mut locs);
let mut loc_idx = 0usize;
scan_items(&file.items, "", &locs, &mut loc_idx, false, &fields, &returns, traits, elems, &std::collections::HashSet::new(), &mut us2, &mut fns);
fns.into_iter().map(|f| (f.qual, f.unresolved)).collect()
}
fn locs_of(src: &str) -> HashMap<String, String> {
let file: syn::File = syn::parse_str(src).unwrap();
let mut uses = HashMap::new();
let mut fields = FieldIndex::new();
let mut field_elem = FieldElemIndex::new();
let mut rets: HashMap<String, Option<String>> = HashMap::new();
let mut enum_tmp: HashMap<String, Option<String>> = HashMap::new();
let mut ti = TraitImplIndex::new();
let mut td: HashMap<String, LocalTrait> = HashMap::new();
let mut tf = TraitFieldIndex::new();
collect_decls(&file.items, false, &mut uses, &mut fields, &mut field_elem, &mut rets,
&mut enum_tmp, &mut ti, &mut td, &mut tf, &mut std::collections::HashSet::new(),
&mut std::collections::HashSet::new(), &mut std::collections::HashSet::new(), &mut std::collections::HashMap::new(), &mut std::collections::HashSet::new());
let returns: ReturnIndex = rets.into_iter().filter_map(|(k, v)| v.map(|t| (k, t))).collect();
let enum_variants: EnumVariantIndex =
enum_tmp.into_iter().filter_map(|(k, v)| v.map(|t| (k, t))).collect();
let traits = TraitIndexes { impls: &ti, decls: &td, fields: &tf };
let elems = ElemIndexes { field_elem: &field_elem, enum_variants: &enum_variants };
let mut fns: Vec<FnInfo> = Vec::new();
let mut us2 = HashMap::new();
let mut locs = Vec::new();
fn_locs(&file.items, "lib.rs", false, &mut locs);
let mut loc_idx = 0usize;
scan_items(&file.items, "", &locs, &mut loc_idx, false, &fields, &returns, traits, elems, &std::collections::HashSet::new(), &mut us2, &mut fns);
fns.into_iter().map(|f| (f.qual, f.loc)).collect()
}
#[test]
fn loc_carries_actual_line_and_col() {
let src = "\
fn alpha() {}
fn beta() {}
struct T;
impl T {
fn gamma(&self) {}
}
mod inner {
fn delta() {}
}
trait G {
fn hello(&self) {}
}
";
let m = locs_of(src);
assert_eq!(m["alpha"], "lib.rs:1:1");
assert_eq!(m["beta"], "lib.rs:2:5");
assert_eq!(m["T::gamma"], "lib.rs:5:5");
assert_eq!(m["inner::delta"], "lib.rs:8:5");
assert_eq!(m["G::hello"], "lib.rs:11:5");
}
#[test]
fn fn_typed_callback_invocation_is_unresolved() {
for hof in [
"fn h(cb: fn()) { cb(); }",
"fn h(cb: impl Fn()) { cb(); }",
"fn h<F: Fn()>(cb: F) { cb(); }",
"fn h(cb: &dyn Fn()) { cb(); }",
"fn h(cb: Box<dyn Fn()>) { cb(); }",
] {
let m = unresolved_of(hof);
assert!(m["h"], "fn-typed callback invocation silently dropped (not unresolved): {hof}");
}
for hof in [
"fn h(cb: impl Fn()) { let g = cb; g(); }",
"fn h(cb: fn()) { let g = if true { cb } else { return }; g(); }",
"fn s() {} fn h() { let g: fn() = s; g(); }",
] {
let m = unresolved_of(hof);
assert!(m["h"], "fn-typed callback rebound to a local silently dropped: {hof}");
}
let m = unresolved_of("fn helper() {} fn caller() { helper(); }");
assert!(!m["caller"], "a normal free-fn call must not be flagged unresolved");
let m = unresolved_of("struct T; impl T { fn m(&self) {} } fn f() { let x = T; let y = x; y.m(); }");
assert!(!m["f"], "a normal value rebind must not be flagged unresolved");
}
#[test]
fn cfg_test_items_excluded_from_default_report() {
let m = typed_calls_of(
"pub fn prod() {}\n\
#[cfg(test)] fn freefn() {}\n\
struct S;\n\
#[cfg(test)] impl S { fn blk(&self) {} }\n\
struct P; impl P { #[cfg(test)] fn meth(&self) {} fn keep(&self) {} }",
);
assert!(m.contains_key("prod"), "a production fn must be in the report");
assert!(m.keys().any(|k| k.ends_with("P::keep")), "a production method must be in the report");
for leaked in ["freefn", "S::blk", "P::meth"] {
assert!(!m.keys().any(|k| k.ends_with(leaked)), "a #[cfg(test)] item leaked: {leaked}");
}
}
#[test]
fn dropped_receiver_idioms_now_resolve() {
let prelude = "struct Sender; impl Sender { fn send(&self) {} }\n\
struct Pool { senders: Vec<Sender> }\n\
enum Conn { Active(Sender), Idle }\n";
let cases: &[(&str, &str)] = &[
("fn f(xs: Vec<Sender>) { for c in xs { c.send(); } }", "f"),
("fn f(xs: &[Sender]) { for c in xs { c.send(); } }", "f"),
("fn f(xs: Vec<Sender>) { xs.iter().for_each(|c| c.send()); }", "f"),
("fn f(xs: Vec<Sender>) { let _ = xs.iter().map(|c| c.send()).count(); }", "f"),
("fn f(xs: Vec<Sender>) { xs[0].send(); }", "f"),
("fn f(p: (Sender, usize)) { let (s, _) = p; s.send(); }", "f"),
];
for (body, fnname) in cases {
let src = format!("{prelude}{body}");
let m = typed_calls_of(&src);
let calls = m.get(*fnname).cloned().unwrap_or_default();
assert!(
calls.iter().any(|c| c == "Sender::send"),
"idiom dropped the effectful receiver (silent under-report): {body}\n typed calls: {calls:?}"
);
}
let m = typed_calls_of(&format!(
"{prelude}impl Pool {{ fn first(&self) {{ self.senders[0].send(); }} \
fn each(&self) {{ for c in &self.senders {{ c.send(); }} }} }}"
));
assert!(m["Pool::first"].iter().any(|c| c == "Sender::send"), "nested field+subscript dropped: {:?}", m["Pool::first"]);
assert!(m["Pool::each"].iter().any(|c| c == "Sender::send"), "for-loop over field dropped: {:?}", m["Pool::each"]);
let m = typed_calls_of(&format!(
"{prelude}fn g(c: Conn) {{ match c {{ Conn::Active(s) => s.send(), Conn::Idle => {{}} }} }}"
));
assert!(m["g"].iter().any(|c| c == "Sender::send"), "enum-payload match binding dropped: {:?}", m["g"]);
}
#[test]
fn idioms_never_fabricate_on_pure_elements() {
let prelude = "struct Pure; impl Pure { fn send(&self) {} }\n\
struct Bag { items: Vec<Pure> }\n";
let bodies: &[&str] = &[
"fn f(xs: Vec<Pure>) { for c in xs { c.send(); } }",
"fn f(xs: Vec<Pure>) { xs.iter().for_each(|c| c.send()); }",
"fn f(xs: Vec<Pure>) { xs[0].send(); }",
"fn f(xs: Vec<i32>) { for c in xs { let _ = c + 1; } }",
"fn f(p: (Pure, usize)) { let (s, _) = p; s.send(); }",
];
for body in bodies {
let m = typed_calls_of(&format!("{prelude}{body}"));
let calls = m.get("f").cloned().unwrap_or_default();
assert!(
calls.iter().all(|c| c != "Sender::send" && !c.contains("TcpStream")),
"fabricated an effectful edge on a pure element: {body}\n typed: {calls:?}"
);
}
}
#[test]
fn scoped_bindings_do_not_leak() {
let prelude = "struct Sender; impl Sender { fn send(&self) {} }\n\
struct Pure; impl Pure { fn send(&self) {} }\n\
fn mk() -> Pure { Pure }\n";
let m = typed_calls_of(&format!(
"{prelude}fn f(xs: Vec<Sender>, ys: Vec<Pure>) {{ for c in xs {{ c.send(); }} for c in ys {{ c.send(); }} }}"
));
let calls = &m["f"];
assert_eq!(
calls.iter().filter(|c| *c == "Sender::send").count(),
1,
"loop binding leaked into the next same-named loop (fabrication): {calls:?}"
);
let m = typed_calls_of(&format!(
"{prelude}fn f(xs: Vec<Sender>) {{ xs.iter().for_each(|c| c.send()); let c = mk(); c.send(); }}"
));
let calls = &m["f"];
assert!(
!calls.iter().any(|c| *c == "Sender::send" && calls.iter().filter(|x| *x == "Sender::send").count() > 1),
"closure param leaked"
);
assert_eq!(calls.iter().filter(|c| *c == "Sender::send").count(), 1,
"closure param binding leaked into a later same-named var: {calls:?}");
assert!(calls.iter().any(|c| c == "Pure::send"), "later c should type Pure::send: {calls:?}");
}
#[test]
fn elem_type_covers_the_collection_shapes() {
let u = uses(&[("Sender", "net::Sender")]);
let p = |s: &str| -> Option<String> {
let t: syn::Type = syn::parse_str(s).unwrap();
elem_type(&t, &u)
};
assert_eq!(p("Vec<Sender>").as_deref(), Some("net::Sender"));
assert_eq!(p("&[Sender]").as_deref(), Some("net::Sender"));
assert_eq!(p("[Sender; 4]").as_deref(), Some("net::Sender"));
assert_eq!(p("HashSet<Sender>").as_deref(), Some("net::Sender"));
assert_eq!(p("BTreeSet<Sender>").as_deref(), Some("net::Sender"));
assert_eq!(p("VecDeque<Sender>").as_deref(), Some("net::Sender"));
assert_eq!(p("Box<[Sender]>").as_deref(), Some("net::Sender"));
assert_eq!(p("Arc<Vec<Sender>>").as_deref(), Some("net::Sender"));
assert_eq!(p("Sender"), None);
assert_eq!(p("Option<Sender>"), None);
assert_eq!(p("HashMap<String, Sender>"), None);
}
#[test]
fn enum_variant_index_drops_ambiguous_leaves() {
let src = "enum A { One(i32), Pair(i32, i32), Unit }\n\
enum B { Two(String) }\n\
enum C { Two(Vec<u8>) }\n"; let file: syn::File = syn::parse_str(src).unwrap();
let mut uses = HashMap::new();
let mut fields = FieldIndex::new();
let mut field_elem = FieldElemIndex::new();
let mut rets: HashMap<String, Option<String>> = HashMap::new();
let mut enum_tmp: HashMap<String, Option<String>> = HashMap::new();
let (mut ti, mut td, mut tf) = (TraitImplIndex::new(), HashMap::new(), TraitFieldIndex::new());
collect_decls(&file.items, false, &mut uses, &mut fields, &mut field_elem, &mut rets,
&mut enum_tmp, &mut ti, &mut td, &mut tf, &mut std::collections::HashSet::new(),
&mut std::collections::HashSet::new(), &mut std::collections::HashSet::new(), &mut std::collections::HashMap::new(), &mut std::collections::HashSet::new());
let ev: EnumVariantIndex = enum_tmp.into_iter().filter_map(|(k, v)| v.map(|t| (k, t))).collect();
assert_eq!(ev.get("One").map(String::as_str), Some("i32")); assert_eq!(ev.get("Pair"), None); assert_eq!(ev.get("Unit"), None); assert_eq!(ev.get("Two"), None); }
#[test]
fn embedded_agents_contract_matches_the_repo_doc() {
let embedded = include_str!("../AGENTS.md");
assert!(embedded.contains("candor-scan"), "the contract must describe this tool");
match std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../AGENTS.md")) {
Ok(root) if root.contains("instructions for an AI coding agent") => {
assert_eq!(embedded, root, "crate AGENTS.md drifted from the repo root — re-copy it");
}
_ => { }
}
}
#[test]
fn scan_emits_unknown_on_an_extern_call_as_the_agents_contract_states() {
let d = std::env::temp_dir().join(format!("candor-scan-agentsunknown-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"agentsunknown\"\n").unwrap();
std::fs::write(
d.join("src/lib.rs"),
"extern \"C\" { fn my_native_op(n: i32) -> i32; }\n\
pub fn wraps_ffi(n: i32) -> i32 { unsafe { my_native_op(n) } }\n",
)
.unwrap();
let idx = DepIndex::default();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix: String::new(), want_json: true, include_tests: false, policy: None,
baseline: None, quiet: true, deps_idx: &idx,
});
let _ = std::fs::remove_dir_all(&d);
assert_eq!(rc, 0);
let body = body.unwrap();
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
let f = v["functions"].as_array().unwrap().iter().find(|f| f["fn"] == "wraps_ffi")
.unwrap_or_else(|| panic!("wraps_ffi must be in the report (not silently pure):\n{body}"));
assert!(f["inferred"].as_array().unwrap().iter().any(|e| e == "Unknown"),
"an extern call is DISCLOSED as Unknown, exactly as AGENTS.md now claims:\n{body}");
assert!(f["unknownWhy"].as_array().unwrap().iter().any(|w| w.as_str().unwrap_or("").starts_with("native:")),
"the why-tag names the FFI boundary:\n{body}");
}
#[test]
fn repo_docs_carry_the_family_attribution_and_spec_floor() {
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/../..");
let (Ok(readme), Ok(agents)) = (
std::fs::read_to_string(format!("{root}/README.md")),
std::fs::read_to_string(format!("{root}/AGENTS.md")),
) else { return };
if !agents.contains("instructions for an AI coding agent") {
return; }
assert!(readme.contains("reference engine is [candor-java]") || readme.contains("reference engine**; this"),
"README must attribute reference-engine status to candor-java");
assert!(!readme.to_lowercase().contains("the reference implementation of"),
"README must not claim reference-implementation status (family ruling: candor-java is the reference)");
assert!(!agents.to_lowercase().contains("the reference implementation of"),
"AGENTS must not claim reference-implementation status");
assert!(readme.contains("spec 0.8"), "README must state the spec 0.8 floor");
assert!(agents.contains("spec 0.8"), "AGENTS must state the spec 0.8 floor");
}
#[test]
fn baseline_guard_resolution_union_and_gain_logic() {
let d = std::env::temp_dir().join(format!("candor-scan-blunit-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
let this_build = format!("scan-{}", env!("CARGO_PKG_VERSION"));
let all: Vec<String> = vec!["a".into(), "b".into(), "newfn".into()];
let mut inferred: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
inferred.insert("a".into(), ["Fs", "Exec"].into_iter().collect()); inferred.insert("b".into(), ["Net"].into_iter().collect()); inferred.insert("newfn".into(), ["Db"].into_iter().collect()); let report = |ver: &str| format!(
r#"{{"candor":{{"version":"{ver}","toolchain":"stable","spec":"0.8"}},
"functions":[{{"fn":"a","inferred":["Fs"]}},
{{"fn":"b","inferred":[]}},
{{"fn":"b","inferred":["Net"]}}]}}"#
);
std::fs::write(d.join("base.mycrate.scan.json"), report(&this_build)).unwrap();
let pre = d.join("base").to_string_lossy().into_owned();
match check_baseline(&pre, ".", "mycrate", &all, &inferred) {
BaselineOutcome::Checked(v) => {
assert_eq!(v.len(), 1, "only the real gain flags: {v:?}",
v = v.iter().map(|x| x.detail.clone()).collect::<Vec<_>>());
assert_eq!(v[0].rule, "AS-EFF-005");
assert_eq!(v[0].func, "a");
assert_eq!(v[0].effects, vec!["Exec".to_string()]);
assert!(v[0].detail.contains("`a` gained effect { Exec }"), "{}", v[0].detail);
}
_ => panic!("a valid same-build baseline must be evaluated"),
}
let direct = d.join("base.mycrate.scan.json").to_string_lossy().into_owned();
assert!(matches!(check_baseline(&direct, ".", "mycrate", &all, &inferred),
BaselineOutcome::Checked(v) if v.len() == 1));
std::fs::write(d.join("stale.mycrate.scan.json"), report("scan-0.0.1")).unwrap();
let stale = d.join("stale").to_string_lossy().into_owned();
assert!(matches!(check_baseline(&stale, ".", "mycrate", &all, &inferred), BaselineOutcome::Invalid));
std::fs::write(d.join("bare.mycrate.scan.json"), r#"[{"fn":"a","inferred":["Fs"]}]"#).unwrap();
let bare = d.join("bare").to_string_lossy().into_owned();
assert!(matches!(check_baseline(&bare, ".", "mycrate", &all, &inferred), BaselineOutcome::Invalid));
assert!(matches!(check_baseline("", ".", "mycrate", &all, &inferred), BaselineOutcome::Invalid));
let absent = d.join("nosuch").to_string_lossy().into_owned();
assert!(matches!(check_baseline(&absent, ".", "mycrate", &all, &inferred), BaselineOutcome::Inactive));
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn toml_primitives_tolerate_spacing_and_comments() {
assert_eq!(toml_section("[ workspace ]"), Some("workspace"));
assert_eq!(toml_section("[package]"), Some("package"));
assert_eq!(toml_section("name = \"x\""), None);
assert_eq!(toml_scalar("name = \"my-crate\" # the name", "name"), Some("my-crate"));
assert_eq!(toml_scalar("name=bare # c", "name"), Some("bare"));
assert_eq!(toml_scalar("namespace = \"x\"", "name"), None); let d = std::env::temp_dir().join(format!("candor-scan-tomlhdr-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("Cargo.toml"), "[ package ]\nname = \"spaced-crate\" # trailing\n").unwrap();
assert_eq!(read_crate_name(&d).as_deref(), Some("spaced_crate"));
assert_eq!(
toml_string_array("[ workspace ]\nmembers = [\"a\", \"b\"]\n", "workspace", "members"),
vec!["a", "b"]
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn cargo_deps_excludes_nested_package_manifests() {
let d = std::env::temp_dir().join(format!("candor-scan-nesteddeps-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("fixture")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"outer\"\n[dependencies]\nserde = \"1\"\n").unwrap();
std::fs::write(d.join("fixture/Cargo.toml"), "[package]\nname = \"inner\"\n[dependencies]\nreqwest = \"0.12\"\n").unwrap();
let (deps, _) = cargo_deps(&d.to_string_lossy());
assert!(deps.contains("serde"), "the crate's own dep is present: {deps:?}");
assert!(!deps.contains("reqwest"), "a nested package's dep leaked into the parent: {deps:?}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn toml_string_array_reads_inline_and_multiline_members() {
let txt = "[package]\nname = \"x\"\n\n[workspace]\nmembers = [\"crates/a\", \"crates/b\"]\nexclude = [\n \"eval\",\n \"sample\",\n]\n";
assert_eq!(toml_string_array(txt, "workspace", "members"), vec!["crates/a", "crates/b"]);
assert_eq!(toml_string_array(txt, "workspace", "exclude"), vec!["eval", "sample"]);
assert!(toml_string_array(txt, "workspace", "default-members").is_empty());
assert!(toml_string_array("[dependencies]\nserde = \"1\"\n", "workspace", "members").is_empty());
}
#[test]
fn workspace_members_expand_globs_and_honour_exclude() {
let d = std::env::temp_dir().join(format!("candor-scan-ws-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
for m in ["crates/a", "crates/skipme", "tools/one", "crates/no-manifest"] {
std::fs::create_dir_all(d.join(m)).unwrap();
if m != "crates/no-manifest" {
std::fs::write(d.join(m).join("Cargo.toml"), "[package]\nname = \"m\"\n").unwrap();
}
}
std::fs::write(
d.join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/*\", \"tools/one\", \"gone/away\"]\nexclude = [\"crates/skipme\"]\n",
)
.unwrap();
let got: Vec<String> = workspace_members(&d)
.into_iter()
.map(|p| p.strip_prefix(&format!("{}/", d.to_string_lossy())).unwrap().to_string())
.collect();
assert_eq!(got, vec!["crates/a", "tools/one"], "glob expands, exclude + missing-manifest drop");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn workspace_members_bare_star_and_dedup() {
let d = std::env::temp_dir().join(format!("candor-scan-ws2-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
for m in ["a", "b"] {
std::fs::create_dir_all(d.join(m)).unwrap();
std::fs::write(d.join(m).join("Cargo.toml"), "[package]\nname = \"m\"\n").unwrap();
}
std::fs::write(d.join("Cargo.toml"), "[workspace]\nmembers = [\"*\", \"a\"]\n").unwrap();
let got: Vec<String> = workspace_members(&d)
.into_iter()
.map(|p| p.strip_prefix(&format!("{}/", d.to_string_lossy())).unwrap().to_string())
.collect();
assert_eq!(got, vec!["a", "b"], "bare * expands to children, deduped against the explicit `a`");
assert!(has_workspace_table(&d));
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn workspace_root_scans_members_even_under_deps_filter() {
let d = std::env::temp_dir().join(format!("candor-scan-wsfan-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
for (m, body) in [("a", "pub fn ea() { let _ = std::fs::read(\"/x\"); }"),
("b", "pub fn eb() { let _ = std::process::Command::new(\"x\"); }")] {
std::fs::create_dir_all(d.join(m).join("src")).unwrap();
std::fs::write(d.join(m).join("Cargo.toml"), format!("[package]\nname = \"{m}\"\n")).unwrap();
std::fs::write(d.join(m).join("src/lib.rs"), body).unwrap();
}
std::fs::write(d.join("Cargo.toml"), "[workspace]\nmembers = [\"a\", \"b\"]\n").unwrap();
let idx = load_dep_reports(None);
let prefix = d.join("out/r").to_string_lossy().into_owned();
let rc = scan_target(&d.to_string_lossy(), prefix.clone(), false, false, None, None, &idx);
assert_eq!(rc, 0);
let ra = std::fs::read_to_string(format!("{prefix}.a.scan.json")).unwrap();
let rb = std::fs::read_to_string(format!("{prefix}.b.scan.json")).unwrap();
assert!(ra.contains("ea") && ra.contains("Fs"), "member a not scanned: {ra}");
assert!(rb.contains("eb") && rb.contains("Exec"), "member b not scanned: {rb}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn nested_packages_are_not_modules_of_the_parent() {
let d = std::env::temp_dir().join(format!("candor-scan-nested-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::create_dir_all(d.join("fixture/src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"outer\"\n").unwrap();
std::fs::write(d.join("src/lib.rs"), "pub fn outer_eff() { let _ = std::fs::read(\"/x\"); }\n").unwrap();
std::fs::write(d.join("fixture/Cargo.toml"), "[package]\nname = \"inner\"\n").unwrap();
std::fs::write(
d.join("fixture/src/lib.rs"),
"pub fn inner_eff() { let _ = std::process::Command::new(\"x\"); }\n",
)
.unwrap();
let idx = load_dep_reports(None);
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, _) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix: prefix.clone(), want_json: false, include_tests: false,
policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let rep = std::fs::read_to_string(format!("{prefix}.outer.scan.json")).unwrap();
assert!(rep.contains("outer_eff"), "the parent's own fn must report: {rep}");
assert!(!rep.contains("inner_eff") && !rep.contains("Exec"),
"nested package's fn leaked into the parent report: {rep}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn report_is_written_atomically_no_tmp_leftovers() {
let d = std::env::temp_dir().join(format!("candor-scan-atomic-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"atomiccrate\"\n").unwrap();
std::fs::write(d.join("src/lib.rs"), "pub fn eff() { let _ = std::fs::read(\"/x\"); }\n").unwrap();
let idx = load_dep_reports(None);
let outdir = d.join("out");
let prefix = outdir.join("r").to_string_lossy().into_owned();
let (rc, _) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: false, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let leftovers: Vec<String> = std::fs::read_dir(&outdir).unwrap()
.filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
.filter(|n| n.contains(".tmp")).collect();
assert!(leftovers.is_empty(), "atomic write left temp files behind: {leftovers:?}");
for name in ["r.atomiccrate.scan.json", "r.atomiccrate.scan.callgraph.json"] {
let body = std::fs::read_to_string(outdir.join(name)).unwrap();
serde_json::from_str::<serde_json::Value>(&body)
.unwrap_or_else(|e| panic!("{name} is not whole JSON ({e}): {body}"));
}
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn classifier_resolves_a_std_fs_call() {
assert_eq!(candor_classify::classify("std", "std::fs::read_to_string"), Some("Fs"));
assert_eq!(candor_classify::classify("std", "std::process::Command::new"), Some("Exec"));
}
#[test]
fn builder_chain_entries_over_approximate_for_the_syntactic_scanner() {
assert_eq!(scan_builder_entry_effect("duct", "duct::cmd"), Some("Exec"));
assert_eq!(scan_builder_entry_effect("duct", "duct::sh"), Some("Exec"));
assert_eq!(scan_builder_entry_effect("ureq", "ureq::get"), Some("Net"));
assert_eq!(scan_builder_entry_effect("ureq", "ureq::post"), Some("Net"));
assert_eq!(scan_builder_entry_effect("sqlx", "sqlx::query"), Some("Db"));
assert_eq!(scan_builder_entry_effect("diesel", "diesel::sql_query"), Some("Db"));
assert_eq!(scan_builder_entry_effect("duct", "duct::Expression::run"), None);
assert_eq!(scan_builder_entry_effect("ureq", "ureq::Request::call"), None);
assert_eq!(scan_builder_entry_effect("std", "std::process::Command::new"), None);
assert_eq!(candor_classify::classify("duct", "duct::cmd"), None);
assert_eq!(candor_classify::classify("ureq", "ureq::get"), None);
}
#[test]
fn macro_invocation_never_mints_a_local_edge() {
let idx = load_dep_reports(None);
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-macroedge-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let local = "mod helpers { pub fn trace() { let _ = std::fs::read(\"/x\"); } }\n";
let bug = run("macedge", &format!("{local}pub fn pure_caller() {{ crate::helpers::trace!(); }}"));
assert!(eff(&bug, "pure_caller").is_empty(),
"macro invocation FABRICATED a same-named local fn's effect onto a pure caller:\n{bug}");
let real = run("macedge2", &format!("{local}pub fn real_caller() {{ crate::helpers::trace(); }}"));
assert!(eff(&real, "real_caller").contains(&"Fs".to_string()),
"the is_macro guard wrongly suppressed a genuine local fn edge:\n{real}");
let ext = run("macedge3", "pub fn logs() { log::info!(\"hi\"); }");
assert!(eff(&ext, "logs").contains(&"Log".to_string()),
"an external classified emit-macro must still attribute its effect:\n{ext}");
}
#[test]
fn write_macro_charges_the_local_writer_side() {
let idx = load_dep_reports(None);
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-wr-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let w = "use std::fmt::Write as _;\nstruct Loud;\nimpl std::fmt::Write for Loud { fn write_str(&mut self, _s: &str) -> std::fmt::Result { let _ = std::fs::read(\"/x\"); Ok(()) } }\n";
let hit = run("wrfmt", &format!("{w}pub fn via_write(w: &mut Loud) {{ let _ = write!(w, \"hi {{}}\", 1); }}"));
assert!(eff(&hit, "via_write").contains(&"Fs".to_string()),
"write! to a local effectful fmt::Write writer must charge the writer side:\n{hit}");
let pure = run("wrstr", "use std::fmt::Write as _;\npub fn via_str(s: &mut String) { let _ = write!(s, \"hi {}\", 1); }");
assert!(eff(&pure, "via_str").is_empty(),
"write! to a std String writer must stay pure:\n{pure}");
let asrt = run("wrassert", &format!("{w}#[derive(Debug, PartialEq)]\nstruct Tag;\npub fn via_assert(a: Tag, b: Tag) {{ assert_eq!(a, b, \"ctx {{}}\", 1); }}"));
assert!(eff(&asrt, "via_assert").is_empty(),
"an assert_eq! operand was wrongly charged as a writer:\n{asrt}");
}
#[test]
fn qself_call_never_mints_a_bare_leaf_local_edge() {
let idx = load_dep_reports(None);
let run = |name: &str, src: &str| -> serde_json::Value {
let d = std::env::temp_dir().join(format!("candor-qself-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v: serde_json::Value = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
v
};
let eff = |v: &serde_json::Value, needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
let bug = run("qselfnew",
"use std::process::Command;\npub struct Daemon;\nimpl Daemon { pub fn new() -> Self { Command::new(\"/bin/sh\").status().unwrap(); Daemon } }\npub fn pure_build() -> Vec<u8> { let mut v = <Vec<u8>>::new(); v.push(1); v }");
assert!(eff(&bug, "pure_build").is_empty(),
"a qself assoc call FABRICATED a same-named local fn's effect onto a pure caller:\n{bug}");
let real = run("qselfreal",
"use std::fs;\npub fn dump() { fs::write(\"/x\", b\"d\").unwrap(); }\npub fn real_caller() { dump(); }");
assert!(eff(&real, "real_caller").contains(&"Fs".to_string()),
"the qself guard wrongly suppressed a genuine bare free-fn edge:\n{real}");
let band = run("qselfband",
"use std::fs;\npub trait Marker { fn go() { fs::write(\"/x\", b\"d\").unwrap(); } }\npub fn via_trait_ufcs() { <SomeType as Marker>::go(); }");
assert!(eff(&band, "via_trait_ufcs").contains(&"Fs".to_string()),
"a qualified-tail qself into a local trait default must still link:\n{band}");
let local = run("qselflocal",
"use std::process::Command;\npub struct Daemon;\nimpl Daemon { pub fn new() -> Self { Command::new(\"/bin/sh\").status().unwrap(); Daemon } }\npub fn boot() { let _d = <Daemon>::new(); }");
assert!(eff(&local, "boot").contains(&"Exec".to_string()),
"an inherent qself on a LOCAL effectful assoc fn must NOT be under-reported:\n{local}");
}
#[test]
fn resolve_target_is_precise_and_never_fabricates() {
let mut by_leaf: HashMap<String, Vec<String>> = HashMap::new();
let mut by_tail2: HashMap<String, Vec<String>> = HashMap::new();
for q in ["random::bool::bool", "clip::ClipboardThread::start", "util::helper",
"app::Worker::run", "a::Job::run", "b::Job::run"] {
by_leaf.entry(q.rsplit("::").next().unwrap().into()).or_default().push(q.into());
by_tail2.entry(tail2(q).unwrap()).or_default().push(q.into());
}
assert_eq!(resolve_target("Value::bool", "bool", false, &by_tail2, &by_leaf), None);
assert_eq!(resolve_target("start", "start", true, &by_tail2, &by_leaf), None);
assert_eq!(resolve_target("helper", "helper", false, &by_tail2, &by_leaf),
Some(&vec!["util::helper".to_string()]));
assert_eq!(resolve_target("Worker::run", "run", false, &by_tail2, &by_leaf),
Some(&vec!["app::Worker::run".to_string()]));
assert_eq!(resolve_target("Job::run", "run", false, &by_tail2, &by_leaf), None);
}
#[test]
fn every_merged_decls_field_is_folded_into_the_digest() {
let MergedDecls {
fields: _,
field_elem: _,
rets: _,
enum_tmp: _,
trait_impls: _,
trait_decls: _,
trait_fields: _,
prim_aliases: _,
extern_fns: _,
drop_types: _,
deref_target: _,
lazy_statics: _,
} = MergedDecls::default();
let empty = decl_index_digest(&MergedDecls::default());
type Mutator = fn(&mut MergedDecls);
let mutators: Vec<(&str, Mutator)> = vec![
("fields", |m| { m.fields.entry("S".into()).or_default().insert("f".into(), "T".into()); }),
("field_elem", |m| { m.field_elem.entry("S".into()).or_default().insert("f".into(), "E".into()); }),
("rets", |m| { m.rets.insert("f".into(), Some("T".into())); }),
("enum_tmp", |m| { m.enum_tmp.insert("v".into(), Some("E".into())); }),
("trait_impls", |m| { m.trait_impls.entry("Tr".into()).or_default().push("Ty".into()); }),
("trait_decls", |m| { m.trait_decls.entry("Tr".into()).or_default().count += 1; }),
("trait_fields", |m| { m.trait_fields.entry("S".into()).or_default().insert("f".into(), vec!["b".into()]); }),
("prim_aliases", |m| { m.prim_aliases.insert("A".into()); }),
("extern_fns", |m| { m.extern_fns.insert("system".into()); }),
("drop_types", |m| { m.drop_types.insert("Guard".into()); }),
("lazy_statics", |m| { m.lazy_statics.insert("CONFIG".into()); }),
];
for (name, mutate) in mutators {
let mut m = MergedDecls::default();
mutate(&mut m);
assert_ne!(
decl_index_digest(&m), empty,
"MergedDecls.{name} changes the index but NOT the digest — the --incremental cache would \
reuse stale FnInfos. Fold `{name}` into decl_index_digest().",
);
}
}
fn eval_cfg(cfg: &str, active: &[&str], declared: &[&str]) -> Option<bool> {
let item: syn::ItemFn = syn::parse_str(&format!("#[cfg({cfg})] fn f() {{}}")).unwrap();
let attr = item.attrs.iter().find(|a| a.path().is_ident("cfg")).unwrap();
let active: std::collections::HashSet<String> = active.iter().map(|s| s.to_string()).collect();
let declared: std::collections::HashSet<String> =
declared.iter().map(|s| s.to_string()).collect();
let mut verdict = None;
let _ = attr.parse_nested_meta(|m| {
verdict = cfg_eval(&m, &active, &declared);
Ok(())
});
verdict
}
#[test]
fn cfg_eval_feature_predicate_is_three_valued() {
let d = &["on", "off"];
assert_eq!(eval_cfg(r#"feature = "on""#, &["on"], d), Some(true));
assert_eq!(eval_cfg(r#"feature = "off""#, &["on"], d), Some(false));
assert_eq!(eval_cfg(r#"feature = "mystery""#, &["on"], d), None);
}
#[test]
fn cfg_eval_unknown_predicates_stay_unknown() {
for p in [r#"target_os = "linux""#, "unix", "windows", "test", "doc"] {
assert_eq!(eval_cfg(p, &["on"], &["on"]), None, "predicate `{p}` must stay unknown");
}
}
#[test]
fn cfg_eval_not_folds_kleene() {
let (a, d) = (&["on"][..], &["on", "off"][..]);
assert_eq!(eval_cfg(r#"not(feature = "on")"#, a, d), Some(false));
assert_eq!(eval_cfg(r#"not(feature = "off")"#, a, d), Some(true));
assert_eq!(eval_cfg("not(unix)", a, d), None);
}
#[test]
fn cfg_eval_all_folds_kleene() {
let (a, d) = (&["on"][..], &["on", "off"][..]);
assert_eq!(eval_cfg(r#"all(feature = "on", feature = "on")"#, a, d), Some(true));
assert_eq!(eval_cfg(r#"all(feature = "on", feature = "off")"#, a, d), Some(false));
assert_eq!(eval_cfg(r#"all(unix, feature = "off")"#, a, d), Some(false));
assert_eq!(eval_cfg(r#"all(feature = "on", unix)"#, a, d), None);
}
#[test]
fn cfg_eval_any_folds_kleene() {
let (a, d) = (&["on"][..], &["on", "off"][..]);
assert_eq!(eval_cfg(r#"any(feature = "off", feature = "on")"#, a, d), Some(true));
assert_eq!(eval_cfg(r#"any(unix, feature = "on")"#, a, d), Some(true));
assert_eq!(eval_cfg(r#"any(feature = "off", not(feature = "on"))"#, a, d), Some(false));
assert_eq!(eval_cfg(r#"any(feature = "off", unix)"#, a, d), None);
}
#[test]
fn cfg_eval_nested_combinations() {
let (a, d) = (&["on"][..], &["on", "off"][..]);
assert_eq!(
eval_cfg(r#"all(any(feature = "off", feature = "on"), not(feature = "off"))"#, a, d),
Some(true)
);
assert_eq!(
eval_cfg(r#"any(all(feature = "on", feature = "off"), feature = "off")"#, a, d),
Some(false)
);
assert_eq!(eval_cfg(r#"not(all(feature = "on", unix))"#, a, d), None);
assert_eq!(
eval_cfg(r#"any(all(not(feature = "off"), feature = "on"), target_os = "linux")"#, a, d),
Some(true)
);
}
#[test]
fn push_quoted_pulls_double_quoted_tokens() {
let mut out = Vec::new();
push_quoted(r#""std", "alloc-dep""#, &mut out);
assert_eq!(out, vec!["std", "alloc-dep"]);
push_quoted(r#""ok", "dangling"#, &mut out);
assert_eq!(out, vec!["std", "alloc-dep", "ok"]);
push_quoted("plain ] tokens", &mut out);
assert_eq!(out.len(), 3);
push_quoted(r#""""#, &mut out);
assert_eq!(out.last().map(String::as_str), Some(""));
}
#[test]
fn non_nominal_types_cannot_carry_local_impls() {
let non: &[&str] = &[
"[u8; 32]", "[u8]", "(A, B)", "*const u8", "&str", "fn(u32) -> u32",
"u8", "u128", "usize", "i64", "f64", "bool", "char", "str",
];
for t in non {
let ty: syn::Type = syn::parse_str(t).unwrap();
assert!(is_non_nominal_type(&ty), "`{t}` must be non-nominal");
}
let nominal: &[&str] = &["Vec<u8>", "MyStruct", "std::path::PathBuf", "Option<u8>", "String"];
for t in nominal {
let ty: syn::Type = syn::parse_str(t).unwrap();
assert!(!is_non_nominal_type(&ty), "`{t}` must stay nominal");
}
}
#[test]
fn annotated_tuple_destructure_types_its_elements() {
let src = r#"
struct Runner;
impl Runner { fn go(&self) { std::process::Command::new("ls").status().unwrap(); } }
fn make() -> (Runner, u32) { (Runner, 0) }
pub fn user() { let (r, _): (Runner, u32) = make(); r.go(); }
pub fn rebound() { let (r, _): (Runner, u32) = make(); let (r, _): (u32, u32) = (1, 2); r.go(); }
"#;
let d = std::env::temp_dir().join(format!("candor-bindtuple-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"bt\"\n").unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
let idx = load_dep_reports(None);
let prefix = d.join("out/r").to_string_lossy().into_owned();
let (rc, body) = scan_one(&d.to_string_lossy(), ScanOpts {
prefix, want_json: true, include_tests: false, policy: None, baseline: None, quiet: true, deps_idx: &idx,
});
assert_eq!(rc, 0);
let v: serde_json::Value = serde_json::from_str(&body.unwrap()).unwrap();
let _ = std::fs::remove_dir_all(&d);
let eff = |needle: &str| -> Vec<String> {
v["functions"].as_array().into_iter().flatten()
.filter(|f| f["fn"].as_str().is_some_and(|q| q.contains(needle)))
.flat_map(|f| f["inferred"].as_array().into_iter().flatten()
.filter_map(|e| e.as_str().map(String::from)).collect::<Vec<_>>()).collect()
};
assert!(eff("user").contains(&"Exec".to_string()),
"a tuple-destructured effectful binding must still attribute (bind_tuple):\n{v}");
assert!(!eff("rebound").contains(&"Exec".to_string()),
"a tuple REBIND must clear the stale Runner binding — Exec here is fabricated:\n{v}");
}
#[test]
fn dirs_cargo_registry_src_honours_cargo_home() {
let ch = std::env::temp_dir().join(format!("candor-regsrc-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&ch);
let src = ch.join("registry").join("src");
std::fs::create_dir_all(src.join("index.crates.io-aaaa")).unwrap();
std::fs::create_dir_all(src.join("index.mirror-bbbb")).unwrap();
std::fs::write(src.join("not-an-index.txt"), "x").unwrap();
let prior = std::env::var("CARGO_HOME").ok();
std::env::set_var("CARGO_HOME", &ch);
let mut roots = dirs_cargo_registry_src();
roots.sort();
let empty_home = std::env::temp_dir().join(format!("candor-regsrc-empty-{}", std::process::id()));
let _ = std::fs::create_dir_all(&empty_home);
std::env::set_var("CARGO_HOME", &empty_home);
let none = dirs_cargo_registry_src();
match prior {
Some(v) => std::env::set_var("CARGO_HOME", v),
None => std::env::remove_var("CARGO_HOME"),
}
let _ = std::fs::remove_dir_all(&ch);
let _ = std::fs::remove_dir_all(&empty_home);
assert_eq!(
roots,
vec![src.join("index.crates.io-aaaa"), src.join("index.mirror-bbbb")],
"exactly the index DIRECTORIES under <CARGO_HOME>/registry/src, files excluded"
);
assert!(none.is_empty(), "no registry under CARGO_HOME → empty, not an error");
}
#[test]
fn run_with_deps_without_lockfile_returns_2() {
let d = std::env::temp_dir().join(format!("candor-depsnolock-unit-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), "[package]\nname = \"nl\"\n").unwrap();
std::fs::write(d.join("src/lib.rs"), "pub fn f() {}\n").unwrap();
let rc = run_with_deps(&d.to_string_lossy(), String::new(), true, false, None, None);
let _ = std::fs::remove_dir_all(&d);
assert_eq!(rc, 2, "--deps without Cargo.lock must fail closed");
}