use super::super::*;
use super::dyn_trait::{dyn_findings, dyn_mod};
use super::helpers::*;
pub(super) fn async_findings(
name: &str,
files: &[(&str, &str)],
module: &str,
) -> Result<Vec<String>, String> {
shape_findings("async", name, files, module, async_exposure_module_findings)
}
pub(super) fn async_mod(name: &str, body: &str) -> Result<Vec<String>, String> {
async_findings(
name,
&[("lib.rs", "pub mod m;\n"), ("m.rs", body)],
"crate::m",
)
}
pub(super) fn async_observations(
name: &str,
body: &str,
) -> Result<Vec<(StructuredFactIdentity, String)>, String> {
let tree = TempSrcTree::new(&format!("async-observation-{name}"));
tree.write_all(&[("lib.rs", "pub mod registry;\n"), ("registry.rs", body)]);
async_exposure_module_findings(tree.src(), &tree.root(), "crate::registry", "x").map(|facts| {
facts
.into_iter()
.map(|(fact, _)| {
let finding = fact.into_finding("app", "src/lib.rs");
(finding.key().clone(), finding.text().to_string())
})
.collect()
})
}
#[test]
pub(super) fn pacta_shaped_registry_signature_changes_preserve_async_seam_identity() {
let first = async_observations(
"pacta-v1",
"pub struct Registry;\npub struct Contract;\nimpl Registry { pub async fn register(&self, contract: Contract) {} }\n",
)
.unwrap();
let second = async_observations(
"pacta-v2",
"pub struct Registry;\npub struct Receipt;\nimpl Registry { pub async fn register(&mut self, name: &str, version: u64) -> Receipt { Receipt } }\n",
)
.unwrap();
assert_eq!(first.len(), 1);
assert_eq!(second.len(), 1);
assert_eq!(first[0].0, second[0].0);
assert_ne!(first[0].1, second[0].1);
}
#[test]
pub(super) fn async_production_violation_separates_target_rule_and_seam() {
let (metadata, _fixture) = fixture_metadata(
"async-identity",
&[
("lib.rs", "pub mod registry;\n"),
("registry.rs", "pub async fn register(name: &str) {}\n"),
],
);
let boundary = AsyncExposureBoundary::in_crate("x")
.module("crate::registry")
.must_not_expose_async_fn()
.because("registry operations keep a synchronous seam");
let mut violations = Vec::new();
check_async_exposure_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(violations.len(), 1);
let id = violations[0].id();
assert_eq!(id.target(), "crate::registry");
let rule = id.rule_key();
assert_eq!(rule.rule_type(), "tianheng.rule/hunyi/async-exposure");
assert_eq!(
rule.fields().collect::<Vec<_>>(),
Vec::<(&str, &str)>::new()
);
let fact = id.fact();
assert_eq!(fact.fact_type(), "tianheng.fact/hunyi/async-exposure");
assert_eq!(fact.shape(), "async-free-function");
assert_eq!(
fact.fields().collect::<Vec<_>>(),
vec![
("governing_package", "x"),
("module", "crate::registry"),
("name", "register"),
("owner", "crate::registry"),
("owner_kind", "module"),
("unit", "lib.rs"),
]
);
}
#[test]
pub(super) fn subtree_opt_in_preserves_anchored_finding_violation_id_identity() {
let default_rule = AsyncExposureBoundary::in_crate("pkg")
.module("crate::m")
.must_not_expose_async_fn()
.because("test");
let subtree_rule = AsyncExposureBoundary::in_crate("pkg")
.module("crate::m")
.must_not_expose_async_fn()
.including_submodules()
.because("test");
assert_eq!(
default_rule.rule_key(),
subtree_rule.rule_key(),
"toggling including_submodules must not alter RuleKey identity"
);
}
#[test]
pub(super) fn async_exposure_flags_a_public_async_free_fn() {
assert_eq!(
async_mod("free", "pub async fn connect() -> u8 { 0 }\n").unwrap(),
["async fn crate::m::connect() -> u8"],
);
}
#[test]
pub(super) fn async_exposure_flags_a_public_inherent_async_method() {
assert_eq!(
async_mod(
"inherent",
"pub struct Service; impl Service { pub async fn run(&self) {} }\n"
)
.unwrap(),
["async fn <crate::m::Service>::run(&self)"],
);
}
#[test]
pub(super) fn async_exposure_flags_a_public_trait_async_method_declaration() {
assert_eq!(
async_mod("trait", "pub trait Port { async fn fetch(&self) -> u8; }\n").unwrap(),
["async fn trait crate::m::Port::fetch(&self) -> u8"],
);
}
#[test]
pub(super) fn async_exposure_does_not_flag_trait_impl_private_or_nonasync() {
assert!(
async_mod(
"traitimpl",
"pub struct S; impl crate::T for S { async fn run(&self) {} }\n"
)
.unwrap()
.is_empty(),
);
assert!(
async_mod("priv", "async fn helper() {}\n")
.unwrap()
.is_empty(),
);
assert!(
async_mod("sync", "pub fn ready() -> u8 { 0 }\n")
.unwrap()
.is_empty(),
);
}
#[test]
pub(super) fn async_exposure_finding_is_injective_across_same_named_owners() {
let two_impls = async_mod(
"two-impls",
"pub struct A; pub struct B;\n\
impl A { pub async fn run(&self) {} }\n\
impl B { pub async fn run(&self) {} }\n",
)
.unwrap();
assert_eq!(
two_impls,
[
"async fn <crate::m::A>::run(&self)".to_string(),
"async fn <crate::m::B>::run(&self)".to_string(),
],
"same-named async methods across two impls yield two distinct owner-qualified findings",
);
let two_traits = async_mod(
"two-traits",
"pub trait T { async fn run(&self); }\npub trait U { async fn run(&self); }\n",
)
.unwrap();
assert_eq!(
two_traits,
[
"async fn trait crate::m::T::run(&self)".to_string(),
"async fn trait crate::m::U::run(&self)".to_string(),
],
);
}
#[test]
pub(super) fn async_exposure_boundary_carries_anchor_and_severity() {
let b = AsyncExposureBoundary::in_crate("core")
.module("crate::core")
.must_not_expose_async_fn()
.warn()
.because("the core seam is synchronous");
assert_eq!(b.crate_package(), "core");
assert_eq!(b.module(), "crate::core");
assert_eq!(b.severity(), Severity::Warn);
assert!(!b.including_submodules());
let sub = AsyncExposureBoundary::in_crate("core")
.module("crate")
.must_not_expose_async_fn()
.including_submodules()
.because("no async anywhere under the kernel");
assert!(sub.including_submodules());
}
pub(super) fn async_subtree(
name: &str,
files: &[(&str, &str)],
module: &str,
) -> Result<Vec<(String, String)>, String> {
subtree_findings(
"async",
name,
files,
module,
async_exposure_subtree_findings,
)
}
pub(super) fn async_subtree_labels(
name: &str,
files: &[(&str, &str)],
module: &str,
) -> Vec<String> {
async_subtree(name, files, module)
.unwrap()
.into_iter()
.map(|(finding, _module)| finding)
.collect()
}
#[test]
pub(super) fn async_subtree_reacts_to_a_submodule_async_fn_the_seam_scope_misses() {
let files = &[
("lib.rs", "pub mod net;\n"),
("net.rs", "pub async fn connect() {}\n"),
];
assert_eq!(
async_findings("seam-misses-sub", files, "crate").unwrap(),
Vec::<String>::new(),
);
assert_eq!(
async_subtree("sub-reacts", files, "crate").unwrap(),
[(
"async fn crate::net::connect()".to_string(),
"crate::net".to_string()
)],
);
}
#[test]
pub(super) fn async_subtree_includes_the_anchor_modules_own_seam_byte_identically() {
let files = &[
("lib.rs", "pub mod m;\n"),
("m.rs", "pub async fn own() {}\npub mod deep;\n"),
("m/deep.rs", "pub async fn nested() {}\n"),
];
let seam = async_findings("seam-parity", files, "crate::m").unwrap();
assert_eq!(seam, ["async fn crate::m::own()"]);
let subtree = async_subtree_labels("subtree-parity", files, "crate::m");
assert_eq!(
subtree,
[
"async fn crate::m::deep::nested()",
"async fn crate::m::own()",
],
);
assert!(subtree.contains(&seam[0]));
}
#[test]
pub(super) fn async_subtree_and_seam_both_fail_loud_on_an_unrenderable_owner() {
let files = &[(
"lib.rs",
"pub struct Arr<const N: usize>;\npub struct Marker;\nimpl Marker { pub async fn before() {} }\nimpl<const N: usize> Arr<{ N + 1 }> { pub async fn unrenderable() {} }\n",
)];
let seam = async_findings("const-generic-owner-parity-seam", files, "crate").unwrap_err();
let subtree = async_subtree("const-generic-owner-parity-subtree", files, "crate").unwrap_err();
assert!(seam.contains("without a positional fallback"), "{seam}");
assert!(
subtree.contains("without a positional fallback"),
"{subtree}"
);
assert!(!seam.contains("_#") && !subtree.contains("_#"));
}
#[test]
pub(super) fn async_cfg_branches_never_share_an_unrenderable_owner_fallback() {
let files = &[
(
"lib.rs",
"#[cfg(feature = \"u\")]\npub mod m;\n#[cfg(feature = \"w\")]\n#[path = \"m_w.rs\"]\npub mod m;\n",
),
(
"m.rs",
"pub struct Arr<const N: usize>;\nimpl<const N: usize> Arr<{ N + 1 }> { pub async fn run() {} }\n",
),
(
"m_w.rs",
"pub struct Arr<const N: usize>;\nimpl<const N: usize> Arr<{ N + 2 }> { pub async fn run() {} }\n",
),
];
let error = async_subtree("cfg-split-owner-fallback-collision", files, "crate::m").unwrap_err();
assert!(error.contains("without a positional fallback"), "{error}");
assert!(!error.contains("_#"), "{error}");
}
#[test]
pub(super) fn async_subtree_reacts_through_inline_and_nested_modules() {
let files = &[
(
"lib.rs",
"pub mod outer { pub async fn a() {} pub mod middle; }\n",
),
("outer/middle.rs", "pub async fn b() {}\npub mod leaf;\n"),
("outer/middle/leaf.rs", "pub async fn c() {}\n"),
];
assert_eq!(
async_subtree_labels("nested", files, "crate"),
[
"async fn crate::outer::a()",
"async fn crate::outer::middle::b()",
"async fn crate::outer::middle::leaf::c()",
],
);
}
#[test]
pub(super) fn async_subtree_anchored_at_an_inline_module_follows_its_own_further_path_child() {
let files = &[
(
"lib.rs",
"pub mod outer {\n #[path = \"moved/leaf.rs\"]\n pub mod leaf;\n}\n",
),
("outer/moved/leaf.rs", "pub async fn seam() {}\n"),
];
assert_eq!(
async_subtree_labels("inline-anchor-path-child", files, "crate::outer"),
["async fn crate::outer::leaf::seam()"],
);
}
#[test]
pub(super) fn async_subtree_walks_every_branch_of_a_cfg_split_anchor_not_just_the_first() {
let files = &[
(
"lib.rs",
"#[cfg(feature = \"u\")]\npub mod foo;\n#[cfg(feature = \"w\")]\n#[path = \"win/foo.rs\"]\npub mod foo;\n",
),
("foo.rs", "pub mod bar;\n"),
("foo/bar.rs", "pub async fn unix_leaf() {}\n"),
("win/foo.rs", "pub mod bar;\n"),
("win/bar.rs", "pub async fn win_leaf() {}\n"),
];
assert_eq!(
async_subtree_labels("cfg-split-anchor-both-branches", files, "crate::foo"),
[
"async fn crate::foo::bar::unix_leaf()",
"async fn crate::foo::bar::win_leaf()",
],
);
}
#[test]
pub(super) fn async_subtree_violations_name_each_branchs_own_file_not_a_shared_module_string_cache()
{
let (metadata, _fixture) = fixture_metadata(
"cfg-split-anchor-file-attribution",
&[
(
"lib.rs",
"#[cfg(feature = \"u\")]\npub mod foo;\n#[cfg(feature = \"w\")]\n#[path = \"win/foo.rs\"]\npub mod foo;\n",
),
("foo.rs", "pub mod bar;\n"),
("foo/bar.rs", "pub async fn unix_leaf() {}\n"),
("win/foo.rs", "pub mod bar;\n"),
("win/bar.rs", "pub async fn win_leaf() {}\n"),
],
);
let boundary = AsyncExposureBoundary::in_crate("x")
.module("crate::foo")
.must_not_expose_async_fn()
.including_submodules()
.because("each branch's finding must name its own real file");
let mut violations = Vec::new();
check_async_exposure_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(violations.len(), 2, "{violations:?}");
let mut by_finding: std::collections::BTreeMap<String, &str> = Default::default();
for v in &violations {
by_finding.insert(
v.finding.clone(),
v.file
.as_deref()
.expect("a subtree finding carries its file"),
);
}
assert!(
by_finding["async fn crate::foo::bar::unix_leaf()"].ends_with("foo/bar.rs"),
"unix_leaf must name foo/bar.rs: {by_finding:?}"
);
assert!(
by_finding["async fn crate::foo::bar::win_leaf()"].ends_with("win/bar.rs"),
"win_leaf must name win/bar.rs, never foo/bar.rs (a shared-cache misattribution): {by_finding:?}"
);
}
#[test]
pub(super) fn async_subtree_does_not_duplicate_a_file_shared_by_two_plain_cfg_siblings() {
let files = &[
(
"lib.rs",
"#[cfg(feature = \"u\")]\npub mod foo;\n#[cfg(feature = \"w\")]\npub mod foo;\n",
),
("foo.rs", "pub async fn seam() {}\n"),
];
assert_eq!(
async_subtree_labels("plain-cfg-siblings-one-file", files, "crate::foo"),
["async fn crate::foo::seam()"],
);
}
#[test]
pub(super) fn async_subtree_scopes_to_the_anchored_subtree_not_the_whole_crate() {
let files = &[
("lib.rs", "pub mod a;\npub mod c;\n"),
("a.rs", "pub async fn af() {}\npub mod b;\n"),
("a/b.rs", "pub async fn bf() {}\n"),
("c.rs", "pub async fn cf() {}\n"),
];
assert_eq!(
async_subtree_labels("bounded", files, "crate::a"),
["async fn crate::a::af()", "async fn crate::a::b::bf()"],
);
}
#[test]
pub(super) fn async_subtree_tolerates_a_cfg_gated_fileless_submodule() {
let files = &[
(
"lib.rs",
"#[cfg(feature = \"absent\")]\npub mod gated;\npub mod present;\n",
),
("present.rs", "pub async fn here() {}\n"),
];
assert_eq!(
async_subtree_labels("cfg-tolerated", files, "crate"),
["async fn crate::present::here()"],
);
}
#[test]
pub(super) fn async_subtree_errors_on_a_non_cfg_missing_submodule() {
let files = &[("lib.rs", "pub mod gone;\n")];
assert!(async_subtree("non-cfg-missing", files, "crate").is_err());
}
#[test]
pub(super) fn async_subtree_reacts_through_a_cfg_attr_wrapped_path_submodule() {
let files = &[
(
"lib.rs",
"#[cfg_attr(any(), path = \"never.rs\")]\npub mod net;\n",
),
("net.rs", "pub async fn connect() {}\n"),
];
assert_eq!(
async_subtree("cfg-attr-path", files, "crate").unwrap(),
[(
"async fn crate::net::connect()".to_string(),
"crate::net".to_string()
)],
);
}
#[test]
pub(super) fn async_subtree_distinguishes_same_named_async_methods_across_modules() {
let files = &[
("lib.rs", "pub mod a;\npub mod b;\n"),
(
"a.rs",
"pub struct S;\nimpl S { pub async fn run(&self) {} }\n",
),
(
"b.rs",
"pub struct S;\nimpl S { pub async fn run(&self) {} }\n",
),
];
assert_eq!(
async_subtree_labels("cross-mod-owners", files, "crate"),
[
"async fn <crate::a::S>::run(&self)",
"async fn <crate::b::S>::run(&self)",
],
);
}
#[test]
pub(super) fn async_subtree_does_not_observe_a_body_nested_module() {
let files = &[(
"lib.rs",
"pub fn outer() { mod inner { pub async fn hidden() {} } }\n",
)];
assert_eq!(
async_subtree_labels("body-nested", files, "crate"),
Vec::<String>::new(),
);
}
#[test]
pub(super) fn dyn_in_public_return_param_and_field_react() {
assert_eq!(
dyn_mod(
"ret",
"pub fn connect() -> Box<dyn crate::Port> { todo!() }\n"
)
.unwrap(),
["dyn crate::Port exposed by fn crate::m::connect"]
);
assert_eq!(
dyn_mod(
"param",
"pub fn drive(x: &dyn crate::Port) { let _ = x; }\n"
)
.unwrap(),
["dyn crate::Port exposed by fn crate::m::drive"]
);
assert_eq!(
dyn_mod("field", "pub struct S { pub p: Box<dyn crate::Port> }\n").unwrap(),
["dyn crate::Port exposed by field crate::m::S::p"]
);
}
#[test]
pub(super) fn dyn_reacts_at_any_nesting_depth() {
assert_eq!(
dyn_mod(
"vec",
"pub fn all() -> Vec<Box<dyn crate::Port>> { todo!() }\n"
)
.unwrap(),
["dyn crate::Port exposed by fn crate::m::all"]
);
assert_eq!(
dyn_mod(
"opt",
"pub fn maybe(x: Option<&dyn crate::Port>) { let _ = x; }\n"
)
.unwrap(),
["dyn crate::Port exposed by fn crate::m::maybe"]
);
assert_eq!(
dyn_mod(
"impl-iter",
"pub fn ports() -> impl Iterator<Item = Box<dyn crate::Port>> { std::iter::empty() }\n"
)
.unwrap(),
["dyn crate::Port exposed by fn crate::m::ports"]
);
}
#[test]
pub(super) fn impl_trait_with_no_dyn_node_is_clean() {
let out = dyn_mod(
"impl-trait",
"pub fn port() -> impl crate::Port { todo!() }\n",
)
.unwrap();
assert!(out.is_empty(), "impl Trait carries no dyn node: {out:?}");
}
#[test]
pub(super) fn dyn_in_const_static_trait_method_assoc_default_and_where_react() {
assert_eq!(
dyn_mod("const", "pub const C: &dyn crate::Port = todo!();\n").unwrap(),
["dyn crate::Port exposed by const crate::m::C"]
);
assert_eq!(
dyn_mod("static", "pub static S: &dyn crate::Port = todo!();\n").unwrap(),
["dyn crate::Port exposed by static crate::m::S"]
);
assert_eq!(
dyn_mod(
"trait-method",
"pub trait Service { fn port(&self) -> Box<dyn crate::Port>; }\n"
)
.unwrap(),
["dyn crate::Port exposed by fn trait crate::m::Service::port"]
);
assert_eq!(
dyn_mod(
"assoc-default",
"pub trait Service { type Out = Box<dyn crate::Port>; }\n"
)
.unwrap(),
["dyn crate::Port exposed by type trait crate::m::Service::Out"]
);
assert_eq!(
dyn_mod(
"where",
"pub fn run<T>() where Box<dyn crate::Port>: Into<T> { todo!() }\n"
)
.unwrap(),
["dyn crate::Port exposed by fn crate::m::run"]
);
}
#[test]
pub(super) fn dyn_in_an_inherent_impl_public_assoc_const_reacts() {
assert_eq!(
dyn_mod(
"inherent-assoc-const",
"pub struct Config;\nimpl Config { pub const DEFAULT: &dyn crate::Port = todo!(); }\n",
)
.unwrap(),
["dyn crate::Port exposed by const <crate::m::Config>::DEFAULT"]
);
}
#[test]
pub(super) fn public_alias_target_reacts_but_named_alias_is_not_expanded() {
assert_eq!(
dyn_mod("alias-item", "pub type Handler = Box<dyn crate::Port>;\n").unwrap(),
["dyn crate::Port exposed by type crate::m::Handler"]
);
let out = dyn_mod(
"alias-named",
"type Handler = Box<dyn crate::Port>;\npub fn make() -> Handler { todo!() }\n",
)
.unwrap();
assert!(
out.is_empty(),
"named private alias is not expanded: {out:?}"
);
}
#[test]
pub(super) fn internal_dyn_is_structurally_clean() {
let out = dyn_mod(
"internal",
"fn helper() -> Box<dyn crate::Port> { todo!() }\nstruct Private { p: Box<dyn crate::Port> }\n",
)
.unwrap();
assert!(out.is_empty(), "internal dyn is never exposed: {out:?}");
}
#[test]
pub(super) fn dyn_with_multiple_bounds_renders_stably() {
assert_eq!(
dyn_mod(
"bounds",
"pub fn f() -> Box<dyn crate::Port + Send> { todo!() }\n"
)
.unwrap(),
["dyn crate::Port + Send exposed by fn crate::m::f"]
);
}
#[test]
pub(super) fn distinct_closures_and_nested_dyns_do_not_collide_into_one_finding() {
let out = dyn_mod(
"closures",
"pub fn a(cb: Box<dyn Fn(i32) -> i32>) { let _ = cb; }\n\
pub fn b(cb: Box<dyn FnMut(String) -> bool>) { let _ = cb; }\n",
)
.unwrap();
assert_eq!(
out,
[
"dyn Fn(i32) -> i32 exposed by fn crate::m::a",
"dyn FnMut(String) -> bool exposed by fn crate::m::b"
]
);
assert_eq!(
dyn_mod(
"nested",
"pub fn f() -> Box<dyn crate::Foo<Box<dyn crate::Bar>>> { todo!() }\n"
)
.unwrap(),
[
"dyn crate::Bar exposed by fn crate::m::f",
"dyn crate::Foo<Box<dyn crate::Bar>> exposed by fn crate::m::f"
]
);
let out = dyn_mod(
"assoc",
"pub fn a(x: Box<dyn Iterator<Item = u8>>) { let _ = x; }\n\
pub fn b(x: Box<dyn Iterator<Item = u16>>) { let _ = x; }\n",
)
.unwrap();
assert_eq!(
out,
[
"dyn Iterator<Item = u16> exposed by fn crate::m::b",
"dyn Iterator<Item = u8> exposed by fn crate::m::a"
]
);
let out = dyn_mod(
"macro-fnptr",
"pub fn a(x: Box<dyn crate::Foo<fn(i32)>>) { let _ = x; }\n\
pub fn b(x: Box<dyn crate::Foo<fn(u8)>>) { let _ = x; }\n",
)
.unwrap();
assert_eq!(
out,
[
"dyn crate::Foo<fn(i32)> exposed by fn crate::m::a",
"dyn crate::Foo<fn(u8)> exposed by fn crate::m::b"
]
);
}
#[test]
pub(super) fn same_shape_at_two_seams_stays_two_findings() {
let out = dyn_mod(
"two-seams",
"pub fn a() -> Box<dyn crate::infra::Port> { todo!() }\n\
pub fn b() -> Box<dyn crate::infra::Port> { todo!() }\n",
)
.unwrap();
assert_eq!(
out,
[
"dyn crate::infra::Port exposed by fn crate::m::a",
"dyn crate::infra::Port exposed by fn crate::m::b"
],
"the same dyn shape at two seams must not collapse to one finding",
);
let out = findings(
"two-seams-sig",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub fn a() -> crate::infra::DbPool { todo!() }\n\
pub fn b() -> crate::infra::DbPool { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::DbPool exposed by fn crate::domain::a",
"crate::infra::DbPool exposed by fn crate::domain::b"
],
"the same forbidden type at two seams must not collapse to one finding",
);
}
#[test]
pub(super) fn the_dyn_trait_builder_carries_anchor_and_severity() {
let b = DynTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_dyn()
.warn()
.because("the core seam is statically dispatched");
assert_eq!(b.crate_package(), "app");
assert_eq!(b.module(), "crate::core");
assert_eq!(b.severity(), Severity::Warn);
assert_eq!(b.reason(), "the core seam is statically dispatched");
}
#[test]
pub(super) fn dyn_unknown_module_is_a_constitution_error() {
let err = dyn_findings(
"unknown",
&[("lib.rs", "pub mod m;\n"), ("m.rs", "// nothing\n")],
"crate::ghost",
)
.unwrap_err();
assert_eq!(err, unknown_module_error("crate::ghost", "x"));
}