use super::*;
use std::path::{Path, PathBuf};
use crate::containment::leaf_of;
use crate::crate_scope::dependency_names;
use crate::errors::{missing_module_file_error, unknown_module_error, unknown_trait_error};
use crate::finding::SemanticFact;
use crate::module_resolve::resolve_module_file;
struct TempSrcTree {
dir: PathBuf,
src: PathBuf,
}
impl TempSrcTree {
fn new(label: &str) -> Self {
let dir = std::env::temp_dir().join(format!("hunyi-{label}-{}", std::process::id()));
let src = dir.join("src");
std::fs::create_dir_all(&src).expect("mkdir src");
Self { dir, src }
}
fn write(&self, rel: &str, contents: &str) -> PathBuf {
let path = self.src.join(rel);
std::fs::create_dir_all(path.parent().expect("file has a parent")).expect("mkdir");
std::fs::write(&path, contents).expect("write source");
path
}
fn write_all(&self, files: &[(&str, &str)]) {
for (rel, contents) in files {
self.write(rel, contents);
}
}
fn src(&self) -> &Path {
&self.src
}
fn root(&self) -> PathBuf {
self.src.join("lib.rs")
}
#[cfg(unix)]
fn symlink(&self, target: impl AsRef<Path>, link_rel_to_src: &str) -> &Self {
std::os::unix::fs::symlink(target, self.src.join(link_rel_to_src)).expect("create symlink");
self
}
fn metadata(&self) -> Value {
serde_json::json!({
"packages": [{
"name": "x",
"dependencies": [],
"targets": [{ "kind": ["lib"], "src_path": self.root().to_string_lossy().into_owned() }],
}],
})
}
}
impl Drop for TempSrcTree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn semantic_findings(
name: &str,
files: &[(&str, &str)],
module: &str,
forbidden: &[&str],
include_trait_impls: bool,
deps: &[&str],
) -> Result<Vec<String>, String> {
let tree = TempSrcTree::new(name);
tree.write_all(files);
let forbidden: Vec<String> = forbidden.iter().map(|s| s.to_string()).collect();
let deps: Vec<String> = deps.iter().map(|s| s.to_string()).collect();
let result = module_findings(
tree.src(),
&tree.root(),
module,
&forbidden,
"x",
include_trait_impls,
&deps,
);
result.map(|facts| {
facts
.into_iter()
.map(|(fact, _file)| fact.to_string())
.collect()
})
}
type ShapeModuleEvaluator =
fn(&Path, &Path, &str, &str) -> Result<Vec<(SemanticFact, PathBuf)>, String>;
type ShapeSubtreeEvaluator =
fn(&Path, &Path, &str, &str) -> Result<Vec<(SemanticFact, String, PathBuf)>, String>;
type OperandModuleEvaluator = fn(
&Path,
&Path,
&str,
&[String],
&str,
&[String],
) -> Result<Vec<(SemanticFact, PathBuf)>, String>;
fn shape_findings(
family: &str,
name: &str,
files: &[(&str, &str)],
module: &str,
evaluate: ShapeModuleEvaluator,
) -> Result<Vec<String>, String> {
let tree = TempSrcTree::new(&format!("{family}-{name}"));
tree.write_all(files);
evaluate(tree.src(), &tree.root(), module, "x").map(|facts| {
facts
.into_iter()
.map(|(fact, _file)| fact.to_string())
.collect()
})
}
fn subtree_findings(
family: &str,
name: &str,
files: &[(&str, &str)],
module: &str,
evaluate: ShapeSubtreeEvaluator,
) -> Result<Vec<(String, String)>, String> {
let tree = TempSrcTree::new(&format!("{family}-sub-{name}"));
tree.write_all(files);
evaluate(tree.src(), &tree.root(), module, "x").map(|facts| {
facts
.into_iter()
.map(|(fact, module, _file)| (fact.to_string(), module))
.collect()
})
}
fn operand_findings(
family: &str,
name: &str,
files: &[(&str, &str)],
module: &str,
forbidden: &[&str],
deps: &[&str],
evaluate: OperandModuleEvaluator,
) -> Result<Vec<String>, String> {
let tree = TempSrcTree::new(&format!("{family}op-{name}"));
tree.write_all(files);
let forbidden: Vec<String> = forbidden.iter().map(|value| value.to_string()).collect();
let deps: Vec<String> = deps.iter().map(|value| value.to_string()).collect();
evaluate(tree.src(), &tree.root(), module, &forbidden, "x", &deps).map(|facts| {
facts
.into_iter()
.map(|(fact, _file)| fact.to_string())
.collect()
})
}
fn findings(
name: &str,
files: &[(&str, &str)],
module: &str,
forbidden: &[&str],
) -> Result<Vec<String>, String> {
semantic_findings(name, files, module, forbidden, false, &[])
}
fn findings_with_deps(
name: &str,
files: &[(&str, &str)],
module: &str,
forbidden: &[&str],
deps: &[&str],
) -> Result<Vec<String>, String> {
semantic_findings(name, files, module, forbidden, false, deps)
}
fn findings_including_trait_impls(
name: &str,
files: &[(&str, &str)],
module: &str,
forbidden: &[&str],
) -> Result<Vec<String>, String> {
semantic_findings(name, files, module, forbidden, true, &[])
}
#[test]
fn hyphenated_dependency_name_is_normalized() {
let package = serde_json::json!({
"dependencies": [
{ "name": "async-trait", "rename": null },
{ "name": "serde_json", "rename": "pkg" },
]
});
let mut names = dependency_names(&package);
names.sort();
assert_eq!(names, vec!["async_trait".to_string(), "pkg".to_string()]);
}
#[test]
fn duplicate_semantic_violations_collapse_keeping_the_more_severe() {
let mk = |sev| {
let finding = crate::finding::SemanticFact::Exposed {
kind: crate::finding::ExposureKind::Signature,
subject: "crate::infra::Db".to_string(),
seam: crate::finding::PublicSeam::FreeFn {
module: "crate::m".to_string(),
name: "f".to_string(),
},
}
.into_finding();
Violation::new(
BoundaryKind::Semantic,
ViolationId::new(
"crate::m",
RuleKey::of(
"tianheng.rule/hunyi/signature-exposure",
[
("forbidden", "[\"crate::infra::Db\"]"),
("including_trait_impls", "false"),
],
),
finding.key().clone(),
),
SIGNATURE_RULE,
finding.text(),
"reason".to_string(),
sev,
)
};
match outcome_from(vec![mk(Severity::Warn), mk(Severity::Enforce)]) {
Outcome::Violations(report) => {
assert_eq!(
report.violations.len(),
1,
"the duplicate id collapses to one: {:?}",
report.violations
);
assert_eq!(
report.violations[0].severity,
Severity::Enforce,
"the more-severe reaction is kept"
);
}
other => panic!("expected Violations, got {other:?}"),
}
}
#[test]
fn leaf_of_strips_a_raw_identifier() {
assert_eq!(leaf_of("crate::a::r#Trait"), "Trait");
assert_eq!(leaf_of("Plain"), "Plain");
}
#[test]
fn bare_extern_reexport_reacts() {
let out = findings_with_deps(
"ext-bare",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use worklane_core::spi::Foo;\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by pub use crate::domain::Foo"]
);
}
#[test]
fn sysroot_reexport_reacts_without_a_declared_dependency() {
let out = findings(
"ext-sysroot",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use std::sync::Mutex;\n"),
],
"crate::domain",
&["std::sync"],
)
.unwrap();
assert_eq!(
out,
["std::sync::Mutex exposed by pub use crate::domain::Mutex"]
);
}
#[test]
fn hyphenated_dependency_reexport_reacts_under_the_underscore_spelling() {
let out = findings_with_deps(
"ext-hyphen",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use async_trait::Thing;\n"),
],
"crate::domain",
&["async_trait"],
&["async_trait"], )
.unwrap();
assert_eq!(
out,
["async_trait::Thing exposed by pub use crate::domain::Thing"]
);
}
#[test]
fn aliased_extern_reexport_is_keyed_by_its_alias() {
let out = findings_with_deps(
"ext-alias",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use worklane_core::spi::Foo as Bar;\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by pub use crate::domain::Bar"]
);
}
#[test]
fn grouped_extern_reexport_reacts_per_leaf() {
let out = findings_with_deps(
"ext-group",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use worklane_core::spi::{Foo, Bar};\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
[
"worklane_core::spi::Bar exposed by pub use crate::domain::Bar",
"worklane_core::spi::Foo exposed by pub use crate::domain::Foo",
]
);
}
#[test]
fn single_segment_crate_root_reexport_reacts() {
let out = findings_with_deps(
"ext-single",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use worklane_core;\n"),
],
"crate::domain",
&["worklane_core"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core exposed by pub use crate::domain::worklane_core"]
);
}
#[test]
fn subtree_extern_reexport_reacts_despite_a_crate_root_module_of_the_same_name() {
let out = findings_with_deps(
"ext-subtree-reexport",
&[
(
"lib.rs",
"pub mod worklane_core { pub struct Foo; }\npub mod domain;\n",
),
("domain.rs", "pub use worklane_core::Foo;\n"),
],
"crate::domain",
&["worklane_core"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::Foo exposed by pub use crate::domain::Foo"]
);
}
#[test]
fn signature_child_module_shadowing_a_dependency_is_no_false_positive() {
let out = findings_with_deps(
"ext-sig-shadow",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub mod worklane_core { pub struct Foo; }\npub fn make() -> worklane_core::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core"],
&["worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn inline_extern_field_type_reacts() {
let out = findings_with_deps(
"ext-field",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Handle { pub inner: worklane_core::spi::Conn }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Conn exposed by field crate::domain::Handle::inner"]
);
}
#[test]
fn inline_extern_signature_return_reacts() {
let out = findings_with_deps(
"ext-sig",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub fn make() -> worklane_core::spi::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
}
#[test]
fn signature_child_module_path_is_no_false_positive() {
let out = findings_with_deps(
"ext-child",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub mod child { pub struct Local; }\npub fn make() -> child::Local { unimplemented!() }\n",
),
],
"crate::domain",
&["child"],
&[],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn facade_chain_of_inline_reexports_to_an_extern_type_reacts() {
let out = findings_with_deps(
"ext-facade",
&[
("lib.rs", "pub mod facade;\npub mod domain;\n"),
("facade.rs", "pub use worklane_core::spi::Foo;\n"),
("domain.rs", "pub use crate::facade::Foo;\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by pub use crate::domain::Foo"]
);
}
#[test]
fn facade_hop_reexporting_a_privately_used_bare_name_is_a_stated_bound() {
let out = findings_with_deps(
"ext-facade-priv",
&[
("lib.rs", "pub mod facade;\npub mod domain;\n"),
("facade.rs", "use worklane_core::spi::Foo;\npub use Foo;\n"),
("domain.rs", "pub use crate::facade::Foo;\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn facade_reaching_a_child_shadowed_extern_head_does_not_react() {
let out = findings_with_deps(
"facade-child-shadow-extern",
&[
("lib.rs", "pub mod a;\npub mod b;\n"),
(
"a.rs",
"pub mod dep { pub mod spi { pub struct Foo; } }\npub use dep::spi::Foo;\n",
),
("b.rs", "pub use crate::a::Foo;\n"),
],
"crate::b",
&["dep::spi"],
&["dep"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn facade_reaching_a_child_shadowed_rename_alias_head_does_not_react() {
let out = findings_with_deps(
"facade-child-shadow-rename",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod a;\npub mod b;\n",
),
(
"a.rs",
"pub mod wc { pub mod spi { pub struct Foo; } }\npub use wc::spi::Foo;\n",
),
("b.rs", "pub use crate::a::Foo;\n"),
],
"crate::b",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn leading_colon_facade_hop_reacts_through_the_closure_despite_a_child_module() {
let out = findings_with_deps(
"facade-leading-colon",
&[
("lib.rs", "pub mod a;\npub mod b;\n"),
(
"a.rs",
"pub mod dep { pub mod spi { pub struct Foo; } }\npub use ::dep::spi::Foo;\n",
),
("b.rs", "pub use crate::a::Foo;\n"),
],
"crate::b",
&["dep::spi"],
&["dep"],
)
.unwrap();
assert_eq!(out, ["dep::spi::Foo exposed by pub use crate::b::Foo"]);
}
#[test]
fn crate_root_mod_does_not_suppress_a_child_facade_reexport_through_the_closure() {
let out = findings_with_deps(
"facade-crate-root-mod",
&[
(
"lib.rs",
"pub mod dep { pub struct Foo; }\npub mod a;\npub mod b;\n",
),
("a.rs", "pub use dep::Foo;\n"),
("b.rs", "pub use crate::a::Foo;\n"),
],
"crate::b",
&["dep"],
&["dep"],
)
.unwrap();
assert_eq!(out, ["dep::Foo exposed by pub use crate::b::Foo"]);
}
#[test]
fn private_alias_in_a_public_seam_reacts() {
let out = findings(
"alias-private",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type H = crate::infra::Db;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra::Db exposed by fn crate::domain::make"]);
}
#[test]
fn a_generic_param_shadowing_a_same_module_alias_is_not_a_finding() {
let out = findings(
"param-shadows-alias",
&[
("lib.rs", "pub mod api;\n"),
(
"api.rs",
"type Secret = crate::infra::Real;\npub fn f<Secret>(x: Secret) {}\n",
),
],
"crate::api",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"a generic param shadowing a same-module alias must not react: {out:?}"
);
let out = findings(
"alias-used-as-type",
&[
("lib.rs", "pub mod api;\n"),
(
"api.rs",
"type Secret = crate::infra::Real;\npub fn g(x: Secret) {}\n",
),
],
"crate::api",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Real exposed by fn crate::api::g"],
"the alias used as a real type still reacts: {out:?}"
);
}
#[test]
fn a_def_site_generic_param_shadowing_a_use_alias_is_not_a_finding() {
let out = findings(
"def-generics-param-shadows-alias",
&[
("lib.rs", "pub mod api;\npub mod infra;\n"),
("infra.rs", "pub struct Secret;\n"),
(
"api.rs",
"use crate::infra::Secret as T;\npub struct S<T, U> where U: AsRef<T> { pub f: U }\n",
),
],
"crate::api",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"a def-site generic param shadowing a use-alias must not react: {out:?}"
);
let out = findings(
"def-generics-real-leak",
&[
("lib.rs", "pub mod api;\npub mod infra;\n"),
("infra.rs", "pub struct Secret;\n"),
(
"api.rs",
"pub struct S<U> where U: AsRef<crate::infra::Secret> { pub f: U }\n",
),
],
"crate::api",
&["crate::infra"],
)
.unwrap();
assert!(
out.iter().any(|f| f.contains("crate::infra::Secret")),
"a real forbidden bound in the def-site where-clause still reacts: {out:?}"
);
}
#[test]
fn an_assoc_type_projection_off_a_shadowing_param_is_not_a_finding() {
let out = findings(
"assoc-projection-shadows-alias",
&[
("lib.rs", "pub mod api;\npub mod infra;\n"),
("infra.rs", "pub struct Secret;\n"),
(
"api.rs",
"use crate::infra::Secret as T;\npub fn f<T: Iterator>() -> T::Item { unimplemented!() }\n",
),
],
"crate::api",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"an assoc-type projection off a shadowing param must not react: {out:?}"
);
let out = findings(
"assoc-projection-real-leak",
&[
("lib.rs", "pub mod api;\npub mod infra;\n"),
("infra.rs", "pub struct Secret;\n"),
(
"api.rs",
"pub fn g() -> crate::infra::Secret { unimplemented!() }\n",
),
],
"crate::api",
&["crate::infra"],
)
.unwrap();
assert!(
out.iter().any(|f| f.contains("crate::infra::Secret")),
"a real forbidden return type still reacts: {out:?}"
);
}
#[test]
fn cross_module_alias_reached_via_use_reacts() {
let out = findings(
"alias-cross",
&[
("lib.rs", "pub mod domain;\npub mod other;\n"),
("other.rs", "pub type H = crate::infra::Db;\n"),
(
"domain.rs",
"use crate::other::H;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra::Db exposed by fn crate::domain::make"]);
}
#[test]
fn alias_through_a_reexport_chain_reacts() {
let out = findings(
"alias-reexport-chain",
&[
("lib.rs", "pub mod domain;\npub mod facade;\n"),
("facade.rs", "pub use crate::infra::Db;\n"),
(
"domain.rs",
"type H = crate::facade::Db;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra::Db exposed by fn crate::domain::make"]);
}
#[test]
fn a_type_reached_through_a_reexported_module_facade_reacts() {
let out = findings(
"module-facade",
&[
(
"lib.rs",
"pub mod real;\npub mod facade;\npub mod domain;\n",
),
("real.rs", "pub mod sub { pub struct Foo; }\n"),
("facade.rs", "pub use crate::real::sub;\n"),
(
"domain.rs",
"use crate::facade::sub;\npub fn f() -> sub::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::real::sub"],
)
.unwrap();
assert_eq!(
out,
["crate::real::sub::Foo exposed by fn crate::domain::f"],
"a type reached through a re-exported module facade must canonicalize and react: {out:?}"
);
}
#[test]
fn a_reexport_whose_key_prefixes_its_value_does_not_diverge() {
use crate::resolve::{ReexportMap, canonicalize_through_reexports};
let mut map = ReexportMap::new();
map.insert("crate::a".to_string(), "crate::a::b".to_string());
let out = canonicalize_through_reexports("crate::a::foo", &map);
assert!(
!out.is_empty(),
"canonicalization must terminate on a key⊂value reexport entry: {out:?}"
);
}
#[test]
fn resolve_self_type_does_not_diverge_on_a_reexport_whose_key_prefixes_its_value() {
use crate::containment::resolve_self_type;
use crate::resolve::{ReexportMap, UseMap};
use std::collections::HashSet;
let self_ty: syn::Type = syn::parse_str("Foo").unwrap();
let uses = UseMap::new();
let aliases = std::collections::HashMap::new();
let mut reexports = ReexportMap::new();
reexports.insert("crate::a::Foo".to_string(), "crate::a::Foo::b".to_string());
let landing = resolve_self_type(
&self_ty,
&uses,
"crate::a",
&aliases,
&reexports,
&HashSet::new(),
);
assert!(
landing.is_some(),
"canonicalization must terminate on a key⊂value reexport entry: {landing:?}"
);
}
#[test]
fn diamond_alias_graph_expansion_terminates_and_memoizes_intermediate_nodes() {
use crate::resolve::{AliasMap, ReexportMap, expand_canonical_paths};
let mut aliases: AliasMap = std::collections::HashMap::new();
let reexports = ReexportMap::new();
aliases.insert(
"crate::A".to_string(),
vec!["crate::B".to_string(), "crate::C".to_string()],
);
aliases.insert("crate::B".to_string(), vec!["crate::D".to_string()]);
aliases.insert("crate::C".to_string(), vec!["crate::D".to_string()]);
aliases.insert("crate::D".to_string(), vec!["crate::Secret".to_string()]);
let res = expand_canonical_paths("crate::A", &aliases, &reexports);
assert_eq!(res, vec!["crate::Secret".to_string()]);
}
#[test]
fn cycle_branch_with_terminal_sibling_preserves_sibling() {
use crate::resolve::{AliasMap, ReexportMap, expand_canonical_paths};
let mut aliases: AliasMap = std::collections::HashMap::new();
let reexports = ReexportMap::new();
aliases.insert(
"crate::A".to_string(),
vec!["crate::B".to_string(), "crate::Secret".to_string()],
);
aliases.insert("crate::B".to_string(), vec!["crate::A".to_string()]);
let res = expand_canonical_paths("crate::A", &aliases, &reexports);
assert!(
res.contains(&"crate::Secret".to_string()),
"sibling Secret target must be preserved even if child branch B cycles back to A: got {res:?}"
);
}
#[test]
fn deep_chain_expansion_reaches_terminal_without_truncation() {
use crate::resolve::{AliasMap, ReexportMap, expand_canonical_paths};
let mut aliases: AliasMap = std::collections::HashMap::new();
let reexports = ReexportMap::new();
for i in 0..99usize {
aliases.insert(format!("crate::N{i}"), vec![format!("crate::N{}", i + 1)]);
}
let res = expand_canonical_paths("crate::N0", &aliases, &reexports);
assert_eq!(
res,
vec!["crate::N99".to_string()],
"full 99-hop chain must resolve to the terminal node N99, got {res:?}"
);
}
#[test]
fn self_growing_reexport_prefix_loop_terminates_without_hanging() {
use crate::resolve::{AliasMap, ReexportMap, expand_canonical_paths};
let aliases: AliasMap = std::collections::HashMap::new();
let mut reexports = ReexportMap::new();
reexports.insert("crate::a".to_string(), "crate::a::b".to_string());
let res = expand_canonical_paths("crate::a::foo", &aliases, &reexports);
assert!(
!res.is_empty(),
"expansion must terminate without hanging on self-growing prefix loops: got {res:?}"
);
}
#[test]
fn a_self_similar_reexport_is_dropped_and_the_real_type_still_reacts() {
let out = findings(
"self-similar-reexport",
&[
(
"lib.rs",
"pub mod sub;\npub mod domain;\npub use self::sub::sub;\n",
),
("sub.rs", "pub fn sub() {}\npub struct Thing;\n"),
("domain.rs", "pub fn f(_x: crate::sub::Thing) {}\n"),
],
"crate::domain",
&["crate::sub"],
)
.unwrap();
assert_eq!(
out,
["crate::sub::Thing exposed by fn crate::domain::f"],
"the real type under crate::sub reacts at its own path, never a fabricated one: {out:?}"
);
}
#[test]
fn alias_of_an_alias_reacts() {
let out = findings(
"alias-of-alias",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type A = crate::infra::Db;\ntype H = crate::domain::A;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra::Db exposed by fn crate::domain::make"]);
}
#[test]
fn alias_to_an_extern_path_reacts() {
let out = findings_with_deps(
"alias-extern",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type H = worklane_core::spi::Foo;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
}
#[test]
fn public_same_module_alias_still_reacts() {
let out = findings(
"alias-public-target",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub type H = crate::infra::Db;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra::Db exposed by type crate::domain::H"]);
}
#[test]
fn complex_target_alias_is_a_stated_bound() {
let out = findings(
"alias-complex-target",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type H = Vec<crate::infra::Db>;\npub fn hidden() -> H { unimplemented!() }\npub fn direct() -> Vec<crate::infra::Db> { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Db exposed by fn crate::domain::direct"]
);
}
#[test]
fn generic_alias_is_a_stated_bound() {
let out = findings(
"alias-generic",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type H<T> = crate::infra::Db;\npub fn make() -> H<u8> { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn a_local_module_shadows_a_dependency_in_an_alias_target() {
let out = findings_with_deps(
"alias-shadow",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub mod serde { pub struct Foo; }\ntype H = serde::Foo;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["serde"],
&["serde"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn alias_to_a_nonforbidden_path_is_clean() {
let out = findings(
"alias-clean",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type H = crate::safe::Thing;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn alias_hidden_and_direct_exposures_share_the_canonical_type() {
let out = findings(
"alias-identity",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type H = crate::infra::Db;\npub fn viaalias() -> H { unimplemented!() }\npub fn direct() -> crate::infra::Db { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::Db exposed by fn crate::domain::direct",
"crate::infra::Db exposed by fn crate::domain::viaalias",
]
);
}
#[test]
fn a_single_segment_alias_named_like_a_dependency_resolves_to_the_local_alias() {
let out = findings_with_deps(
"alias-dep-collision",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type serde = crate::infra::Db;\npub fn make() -> serde { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
&["serde"],
)
.unwrap();
assert_eq!(out, ["crate::infra::Db exposed by fn crate::domain::make"]);
}
#[test]
fn alias_target_reached_via_use_reacts() {
let out = findings(
"alias-use-target",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::infra::Db;\ntype H = Db;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra::Db exposed by fn crate::domain::make"]);
}
#[test]
fn alias_in_a_trait_impl_position_reacts_under_the_opt_in() {
let out = findings_including_trait_impls(
"alias-trait-impl",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type H = crate::infra::DbPool;\npub struct Service;\nimpl From<H> for Service {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by impl From<H> for crate::domain::Service (trait-arg)"]
);
}
#[test]
fn extern_glob_forbidden_root_reacts() {
let out = findings_with_deps(
"ext-glob-hit",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use worklane_core::spi::*;\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi exposed by pub use crate::domain::*"]
);
}
#[test]
fn extern_glob_nonforbidden_root_is_a_stated_bound() {
let out = findings_with_deps(
"ext-glob-miss",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use worklane_core::spi::*;\n"),
],
"crate::domain",
&["worklane_core::other"],
&["worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn foreign_prelude_rename_is_a_stated_bound() {
let out = findings_with_deps(
"ext-prelude",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use worklane_core::prelude::Foo;\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn source_level_crate_root_extern_crate_rename_reacts() {
let out = findings_with_deps(
"ext-externcrate-rename",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
("domain.rs", "pub use wc::spi::Foo;\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by pub use crate::domain::Foo"]
);
}
#[test]
fn source_level_extern_crate_rename_in_a_type_position_reacts() {
let out = findings_with_deps(
"ext-externcrate-rename-type",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"pub fn make() -> wc::spi::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
}
#[test]
fn private_use_of_a_crate_root_extern_rename_reacts() {
let out = findings_with_deps(
"ext-private-use-rename",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"use wc::spi::Foo;\npub fn make() -> Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
}
#[test]
fn private_use_of_a_child_shadowed_rename_alias_does_not_react() {
let out = findings_with_deps(
"ext-private-use-shadowed",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"pub mod wc { pub mod spi { pub struct Foo; } }\nuse wc::spi::Foo;\npub fn make() -> Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn module_scoped_extern_crate_rename_is_a_stated_bound() {
let out = findings_with_deps(
"ext-externcrate-rename-modscoped",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"extern crate worklane_core as wc;\npub fn make() -> wc::spi::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn extern_crate_rename_to_a_nonforbidden_crate_is_clean() {
let out = findings_with_deps(
"ext-externcrate-rename-clean",
&[
("lib.rs", "extern crate serde as s;\npub mod domain;\n"),
("domain.rs", "pub use s::Value;\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["serde", "worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn pub_extern_crate_reacts_as_an_exposure() {
let out = findings_with_deps(
"ext-pub-externcrate",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub extern crate worklane_core;\n"),
],
"crate::domain",
&["worklane_core"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core exposed by pub extern crate worklane_core"]
);
}
#[test]
fn pub_extern_crate_rename_names_the_real_crate() {
let out = findings_with_deps(
"ext-pub-externcrate-rename",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub extern crate worklane_core as wc;\n"),
],
"crate::domain",
&["worklane_core"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core exposed by pub extern crate worklane_core"]
);
}
#[test]
fn private_extern_crate_is_not_an_exposure() {
let out = findings_with_deps(
"ext-priv-externcrate",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "extern crate worklane_core;\n"),
],
"crate::domain",
&["worklane_core"],
&["worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn pub_extern_crate_outside_the_forbidden_set_is_clean() {
let out = findings_with_deps(
"ext-pub-externcrate-clean",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub extern crate serde;\n"),
],
"crate::domain",
&["worklane_core"],
&["serde", "worklane_core"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn a_bare_std_prelude_alias_target_is_not_mis_recorded() {
let out = findings(
"parity-nofp-std",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type H = String;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::domain"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn a_bare_alias_to_a_complex_local_alias_stays_bounded() {
let out = findings(
"parity-complex-intermediate",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type Inner = Vec<crate::infra::Db>;\ntype Public = Inner;\npub fn make() -> Public { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn bare_alias_of_an_alias_reacts() {
let out = findings(
"parity-fn1",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type Inner = crate::infra::Db;\ntype Public = Inner;\npub fn make() -> Public { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra::Db exposed by fn crate::domain::make"]);
}
#[test]
fn bare_alias_of_an_alias_reacts_in_reverse_source_order() {
let out = findings(
"parity-fn1-rev",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type Public = Inner;\ntype Inner = crate::infra::Db;\npub fn make() -> Public { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra::Db exposed by fn crate::domain::make"]);
}
#[test]
fn alias_target_through_a_crate_root_extern_rename_reacts() {
let out = findings_with_deps(
"parity-fn2",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"type H = wc::spi::Foo;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
}
#[test]
fn alias_target_through_extern_rename_reacts_when_alias_precedes_extern_crate() {
let out = findings_with_deps(
"parity-fn2-fwd",
&[
(
"lib.rs",
"type H = wc::spi::Foo;\nextern crate worklane_core as wc;\npub fn make() -> H { unimplemented!() }\n",
),
],
"crate",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(out, ["worklane_core::spi::Foo exposed by fn crate::make"]);
}
#[test]
fn renamed_head_is_not_suppressed_by_a_same_named_child_module_shadow() {
let out = findings_with_deps(
"parity-fn3",
&[
("lib.rs", "extern crate worklane_core as wc;\npub mod domain;\n"),
(
"domain.rs",
"pub mod worklane_core { pub struct Local; }\npub fn make() -> wc::spi::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
}
#[test]
fn facade_reexport_through_an_extern_rename_reacts() {
let out = findings_with_deps(
"parity-facade-rename",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod facade;\npub mod domain;\n",
),
("facade.rs", "pub use wc::spi::Foo;\n"),
("domain.rs", "pub use crate::facade::Foo;\n"),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by pub use crate::domain::Foo"]
);
}
#[test]
fn a_bare_alias_to_a_nonforbidden_local_type_is_clean() {
let out = findings(
"parity-nofp",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Local;\ntype Public = Local;\npub fn make() -> Public { unimplemented!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn trait_impl_exposure_reacts_at_the_trait_arg_position() {
let out = findings_including_trait_impls(
"ti-trait-arg",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl From<crate::infra::DbPool> for Service {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::DbPool exposed by impl From<crate::infra::DbPool> for crate::domain::Service (trait-arg)"
]
);
}
#[test]
fn trait_impl_exposure_reacts_at_the_self_position_bare() {
let out = findings_including_trait_impls(
"ti-self-bare",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub trait Loc {}\nimpl Loc for crate::infra::Forbidden {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Forbidden exposed by impl Loc for crate::infra::Forbidden (self)"]
);
}
#[test]
fn trait_impl_exposure_reacts_at_the_self_position_nested() {
let out = findings_including_trait_impls(
"ti-self-nested",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub trait Loc {}\nimpl Loc for Vec<crate::infra::DbPool> {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out.len(), 1, "one self-position finding expected: {out:?}");
assert!(
out[0].starts_with("crate::infra::DbPool exposed by impl Loc for")
&& out[0].ends_with("(self)"),
"nested Self finding shape: {out:?}"
);
}
#[test]
fn trait_impl_exposure_reacts_at_the_assoc_position() {
let out = findings_including_trait_impls(
"ti-assoc",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl Iterator for Service { type Item = crate::infra::Secret; }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Secret exposed by impl Iterator for crate::domain::Service (assoc Item)"]
);
}
#[test]
fn trait_impl_exposure_reacts_at_the_where_position() {
let out = findings_including_trait_impls(
"ti-where",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl<T: crate::infra::Secret> Loc for Service {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Secret exposed by impl Loc for crate::domain::Service (where T)"]
);
}
#[test]
fn trait_impl_exposure_reacts_at_an_associated_const_type() {
let out = findings_including_trait_impls(
"ti-assoc-const",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl Marker for Service { const MAX: crate::infra::Limit = 0; }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Limit exposed by impl Marker for crate::domain::Service (assoc MAX)"]
);
}
#[test]
fn trait_impl_exposure_reacts_at_a_where_clause_bounded_type() {
let out = findings_including_trait_impls(
"ti-where-lhs",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl Loc for Service where crate::infra::Assoc: Clone {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::Assoc exposed by impl Loc for crate::domain::Service (where crate::infra::Assoc)"
]
);
}
#[test]
fn trait_impl_exposure_reacts_at_a_const_generic_param_type() {
let out = findings_including_trait_impls(
"ti-const-param",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service<const N: usize>;\nimpl<const N: crate::infra::Forbidden> Loc for Service<N> {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Forbidden exposed by impl Loc for crate::domain::Service<N> (where N)"]
);
}
#[test]
fn trait_impl_exposure_reacts_at_a_refined_rpitit_return() {
let out = findings_including_trait_impls(
"ti-rpitit",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl Port for Service { fn items(&self) -> crate::infra::Iter { todo!() } }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::Iter exposed by impl Port for crate::domain::Service (method items return)"
]
);
}
#[test]
fn a_trait_impl_generic_param_shadowing_an_alias_is_not_exposed() {
let out = findings_including_trait_impls(
"ti-param-shadow",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::infra::Forbidden as T;\npub struct Local;\npub trait SomeTrait<X> {}\nimpl<T> SomeTrait<T> for Local {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"an impl generic param must not resolve through a same-named `use … as` alias: {out:?}"
);
}
#[test]
fn trait_impl_method_parameter_is_not_observed_but_the_return_is() {
let out = findings_including_trait_impls(
"ti-param-vs-return",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl Sink for Service { fn put(&self, x: crate::infra::DbPool) -> crate::infra::Iter { todo!() } }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Iter exposed by impl Sink for crate::domain::Service (method put return)"]
);
}
#[test]
fn implementing_a_forbidden_trait_is_a_non_goal() {
let out = findings_including_trait_impls(
"ti-forbidden-trait",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl crate::infra::Sealed for Service {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"implementing a forbidden trait must not react: {out:?}"
);
}
#[test]
fn a_bare_boundary_ignores_trait_impls() {
let out = findings(
"ti-bare-off",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl From<crate::infra::DbPool> for Service {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"a bare boundary must not observe trait impls: {out:?}"
);
}
#[test]
fn two_where_bounds_exposing_the_same_type_stay_distinct() {
let out = findings_including_trait_impls(
"ti-where-distinct",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl<T, U> Loc for Service where T: crate::infra::Secret, U: crate::infra::Secret {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::Secret exposed by impl Loc for crate::domain::Service (where T)",
"crate::infra::Secret exposed by impl Loc for crate::domain::Service (where U)",
]
);
}
#[test]
fn two_positions_exposing_the_same_type_stay_distinct() {
let out = findings_including_trait_impls(
"ti-two-positions",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"impl From<crate::infra::DbPool> for crate::infra::DbPool {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::DbPool exposed by impl From<crate::infra::DbPool> for crate::infra::DbPool (self)",
"crate::infra::DbPool exposed by impl From<crate::infra::DbPool> for crate::infra::DbPool (trait-arg)",
]
);
}
#[test]
fn a_reexported_type_in_a_trait_impl_position_resolves_and_reacts() {
let out = findings_including_trait_impls(
"ti-reexport",
&[
("lib.rs", "pub mod domain;\npub mod facade;\n"),
("facade.rs", "pub use crate::infra::DbPool;\n"),
(
"domain.rs",
"use crate::facade::DbPool;\npub struct Service;\nimpl From<DbPool> for Service {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::DbPool exposed by impl From<DbPool> for crate::domain::Service (trait-arg)"
]
);
}
#[test]
fn a_bare_name_in_a_trait_impl_position_is_not_a_false_positive() {
let out = findings_including_trait_impls(
"ti-bare-name",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service;\nimpl From<DbPool> for Service {}\n",
),
],
"crate::domain",
&["crate::domain"],
)
.unwrap();
assert!(
out.is_empty(),
"a bare name must not resolve against the current module: {out:?}"
);
}
#[test]
fn reexport_of_a_forbidden_type_reacts_by_default() {
let out = findings(
"rx-named",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::infra::DbPool;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by pub use crate::domain::DbPool"]
);
}
#[test]
fn aliased_reexport_is_keyed_by_the_alias() {
let out = findings(
"rx-alias",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::infra::DbPool as Pool;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by pub use crate::domain::Pool"]
);
}
#[test]
fn two_aliases_of_the_same_type_stay_distinct_findings() {
let out = findings(
"rx-two-alias",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub use crate::infra::DbPool;\npub use crate::infra::DbPool as Pool;\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::DbPool exposed by pub use crate::domain::DbPool",
"crate::infra::DbPool exposed by pub use crate::domain::Pool",
]
);
}
#[test]
fn grouped_reexport_reacts_per_leaf() {
let out = findings(
"rx-group",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::infra::{DbPool, Config};\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::Config exposed by pub use crate::domain::Config",
"crate::infra::DbPool exposed by pub use crate::domain::DbPool",
]
);
}
#[test]
fn reexport_through_a_facade_chain_reacts() {
let out = findings(
"rx-facade",
&[
("lib.rs", "pub mod domain;\npub mod facade;\n"),
("facade.rs", "pub use crate::infra::DbPool;\n"),
("domain.rs", "pub use crate::facade::DbPool;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by pub use crate::domain::DbPool"]
);
}
#[test]
fn reexport_through_a_self_group_facade_chain_reacts() {
let out = findings(
"rx-self-facade",
&[
(
"lib.rs",
"pub mod infra;\npub mod facade;\npub mod domain;\n",
),
("infra.rs", "pub struct DbPool;\n"),
("facade.rs", "pub use crate::infra::{self};\n"),
("domain.rs", "pub use crate::facade::infra;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra exposed by pub use crate::domain::infra"]
);
}
#[test]
fn reexport_through_a_renamed_self_facade_chain_reacts_cleanly() {
let out = findings(
"rx-renamed-self-facade",
&[
(
"lib.rs",
"pub mod infra;\npub mod facade;\npub mod domain;\n",
),
("infra.rs", "pub struct DbPool;\n"),
("facade.rs", "pub use crate::infra::{self as fs};\n"),
("domain.rs", "pub use crate::facade::fs;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra exposed by pub use crate::domain::fs"]);
}
#[test]
fn named_whole_module_reexport_reacts() {
let out = findings(
"rx-module",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::infra as fs;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra exposed by pub use crate::domain::fs"]);
}
#[test]
fn self_group_module_reexport_reacts_keyed_by_module_name() {
let out = findings(
"rx-self-group",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::infra::{self, DbPool};\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra exposed by pub use crate::domain::infra",
"crate::infra::DbPool exposed by pub use crate::domain::DbPool",
]
);
}
#[test]
fn reexport_with_raw_identifier_segment_reacts() {
let out = findings(
"rx-raw",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::r#type::DbPool;\n"),
],
"crate::domain",
&["crate::type"],
)
.unwrap();
assert_eq!(
out,
["crate::type::DbPool exposed by pub use crate::domain::DbPool"]
);
}
#[test]
fn renamed_self_module_reexport_reacts_with_correct_type() {
let out = findings(
"rx-self-rename",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::infra::{self as fs};\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra exposed by pub use crate::domain::fs"]);
}
#[test]
fn glob_reexport_with_forbidden_root_reacts() {
let out = findings(
"rx-glob-root",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::infra::*;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(out, ["crate::infra exposed by pub use crate::domain::*"]);
}
#[test]
fn glob_reexport_with_root_deeper_than_forbidden_prefix_reacts() {
let out = findings(
"rx-glob-deep",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::infra::db::*;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::db exposed by pub use crate::domain::*"]
);
}
#[test]
fn sibling_root_glob_does_not_react() {
let out = findings(
"rx-glob-sibling",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::elsewhere::*;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"sibling-root glob is a stated bound: {out:?}"
);
}
#[test]
fn ancestor_root_glob_over_a_deeper_forbidden_prefix_does_not_react() {
let out = findings(
"rx-glob-ancestor",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub use crate::infra::*;\n"),
],
"crate::domain",
&["crate::infra::db"],
)
.unwrap();
assert!(
out.is_empty(),
"ancestor-root glob is a stated bound: {out:?}"
);
}
#[test]
fn restricted_and_private_and_underscore_reexports_do_not_react() {
let out = findings(
"rx-nonpublic",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub(crate) use crate::infra::DbPool;\nuse crate::infra::Config;\npub use crate::infra::Trait as _;\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"pub(crate)/private/`as _` re-exports are not public exposure: {out:?}"
);
}
#[test]
fn forbidden_type_in_a_public_return_is_a_finding() {
let out = findings(
"return",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub fn pool() -> crate::infra::DbPool { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by fn crate::domain::pool"]
);
}
#[test]
fn a_type_used_only_internally_is_not_a_finding() {
let out = findings(
"internal-only",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::infra::DbPool;\nfn helper() -> DbPool { todo!() }\nstruct Private { p: DbPool }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert!(out.is_empty(), "internal use is not exposure: {out:?}");
}
#[test]
fn forbidden_type_in_a_public_field_is_a_finding() {
let out = findings(
"field",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service { pub pool: crate::infra::DbPool, secret: u8 }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by field crate::domain::Service::pool"]
);
}
#[test]
fn a_private_field_does_not_expose() {
let out = findings(
"private-field",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Service { pool: crate::infra::DbPool }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert!(out.is_empty(), "a private field is not public API: {out:?}");
}
#[test]
fn inherent_impl_public_method_exposes() {
let out = findings(
"inherent-impl",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct S;\nimpl S { pub fn pool(&self) -> crate::infra::DbPool { todo!() } }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by fn <crate::domain::S>::pool"]
);
}
#[test]
fn trait_impl_is_out_of_scope() {
let out = findings(
"trait-impl",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct S;\nimpl From<crate::infra::DbPool> for S { fn from(_: crate::infra::DbPool) -> S { S } }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"trait impls are a documented bound: {out:?}"
);
}
#[test]
fn a_renamed_import_resolves_and_reacts() {
let out = findings(
"renamed",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::infra::DbPool as Pool;\npub fn pool() -> Pool { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by fn crate::domain::pool"]
);
}
#[test]
fn a_use_imported_type_resolves_via_its_head() {
let out = findings(
"use-head",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::infra;\npub fn pool() -> infra::DbPool { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by fn crate::domain::pool"]
);
}
#[test]
fn a_glob_import_is_a_documented_bound() {
let out = findings(
"glob",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::infra::*;\npub fn pool() -> DbPool { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert!(
out.is_empty(),
"glob is out of scope, not silently matched: {out:?}"
);
}
#[test]
fn a_forbidden_trait_in_a_generic_bound_is_a_finding() {
let out = findings(
"bound",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub fn run<T: crate::infra::Pooled>(_: T) {}\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Pooled exposed by fn crate::domain::run"]
);
}
#[test]
fn a_module_prefix_matches_beneath_but_not_a_sibling() {
let out = findings(
"prefix",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub fn a() -> crate::infra::db::Pool { todo!() }\npub fn b() -> crate::infrastructure::Helper { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::db::Pool exposed by fn crate::domain::a"],
"sibling must not match: {out:?}"
);
}
#[test]
fn a_nested_generic_argument_is_observed() {
let out = findings(
"nested",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub fn pools() -> Vec<crate::infra::DbPool> { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by fn crate::domain::pools"]
);
}
#[test]
fn an_unknown_module_is_a_constitution_error() {
let err = findings(
"unknown",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "// nothing\n"),
],
"crate::ghost",
&["crate::infra"],
)
.unwrap_err();
assert_eq!(err, unknown_module_error("crate::ghost", "x"));
}
#[test]
fn a_mod_rs_backed_module_resolves() {
let out = findings(
"modrs",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain/mod.rs",
"pub fn pool() -> crate::infra::DbPool { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by fn crate::domain::pool"]
);
}
#[test]
fn an_inline_module_resolves() {
let out = findings(
"inline",
&[(
"lib.rs",
"pub mod domain { pub fn pool() -> crate::infra::DbPool { todo!() } }\n",
)],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by fn crate::domain::pool"]
);
}
#[test]
fn a_forbidden_type_via_a_pub_use_facade_resolves_and_reacts() {
let out = findings(
"reexport-exposure",
&[
("lib.rs", "pub mod domain;\npub mod facade;\n"),
("facade.rs", "pub use crate::infra::DbPool;\n"),
(
"domain.rs",
"use crate::facade::DbPool;\npub fn pool() -> DbPool { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by fn crate::domain::pool"],
"a forbidden type reached through a pub use facade must react"
);
}
#[test]
fn a_forbidden_type_via_a_super_relative_use_resolves_and_reacts() {
let out = findings(
"super-exposure",
&[
("lib.rs", "pub mod domain;\npub mod infra;\n"),
("infra.rs", "pub struct DbPool;\n"),
(
"domain.rs",
"use super::infra::DbPool;\npub fn pool() -> DbPool { todo!() }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::DbPool exposed by fn crate::domain::pool"]
);
}
fn locality_findings(
name: &str,
files: &[(&str, &str)],
trait_path: &str,
allowed: &[&str],
) -> Result<Vec<String>, String> {
let tree = TempSrcTree::new(&format!("loc-{name}"));
tree.write_all(files);
let allowed: Vec<String> = allowed.iter().map(|s| s.to_string()).collect();
let result = trait_impl_findings(tree.src(), &tree.root(), trait_path, &allowed, "x");
result.map(|v| {
v.into_iter()
.map(|(finding, _module, _file)| finding.to_string())
.collect()
})
}
#[test]
fn an_impl_outside_the_allowed_location_is_a_finding() {
let out = locality_findings(
"outside",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::domain (impl crate::command::Command for crate::domain::Foo)"]
);
}
#[test]
fn two_misplaced_impls_do_not_dedup_collapse_when_a_blanket_impls_param_shadows_an_alias() {
let out = locality_findings(
"owner-collapse-blanket-and-direct",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use crate::command::Command;\nuse crate::domain::sub::Foo as T;\npub mod sub { pub struct Foo; }\nimpl<T> Command for T {}\nimpl Command for crate::domain::sub::Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out.len(),
2,
"both the blanket impl (its own param T) and the direct impl on Foo are genuinely distinct \
misplaced-impl violations and must not dedup-collapse into one: {out:?}"
);
}
#[test]
fn a_cfg_dual_declared_module_backed_by_one_file_does_not_duplicate_its_impl_finding() {
let out = locality_findings(
"cfg-dual-same-file",
&[
(
"lib.rs",
"pub trait Command {}\npub struct Arr<const N: usize>;\n\
#[cfg(feature = \"u\")]\npub mod foo;\n#[cfg(feature = \"w\")]\npub mod foo;\n",
),
("foo.rs", "impl crate::Command for crate::Arr<2> {}\n"),
],
"crate::Command",
&["crate::allowed_elsewhere"],
)
.unwrap();
assert_eq!(
out.len(),
1,
"one real impl, backed by one real file under either #[cfg] arm, must be one finding: {out:?}"
);
}
#[test]
fn an_impl_inside_the_allowed_location_is_clean() {
let out = locality_findings(
"inside",
&[
("lib.rs", "pub mod command;\npub mod commands;\n"),
("command.rs", "pub trait Command {}\n"),
(
"commands.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(
out.is_empty(),
"an impl in the allowed location is clean: {out:?}"
);
}
#[test]
fn a_nested_module_beneath_the_allowed_prefix_is_clean() {
let out = locality_findings(
"nested-allowed",
&[
("lib.rs", "pub mod command;\npub mod commands;\n"),
("command.rs", "pub trait Command {}\n"),
("commands.rs", "pub mod greet;\n"),
(
"commands/greet.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(
out.is_empty(),
"beneath an allowed prefix is clean: {out:?}"
);
}
#[test]
fn a_prefix_colliding_sibling_location_is_not_allowed() {
let out = locality_findings(
"sibling",
&[
("lib.rs", "pub mod command;\npub mod commandeer;\n"),
("command.rs", "pub trait Command {}\n"),
(
"commandeer.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::commandeer (impl crate::command::Command for crate::commandeer::Foo)"],
"a sibling of the allowed prefix is not allowed"
);
}
#[test]
fn an_impl_in_any_of_several_allowed_locations_is_clean() {
let out = locality_findings(
"multi-allowed",
&[
("lib.rs", "pub mod command;\npub mod builtins;\n"),
("command.rs", "pub trait Command {}\n"),
(
"builtins.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands", "crate::builtins"],
)
.unwrap();
assert!(out.is_empty(), "any one allowed location suffices: {out:?}");
}
#[test]
fn a_bare_same_module_trait_name_reacts() {
let out = locality_findings(
"bare-same-module",
&[
("lib.rs", "pub mod command;\n"),
(
"command.rs",
"pub trait Command {}\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::command (impl crate::command::Command for crate::command::Foo)"]
);
}
#[test]
fn a_renamed_trait_import_reacts() {
let out = locality_findings(
"renamed-trait",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use crate::command::Command as Cmd;\npub struct Foo;\nimpl Cmd for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::domain (impl crate::command::Command for crate::domain::Foo)"]
);
}
#[test]
fn a_super_relative_trait_import_reacts() {
let out = locality_findings(
"super-trait",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use super::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::domain (impl crate::command::Command for crate::domain::Foo)"]
);
}
#[test]
fn a_cfg_gated_module_with_no_file_is_skipped_not_errored() {
let out = locality_findings(
"cfg-absent-mod",
&[
(
"lib.rs",
"pub mod command;\n#[cfg(feature = \"never\")]\npub mod optional;\n",
),
("command.rs", "pub trait Command {}\n"),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(
out.is_empty(),
"a cfg-gated absent module is skipped: {out:?}"
);
}
#[test]
fn a_reexported_trait_path_reacts() {
let out = locality_findings(
"reexport-impl",
&[
(
"lib.rs",
"pub mod command;\npub mod facade;\npub mod domain;\n",
),
("command.rs", "pub trait Command {}\n"),
("facade.rs", "pub use crate::command::Command;\n"),
(
"domain.rs",
"use crate::facade::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::domain (impl crate::command::Command for crate::domain::Foo)"]
);
}
#[test]
fn an_anchor_named_at_a_reexport_path_resolves_not_a_constitution_error() {
let out = locality_findings(
"reexport-anchor",
&[
(
"lib.rs",
"pub mod command;\npub mod facade;\npub mod domain;\n",
),
("command.rs", "pub trait Command {}\n"),
("facade.rs", "pub use crate::command::Command;\n"),
(
"domain.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::facade::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::domain (impl crate::command::Command for crate::domain::Foo)"]
);
}
#[test]
fn an_unresolvable_trait_anchor_is_a_constitution_error() {
let err = locality_findings(
"ghost-trait",
&[
("lib.rs", "pub mod command;\n"),
("command.rs", "pub trait Command {}\n"),
],
"crate::command::Ghost",
&["crate::commands"],
)
.unwrap_err();
assert_eq!(err, unknown_trait_error("crate::command::Ghost", "x"));
}
#[test]
fn a_non_anchored_traits_impl_is_ignored() {
let out = locality_findings(
"other-trait",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\npub trait Other {}\n"),
(
"domain.rs",
"use crate::command::Other;\npub struct Foo;\nimpl Other for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(out.is_empty(), "only the anchored trait reacts: {out:?}");
}
#[test]
fn an_inline_module_impl_is_located() {
let out = locality_findings(
"inline-impl",
&[
(
"lib.rs",
"pub mod command;\npub mod domain { use crate::command::Command; pub struct Foo; impl Command for Foo {} }\n",
),
("command.rs", "pub trait Command {}\n"),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::domain (impl crate::command::Command for crate::domain::Foo)"]
);
}
#[test]
fn a_glob_imported_trait_is_a_documented_bound() {
let out = locality_findings(
"glob-trait",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use crate::command::*;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(
out.is_empty(),
"a glob-imported trait is out of scope, not silently matched: {out:?}"
);
}
#[test]
fn an_unconditional_path_remapped_module_is_followed_and_its_impl_reacts() {
let out = locality_findings(
"path-remapped",
&[
(
"lib.rs",
"pub mod command;\n#[path = \"weird.rs\"]\npub mod domain;\n",
),
("command.rs", "pub trait Command {}\n"),
(
"weird.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::domain (impl crate::command::Command for crate::domain::Foo)"],
"the impl in the #[path]-relocated module is followed and reacts: {out:?}"
);
}
#[test]
fn a_cfg_attr_remapped_module_is_a_documented_bound() {
let out = locality_findings(
"cfg-attr-remapped",
&[
(
"lib.rs",
"pub mod command;\n#[cfg_attr(windows, path = \"weird.rs\")]\npub mod domain;\n",
),
("command.rs", "pub trait Command {}\n"),
(
"weird.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(
out.is_empty(),
"a cfg_attr-remapped module is out of scope, same as a direct #[path]: {out:?}"
);
let nested = locality_findings(
"cfg-attr-nested-remapped",
&[
(
"lib.rs",
"pub mod command;\n#[cfg_attr(a, cfg_attr(b, path = \"weird.rs\"))]\npub mod domain;\n",
),
("command.rs", "pub trait Command {}\n"),
(
"weird.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(
nested.is_empty(),
"a nested cfg_attr remap is out of scope: {nested:?}"
);
}
#[test]
fn a_cfg_attr_without_a_path_meta_is_scanned_normally() {
let out = locality_findings(
"cfg-attr-no-path",
&[
(
"lib.rs",
"pub mod command;\n#[cfg_attr(test, allow(dead_code))]\npub mod domain;\n",
),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(
!out.is_empty(),
"a cfg_attr without a path meta is a normal module and must be scanned: {out:?}"
);
let bare = locality_findings(
"cfg-attr-bare-path",
&[
(
"lib.rs",
"pub mod command;\n#[cfg_attr(test, path)]\npub mod domain;\n",
),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use crate::command::Command;\npub struct Foo;\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(
!bare.is_empty(),
"a bare `path` meta (not `path = \"…\"`) is not a remap; the module is scanned: {bare:?}"
);
}
#[test]
fn two_impls_in_one_module_are_distinct_findings_by_self_type() {
let out = locality_findings(
"distinct-self",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use crate::command::Command;\npub struct A;\npub struct B;\nimpl Command for A {}\nimpl Command for B {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
[
"crate::domain (impl crate::command::Command for crate::domain::A)",
"crate::domain (impl crate::command::Command for crate::domain::B)"
]
);
}
#[test]
fn const_generic_expr_self_types_fail_loud_without_positional_identity() {
let error = findings(
"const-generic-expr",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Arr<const N: usize>(u8);\n\
impl Arr<{ 1 + 1 }> { pub fn a(&self) -> crate::infra::T { todo!() } }\n\
impl Arr<{ 2 + 2 }> { pub fn a(&self) -> crate::infra::T { todo!() } }\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap_err();
assert!(error.contains("stable structural label"), "{error}");
assert!(!error.contains("_#"), "{error}");
}
#[test]
fn owner_is_canonical_across_written_forms() {
let out = findings(
"canonical-forms",
&[
("lib.rs", "pub mod m;\n"),
(
"m.rs",
"pub struct Foo;\n\
impl Foo { pub fn a(&self) -> crate::infra::T { todo!() } }\n\
impl crate::m::Foo { pub fn b(&self) -> crate::infra::T { todo!() } }\n",
),
],
"crate::m",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::T exposed by fn <crate::m::Foo>::a",
"crate::infra::T exposed by fn <crate::m::Foo>::b",
],
"both written forms of the same self type render the identical canonical owner",
);
}
#[test]
fn a_cfg_gated_impl_is_observed_as_written() {
let out = locality_findings(
"cfg-gated",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use crate::command::Command;\npub struct Foo;\n#[cfg(feature = \"never\")]\nimpl Command for Foo {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
["crate::domain (impl crate::command::Command for crate::domain::Foo)"]
);
}
#[test]
fn a_macro_generated_impl_is_a_documented_bound() {
let out = locality_findings(
"macro-impl",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\n"),
("domain.rs", "make_impl!(Foo);\n"),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert!(
out.is_empty(),
"a macro-generated impl is out of scope, not silently matched: {out:?}"
);
}
#[test]
fn the_builder_carries_severity() {
let warn = TraitImplBoundary::in_crate("app")
.trait_("crate::command::Command")
.only_implemented_in("crate::commands")
.warn()
.because("advisory first");
assert_eq!(warn.severity(), Severity::Warn);
let enforce = TraitImplBoundary::in_crate("app")
.trait_("crate::command::Command")
.only_implemented_in("crate::commands")
.because("enforced");
assert_eq!(enforce.severity(), Severity::Enforce);
}
#[test]
fn every_hunyi_rule_family_has_exact_semantic_identity() {
fn assert_rule(rule: xuanji::RuleKey, expected_type: &str, expected_fields: &[(&str, &str)]) {
assert_eq!(rule.rule_type(), expected_type);
assert_eq!(rule.fields().collect::<Vec<_>>(), expected_fields);
}
let signature = SemanticBoundary::in_crate("x")
.module("crate::api")
.must_not_expose("r#crate::infra")
.and_not_expose("crate::storage")
.including_trait_impls()
.because("presentation only");
assert_rule(
signature.rule_key(),
"tianheng.rule/hunyi/signature-exposure",
&[
("forbidden", "[\"crate::infra\",\"crate::storage\"]"),
("including_trait_impls", "true"),
],
);
let dyn_trait = DynTraitBoundary::in_crate("x")
.module("crate::api")
.must_not_expose_dyn_of(["crate::Port", "crate::Other"])
.because("r");
assert_rule(
dyn_trait.rule_key(),
"tianheng.rule/hunyi/dyn-trait-exposure",
&[("forbidden_operands", "[\"crate::Other\",\"crate::Port\"]")],
);
let impl_trait = ImplTraitBoundary::in_crate("x")
.module("crate::api")
.must_not_expose_impl_trait_of(["crate::Port"])
.including_submodules()
.because("r");
assert_rule(
impl_trait.rule_key(),
"tianheng.rule/hunyi/impl-trait-exposure",
&[("forbidden_operands", "[\"crate::Port\"]")],
);
let locality = TraitImplBoundary::in_crate("x")
.trait_("r#crate::Port")
.only_implemented_in("crate::adapter")
.and_in("crate::infra")
.because("r");
assert_rule(
locality.rule_key(),
"tianheng.rule/hunyi/trait-impl-locality",
&[
("allowed_locations", "[\"crate::adapter\",\"crate::infra\"]"),
("trait", "crate::Port"),
],
);
let marker = ForbiddenMarkerBoundary::in_crate("x")
.module("crate::domain")
.must_not_acquire("serde::Serialize")
.and_not_acquire("serde::Deserialize")
.because("r");
assert_rule(
marker.rule_key(),
"tianheng.rule/hunyi/forbidden-marker",
&[("forbidden", "[\"serde::Deserialize\",\"serde::Serialize\"]")],
);
let visibility = VisibilityBoundary::in_crate("x")
.module("crate::internal")
.max_visibility(VisibilityCeiling::Super)
.because("r");
assert_rule(
visibility.rule_key(),
"tianheng.rule/hunyi/visibility-ceiling",
&[("ceiling", "super")],
);
let async_exposure = AsyncExposureBoundary::in_crate("x")
.module("crate::core")
.must_not_expose_async_fn()
.including_submodules()
.because("r");
assert_rule(
async_exposure.rule_key(),
"tianheng.rule/hunyi/async-exposure",
&[],
);
let unsafe_confinement = UnsafeBoundary::in_crate("x")
.only_under(["crate::ffi", "crate::platform"])
.because("r");
assert_rule(
unsafe_confinement.rule_key(),
"tianheng.rule/hunyi/unsafe-confinement",
&[("allowed", "[\"crate::ffi\",\"crate::platform\"]")],
);
}
#[test]
fn hunyi_rule_identity_is_set_order_stable_and_parameter_sensitive() {
let left = SemanticBoundary::in_crate("x")
.module("crate::api")
.must_not_expose("crate::infra")
.and_not_expose("crate::storage")
.because("first wording");
let reordered = SemanticBoundary::in_crate("other")
.module("crate::elsewhere")
.must_not_expose("crate::storage")
.and_not_expose("crate::infra")
.and_not_expose("crate::infra")
.warn()
.because("different wording")
.with_anchor("GOV-1");
let expanded = SemanticBoundary::in_crate("x")
.module("crate::api")
.must_not_expose("crate::infra")
.and_not_expose("crate::storage")
.and_not_expose("crate::transport")
.because("first wording");
let deeper = SemanticBoundary::in_crate("x")
.module("crate::api")
.must_not_expose("crate::infra")
.and_not_expose("crate::storage")
.including_trait_impls()
.because("first wording");
assert_eq!(left.rule_key(), reordered.rule_key());
assert_ne!(left.rule_key(), expanded.rule_key());
assert_ne!(left.rule_key(), deeper.rule_key());
}
fn unsafe_labels(
name: &str,
files: &[(&str, &str)],
allowed: &[&str],
) -> Result<Vec<String>, String> {
let tree = TempSrcTree::new(&format!("unsafe-{name}"));
tree.write_all(files);
let allowed: Vec<String> = allowed.iter().map(|a| a.to_string()).collect();
unsafe_findings(tree.src(), &tree.root(), &allowed, "x").map(|fs| {
fs.into_iter()
.map(|(finding, _, _)| finding.to_string())
.collect()
})
}
fn unsafe_keys(name: &str, source: &str) -> Result<Vec<StructuredFactIdentity>, String> {
let tree = TempSrcTree::new(&format!("unsafe-keys-{name}"));
tree.write_all(&[("lib.rs", "pub mod net;\n"), ("net.rs", source)]);
unsafe_findings(tree.src(), &tree.root(), &["crate::ffi".to_string()], "x").map(|findings| {
findings
.into_iter()
.map(|(fact, _, _)| fact.into_finding().key().clone())
.collect()
})
}
#[test]
fn unsafe_identity_survives_reorder_and_unrelated_insertion() {
let before = unsafe_keys(
"reorder-before",
"pub struct Api;\nunsafe impl Send for Api {}\n",
)
.unwrap();
let after = unsafe_keys(
"reorder-after",
"pub const UNRELATED: usize = 1;\npub struct Api;\nunsafe impl Send for Api {}\n",
)
.unwrap();
assert_eq!(before, after);
}
#[test]
fn unrenderable_unsafe_owner_fails_loud_without_an_ordinal_identity() {
let error = unsafe_keys(
"unrenderable-owner",
"pub struct Arr<const N: usize>;\npub const N: usize = 1;\nunsafe impl Send for Arr<{ N + 1 }> {}\n",
)
.unwrap_err();
assert!(error.contains("without a positional fallback"), "{error}");
assert!(!error.contains("_#"), "{error}");
}
#[test]
fn unsafe_production_violation_separates_target_rule_and_fact_roles() {
let (metadata, _fixture) = fixture_metadata(
"unsafe-identity",
&[
("lib.rs", "pub mod net;\npub mod ffi;\n"),
("net.rs", "pub unsafe fn decode() {}\n"),
("ffi.rs", ""),
],
);
let boundary = UnsafeBoundary::in_crate("x")
.only_under(["crate::raw", "crate::ffi"])
.because("unsafe stays behind the audited adapter");
let mut violations = Vec::new();
check_unsafe_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(violations.len(), 1);
let id = violations[0].id();
assert_eq!(id.target(), "x");
let rule = id.rule_key();
assert_eq!(rule.rule_type(), "tianheng.rule/hunyi/unsafe-confinement");
assert_eq!(
rule.fields().collect::<Vec<_>>(),
vec![("allowed", "[\"crate::ffi\",\"crate::raw\"]")]
);
let fact = id.fact();
assert_eq!(fact.fact_type(), "tianheng.fact/hunyi/unsafe-site");
assert_eq!(fact.shape(), "unsafe-free-function");
assert_eq!(
fact.fields().collect::<Vec<_>>(),
vec![("module", "crate::net"), ("name", "decode")]
);
}
#[test]
fn unsafe_block_outside_subtree_reacts() {
let out = unsafe_labels(
"block",
&[
("lib.rs", "pub mod ffi;\npub mod net;\n"),
(
"ffi.rs",
"pub fn ok() { unsafe { core::ptr::null::<u8>(); } }\n",
),
(
"net.rs",
"pub fn f() { unsafe { core::ptr::null::<u8>(); } }\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
["unsafe block in crate::net"],
"a block outside the subtree reacts; one under it is clean: {out:?}"
);
}
#[test]
fn unsafe_fn_impl_trait_extern_outside_react() {
let out = unsafe_labels(
"kinds",
&[
("lib.rs", "pub mod ffi;\npub mod net;\n"),
("ffi.rs", "\n"),
(
"net.rs",
"pub unsafe trait Zeroable {}\npub unsafe fn decode() {}\nunsafe impl Zeroable for u8 {}\nunsafe extern \"C\" { fn c(); }\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
[
"unsafe extern block in crate::net",
"unsafe fn decode in crate::net",
"unsafe impl Zeroable for u8 in crate::net",
"unsafe trait Zeroable in crate::net",
],
"every unsafe-keyword site outside the subtree reacts: {out:?}"
);
}
#[test]
fn unsafe_under_the_subtree_is_clean() {
let out = unsafe_labels(
"clean",
&[
("lib.rs", "pub mod ffi;\n"),
("ffi.rs", "pub mod raw;\npub unsafe fn a() {}\n"),
(
"ffi/raw.rs",
"pub fn b() { unsafe { core::ptr::null::<u8>(); } }\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert!(
out.is_empty(),
"unsafe at the subtree and beneath it is clean: {out:?}"
);
}
#[test]
fn empty_allowed_set_is_a_constitution_error() {
let err = unsafe_labels("empty", &[("lib.rs", "pub fn f() { unsafe {} }\n")], &[]).unwrap_err();
assert!(
err.contains("forbid(unsafe_code)"),
"empty only_under points at #![forbid(unsafe_code)]: {err}"
);
}
#[test]
fn crate_root_allowed_set_is_a_constitution_error() {
let err = unsafe_labels("root", &[("lib.rs", "pub fn f() {}\n")], &["crate"]).unwrap_err();
assert!(err.contains("crate root"), "{err}");
}
#[test]
fn unsafe_blocks_dedup_per_module() {
let out = unsafe_labels(
"dedup",
&[
("lib.rs", "pub mod net;\n"),
(
"net.rs",
"pub fn f() { unsafe {} unsafe {} }\npub fn g() { unsafe {} }\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
["unsafe block in crate::net"],
"N blocks in one module dedup to one stable finding: {out:?}"
);
}
#[test]
fn two_unsafe_impls_of_different_traits_stay_distinct() {
let out = unsafe_labels(
"impls",
&[
("lib.rs", "pub mod net;\n"),
(
"net.rs",
"pub struct Foo;\nunsafe impl Send for Foo {}\nunsafe impl Sync for Foo {}\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
[
"unsafe impl Send for Foo in crate::net",
"unsafe impl Sync for Foo in crate::net",
],
"the trait is in the finding, so two unsafe impls do not collapse: {out:?}"
);
}
#[test]
fn two_unsafe_impls_of_one_trait_for_different_types_stay_distinct() {
let out = unsafe_labels(
"impls-same-trait",
&[
("lib.rs", "pub mod net;\n"),
(
"net.rs",
"pub struct Foo;\npub struct Bar;\nunsafe impl Send for Foo {}\nunsafe impl Send for Bar {}\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
[
"unsafe impl Send for Bar in crate::net",
"unsafe impl Send for Foo in crate::net",
],
"the self type is in the finding, so same-trait impls for different types do not collapse: {out:?}"
);
}
#[test]
fn two_same_named_unsafe_fns_on_different_owners_stay_distinct() {
let out = unsafe_labels(
"unsafe-fns-same-name",
&[
("lib.rs", "pub mod net;\n"),
(
"net.rs",
"pub struct Foo;\npub struct Bar;\nimpl Foo { unsafe fn m(&self) {} }\nimpl Bar { unsafe fn m(&self) {} }\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
[
"unsafe fn Bar::m in crate::net",
"unsafe fn Foo::m in crate::net",
],
"same-named unsafe fns on different owners must not collapse: {out:?}"
);
}
#[test]
fn two_same_named_unsafe_trait_fns_stay_distinct() {
let out = unsafe_labels(
"unsafe-trait-fns",
&[
("lib.rs", "pub mod net;\n"),
(
"net.rs",
"pub trait A { unsafe fn m(&self); }\npub trait B { unsafe fn m(&self); }\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
[
"unsafe fn A::m in crate::net",
"unsafe fn B::m in crate::net"
],
"trait-declared unsafe fns must be qualified by their trait: {out:?}"
);
}
#[test]
fn trait_impl_unsafe_fn_stays_distinct_from_inherent_and_other_traits() {
let out = unsafe_labels(
"unsafe-fns-trait-impl",
&[
("lib.rs", "pub mod net;\n"),
(
"net.rs",
"pub struct Foo;\npub trait A { fn m(&self); }\npub trait B { fn m(&self); }\n\
impl Foo { unsafe fn m(&self) {} }\n\
impl A for Foo { unsafe fn m(&self) {} }\n\
impl B for Foo { unsafe fn m(&self) {} }\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
[
"unsafe fn <A for Foo>::m in crate::net",
"unsafe fn <B for Foo>::m in crate::net",
"unsafe fn Foo::m in crate::net",
],
"a trait-impl unsafe fn must be qualified by <trait for self>, distinct from the inherent \
method and other trait impls on the same type: {out:?}"
);
}
#[test]
fn unsafe_in_an_unconditional_path_remapped_module_reacts() {
let out = unsafe_labels(
"path-remap-unsafe",
&[
("lib.rs", "#[path = \"relocated.rs\"]\npub mod net;\n"),
("relocated.rs", "pub unsafe fn poke() {}\n"),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
["unsafe fn poke in crate::net"],
"unsafe in an unconditional #[path] module is followed and reacts: {out:?}"
);
}
#[test]
fn path_in_a_non_mod_rs_file_resolves_from_the_containing_files_own_dir() {
let out = unsafe_labels(
"path-nonmodrs",
&[
("lib.rs", "pub mod foo;\n"),
("foo.rs", "#[path = \"bar.rs\"]\npub mod bar;\n"),
("bar.rs", "pub unsafe fn poke() {}\n"),
("foo/bar.rs", "pub fn decoy() {}\n"),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
["unsafe fn poke in crate::foo::bar"],
"a #[path] inside a non-mod.rs file resolves from that file's own dir (src/bar.rs), not \
src/foo/bar.rs: {out:?}"
);
}
#[test]
fn path_nested_in_an_inline_block_resolves_from_the_accumulated_dir() {
let out = unsafe_labels(
"path-inline-modrs",
&[
(
"lib.rs",
"pub mod inline { #[path = \"other.rs\"] pub mod inner; }\n",
),
("inline/other.rs", "pub unsafe fn poke() {}\n"),
("other.rs", "pub fn decoy() {}\n"),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
["unsafe fn poke in crate::inline::inner"],
"a #[path] nested in an inline block resolves from <file_dir>/inline (src/inline/other.rs), \
not the src/other.rs orphan: {out:?}"
);
}
#[test]
fn path_nested_in_an_inline_block_in_a_non_mod_rs_file_accumulates_both_components() {
let out = unsafe_labels(
"path-inline-nonmodrs",
&[
("lib.rs", "pub mod bar;\n"),
(
"bar.rs",
"pub mod inline { #[path = \"p.rs\"] pub mod inner; }\n",
),
("bar/inline/p.rs", "pub unsafe fn poke() {}\n"),
("p.rs", "pub fn decoy() {}\n"),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
["unsafe fn poke in crate::bar::inline::inner"],
"the #[path] base accumulates bar/ and inline/ (src/bar/inline/p.rs), not src/p.rs: {out:?}"
);
}
#[test]
fn two_modules_sharing_one_path_target_are_not_a_false_cycle() {
let out = unsafe_labels(
"path-shared-target",
&[
(
"lib.rs",
"#[path = \"shared.rs\"]\npub mod a;\n#[path = \"shared.rs\"]\npub mod b;\n",
),
("shared.rs", "pub unsafe fn poke() {}\n"),
],
&["crate::ffi"],
)
.expect("two modules sharing one #[path] target is not a cycle (rustc compiles it)");
assert_eq!(
out,
["unsafe fn poke in crate::a", "unsafe fn poke in crate::b",],
"a file shared by two #[path] declarations reacts under both module paths, no false cycle: \
{out:?}"
);
}
#[test]
fn a_conventional_module_and_a_path_alias_to_it_are_not_a_false_cycle() {
let out = unsafe_labels(
"path-alias-conventional",
&[
(
"lib.rs",
"pub mod foo;\n#[path = \"foo.rs\"]\npub mod bar;\n",
),
("foo.rs", "pub unsafe fn poke() {}\n"),
],
&["crate::ffi"],
)
.expect("a conventional module and a #[path] alias to the same file is not a cycle");
assert_eq!(
out,
[
"unsafe fn poke in crate::bar",
"unsafe fn poke in crate::foo",
],
"one file reached conventionally and via a #[path] alias reacts under both paths: {out:?}"
);
}
#[test]
fn unsafe_in_a_body_nested_mod_reacts() {
let out = unsafe_labels(
"body-nested",
&[
("lib.rs", "pub mod net;\n"),
(
"net.rs",
"pub fn f() { mod raw { pub unsafe fn poke() {} } }\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert_eq!(
out,
["unsafe fn poke in crate::net"],
"unsafe in a body-nested mod is attributed to the enclosing module, never dropped: {out:?}"
);
}
#[test]
fn unsafe_in_a_macro_body_is_a_stated_bound() {
let out = unsafe_labels(
"macro",
&[
("lib.rs", "pub mod net;\n"),
(
"net.rs",
"macro_rules! m { () => { unsafe {} }; }\npub fn f() {}\n",
),
],
&["crate::ffi"],
)
.unwrap();
assert!(
out.is_empty(),
"unsafe in a macro body is not observed: {out:?}"
);
}
fn vis_findings(name: &str, files: &[(&str, &str)], module: &str) -> Result<Vec<String>, String> {
vis_findings_at(name, files, module, VisibilityCeiling::Crate.rank())
}
fn vis_findings_at(
name: &str,
files: &[(&str, &str)],
module: &str,
ceiling_rank: u8,
) -> Result<Vec<String>, String> {
let tree = TempSrcTree::new(&format!("vis-{name}"));
tree.write_all(files);
let result = visibility_findings(tree.src(), &tree.root(), module, "x", ceiling_rank);
result.map(|facts| {
facts
.into_iter()
.map(|(fact, _file)| fact.to_string())
.collect()
})
}
#[test]
fn visibility_rank_is_false_negative_safe_for_every_form() {
use crate::syn_util::visibility_rank;
let rank = |vis: &str| {
let src = format!("{vis} fn f() {{}}");
visibility_rank(&syn::parse_str::<syn::ItemFn>(&src).expect("parse vis").vis)
};
assert_eq!(rank("pub"), 3);
assert_eq!(rank("pub(crate)"), 2);
assert_eq!(rank("pub(super)"), 1);
assert_eq!(rank("pub(self)"), 0);
assert_eq!(rank(""), 0, "inherited/private");
assert_eq!(rank("pub(in crate)"), 2);
assert_eq!(rank("pub(in super)"), 1);
assert_eq!(rank("pub(in self)"), 0);
assert_eq!(
rank("pub(in crate::a::b)"),
2,
"in-crate path is at most crate-visible"
);
assert_eq!(rank("pub(in super::super)"), 2);
}
#[test]
fn super_ceiling_reacts_on_pub_and_pub_crate_only() {
let out = vis_findings_at(
"super-ceiling",
&[
("lib.rs", "pub mod m;\n"),
(
"m.rs",
"pub fn a() {}\npub(crate) fn b() {}\npub(super) fn c() {}\nfn d() {}\n",
),
],
"crate::m",
VisibilityCeiling::Super.rank(),
)
.unwrap();
assert_eq!(
out,
["pub fn a", "pub(crate) fn b"],
"Super ceiling reacts on pub + pub(crate), not pub(super)/private: {out:?}"
);
}
#[test]
fn module_ceiling_reacts_on_pub_super_but_not_private() {
let out = vis_findings_at(
"module-ceiling",
&[
("lib.rs", "pub mod m;\n"),
("m.rs", "pub(super) fn c() {}\nfn d() {}\n"),
],
"crate::m",
VisibilityCeiling::Module.rank(),
)
.unwrap();
assert_eq!(
out,
["pub(super) fn c"],
"Module ceiling reacts on pub(super), not private: {out:?}"
);
}
#[test]
fn pub_in_crate_path_is_clean_under_crate_ceiling() {
let out = vis_findings_at(
"pub-in-crate-path",
&[
("lib.rs", "pub mod m;\n"),
("m.rs", "pub(in crate::a::b) fn f() {}\n"),
],
"crate::m",
VisibilityCeiling::Crate.rank(),
)
.unwrap();
assert!(
out.is_empty(),
"pub(in crate path) is at most crate-visible, clean under a Crate ceiling: {out:?}"
);
}
#[test]
fn pub_in_super_super_reacts_under_super_ceiling() {
let out = vis_findings_at(
"pub-in-super-super",
&[
("lib.rs", "pub mod a;\n"),
("a.rs", "pub mod b;\n"),
("a/b.rs", "pub(in super::super) fn f() {}\n"),
],
"crate::a::b",
VisibilityCeiling::Super.rank(),
)
.unwrap();
assert_eq!(
out,
["pub(in super::super) fn f"],
"multi-segment pub(in super::super) ranks Crate and reacts under Super: {out:?}"
);
}
#[test]
fn max_visibility_and_the_sugar_carry_the_ceiling() {
let sugar = VisibilityBoundary::in_crate("app")
.module("crate::m")
.must_not_declare_pub()
.because("r");
assert_eq!(sugar.ceiling(), VisibilityCeiling::Crate);
assert_eq!(sugar.ceiling().rule(), VISIBILITY_RULE);
let sup = VisibilityBoundary::in_crate("app")
.module("crate::m")
.max_visibility(VisibilityCeiling::Super)
.because("r");
assert_eq!(sup.ceiling(), VisibilityCeiling::Super);
}
#[test]
fn ceiling_rule_strings_are_distinct_across_the_semantic_family() {
assert_eq!(
VisibilityCeiling::Crate.rule(),
"must not declare pub items"
);
let all = [
VISIBILITY_RULE,
VISIBILITY_SUPER_RULE,
VISIBILITY_MODULE_RULE,
SIGNATURE_RULE,
DYN_TRAIT_RULE,
IMPL_TRAIT_RULE,
ASYNC_EXPOSURE_RULE,
TRAIT_IMPL_RULE,
FORBIDDEN_MARKER_RULE,
];
let set: std::collections::HashSet<&str> = all.iter().copied().collect();
assert_eq!(
set.len(),
all.len(),
"all semantic rule strings are distinct"
);
}
#[test]
fn pub_items_react_and_non_pub_items_are_clean() {
let out = vis_findings(
"pub-mix",
&[
("lib.rs", "pub mod internal;\n"),
(
"internal.rs",
"pub fn a() {}\npub struct B;\npub trait C {}\npub(crate) fn d() {}\npub(super) fn e() {}\nfn f() {}\n",
),
],
"crate::internal",
)
.unwrap();
assert_eq!(
out,
["pub fn a", "pub struct B", "pub trait C"],
"only bare-pub items react: {out:?}"
);
}
#[test]
fn a_pub_use_and_glob_react() {
let out = vis_findings(
"pub-use",
&[
("lib.rs", "pub mod internal;\n"),
(
"internal.rs",
"pub use crate::db::Handle;\npub use crate::db::*;\npub(crate) use crate::db::Hidden;\n",
),
],
"crate::internal",
)
.unwrap();
assert_eq!(out, ["pub use crate::db::*", "pub use crate::db::Handle"]);
}
#[test]
fn a_pub_submodule_reacts() {
let out = vis_findings(
"pub-mod",
&[
("lib.rs", "pub mod internal;\n"),
("internal.rs", "pub mod sub;\nmod hidden;\n"),
("internal/sub.rs", "\n"),
("internal/hidden.rs", "\n"),
],
"crate::internal",
)
.unwrap();
assert_eq!(out, ["pub mod sub"]);
}
#[test]
fn a_bare_pub_item_in_a_non_pub_module_still_reacts() {
let out = vis_findings(
"pub-in-crate-mod",
&[
("lib.rs", "pub(crate) mod internal;\n"),
("internal.rs", "pub fn helper() {}\n"),
],
"crate::internal",
)
.unwrap();
assert_eq!(
out,
["pub fn helper"],
"the rule governs the declared pub keyword, not crate-reachability"
);
}
#[test]
fn a_pub_extern_crate_and_pub_trait_alias_react() {
let out = vis_findings(
"extern-and-alias",
&[
("lib.rs", "pub mod internal;\n"),
(
"internal.rs",
"pub extern crate serde;\npub trait Alias = Clone;\n",
),
],
"crate::internal",
)
.unwrap();
assert_eq!(out, ["pub extern crate serde", "pub trait Alias (alias)"]);
}
#[test]
fn a_leading_colon_pub_use_is_rendered_and_distinct() {
let out = vis_findings(
"leading-colon",
&[
("lib.rs", "pub mod internal;\n"),
(
"internal.rs",
"pub use ::external::X;\npub use external::X;\n",
),
],
"crate::internal",
)
.unwrap();
assert_eq!(out, ["pub use ::external::X", "pub use external::X"]);
}
#[test]
fn a_macro_export_macro_is_out_of_scope() {
let out = vis_findings(
"macro-export",
&[
("lib.rs", "pub mod internal;\n"),
(
"internal.rs",
"#[macro_export]\nmacro_rules! m { () => {} }\npub(crate) fn helper() {}\n",
),
],
"crate::internal",
)
.unwrap();
assert!(
out.is_empty(),
"a #[macro_export] macro carries no pub keyword — out of declared scope: {out:?}"
);
}
#[test]
fn a_macro_invocation_pub_item_is_a_documented_bound() {
let out = vis_findings(
"macro-gen",
&[
("lib.rs", "pub mod internal;\n"),
("internal.rs", "make_public!();\n"),
],
"crate::internal",
)
.unwrap();
assert!(
out.is_empty(),
"a macro-generated item is out of scope, not silently claimed: {out:?}"
);
}
#[test]
fn a_cfg_gated_pub_item_is_observed_as_written() {
let out = vis_findings(
"cfg-pub",
&[
("lib.rs", "pub mod internal;\n"),
(
"internal.rs",
"#[cfg(feature = \"never\")]\npub fn gated() {}\n",
),
],
"crate::internal",
)
.unwrap();
assert_eq!(out, ["pub fn gated"], "cfg is observed as-written");
}
#[test]
fn an_unknown_visibility_module_is_a_constitution_error() {
let err = vis_findings(
"vis-unknown",
&[("lib.rs", "pub mod internal;\n"), ("internal.rs", "\n")],
"crate::ghost",
)
.unwrap_err();
assert_eq!(err, unknown_module_error("crate::ghost", "x"));
}
#[test]
fn an_inline_visibility_module_is_scanned() {
let out = vis_findings(
"vis-inline",
&[("lib.rs", "pub mod internal { pub fn a() {} fn b() {} }\n")],
"crate::internal",
)
.unwrap();
assert_eq!(out, ["pub fn a"]);
}
#[test]
fn the_visibility_builder_carries_severity() {
let warn = VisibilityBoundary::in_crate("app")
.module("crate::internal")
.must_not_declare_pub()
.warn()
.because("advisory first");
assert_eq!(warn.severity(), Severity::Warn);
let enforce = VisibilityBoundary::in_crate("app")
.module("crate::internal")
.must_not_declare_pub()
.because("enforced");
assert_eq!(enforce.severity(), Severity::Enforce);
}
#[test]
fn a_generic_self_type_is_rendered_distinctly() {
let out = locality_findings(
"generic-self",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Command {}\n"),
(
"domain.rs",
"use crate::command::Command;\npub struct W<T>(T);\nimpl Command for W<u8> {}\nimpl Command for W<u16> {}\n",
),
],
"crate::command::Command",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
[
"crate::domain (impl crate::command::Command for crate::domain::W<u16>)",
"crate::domain (impl crate::command::Command for crate::domain::W<u8>)"
]
);
}
#[test]
fn distinct_trait_instantiations_for_one_self_type_stay_distinct_findings() {
let out = locality_findings(
"generic-trait",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Convert<T> {}\n"),
(
"domain.rs",
"use crate::command::Convert;\npub struct Foo;\nimpl Convert<u8> for Foo {}\nimpl Convert<u16> for Foo {}\n",
),
],
"crate::command::Convert",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
[
"crate::domain (impl crate::command::Convert<u16> for crate::domain::Foo)",
"crate::domain (impl crate::command::Convert<u8> for crate::domain::Foo)"
],
"two distinct trait instantiations for one self type must stay distinct: {out:?}"
);
}
#[test]
fn array_length_differing_trait_instantiations_stay_distinct() {
let out = locality_findings(
"array-arg",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Convert<T> {}\n"),
(
"domain.rs",
"use crate::command::Convert;\npub struct Foo;\nimpl Convert<[u8; 4]> for Foo {}\nimpl Convert<[u8; 8]> for Foo {}\n",
),
],
"crate::command::Convert",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
[
"crate::domain (impl crate::command::Convert<[u8; 4]> for crate::domain::Foo)",
"crate::domain (impl crate::command::Convert<[u8; 8]> for crate::domain::Foo)"
],
"array-length-differing instantiations must stay distinct: {out:?}"
);
}
#[test]
fn complex_length_arrays_of_different_element_types_stay_distinct() {
let out = locality_findings(
"complex-array-arg",
&[
("lib.rs", "pub mod command;\npub mod domain;\n"),
("command.rs", "pub trait Convert<T> {}\n"),
(
"domain.rs",
"use crate::command::Convert;\npub struct Foo;\nimpl<const N: usize> Convert<[u8; N + 1]> for Foo {}\nimpl<const N: usize> Convert<[u16; N + 1]> for Foo {}\n",
),
],
"crate::command::Convert",
&["crate::commands"],
)
.unwrap();
assert_eq!(
out,
[
"crate::domain (impl crate::command::Convert<[u16; _]> for crate::domain::Foo)",
"crate::domain (impl crate::command::Convert<[u8; _]> for crate::domain::Foo)"
],
"complex-length arrays of different element types must stay distinct, not collapse to one `_`: {out:?}"
);
}
fn marker_findings(
name: &str,
files: &[(&str, &str)],
subtree: &str,
forbidden: &[&str],
) -> Result<Vec<String>, String> {
let tree = TempSrcTree::new(&format!("mark-{name}"));
tree.write_all(files);
let forbidden: Vec<String> = forbidden.iter().map(|s| s.to_string()).collect();
let result = forbidden_marker_findings(tree.src(), &tree.root(), subtree, &forbidden, "x");
result.map(|v| {
v.into_iter()
.map(|(finding, _module, _file)| finding.to_string())
.collect()
})
}
#[test]
fn a_forbidden_derive_on_a_subtree_type_reacts_and_a_clean_type_does_not() {
let out = marker_findings(
"derive",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"#[derive(serde::Serialize)]\npub struct Order;\n#[derive(Clone, Debug)]\npub struct Plain;\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(out, ["derive serde::Serialize on crate::domain::Order"]);
}
#[test]
fn a_serde_derive_path_and_cfg_attr_derive_react_by_leaf() {
let out = marker_findings(
"leaf-and-cfgattr",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"#[derive(serde_derive::Serialize)]\npub struct A;\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize))]\npub struct B;\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(
out,
[
"derive serde::Serialize on crate::domain::B",
"derive serde_derive::Serialize on crate::domain::A"
],
"serde_derive path (leaf) and cfg_attr-wrapped derive both react, each rendered by its own \
written derive path (so two same-leaf derives stay distinct): {out:?}"
);
}
#[test]
fn a_hand_impl_outside_the_subtree_reacts_via_the_self_type() {
let out = marker_findings(
"hand-impl",
&[
("lib.rs", "pub mod domain;\npub mod wire;\n"),
("domain.rs", "pub struct Order;\n"),
(
"wire.rs",
"impl serde::Serialize for crate::domain::Order {}\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(
out,
["impl serde::Serialize for crate::domain::Order in crate::wire"],
"a hand impl written outside the subtree, for a subtree type, reacts: {out:?}"
);
}
#[test]
fn a_foreign_or_prelude_self_type_is_not_a_governed_subtree_type() {
let out = marker_findings(
"foreign-self",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Order;\npub trait Marker {}\nimpl Marker for Vec<u8> {}\nimpl Marker for Box<Order> {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert!(
out.is_empty(),
"a marker acquired by a foreign/prelude self type (Vec/Box) is not a subtree type: {out:?}"
);
let out = marker_findings(
"foreign-self-control",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Order;\npub trait Marker {}\nimpl Marker for Order {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert_eq!(
out,
["impl Marker for crate::domain::Order in crate::domain"]
);
}
#[test]
fn distinct_generic_marker_instantiations_stay_distinct_findings() {
let out = marker_findings(
"generic-marker",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Order;\npub trait Marker<T> {}\nimpl Marker<u8> for Order {}\nimpl Marker<u16> for Order {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert_eq!(
out,
[
"impl Marker<u16> for crate::domain::Order in crate::domain",
"impl Marker<u8> for crate::domain::Order in crate::domain",
],
"two distinct generic instantiations must stay distinct findings: {out:?}"
);
}
#[test]
fn unrenderable_generic_marker_instantiations_fail_loud_without_positional_identity() {
let error = marker_findings(
"const-marker",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Foo;\npub trait Marker<const M: usize> {}\nimpl Marker<{ 1 + 1 }> for Foo {}\nimpl Marker<{ 2 + 2 }> for Foo {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap_err();
assert!(error.contains("stable structural label"), "{error}");
assert!(!error.contains("_#"), "{error}");
}
#[test]
fn a_forbidden_marker_on_a_local_type_alias_reacts() {
let out = marker_findings(
"alias-self",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Real;\ntype Bar = Real;\npub trait Marker {}\nimpl Marker for Bar {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert_eq!(out, ["impl Marker for crate::domain::Bar in crate::domain"]);
let out = marker_findings(
"alias-of-alias-self",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Real;\ntype A = Real;\ntype Bar = A;\npub trait Marker {}\nimpl Marker for Bar {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert_eq!(out, ["impl Marker for crate::domain::Bar in crate::domain"]);
}
#[test]
fn a_blanket_impls_own_generic_param_is_not_resolved_through_a_same_named_alias() {
let out = marker_findings(
"blanket-impl-generic-param-shadow",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::domain::sub::Innocent as T;\npub mod sub { pub struct Innocent; }\npub trait Marker {}\nimpl<T> Marker for T {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert!(
out.is_empty(),
"a blanket impl's own generic param T must not resolve through the unrelated `use ... as T` \
alias in scope in that module — the source never impls Marker for Innocent: {out:?}"
);
}
#[test]
fn a_blanket_impls_generic_param_is_shadowed_even_through_a_multi_segment_projection() {
let out = marker_findings(
"blanket-impl-multi-segment-generic-param-shadow",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::domain::sub as T;\npub mod sub { pub struct Assoc; }\npub trait Marker {}\nimpl<T> Marker for T::Assoc {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert!(
out.is_empty(),
"a blanket impl's own generic param T must stay shadowed even in a projection T::Assoc, \
never resolving through the unrelated `use ... as T` module alias: {out:?}"
);
}
#[test]
fn a_qualified_path_self_type_off_the_impls_own_generic_param_is_not_resolved_through_an_alias() {
let out = marker_findings(
"qself-bracket-projection-shadow-gap",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::domain::sub::Innocent as Item;\npub mod sub { pub struct Innocent; }\npub trait HasItem { type Item; }\npub trait Marker<X> {}\nimpl<T: HasItem> Marker<T> for <T>::Item {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert!(
out.is_empty(),
"a qself'd self type dependent on the impl's own generic param T must not resolve its \
trailing segment through the unrelated `use ... as Item` module alias: {out:?}"
);
}
#[test]
fn a_forbidden_marker_on_an_alias_to_a_foreign_type_is_clean() {
let out = marker_findings(
"foreign-alias-self",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"type Baz = Vec<u8>;\ntype Named = String;\npub trait Marker {}\nimpl Marker for Baz {}\nimpl Marker for Named {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert!(
out.is_empty(),
"a marker on an alias to a foreign type (Vec<u8>/String) lands off the subtree — no finding: {out:?}"
);
let out = marker_findings(
"local-alias-self-control",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Real;\ntype Bar = Real;\npub trait Marker {}\nimpl Marker for Bar {}\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert_eq!(out, ["impl Marker for crate::domain::Bar in crate::domain"]);
}
#[test]
fn two_same_leaf_derives_on_one_type_stay_distinct() {
let out = marker_findings(
"dual-derive",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"#[derive(a::Marker, b::Marker)]\npub struct T;\n",
),
],
"crate::domain",
&["Marker"],
)
.unwrap();
assert_eq!(
out,
[
"derive a::Marker on crate::domain::T",
"derive b::Marker on crate::domain::T"
],
"two same-leaf derives must stay distinct findings: {out:?}"
);
}
#[test]
fn a_submodule_type_is_governed_and_a_sibling_is_not() {
let out = marker_findings(
"subtree",
&[
("lib.rs", "pub mod domain;\npub mod domainx;\n"),
("domain.rs", "pub mod order;\n"),
(
"domain/order.rs",
"#[derive(serde::Serialize)]\npub struct Order;\n",
),
(
"domainx.rs",
"#[derive(serde::Serialize)]\npub struct Other;\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(
out,
["derive serde::Serialize on crate::domain::order::Order"],
"a submodule type is governed; the prefix-colliding sibling crate::domainx is not: {out:?}"
);
}
#[test]
fn a_same_leaf_different_trait_is_a_documented_false_positive() {
let out = marker_findings(
"leaf-fp",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"#[derive(rkyv::Serialize)]\npub struct Order;\n",
),
],
"crate::domain",
&["Serialize"],
)
.unwrap();
assert_eq!(
out,
["derive rkyv::Serialize on crate::domain::Order"],
"leaf-match reacts (accepted false positive; the finding now shows the written derive path, \
rkyv::Serialize, making the leaf-only match visible)"
);
}
#[test]
fn an_unresolvable_glob_self_type_is_a_documented_bound() {
let out = marker_findings(
"glob-self",
&[
("lib.rs", "pub mod domain;\npub mod wire;\n"),
("domain.rs", "pub struct Order;\n"),
(
"wire.rs",
"use crate::domain::*;\nimpl serde::Serialize for Order {}\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert!(
out.is_empty(),
"a glob-imported self-type cannot be placed in the subtree — a stated bound: {out:?}"
);
}
#[test]
fn a_nested_cfg_attr_derive_reacts() {
let out = marker_findings(
"nested-cfgattr",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"#[cfg_attr(all(), cfg_attr(all(), derive(serde::Serialize)))]\npub struct Order;\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(out, ["derive serde::Serialize on crate::domain::Order"]);
}
#[test]
fn a_malformed_derive_is_a_scan_error_not_a_silent_pass() {
let result = marker_findings(
"malformed-derive",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "#[derive(0, \"nope\")]\npub struct Order;\n"),
],
"crate::domain",
&["serde::Serialize"],
);
let err =
result.expect_err("a derive whose args are not paths must be a scan error, not clean");
assert!(
err.contains("cannot parse derive"),
"the error must name the parse failure it could not judge: {err}"
);
}
#[cfg(unix)]
#[test]
fn a_symlinked_module_cycle_is_a_scan_error_not_a_stack_overflow() {
let tree = TempSrcTree::new("symcycle");
tree.write("lib.rs", "pub mod foo;\n");
tree.write("foo/mod.rs", "pub mod foo;\n");
tree.symlink("../foo", "foo/foo");
let result = forbidden_marker_findings(tree.src(), &tree.root(), "crate", &[], "x");
let err =
result.expect_err("a symlinked module cycle must be a scan error, not a hang/overflow");
assert!(
err.contains("module cycle") || err.contains("symlink"),
"the error must name the cycle it could not judge: {err}"
);
}
#[test]
fn two_same_named_types_in_different_submodules_stay_distinct() {
let out = marker_findings(
"same-name",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub mod a;\npub mod b;\n"),
(
"domain/a.rs",
"#[derive(serde::Serialize)]\npub struct Order;\n",
),
(
"domain/b.rs",
"#[derive(serde::Serialize)]\npub struct Order;\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(
out,
[
"derive serde::Serialize on crate::domain::a::Order",
"derive serde::Serialize on crate::domain::b::Order"
],
"two same-named types must stay distinct findings: {out:?}"
);
}
#[test]
fn a_cfg_dual_declared_module_backed_by_one_file_does_not_duplicate_its_marker_finding() {
let out = marker_findings(
"cfg-dual-same-file",
&[
(
"lib.rs",
"pub struct Arr<const N: usize>;\n\
#[cfg(feature = \"u\")]\npub mod foo;\n#[cfg(feature = \"w\")]\npub mod foo;\n",
),
("foo.rs", "impl crate::Marker for crate::Arr<2> {}\n"),
],
"crate",
&["crate::Marker"],
)
.unwrap();
assert_eq!(
out.len(),
1,
"one real impl, backed by one real file under either #[cfg] arm, must be one finding: {out:?}"
);
}
#[test]
fn the_forbidden_marker_builder_carries_severity() {
let b = ForbiddenMarkerBoundary::in_crate("app")
.module("crate::domain")
.must_not_acquire("serde::Serialize")
.and_not_acquire("serde::Deserialize")
.warn()
.because("r");
assert_eq!(b.forbidden(), &["serde::Serialize", "serde::Deserialize"]);
assert_eq!(b.severity(), Severity::Warn);
}
fn dyn_findings(name: &str, files: &[(&str, &str)], module: &str) -> Result<Vec<String>, String> {
shape_findings("dyn", name, files, module, dyn_module_findings)
}
fn dyn_mod(name: &str, body: &str) -> Result<Vec<String>, String> {
dyn_findings(
name,
&[("lib.rs", "pub mod m;\n"), ("m.rs", body)],
"crate::m",
)
}
fn dyn_operand_findings(
name: &str,
files: &[(&str, &str)],
module: &str,
forbidden: &[&str],
deps: &[&str],
) -> Result<Vec<String>, String> {
operand_findings(
"dyn",
name,
files,
module,
forbidden,
deps,
dyn_operand_module_findings,
)
}
fn dyn_operand_mod(name: &str, body: &str, forbidden: &[&str]) -> Result<Vec<String>, String> {
dyn_operand_findings(
name,
&[("lib.rs", "pub mod m;\n"), ("m.rs", body)],
"crate::m",
forbidden,
&[],
)
}
#[test]
fn a_dyn_in_a_supertrait_or_assoc_type_bound_is_observed() {
assert!(
dyn_mod(
"supertrait-dyn",
"pub trait Facade: AsRef<Box<dyn crate::ports::Port>> {}\n",
)
.unwrap()
.contains(&"dyn crate::ports::Port exposed by trait crate::m::Facade".to_string()),
"a dyn in a supertrait generic argument must be observed",
);
assert!(
dyn_mod(
"assoc-bound-dyn",
"pub trait F { type Bar: AsRef<Box<dyn crate::ports::Port>>; }\n",
)
.unwrap()
.contains(&"dyn crate::ports::Port exposed by type trait crate::m::F::Bar".to_string()),
"a dyn in an associated-type bound must be observed",
);
}
#[test]
fn a_dyn_in_an_inherent_impl_generic_bound_is_observed() {
let out = dyn_mod(
"dyn-impl-generics",
"pub struct Foo<T>(T);\nimpl<T: AsRef<Box<dyn crate::ports::Port>>> Foo<T> { pub fn m(&self) {} }\n",
)
.unwrap();
assert!(
out.iter()
.any(|f| f.contains("dyn crate::ports::Port") && f.contains("(generics)")),
"a dyn in an inherent-impl generic bound must be observed: {out:?}"
);
}
#[test]
fn dyn_operand_flags_a_named_trait_and_passes_others() {
assert_eq!(
dyn_operand_mod(
"named",
"pub fn c() -> Box<dyn crate::ports::Port> { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap(),
["dyn crate::ports::Port exposed by fn crate::m::c"],
);
assert!(
dyn_operand_mod(
"other",
"pub fn e() -> Box<dyn std::error::Error> { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap()
.is_empty(),
"a dyn of an unlisted trait passes",
);
}
#[test]
fn dyn_operand_honors_a_module_prefix() {
assert_eq!(
dyn_operand_mod(
"prefix",
"pub fn c() -> Box<dyn crate::ports::Port> { todo!() }\n",
&["crate::ports"],
)
.unwrap(),
["dyn crate::ports::Port exposed by fn crate::m::c"],
);
}
#[test]
fn dyn_operand_matches_a_reexported_trait_by_its_defining_path() {
let files = &[
(
"lib.rs",
"pub mod ports;\npub use crate::ports::Port;\npub mod m;\n",
),
("ports.rs", "pub trait Port {}\n"),
("m.rs", "pub fn c() -> Box<dyn crate::Port> { todo!() }\n"),
];
assert_eq!(
dyn_operand_findings(
"reexport-defining",
files,
"crate::m",
&["crate::ports::Port"],
&[],
)
.unwrap(),
["dyn crate::Port exposed by fn crate::m::c"],
"a dyn written through a re-export facade matches the forbidden defining path",
);
}
#[test]
fn a_cfg_sibling_child_module_does_not_shadow_a_different_branchs_own_extern_principal() {
let files = &[
(
"lib.rs",
"#[cfg(feature = \"u\")] pub mod platform;\n\
#[cfg(feature = \"w\")] #[path = \"win_platform.rs\"] pub mod platform;\n",
),
(
"platform.rs",
"pub mod traits { pub trait Marker {} }\npub fn open() -> u8 { 0 }\n",
),
(
"win_platform.rs",
"pub fn f() -> Box<dyn traits::Marker> { todo!() }\n",
),
];
assert_eq!(
dyn_operand_findings(
"cfg-sibling-childmod-shadow",
files,
"crate::platform",
&["traits::Marker"],
&["traits"],
)
.unwrap(),
["dyn traits::Marker exposed by fn crate::platform::f"],
"the w branch's own genuine extern dyn-principal must react, regardless of the u \
branch's own local mod traits",
);
}
#[test]
fn a_cfg_split_module_with_two_inline_siblings_child_module_does_not_shadow_the_others_own_extern_principal()
{
let files = &[(
"lib.rs",
"#[cfg(feature = \"u\")] pub mod platform {\n\
pub mod traits { pub trait Marker {} }\n\
pub fn open() -> u8 { 0 }\n}\n\
#[cfg(feature = \"w\")] pub mod platform {\n\
pub fn f() -> Box<dyn traits::Marker> { todo!() }\n}\n",
)];
assert_eq!(
dyn_operand_findings(
"cfg-split-inline-inline-childmod-shadow",
files,
"crate::platform",
&["traits::Marker"],
&["traits"],
)
.unwrap(),
["dyn traits::Marker exposed by fn crate::platform::f"],
"the w arm's own genuine extern dyn-principal must react, regardless of the u arm's own \
local mod traits, even though both arms are inline and share lib.rs",
);
}
#[test]
fn dyn_operand_ignores_auto_trait_markers() {
assert_eq!(
dyn_operand_mod(
"marker-port",
"pub fn c() -> Box<dyn crate::ports::Port + Send> { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap(),
["dyn crate::ports::Port + Send exposed by fn crate::m::c"],
);
assert!(
dyn_operand_mod(
"marker-send",
"pub fn c() -> Box<dyn crate::ports::Port + Send> { todo!() }\n",
&["Send"],
)
.unwrap()
.is_empty(),
"the trailing Send marker is not the operand",
);
}
#[test]
fn dyn_operand_matches_when_an_auto_trait_is_written_before_the_principal() {
assert_eq!(
dyn_operand_mod(
"auto-first",
"pub fn c() -> Box<dyn Send + crate::ports::Port> { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap(),
["dyn Send + crate::ports::Port exposed by fn crate::m::c"],
);
assert_eq!(
dyn_operand_mod(
"auto-first-2",
"pub fn c() -> Box<dyn Send + Sync + crate::ports::Port> { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap(),
["dyn Send + Sync + crate::ports::Port exposed by fn crate::m::c"],
);
}
#[test]
fn dyn_operand_matches_a_dyn_nested_deep() {
assert_eq!(
dyn_operand_mod(
"nested",
"pub fn c() -> Vec<Box<dyn crate::ports::Port>> { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap(),
["dyn crate::ports::Port exposed by fn crate::m::c"],
);
}
#[test]
fn dyn_operand_empty_set_degenerates_to_any() {
let body = "pub fn c() -> Box<dyn crate::ports::Port> { todo!() }\n";
assert_eq!(
dyn_operand_mod("empty", body, &[]).unwrap(),
dyn_mod("empty-shape", body).unwrap(),
"must_not_expose_dyn_of([]) matches exactly what shape-only must_not_expose_dyn does",
);
assert_eq!(
dyn_operand_mod("empty2", body, &[]).unwrap(),
["dyn crate::ports::Port exposed by fn crate::m::c"],
);
}
#[test]
fn dyn_operand_boundary_carries_its_operands_and_severity() {
let b = DynTraitBoundary::in_crate("core")
.module("crate::core")
.must_not_expose_dyn_of(["crate::ports::Port"])
.warn()
.because("the core seam must not leak a dyn Port");
assert_eq!(b.forbidden_operands(), ["crate::ports::Port"]);
assert_eq!(b.severity(), Severity::Warn);
let shape = DynTraitBoundary::in_crate("core")
.module("crate::core")
.must_not_expose_dyn()
.because("no dyn at all");
assert!(shape.forbidden_operands().is_empty());
}
fn impl_trait_findings(
name: &str,
files: &[(&str, &str)],
module: &str,
) -> Result<Vec<String>, String> {
shape_findings("impl", name, files, module, impl_trait_module_findings)
}
fn impl_trait_mod(name: &str, body: &str) -> Result<Vec<String>, String> {
impl_trait_findings(
name,
&[("lib.rs", "pub mod m;\n"), ("m.rs", body)],
"crate::m",
)
}
#[test]
fn impl_trait_flags_a_returned_impl_trait() {
assert_eq!(
impl_trait_mod("ret", "pub fn make() -> impl crate::Port { todo!() }\n").unwrap(),
["impl crate::Port exposed by fn crate::m::make"],
);
}
#[test]
fn impl_trait_flags_a_nested_returned_impl_trait() {
assert_eq!(
impl_trait_mod(
"nested",
"pub fn maybe() -> Option<impl crate::Port> { todo!() }\n"
)
.unwrap(),
["impl crate::Port exposed by fn crate::m::maybe"],
"an impl Trait at depth in the return type is existential and reacts",
);
}
#[test]
fn impl_trait_flags_a_trait_method_rpit() {
assert_eq!(
impl_trait_mod(
"rpitit",
"pub trait T { fn make(&self) -> impl crate::Port; }\n"
)
.unwrap(),
["impl crate::Port exposed by fn trait crate::m::T::make"],
"a trait method's declared RPIT is the existential, governed at the declaration",
);
}
#[test]
fn impl_trait_does_not_flag_an_argument_position() {
assert!(
impl_trait_mod("apit", "pub fn drive(p: impl crate::Port) { let _ = p; }\n")
.unwrap()
.is_empty(),
"argument-position impl Trait is not governed",
);
}
#[test]
fn impl_trait_does_not_flag_an_async_fn() {
assert!(
impl_trait_mod("async", "pub async fn connect() -> u8 { 0 }\n")
.unwrap()
.is_empty(),
"async fn's implicit impl Future is out of scope",
);
}
#[test]
fn impl_trait_does_not_flag_a_private_fn_or_a_trait_impl_method() {
assert!(
impl_trait_mod("priv", "fn make() -> impl crate::Port { todo!() }\n")
.unwrap()
.is_empty(),
"a private fn's RPIT is not public API",
);
assert!(
impl_trait_mod(
"traitimpl",
"pub struct S; impl crate::T for S { fn make(&self) -> impl crate::Port { todo!() } }\n"
)
.unwrap()
.is_empty(),
"a trait-impl method's return is not double-counted",
);
}
#[test]
fn impl_trait_renders_iterator_and_fn_shapes_distinctly() {
assert_eq!(
impl_trait_mod(
"iter",
"pub fn it() -> impl Iterator<Item = u8> { todo!() }\n"
)
.unwrap(),
["impl Iterator<Item = u8> exposed by fn crate::m::it"],
);
assert_eq!(
impl_trait_mod("clo", "pub fn f() -> impl Fn(i32) -> i32 { todo!() }\n").unwrap(),
["impl Fn(i32) -> i32 exposed by fn crate::m::f"],
);
}
#[test]
fn impl_trait_boundary_carries_anchor_and_severity() {
let b = ImplTraitBoundary::in_crate("core")
.module("crate::core")
.must_not_expose_impl_trait()
.warn()
.because("the core seam must return named types");
assert_eq!(b.crate_package(), "core");
assert_eq!(b.module(), "crate::core");
assert_eq!(b.severity(), Severity::Warn);
}
fn impl_trait_operand_findings(
name: &str,
files: &[(&str, &str)],
module: &str,
forbidden: &[&str],
deps: &[&str],
) -> Result<Vec<String>, String> {
operand_findings(
"impl",
name,
files,
module,
forbidden,
deps,
impl_trait_operand_module_findings,
)
}
fn impl_trait_operand_mod(
name: &str,
body: &str,
forbidden: &[&str],
) -> Result<Vec<String>, String> {
impl_trait_operand_findings(
name,
&[("lib.rs", "pub mod m;\n"), ("m.rs", body)],
"crate::m",
forbidden,
&[],
)
}
#[test]
fn impl_trait_operand_flags_a_named_trait_and_passes_others() {
assert_eq!(
impl_trait_operand_mod(
"named",
"pub fn make() -> impl crate::ports::Port { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap(),
["impl crate::ports::Port exposed by fn crate::m::make"],
);
assert!(
impl_trait_operand_mod(
"iter",
"pub fn it() -> impl Iterator<Item = u8> { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap()
.is_empty(),
"a returned impl of an unlisted (and bare-std) trait passes",
);
}
#[test]
fn impl_trait_operand_honors_a_module_prefix() {
assert_eq!(
impl_trait_operand_mod(
"prefix",
"pub fn make() -> impl crate::ports::Port { todo!() }\n",
&["crate::ports"],
)
.unwrap(),
["impl crate::ports::Port exposed by fn crate::m::make"],
);
}
#[test]
fn impl_trait_operand_matches_a_reexported_trait_by_its_defining_path() {
let files = &[
(
"lib.rs",
"pub mod ports;\npub use crate::ports::Port;\npub mod m;\n",
),
("ports.rs", "pub trait Port {}\n"),
("m.rs", "pub fn make() -> impl crate::Port { todo!() }\n"),
];
assert_eq!(
impl_trait_operand_findings("reexport", files, "crate::m", &["crate::ports::Port"], &[])
.unwrap(),
["impl crate::Port exposed by fn crate::m::make"],
);
}
#[test]
fn impl_trait_operand_ignores_auto_trait_markers() {
assert_eq!(
impl_trait_operand_mod(
"marker-port",
"pub fn make() -> impl crate::ports::Port + Send { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap(),
["impl crate::ports::Port + Send exposed by fn crate::m::make"],
);
assert!(
impl_trait_operand_mod(
"marker-send",
"pub fn make() -> impl crate::ports::Port + Send { todo!() }\n",
&["Send"],
)
.unwrap()
.is_empty(),
"the trailing Send marker is not the operand",
);
}
#[test]
fn impl_trait_operand_matches_an_auto_trait_written_before_the_principal() {
assert_eq!(
impl_trait_operand_mod(
"auto-first",
"pub fn make() -> impl Send + crate::ports::Port { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap(),
["impl Send + crate::ports::Port exposed by fn crate::m::make"],
);
}
#[test]
fn impl_trait_operand_matches_a_second_non_auto_trait() {
assert_eq!(
impl_trait_operand_mod(
"second-trait",
"pub fn make() -> impl crate::ports::Port + crate::ports::Sink { todo!() }\n",
&["crate::ports::Sink"],
)
.unwrap(),
["impl crate::ports::Port + crate::ports::Sink exposed by fn crate::m::make"],
);
}
#[test]
fn impl_trait_operand_matches_a_nested_returned_impl() {
assert_eq!(
impl_trait_operand_mod(
"nested",
"pub fn maybe() -> Option<impl crate::ports::Port> { todo!() }\n",
&["crate::ports::Port"],
)
.unwrap(),
["impl crate::ports::Port exposed by fn crate::m::maybe"],
);
}
#[test]
fn impl_trait_operand_empty_set_degenerates_to_any() {
let body = "pub fn make() -> impl crate::ports::Port { todo!() }\n";
assert_eq!(
impl_trait_operand_mod("empty", body, &[]).unwrap(),
impl_trait_mod("empty-shape", body).unwrap(),
"must_not_expose_impl_trait_of([]) matches exactly what shape-only does",
);
}
#[test]
fn impl_trait_operand_inherits_return_position_scoping() {
assert!(
impl_trait_operand_mod(
"apit",
"pub fn drive(p: impl crate::ports::Port) { let _ = p; }\n",
&["crate::ports::Port"],
)
.unwrap()
.is_empty(),
"argument-position impl Trait is not governed even with a matching operand",
);
assert!(
impl_trait_operand_mod(
"async",
"pub async fn c() -> u8 { 0 }\n",
&["crate::ports::Port"]
)
.unwrap()
.is_empty(),
);
}
#[test]
fn impl_trait_operand_boundary_carries_operands_and_severity() {
let b = ImplTraitBoundary::in_crate("core")
.module("crate::core")
.must_not_expose_impl_trait_of(["crate::ports::Port"])
.warn()
.because("the core seam must not return an existential Port");
assert_eq!(b.forbidden_operands(), ["crate::ports::Port"]);
assert_eq!(b.severity(), Severity::Warn);
let shape = ImplTraitBoundary::in_crate("core")
.module("crate::core")
.must_not_expose_impl_trait()
.because("no existential at all");
assert!(shape.forbidden_operands().is_empty());
}
#[test]
fn impl_trait_boundary_carries_anchor_and_including_submodules() {
let b = ImplTraitBoundary::in_crate("core")
.module("crate::core")
.must_not_expose_impl_trait()
.warn()
.because("the core seam must return named types, not an existential");
assert_eq!(b.severity(), Severity::Warn);
assert!(!b.including_submodules());
let sub = ImplTraitBoundary::in_crate("core")
.module("crate")
.must_not_expose_impl_trait()
.including_submodules()
.because("no existential anywhere under the kernel");
assert!(sub.including_submodules());
}
fn impl_trait_subtree(
name: &str,
files: &[(&str, &str)],
module: &str,
) -> Result<Vec<(String, String)>, String> {
subtree_findings("impl", name, files, module, impl_trait_subtree_findings)
}
fn impl_trait_subtree_labels(name: &str, files: &[(&str, &str)], module: &str) -> Vec<String> {
impl_trait_subtree(name, files, module)
.unwrap()
.into_iter()
.map(|(finding, _module)| finding)
.collect()
}
#[test]
fn impl_trait_subtree_reacts_to_a_submodule_return_the_seam_scope_misses() {
let files = &[
("lib.rs", "pub mod net;\n"),
("net.rs", "pub fn make() -> impl crate::Port { todo!() }\n"),
];
assert_eq!(
impl_trait_findings("seam-misses-sub", files, "crate").unwrap(),
Vec::<String>::new(),
);
let subtree = impl_trait_subtree("sub-reacts", files, "crate").unwrap();
assert_eq!(subtree.len(), 1);
assert_eq!(subtree[0].1, "crate::net");
assert!(subtree[0].0.contains("impl crate::Port"), "{:?}", subtree);
}
#[test]
fn impl_trait_subtree_includes_the_anchor_modules_own_seam_byte_identically() {
let files = &[
("lib.rs", "pub mod m;\n"),
(
"m.rs",
"pub fn own() -> impl crate::Port { todo!() }\npub mod deep;\n",
),
(
"m/deep.rs",
"pub fn nested() -> impl crate::Port { todo!() }\n",
),
];
let seam = impl_trait_findings("seam-parity", files, "crate::m").unwrap();
assert_eq!(seam.len(), 1);
let subtree = impl_trait_subtree_labels("subtree-parity", files, "crate::m");
assert_eq!(subtree.len(), 2);
assert!(subtree.contains(&seam[0]));
}
#[test]
fn impl_trait_subtree_scopes_to_the_anchored_subtree_not_the_whole_crate() {
let files = &[
("lib.rs", "pub mod a;\npub mod c;\n"),
(
"a.rs",
"pub mod b;\npub fn make() -> impl crate::Port { todo!() }\n",
),
("a/b.rs", "pub fn make() -> impl crate::Port { todo!() }\n"),
("c.rs", "pub fn make() -> impl crate::Port { todo!() }\n"),
];
let subtree = impl_trait_subtree("bounded", files, "crate::a").unwrap();
let modules: Vec<&str> = subtree.iter().map(|(_, m)| m.as_str()).collect();
assert!(modules.contains(&"crate::a"));
assert!(modules.contains(&"crate::a::b"));
assert!(!modules.contains(&"crate::c"), "{:?}", modules);
}
#[test]
fn impl_trait_subtree_tolerates_a_cfg_gated_fileless_submodule() {
let files = &[
(
"lib.rs",
"#[cfg(feature = \"never\")]\npub mod optional;\npub mod present;\n",
),
(
"present.rs",
"pub fn make() -> impl crate::Port { todo!() }\n",
),
];
let subtree = impl_trait_subtree("cfg-fileless", files, "crate").unwrap();
assert_eq!(subtree.len(), 1);
assert_eq!(subtree[0].1, "crate::present");
}
#[test]
fn impl_trait_subtree_errors_on_a_non_cfg_missing_submodule() {
let files = &[("lib.rs", "pub mod missing;\n")];
let err = impl_trait_subtree("non-cfg-missing", files, "crate").unwrap_err();
assert!(err.contains("missing"), "{err}");
}
#[test]
fn impl_trait_subtree_does_not_observe_a_body_nested_module() {
let files = &[(
"lib.rs",
"pub fn outer() { mod inner { pub fn hidden() -> impl crate::Port { todo!() } } }\n",
)];
let subtree = impl_trait_subtree("body-nested", files, "crate").unwrap();
assert!(subtree.is_empty(), "{:?}", subtree);
}
#[test]
fn impl_trait_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 fn before() -> impl crate::Port { todo!() } }\nimpl<const N: usize> Arr<{ N + 1 }> { pub fn unrenderable() -> impl crate::Port { todo!() } }\n",
)];
let seam = impl_trait_findings("const-generic-owner-parity-seam", files, "crate").unwrap_err();
let subtree =
impl_trait_subtree("const-generic-owner-parity-subtree", files, "crate").unwrap_err();
assert!(seam.contains("without a stable structural label"), "{seam}");
assert!(
subtree.contains("without a stable structural label"),
"{subtree}"
);
assert!(!seam.contains("_#") && !subtree.contains("_#"));
}
#[test]
fn impl_trait_subtree_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 fn run() -> impl crate::Port { todo!() } }\n",
),
(
"m_w.rs",
"pub struct Arr<const N: usize>;\nimpl<const N: usize> Arr<{ N + 2 }> { pub fn run() -> impl crate::Port { todo!() } }\n",
),
];
let error =
impl_trait_subtree("cfg-split-owner-fallback-collision", files, "crate::m").unwrap_err();
assert!(
error.contains("without a stable structural label"),
"{error}"
);
assert!(!error.contains("_#"), "{error}");
}
#[test]
fn impl_trait_operand_scoped_boundary_reacts_across_file_and_inline_submodules() {
let (metadata, _fixture) = fixture_metadata(
"impltrait-operand-subtree",
&[
(
"lib.rs",
"pub trait Port {}\npub trait Other {}\npub mod file;\n\
pub mod inline { pub fn other() -> impl crate::Other { todo!() } }\n",
),
(
"file.rs",
"use crate::Port as ApiPort;\npub fn port() -> impl ApiPort { todo!() }\n",
),
],
);
let boundary = ImplTraitBoundary::in_crate("x")
.module("crate")
.must_not_expose_impl_trait_of(["crate::Port"])
.including_submodules()
.because("r");
let mut violations = Vec::new();
check_impl_trait_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].target(), "crate");
assert!(violations[0].finding.contains("port"));
assert!(
violations[0]
.file
.as_deref()
.is_some_and(|file| file.ends_with("file.rs"))
);
}
fn async_findings(name: &str, files: &[(&str, &str)], module: &str) -> Result<Vec<String>, String> {
shape_findings("async", name, files, module, async_exposure_module_findings)
}
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",
)
}
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();
(finding.key().clone(), finding.text().to_string())
})
.collect()
})
}
#[test]
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]
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![
("module", "crate::registry"),
("name", "register"),
("owner", "crate::registry"),
("owner_kind", "module"),
]
);
}
#[test]
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]
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]
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]
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]
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]
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]
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());
}
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,
)
}
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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"));
}
fn resolve_file(name: &str, files: &[(&str, &str)], module: &str) -> Result<PathBuf, String> {
let tree = TempSrcTree::new(&format!("file-{name}"));
tree.write_all(files);
resolve_module_file(tree.src(), &tree.root(), module, "x")
}
#[test]
fn module_file_is_the_crate_root_for_the_root_module() {
let file = resolve_file("root", &[("lib.rs", "pub struct A;\n")], "crate").unwrap();
assert!(file.ends_with("src/lib.rs"), "got {}", file.display());
}
#[test]
fn module_file_is_the_file_module_source() {
let file = resolve_file(
"filemod",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub struct A;\n"),
],
"crate::domain",
)
.unwrap();
assert!(file.ends_with("domain.rs"), "got {}", file.display());
}
#[test]
fn descend_tolerates_a_cfg_gated_missing_sibling_when_an_inline_arm_resolves() {
let file = resolve_file(
"cfg-shim",
&[(
"lib.rs",
"#[cfg(unix)]\npub mod shared { pub struct A; }\n\
#[cfg(windows)]\npub mod shared;\n",
)],
"crate::shared",
)
.expect("the inline arm resolves even though the windows-only file-form sibling has no file");
assert!(file.ends_with("lib.rs"), "got {}", file.display());
}
#[test]
fn descend_still_errors_when_every_candidate_for_a_module_is_cfg_gated_missing() {
let err = resolve_file(
"cfg-only-missing",
&[("lib.rs", "#[cfg(feature = \"absent\")]\npub mod gated;\n")],
"crate::gated",
)
.expect_err("a module with no surviving branch must be a scan error, never a vacuous pass");
assert_eq!(err, unknown_module_error("crate::gated", "x"));
}
#[test]
fn descend_does_not_tolerate_a_cfg_attr_decorated_missing_file_only_bare_cfg() {
let err = resolve_file(
"cfg-attr-not-tolerated",
&[(
"lib.rs",
"#[cfg_attr(unix, allow(dead_code))]\npub mod gated;\n",
)],
"crate::gated",
)
.expect_err("a cfg_attr-decorated (not cfg-gated) missing file must still be a scan error");
assert_eq!(err, missing_module_file_error("crate::gated", "x"));
}
#[test]
fn descend_tolerates_a_cfg_gated_unconditional_path_target_when_missing() {
let file = resolve_file(
"cfg-path-shim",
&[(
"lib.rs",
"#[cfg(unix)]\npub mod shared { pub struct A; }\n\
#[cfg(windows)]\n#[path = \"windows_impl.rs\"]\npub mod shared;\n",
)],
"crate::shared",
)
.expect("the inline arm resolves even though the windows-only #[path] target has no file");
assert!(file.ends_with("lib.rs"), "got {}", file.display());
}
#[test]
fn resolve_child_modules_tolerates_a_cfg_gated_unconditional_path_target_when_missing() {
let out = unsafe_labels(
"cfg-path-shim-crate-wide",
&[
(
"lib.rs",
"#[cfg(windows)]\n#[path = \"windows_impl.rs\"]\npub mod imp;\npub mod live;\n",
),
("live.rs", "unsafe fn f() {}\n"),
],
&["crate::allowed_elsewhere"],
)
.expect("a cfg-gated #[path] target with no file must not fail the crate-wide scan");
assert_eq!(out, ["unsafe fn f in crate::live"]);
}
#[test]
fn module_file_is_mod_rs_for_a_nested_module() {
let file = resolve_file(
"nested",
&[
("lib.rs", "pub mod domain;\n"),
("domain/mod.rs", "pub struct A;\n"),
],
"crate::domain",
)
.unwrap();
assert!(file.ends_with("domain/mod.rs"), "got {}", file.display());
}
#[test]
fn module_file_is_the_enclosing_file_for_an_inline_submodule() {
let file = resolve_file(
"inline",
&[("lib.rs", "pub mod inner { pub struct A; }\n")],
"crate::inner",
)
.unwrap();
assert!(file.ends_with("src/lib.rs"), "got {}", file.display());
}
#[test]
fn module_file_descends_a_deep_file_module() {
let file = resolve_file(
"deep",
&[
("lib.rs", "pub mod a;\n"),
("a.rs", "pub mod b;\n"),
("a/b.rs", "pub struct A;\n"),
],
"crate::a::b",
)
.unwrap();
assert!(file.ends_with("a/b.rs"), "got {}", file.display());
}
#[test]
fn module_file_follows_an_unconditional_path_on_an_inline_module_to_its_relocated_child() {
let file = resolve_file(
"inline-path-relocate",
&[
(
"lib.rs",
"#[path = \"thread_files\"]\npub mod thread {\n pub mod local_data;\n}\n",
),
("thread_files/local_data.rs", "pub struct A;\n"),
],
"crate::thread::local_data",
)
.unwrap();
assert!(
file.ends_with("thread_files/local_data.rs")
|| file.ends_with("thread_files\\local_data.rs"),
"got {}",
file.display()
);
}
fn fixture_metadata(name: &str, files: &[(&str, &str)]) -> (Value, TempSrcTree) {
let tree = TempSrcTree::new(&format!("meta-{name}"));
tree.write_all(files);
(tree.metadata(), tree)
}
#[test]
fn semantic_violation_carries_the_governed_module_file_not_the_types_file() {
let (metadata, _fixture) = fixture_metadata(
"seam",
&[
("lib.rs", "pub mod infra;\npub mod domain;\n"),
("infra.rs", "pub struct Db;\n"),
(
"domain.rs",
"pub fn leak() -> crate::infra::Db { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("domain must not expose infra");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(violations.len(), 1, "one exposure violation");
assert_eq!(violations[0].target(), "crate::domain");
assert_eq!(violations[0].rule, SIGNATURE_RULE);
let id = violations[0].id();
let key = id.fact();
let rule = id.rule_key();
assert_eq!(rule.rule_type(), "tianheng.rule/hunyi/signature-exposure");
assert_eq!(
rule.fields().collect::<Vec<_>>(),
vec![
("forbidden", "[\"crate::infra\"]"),
("including_trait_impls", "false"),
]
);
assert_eq!(key.fact_type(), "tianheng.fact/hunyi/signature-exposure");
assert_eq!(key.shape(), "public-seam");
assert_eq!(
key.fields().collect::<Vec<_>>(),
vec![
("seam_kind", "free_fn"),
("seam_module", "crate::domain"),
("seam_name", "leak"),
("subject", "crate::infra::Db"),
]
);
let file = violations[0]
.file
.as_deref()
.expect("a governed-module file");
assert!(
file.ends_with("domain.rs"),
"the file is the seam's module (domain.rs), not the type's file (infra.rs): got {file}"
);
}
#[test]
fn the_semantic_file_is_not_part_of_the_baseline_identity() {
let (metadata, _fixture) = fixture_metadata(
"baseline",
&[
("lib.rs", "pub mod infra;\npub mod domain;\n"),
("infra.rs", "pub struct Db;\n"),
(
"domain.rs",
"pub fn leak() -> crate::infra::Db { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("r");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
let v = &violations[0];
assert!(v.file.is_some(), "the violation now carries a file");
assert_eq!(v.id(), v.clone().with_file(None).id());
}
#[test]
fn cfg_duplicated_inline_modules_are_all_governed() {
let (metadata, _fixture) = fixture_metadata(
"cfg-dup-platform",
&[
(
"lib.rs",
"pub mod infra;\n\
#[cfg(unix)] pub mod platform { pub fn open() -> u8 { 0 } }\n\
#[cfg(windows)] pub mod platform { pub fn open() -> crate::infra::Db { unimplemented!() } }\n",
),
("infra.rs", "pub struct Db;\n"),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::platform")
.must_not_expose("crate::infra")
.because("platform must not expose infra in any cfg variant");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(
violations.len(),
1,
"the non-source-first cfg variant's exposure must react: {violations:?}"
);
}
#[test]
fn cfg_mixed_inline_and_file_form_siblings_are_both_governed() {
let (metadata, _fixture) = fixture_metadata(
"cfg-mixed-inline-file",
&[
(
"lib.rs",
"pub mod infra;\n\
#[cfg(feature = \"a\")] pub mod platform { pub fn open() -> u8 { 0 } }\n\
#[cfg(feature = \"b\")] pub mod platform;\n",
),
("infra.rs", "pub struct Db;\n"),
(
"platform.rs",
"pub fn open() -> crate::infra::Db { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::platform")
.must_not_expose("crate::infra")
.because("platform must not expose infra in any cfg variant, inline or file-form");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(
violations.len(),
1,
"the file-form sibling's exposure must react even though an inline variant exists: {violations:?}"
);
}
#[test]
fn a_semantic_boundary_anchored_at_an_inline_module_with_an_unconditional_path_reacts_instead_of_erroring()
{
let (metadata, _fixture) = fixture_metadata(
"inline-path-boundary",
&[
(
"lib.rs",
"pub mod infra;\n\
#[path = \"thread_files\"]\npub mod thread {\n pub mod local_data;\n}\n",
),
("infra.rs", "pub struct Db;\n"),
(
"thread_files/local_data.rs",
"pub fn leak() -> crate::infra::Db { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::thread::local_data")
.must_not_expose("crate::infra")
.because("an inline module's own #[path] must not make its children unresolvable");
let mut violations = Vec::new();
let result = check_boundary(&metadata, &boundary, &mut violations);
result.expect("crate::thread::local_data must resolve, not hard-error as an unknown module");
assert_eq!(
violations.len(),
1,
"the relocated child's exposure must still be observed: {violations:?}"
);
}
#[test]
fn a_further_segment_beneath_a_flat_file_form_cfg_sibling_resolves_from_its_own_directory() {
let (metadata, _fixture) = fixture_metadata(
"cfg-mixed-flat-further-segment",
&[
(
"lib.rs",
"pub mod infra;\n\
#[cfg(feature = \"a\")] pub mod plat { pub struct Marker; }\n\
#[cfg(feature = \"b\")] #[path = \"moved/plat_moved.rs\"] pub mod plat;\n",
),
("infra.rs", "pub struct Db;\n"),
(
"moved/plat_moved.rs",
"#[path = \"elsewhere.rs\"]\npub mod target;\n",
),
(
"moved/elsewhere.rs",
"pub fn get() -> crate::infra::Db { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::plat::target")
.must_not_expose("crate::infra")
.because(
"plat::target must not expose infra even through a flat cfg-sibling's own #[path]",
);
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(
violations.len(),
1,
"the flat file-form sibling's own #[path] target (moved/elsewhere.rs, a SIBLING of \
plat_moved.rs, not a child of a plat/ subdirectory) must be read and react: {violations:?}"
);
}
#[test]
fn a_plain_child_of_a_path_remapped_module_resolves_from_the_remaps_own_directory() {
let (metadata, _fixture) = fixture_metadata(
"path-remap-plain-child",
&[
(
"lib.rs",
"pub mod infra;\n#[path = \"moved/thing.rs\"]\npub mod net;\n",
),
("infra.rs", "pub struct Db;\n"),
("moved/thing.rs", "pub mod inner;\n"),
(
"moved/inner.rs",
"pub fn get() -> crate::infra::Db { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::net::inner")
.must_not_expose("crate::infra")
.because("net::inner must not expose infra even though net is #[path]-remapped");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(
violations.len(),
1,
"moved/inner.rs (the real, rustc-compiled file) must be read and react: {violations:?}"
);
}
#[test]
fn cfg_mixed_plain_and_path_remapped_file_form_siblings_are_both_governed() {
let (metadata, _fixture) = fixture_metadata(
"cfg-mixed-plain-and-remapped-file-form",
&[
(
"lib.rs",
"pub mod infra;\n\
#[cfg(feature = \"a\")] pub mod platform;\n\
#[cfg(feature = \"b\")] #[path = \"win_platform.rs\"] pub mod platform;\n",
),
("infra.rs", "pub struct Db;\n"),
("platform.rs", "pub fn open() -> u8 { 0 }\n"),
(
"win_platform.rs",
"pub fn open() -> crate::infra::Db { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::platform")
.must_not_expose("crate::infra")
.because("platform must not expose infra in either the plain or #[path]-remapped arm");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(
violations.len(),
1,
"the #[path]-remapped sibling's exposure must react even though a plain sibling was \
declared first in source order: {violations:?}"
);
}
#[test]
fn a_cfg_mixed_single_module_violation_names_the_offending_sibling_not_the_first_branch() {
let (metadata, _fixture) = fixture_metadata(
"cfg-mixed-file-names-offending-branch",
&[
(
"lib.rs",
"pub mod infra;\n\
#[cfg(feature = \"a\")] pub mod platform;\n\
#[cfg(feature = \"b\")] #[path = \"win_platform.rs\"] pub mod platform;\n",
),
("infra.rs", "pub struct Db;\n"),
("platform.rs", "pub fn open() -> u8 { 0 }\n"),
(
"win_platform.rs",
"pub fn open() -> crate::infra::Db { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::platform")
.must_not_expose("crate::infra")
.because("the reported file must name the sibling that actually exposes infra");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(violations.len(), 1, "{violations:?}");
let file = violations[0]
.file
.as_deref()
.expect("a semantic exposure violation carries its source file");
assert!(
file.ends_with("win_platform.rs"),
"expected the offending sibling win_platform.rs, got {file} — a clean file must never \
be reported as the source of a real violation: {violations:?}"
);
}
#[test]
fn a_cfg_split_module_does_not_let_one_arms_use_alias_shadow_the_others() {
let (metadata, _fixture) = fixture_metadata(
"cfg-split-use-alias-collision",
&[
(
"lib.rs",
"pub mod infra;\npub mod other;\n\
#[cfg(feature = \"u\")] pub mod platform;\n\
#[cfg(feature = \"w\")] #[path = \"win_platform.rs\"] pub mod platform;\n",
),
("infra.rs", "pub struct Db;\n"),
("other.rs", "pub struct Widget;\n"),
(
"platform.rs",
"use crate::infra::Db as Handle;\npub fn leak() -> Handle { unimplemented!() }\n",
),
(
"win_platform.rs",
"use crate::other::Widget as Handle;\npub fn leak2() -> Handle { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::platform")
.must_not_expose("crate::infra")
.because("the unix arm's own Handle alias must resolve to infra, not the windows arm's");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(
violations.len(),
1,
"the unix arm's leak() -> Handle genuinely exposes crate::infra::Db and must react: {violations:?}"
);
}
#[test]
fn a_cfg_sibling_child_module_does_not_shadow_a_different_branchs_own_extern_reexport() {
let out = findings_with_deps(
"cfg-sibling-childmod-shadow",
&[
(
"lib.rs",
"#[cfg(feature = \"u\")] pub mod platform;\n\
#[cfg(feature = \"w\")] #[path = \"win_platform.rs\"] pub mod platform;\n",
),
(
"platform.rs",
"pub mod net { pub struct Something; }\npub fn open() -> u8 { 0 }\n",
),
("win_platform.rs", "pub use net::Something;\n"),
],
"crate::platform",
&["net::Something"],
&["net"],
)
.unwrap();
assert_eq!(
out,
["net::Something exposed by pub use crate::platform::Something"],
"the w branch's own genuine extern re-export must react, regardless of the u branch's \
own local mod net: {out:?}"
);
}
#[test]
fn a_cfg_split_module_with_two_inline_siblings_does_not_let_one_arms_use_alias_shadow_the_others() {
let out = findings(
"cfg-split-inline-inline-use-alias-collision",
&[
(
"lib.rs",
"pub mod infra;\npub mod other;\n\
#[cfg(feature = \"u\")] pub mod platform {\n\
use crate::infra::Db as Handle;\n\
pub fn leak() -> Handle { unimplemented!() }\n}\n\
#[cfg(feature = \"w\")] pub mod platform {\n\
use crate::other::Widget as Handle;\n\
pub fn leak2() -> Handle { unimplemented!() }\n}\n",
),
("infra.rs", "pub struct Db;\n"),
("other.rs", "pub struct Widget;\n"),
],
"crate::platform",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Db exposed by fn crate::platform::leak"],
"the u arm's own Handle alias must resolve to infra, not the w arm's, even though both \
arms are inline and share lib.rs: {out:?}"
);
}
#[test]
fn a_cfg_split_module_with_two_inline_siblings_child_module_does_not_shadow_the_others_extern_reexport()
{
let out = findings_with_deps(
"cfg-split-inline-inline-childmod-shadow",
&[(
"lib.rs",
"#[cfg(feature = \"u\")] pub mod platform {\n\
pub mod net { pub struct Something; }\n\
pub fn open() -> u8 { 0 }\n}\n\
#[cfg(feature = \"w\")] pub mod platform {\n\
pub use net::Something;\n}\n",
)],
"crate::platform",
&["net::Something"],
&["net"],
)
.unwrap();
assert_eq!(
out,
["net::Something exposed by pub use crate::platform::Something"],
"the w arm's own genuine extern re-export must react, regardless of the u arm's own \
local mod net, even though both arms are inline and share lib.rs: {out:?}"
);
}
#[test]
fn async_subtree_observes_both_arms_of_a_two_inline_sibling_cfg_split_anchor() {
let files = &[(
"lib.rs",
"#[cfg(feature = \"u\")] pub mod platform { pub async fn unix_seam() {} }\n\
#[cfg(feature = \"w\")] pub mod platform { pub async fn win_seam() {} }\n",
)];
let mut labels =
async_subtree_labels("inline-inline-cfg-split-anchor", files, "crate::platform");
labels.sort();
assert_eq!(
labels,
[
"async fn crate::platform::unix_seam()",
"async fn crate::platform::win_seam()",
],
"both inline cfg arms' own async fns must be observed, even though they share lib.rs: {labels:?}"
);
}
#[test]
fn a_visibility_violation_carries_its_module_file() {
let (metadata, _fixture) = fixture_metadata(
"vis",
&[
("lib.rs", "pub mod internal;\n"),
("internal.rs", "pub struct Leaked;\n"),
],
);
let boundary = VisibilityBoundary::in_crate("x")
.module("crate::internal")
.must_not_declare_pub()
.because("internal exposes no pub");
let mut violations = Vec::new();
check_visibility_boundary(&metadata, &boundary, &mut violations).unwrap();
assert!(!violations.is_empty(), "a pub item in internal violates");
let file = violations[0]
.file
.as_deref()
.expect("a governed-module file");
assert!(file.ends_with("internal.rs"), "got {file}");
}
#[test]
fn a_trait_impl_locality_violation_carries_its_impl_site_file() {
let (metadata, _fixture) = fixture_metadata(
"locality",
&[
("lib.rs", "pub mod plugins;\npub trait Command {}\n"),
(
"plugins.rs",
"pub struct P;\nimpl crate::Command for P {}\n",
),
],
);
let boundary = TraitImplBoundary::in_crate("x")
.trait_("crate::Command")
.only_implemented_in("crate::allowed")
.because("Command impls live in crate::allowed");
let mut violations = Vec::new();
check_trait_impl_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(violations.len(), 1, "the misplaced impl violates");
let file = violations[0].file.as_deref().expect("the impl site's file");
assert!(file.ends_with("plugins.rs"), "got {file}");
assert_eq!(
violations[0].id(),
violations[0].clone().with_file(None).id()
);
}
#[test]
fn a_trait_impl_in_a_nested_module_resolves_to_mod_rs() {
let (metadata, _fixture) = fixture_metadata(
"locality-nested",
&[
("lib.rs", "pub mod plugins;\npub trait Command {}\n"),
(
"plugins/mod.rs",
"pub struct P;\nimpl crate::Command for P {}\n",
),
],
);
let boundary = TraitImplBoundary::in_crate("x")
.trait_("crate::Command")
.only_implemented_in("crate::allowed")
.because("Command impls live in crate::allowed");
let mut violations = Vec::new();
check_trait_impl_boundary(&metadata, &boundary, &mut violations).unwrap();
let file = violations[0].file.as_deref().expect("the impl site's file");
assert!(file.ends_with("plugins/mod.rs"), "got {file}");
}
#[test]
fn forbidden_marker_impl_and_derive_each_name_their_own_module_file() {
let (metadata, _fixture) = fixture_metadata(
"marker",
&[
(
"lib.rs",
"pub mod internal;\npub mod models;\npub trait Secret {}\n",
),
(
"internal.rs",
"pub struct Bar;\nimpl crate::Secret for Bar {}\n",
),
("models.rs", "#[derive(Secret)]\npub struct Foo;\n"),
],
);
let boundary = ForbiddenMarkerBoundary::in_crate("x")
.module("crate") .must_not_acquire("crate::Secret")
.because("nothing may acquire Secret");
let mut violations = Vec::new();
check_forbidden_marker_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(violations.len(), 2, "one impl finding + one derive finding");
let impl_v = violations
.iter()
.find(|v| v.finding.starts_with("impl "))
.expect("an impl finding");
let derive_v = violations
.iter()
.find(|v| v.finding.starts_with("derive "))
.expect("a derive finding");
assert!(
impl_v.file.as_deref().unwrap().ends_with("internal.rs"),
"impl file: {:?}",
impl_v.file
);
assert!(
derive_v.file.as_deref().unwrap().ends_with("models.rs"),
"derive file is the defining type's module, not an impl site: {:?}",
derive_v.file
);
}
#[test]
fn a_dyn_trait_violation_carries_its_module_file() {
let (metadata, _fixture) = fixture_metadata(
"dyn",
&[
("lib.rs", "pub mod api;\npub trait Port {}\n"),
(
"api.rs",
"pub fn f() -> Box<dyn crate::Port> { unimplemented!() }\n",
),
],
);
let boundary = DynTraitBoundary::in_crate("x")
.module("crate::api")
.must_not_expose_dyn()
.because("the api seam is statically dispatched");
let mut violations = Vec::new();
check_dyn_trait_boundary(&metadata, &boundary, &mut violations).unwrap();
assert!(!violations.is_empty(), "the exposed dyn violates");
let file = violations[0]
.file
.as_deref()
.expect("a governed-module file");
assert!(file.ends_with("api.rs"), "got {file}");
}
#[test]
fn an_impl_trait_violation_carries_its_module_file() {
let (metadata, _fixture) = fixture_metadata(
"impltrait",
&[
("lib.rs", "pub mod api;\n"),
(
"api.rs",
"pub fn f() -> impl Iterator<Item = u8> { std::iter::empty() }\n",
),
],
);
let boundary = ImplTraitBoundary::in_crate("x")
.module("crate::api")
.must_not_expose_impl_trait()
.because("the api seam returns no existential");
let mut violations = Vec::new();
check_impl_trait_boundary(&metadata, &boundary, &mut violations).unwrap();
assert!(!violations.is_empty(), "the returned impl Trait violates");
let file = violations[0]
.file
.as_deref()
.expect("a governed-module file");
assert!(file.ends_with("api.rs"), "got {file}");
}
#[test]
fn an_async_exposure_violation_carries_its_module_file() {
let (metadata, _fixture) = fixture_metadata(
"async",
&[
("lib.rs", "pub mod api;\n"),
("api.rs", "pub async fn f() {}\n"),
],
);
let boundary = AsyncExposureBoundary::in_crate("x")
.module("crate::api")
.must_not_expose_async_fn()
.because("the api seam exposes no async fn");
let mut violations = Vec::new();
check_async_exposure_boundary(&metadata, &boundary, &mut violations).unwrap();
assert!(!violations.is_empty(), "the async fn violates");
let file = violations[0]
.file
.as_deref()
.expect("a governed-module file");
assert!(file.ends_with("api.rs"), "got {file}");
}
#[test]
fn a_facade_chain_reexport_reports_the_governed_module_file_not_the_facades() {
let (metadata, _fixture) = fixture_metadata(
"facade",
&[
(
"lib.rs",
"pub mod infra;\npub mod facade;\npub mod domain;\n",
),
("infra.rs", "pub struct Db;\n"),
("facade.rs", "pub use crate::infra::Db;\n"),
("domain.rs", "pub use crate::facade::Db;\n"),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("domain must not re-export infra");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations).unwrap();
assert_eq!(violations.len(), 1, "the facade-chain re-export violates");
let file = violations[0]
.file
.as_deref()
.expect("a governed-module file");
assert!(
file.ends_with("domain.rs"),
"the seam is in domain.rs, not infra.rs/facade.rs: got {file}"
);
}
#[test]
fn path_remapped_module_resolves_to_its_target_not_the_conventional_orphan() {
let file = resolve_file(
"path-remap",
&[
(
"lib.rs",
"#[path = \"weird.rs\"]\npub mod domain;\npub mod normal;\n",
),
("weird.rs", "pub struct Real;\n"),
("domain.rs", "pub struct Orphan;\n"),
("normal.rs", "pub struct Normal;\n"),
],
"crate::domain",
)
.expect("an unconditional #[path] module now resolves to its target");
let file = file.display().to_string();
assert!(
file.ends_with("weird.rs"),
"the resolver follows #[path] to weird.rs, never the conventional orphan domain.rs: {file}"
);
}
#[test]
fn path_nested_in_an_inline_block_resolves_from_the_accumulated_dir_targeted() {
let file = resolve_file(
"path-inline-targeted",
&[
(
"lib.rs",
"pub mod inline { #[path = \"other.rs\"] pub mod inner; }\n",
),
("inline/other.rs", "pub struct Real;\n"),
("other.rs", "pub struct Decoy;\n"),
],
"crate::inline::inner",
)
.expect("a #[path] nested in an inline block resolves to its accumulated target");
let file = file.display().to_string();
assert!(
file.replace('\\', "/").ends_with("inline/other.rs"),
"the resolver accumulates the inline name: src/inline/other.rs, not the src/other.rs decoy: \
{file}"
);
}
#[test]
fn path_remapped_semantic_module_is_governed_at_its_target_not_the_orphan() {
let (metadata, _fixture) = fixture_metadata(
"semantic-path-remap",
&[
(
"lib.rs",
"#[path = \"weird.rs\"]\npub mod domain;\npub mod infra;\n",
),
("infra.rs", "pub struct Db;\n"),
(
"weird.rs",
"pub fn real() -> crate::infra::Db { unimplemented!() }\n",
),
(
"domain.rs",
"pub fn orphan() -> crate::infra::Db { unimplemented!() }\n",
),
],
);
let boundary = SemanticBoundary::in_crate("x")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("an unconditional #[path] module is governed at its target file");
let mut violations = Vec::new();
check_boundary(&metadata, &boundary, &mut violations)
.expect("the #[path] target resolves and is governed");
let file = violations
.first()
.and_then(|v| v.file.as_deref())
.map(str::to_string);
assert_eq!(violations.len(), 1, "weird.rs's exposure of infra reacts");
let file = file.expect("a governed-module file");
assert!(
file.ends_with("weird.rs"),
"the reaction is in the #[path] target weird.rs, never the conventional orphan domain.rs: {file}"
);
}
#[test]
fn fn1_bare_local_alias_shadowing_a_dependency_resolves_and_reacts() {
for domain in [
"type serde = crate::infra::Db;\ntype X = serde;\npub fn f() -> X { unimplemented!() }\n",
"type X = serde;\ntype serde = crate::infra::Db;\npub fn f() -> X { unimplemented!() }\n",
] {
let out = findings_with_deps(
"fn1-alias-shadow",
&[
("lib.rs", "pub mod infra;\npub mod domain;\n"),
("infra.rs", "pub struct Db;\n"),
("domain.rs", domain),
],
"crate::domain",
&["crate::infra"],
&["serde"],
)
.unwrap();
assert_eq!(
out,
["crate::infra::Db exposed by fn crate::domain::f"],
"source order: {domain}"
);
}
}
#[test]
fn fn2_leading_colon_is_an_unambiguous_extern_through_a_local_shadow() {
let out = findings_with_deps(
"fn2-leading-colon-mod",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub mod serde { pub struct Value; }\npub fn f() -> ::serde::Value { unimplemented!() }\n",
),
],
"crate::domain",
&["serde"],
&["serde"],
)
.unwrap();
assert_eq!(out, ["serde::Value exposed by fn crate::domain::f"]);
}
#[test]
fn fn2_leading_colon_bypasses_the_use_map_no_misattribution() {
let files = &[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use crate::vendor::serde;\npub fn f() -> ::serde::Value { unimplemented!() }\n",
),
];
let reacts = findings_with_deps(
"fn2-usemap-extern",
files,
"crate::domain",
&["serde"],
&["serde"],
)
.unwrap();
assert_eq!(reacts, ["serde::Value exposed by fn crate::domain::f"]);
let no_fp = findings_with_deps(
"fn2-usemap-nofp",
files,
"crate::domain",
&["crate::vendor"],
&["serde"],
)
.unwrap();
assert!(
no_fp.is_empty(),
"leading-:: must not be misattributed to crate::vendor: {no_fp:?}"
);
}
#[test]
fn fn2_leading_colon_alias_target_records_the_extern() {
let out = findings_with_deps(
"fn2-leading-colon-alias",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub mod serde { pub struct Value; }\ntype X = ::serde::Value;\npub fn f() -> X { unimplemented!() }\n",
),
],
"crate::domain",
&["serde"],
&["serde"],
)
.unwrap();
assert_eq!(out, ["serde::Value exposed by fn crate::domain::f"]);
}
#[test]
fn fp1_local_type_named_like_a_dependency_is_not_a_false_positive() {
let clean = findings_with_deps(
"fp1-local-struct",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct serde;\npub fn f() -> serde { serde }\n",
),
],
"crate::domain",
&["serde"],
&["serde"],
)
.unwrap();
assert!(
clean.is_empty(),
"a local `struct serde` shadows the dep; got {clean:?}"
);
let reacts = findings_with_deps(
"fp1-real-extern",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use serde::Value;\npub fn g() -> Value { unimplemented!() }\n",
),
],
"crate::domain",
&["serde"],
&["serde"],
)
.unwrap();
assert_eq!(reacts, ["serde::Value exposed by fn crate::domain::g"]);
}
#[test]
fn fn4_enum_variant_fields_get_per_member_seams() {
let out = findings_with_deps(
"fn4-variant-seam",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub enum E { V(crate::infra::Pool, crate::infra::Pool) }\n",
),
],
"crate::domain",
&["crate::infra"],
&[],
)
.unwrap();
assert_eq!(
out,
[
"crate::infra::Pool exposed by variant crate::domain::E::V::0",
"crate::infra::Pool exposed by variant crate::domain::E::V::1",
]
);
}
#[test]
fn fn2_leading_colon_through_a_crate_root_rename_reacts() {
let via_return = findings_with_deps(
"fn2-leadingcolon-rename-return",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"pub fn make() -> ::wc::spi::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
via_return,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
let via_alias = findings_with_deps(
"fn2-leadingcolon-rename-alias",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"type X = ::wc::spi::Foo;\npub fn make() -> X { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
via_alias,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
}
#[test]
fn dyn_operand_inline_sysroot_trait_reacts() {
let inline = dyn_operand_findings(
"op-inline-std",
&[
("lib.rs", "pub mod m;\n"),
(
"m.rs",
"pub fn f() -> Box<dyn std::error::Error> { todo!() }\n",
),
],
"crate::m",
&["std::error::Error"],
&[],
)
.unwrap();
assert_eq!(inline, ["dyn std::error::Error exposed by fn crate::m::f"]);
let aliased = dyn_operand_findings(
"op-aliased-std",
&[
("lib.rs", "pub mod m;\n"),
(
"m.rs",
"use std::error::Error;\npub fn f() -> Box<dyn Error> { todo!() }\n",
),
],
"crate::m",
&["std::error::Error"],
&[],
)
.unwrap();
assert_eq!(aliased, ["dyn Error exposed by fn crate::m::f"]);
let unlisted = dyn_operand_findings(
"op-unlisted-std",
&[
("lib.rs", "pub mod m;\n"),
(
"m.rs",
"pub fn f() -> Box<dyn std::error::Error> { todo!() }\n",
),
],
"crate::m",
&["crate::ports::Port"],
&[],
)
.unwrap();
assert!(
unlisted.is_empty(),
"unlisted operand must pass: {unlisted:?}"
);
}
#[test]
fn dyn_operand_inline_dependency_and_crate_root_rename_react() {
let inline_dep = dyn_operand_findings(
"op-inline-dep",
&[
("lib.rs", "pub mod m;\n"),
(
"m.rs",
"pub fn f() -> Box<dyn dep::spi::Port> { todo!() }\n",
),
],
"crate::m",
&["dep::spi::Port"],
&["dep"],
)
.unwrap();
assert_eq!(inline_dep, ["dyn dep::spi::Port exposed by fn crate::m::f"]);
let renamed = dyn_operand_findings(
"op-rename-dep",
&[
("lib.rs", "extern crate dep as d;\npub mod m;\n"),
("m.rs", "pub fn f() -> Box<dyn d::spi::Port> { todo!() }\n"),
],
"crate::m",
&["dep::spi::Port"],
&["dep"],
)
.unwrap();
assert_eq!(renamed, ["dyn d::spi::Port exposed by fn crate::m::f"]);
}
#[test]
fn dyn_operand_crate_relative_extern_rename_reacts() {
let out = dyn_operand_findings(
"op-crate-rel-rename",
&[
("lib.rs", "extern crate dep as d;\npub mod m;\n"),
(
"m.rs",
"pub fn f() -> Box<dyn crate::d::Port> { todo!() }\n",
),
],
"crate::m",
&["dep::Port"],
&["dep"],
)
.unwrap();
assert_eq!(out, ["dyn crate::d::Port exposed by fn crate::m::f"]);
}
#[test]
fn dyn_operand_child_shadowed_rename_head_does_not_react() {
let out = dyn_operand_findings(
"op-child-shadow-rename",
&[
("lib.rs", "extern crate dep as d;\npub mod m;\n"),
(
"m.rs",
"pub mod d { pub trait Port {} }\npub fn f() -> Box<dyn d::Port> { todo!() }\n",
),
],
"crate::m",
&["dep::Port"],
&["dep"],
)
.unwrap();
assert!(
out.is_empty(),
"a child-shadowed bare rename head must not react: {out:?}"
);
}
#[test]
fn impl_trait_operand_crate_relative_extern_rename_reacts() {
let out = impl_trait_operand_findings(
"op-impl-crate-rel-rename",
&[
("lib.rs", "extern crate dep as d;\npub mod m;\n"),
("m.rs", "pub fn f() -> impl crate::d::Port { todo!() }\n"),
],
"crate::m",
&["dep::Port"],
&["dep"],
)
.unwrap();
assert_eq!(out, ["impl crate::d::Port exposed by fn crate::m::f"]);
}
#[test]
fn dyn_operand_genuinely_unresolvable_bare_principal_is_a_bound() {
let out = dyn_operand_findings(
"op-unresolvable-bare",
&[
("lib.rs", "pub mod m;\n"),
("m.rs", "pub fn f() -> Box<dyn Frobnicate> { todo!() }\n"),
],
"crate::m",
&["Frobnicate"],
&[],
)
.unwrap();
assert!(
out.is_empty(),
"unresolvable bare principal must stay a bound: {out:?}"
);
}
#[test]
fn impl_trait_operand_inline_sysroot_trait_reacts() {
let inline = impl_trait_operand_findings(
"iop-inline-std",
&[
("lib.rs", "pub mod m;\n"),
("m.rs", "pub fn f() -> impl std::error::Error { todo!() }\n"),
],
"crate::m",
&["std::error::Error"],
&[],
)
.unwrap();
assert_eq!(inline, ["impl std::error::Error exposed by fn crate::m::f"]);
let unlisted = impl_trait_operand_findings(
"iop-unlisted-std",
&[
("lib.rs", "pub mod m;\n"),
("m.rs", "pub fn f() -> impl std::error::Error { todo!() }\n"),
],
"crate::m",
&["crate::ports::Port"],
&[],
)
.unwrap();
assert!(
unlisted.is_empty(),
"unlisted impl-trait operand must pass: {unlisted:?}"
);
}
#[test]
fn reexport_head_shadowed_by_a_child_module_does_not_react() {
let out = findings_with_deps(
"reexport-child-shadow",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub mod dep { pub mod spi { pub struct Foo; } }\npub use dep::spi::Foo;\n",
),
],
"crate::domain",
&["dep::spi"],
&["dep"],
)
.unwrap();
assert_eq!(
out,
Vec::<String>::new(),
"the child-module shadow closes the FP: {out:?}"
);
}
#[test]
fn reexport_head_with_crate_root_module_in_a_child_still_reacts() {
let out = findings_with_deps(
"reexport-crateroot-mod",
&[
(
"lib.rs",
"pub mod dep { pub struct Foo; }\npub mod domain;\n",
),
("domain.rs", "pub use dep::Foo;\n"),
],
"crate::domain",
&["dep"],
&["dep"],
)
.unwrap();
assert_eq!(out, ["dep::Foo exposed by pub use crate::domain::Foo"]);
}
#[test]
fn reexport_head_is_not_suppressed_by_a_same_named_local_struct() {
let out = findings_with_deps(
"reexport-struct-not-module",
&[
("lib.rs", "pub mod domain;\n"),
("domain.rs", "pub struct dep;\npub use dep::spi::Foo;\n"),
],
"crate::domain",
&["dep::spi"],
&["dep"],
)
.unwrap();
assert_eq!(out, ["dep::spi::Foo exposed by pub use crate::domain::Foo"]);
}
#[test]
fn reexport_leading_colon_reacts_despite_a_child_module_shadow() {
let out = findings_with_deps(
"reexport-leading-colon",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub mod dep { pub mod spi { pub struct Foo; } }\npub use ::dep::spi::Foo;\n",
),
],
"crate::domain",
&["dep::spi"],
&["dep"],
)
.unwrap();
assert_eq!(out, ["dep::spi::Foo exposed by pub use crate::domain::Foo"]);
}
#[test]
fn crate_relative_spelling_of_a_crate_root_rename_reacts() {
let out = findings_with_deps(
"crate-alias-crate-relative",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"pub fn make() -> crate::wc::spi::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
}
#[test]
fn crate_relative_rename_behind_a_type_alias_and_reexport_reacts() {
let out = findings_with_deps(
"crate-alias-through-alias",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"type H = crate::wc::spi::Foo;\npub fn make() -> H { unimplemented!() }\npub use crate::wc::spi::Bar;\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
[
"worklane_core::spi::Bar exposed by pub use crate::domain::Bar",
"worklane_core::spi::Foo exposed by fn crate::domain::make",
]
);
}
#[test]
fn bare_rename_head_shadowed_by_a_submodule_child_mod_does_not_react() {
let out = findings_with_deps(
"crate-alias-submodule-shadow",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"pub mod wc { pub mod spi { pub struct Foo; } }\npub fn make() -> wc::spi::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
Vec::<String>::new(),
"the child mod wc shadow closes the FP: {out:?}"
);
}
#[test]
fn bare_rename_head_with_no_local_shadow_still_reacts() {
let out = findings_with_deps(
"crate-alias-no-shadow",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"pub fn make() -> wc::spi::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
["worklane_core::spi::Foo exposed by fn crate::domain::make"]
);
}
#[test]
fn a_deeper_crate_relative_alias_segment_is_not_rewritten() {
let out = findings_with_deps(
"crate-alias-deeper-segment",
&[
(
"lib.rs",
"extern crate worklane_core as wc;\npub mod domain;\n",
),
(
"domain.rs",
"pub mod m { pub mod wc { pub mod spi { pub struct Foo; } } }\npub fn make() -> crate::m::wc::spi::Foo { unimplemented!() }\n",
),
],
"crate::domain",
&["worklane_core::spi"],
&["worklane_core"],
)
.unwrap();
assert_eq!(
out,
Vec::<String>::new(),
"a deeper crate::m::wc is local, not the rename: {out:?}"
);
}
#[test]
fn impl_self_type_spelled_through_a_reexport_reacts() {
let out = marker_findings(
"mark-reexport-selftype",
&[
("lib.rs", "pub mod domain;\npub mod wire;\n"),
("domain.rs", "pub struct Order;\n"),
(
"wire.rs",
"pub use crate::domain::Order;\nimpl serde::Serialize for crate::wire::Order {}\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(
out,
["impl serde::Serialize for crate::wire::Order in crate::wire"]
);
}
#[test]
fn impl_self_type_use_imported_from_a_reexport_reacts() {
let out = marker_findings(
"mark-reexport-use-selftype",
&[
(
"lib.rs",
"pub mod domain;\npub mod wire;\npub mod client;\n",
),
("domain.rs", "pub struct Order;\n"),
("wire.rs", "pub use crate::domain::Order;\n"),
(
"client.rs",
"use crate::wire::Order;\nimpl serde::Serialize for Order {}\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(
out,
["impl serde::Serialize for crate::wire::Order in crate::client"]
);
}
#[test]
fn impl_self_type_through_an_alias_to_a_local_struct_still_reacts() {
let out = marker_findings(
"mark-alias-local-struct",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Real;\ntype Bar = Real;\nimpl serde::Serialize for Bar {}\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(
out,
["impl serde::Serialize for crate::domain::Bar in crate::domain"]
);
}
#[test]
fn impl_self_type_interleaved_alias_then_reexport_reacts() {
let out = marker_findings(
"mark-alias-then-reexport",
&[
("lib.rs", "pub mod domain;\npub mod wire;\npub mod mid;\n"),
("domain.rs", "pub struct Order;\n"),
("wire.rs", "pub use crate::domain::Order as Reexp;\n"),
(
"mid.rs",
"type Alias = crate::wire::Reexp;\nimpl serde::Serialize for Alias {}\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(
out,
["impl serde::Serialize for crate::mid::Alias in crate::mid"]
);
}
#[test]
fn impl_self_type_alias_to_a_foreign_type_stays_clean() {
let out = marker_findings(
"mark-alias-foreign",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"pub struct Real;\ntype Baz = String;\nimpl serde::Serialize for Baz {}\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn impl_of_a_locally_renamed_trait_reacts_by_true_leaf() {
let out = marker_findings(
"mark-rename-impl",
&[
("lib.rs", "pub mod domain;\npub mod wire;\n"),
("domain.rs", "pub struct Order;\n"),
(
"wire.rs",
"use serde::Serialize as Ser;\nimpl Ser for crate::domain::Order {}\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(out, ["impl Ser for crate::domain::Order in crate::wire"]);
}
#[test]
fn derive_of_a_locally_renamed_macro_reacts_by_true_leaf() {
let out = marker_findings(
"mark-rename-derive",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"use serde::Serialize as Ser;\n#[derive(Ser)]\npub struct Order;\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(out, ["derive Ser on crate::domain::Order"]);
}
#[test]
fn derive_renamed_to_a_nonforbidden_local_trait_stays_clean() {
let out = marker_findings(
"mark-rename-collision",
&[
("lib.rs", "pub mod domain;\npub mod other;\n"),
("other.rs", "pub struct Bar;\n"),
(
"domain.rs",
"use crate::other::Bar as Serialize;\n#[derive(Serialize)]\npub struct Order;\n",
),
],
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(out, Vec::<String>::new());
}
#[test]
fn hunyi_boundary_depth_getters_and_delegation_parity() {
use xuanji::ScanDepth;
let async_b = AsyncExposureBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_async_fn()
.because("sync core");
assert_eq!(async_b.scan_depth(), ScanDepth::Shallow);
let async_subtree = AsyncExposureBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_async_fn()
.depth(ScanDepth::Subtree)
.because("sync core subtree");
assert_eq!(async_subtree.scan_depth(), ScanDepth::Subtree);
let async_submodules = AsyncExposureBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_async_fn()
.including_submodules()
.because("sync core submodules");
assert_eq!(async_submodules.scan_depth(), ScanDepth::Subtree);
let impl_b = ImplTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_impl_trait()
.because("no RPIT leak");
assert_eq!(impl_b.scan_depth(), ScanDepth::Shallow);
let impl_subtree = ImplTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_impl_trait()
.depth(ScanDepth::Subtree)
.because("no RPIT leak in subtree");
assert_eq!(impl_subtree.scan_depth(), ScanDepth::Subtree);
let impl_submodules = ImplTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_impl_trait()
.including_submodules()
.because("no RPIT leak in submodules");
assert_eq!(impl_submodules.scan_depth(), ScanDepth::Subtree);
}
#[test]
fn non_generic_compound_type_alias_target_walk_detects_nested_exposure() {
let out = findings(
"compound-alias-walk",
&[
("lib.rs", "pub mod domain;\npub mod infra;\n"),
(
"domain.rs",
"pub type TupleAlias = (crate::infra::DbPool, u32);\n\
pub type RefAlias = &'static crate::infra::DbPool;\n\
pub type SliceAlias = [crate::infra::DbPool];\n\
pub fn leak_tuple() -> TupleAlias { loop {} }\n\
pub fn leak_ref() -> RefAlias { loop {} }\n\
pub fn leak_slice() -> &'static SliceAlias { loop {} }\n",
),
("infra.rs", "pub struct DbPool;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out.len(),
6,
"6 exposures (3 type aliases + 3 functions using them) must be detected: {out:?}"
);
}
#[test]
fn tuple_alias_with_forbidden_type_in_first_position_and_private_helper_reacts() {
let out = findings(
"tuple-alias-first-pos",
&[
("lib.rs", "pub mod domain;\npub mod infra;\npub mod api;\n"),
(
"domain.rs",
"type PrivateHelper = (crate::infra::DbPool, crate::api::Public);\n\
pub fn leak_first_pos() -> PrivateHelper { loop {} }\n",
),
("infra.rs", "pub struct DbPool;\n"),
("api.rs", "pub struct Public;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out.len(),
1,
"private helper tuple alias with forbidden type in first position must react: {out:?}"
);
assert!(out[0].contains("crate::infra::DbPool exposed by fn crate::domain::leak_first_pos"));
}
#[test]
fn raw_pointer_type_alias_target_walk_detects_nested_exposure() {
let out = findings(
"ptr-alias-walk",
&[
("lib.rs", "pub mod domain;\npub mod infra;\n"),
(
"domain.rs",
"pub type PtrAlias = *const crate::infra::DbPool;\n\
pub fn leak_ptr() -> PtrAlias { loop {} }\n",
),
("infra.rs", "pub struct DbPool;\n"),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out.len(),
2,
"raw pointer type alias declaration and function return type must react: {out:?}"
);
}
#[test]
fn wide_tuple_alias_with_forbidden_first_member_expands_without_truncation() {
let out = findings(
"wide-tuple-alias",
&[
("lib.rs", "pub mod domain;\npub mod infra;\npub mod api;\n"),
(
"domain.rs",
"type Inner = crate::infra::Secret;\n\
type Wide = (Inner, crate::api::A, crate::api::B, crate::api::C, crate::api::D, crate::api::E);\n\
pub fn leak_wide() -> Wide { loop {} }\n",
),
("infra.rs", "pub struct Secret;\n"),
(
"api.rs",
"pub struct A;\npub struct B;\npub struct C;\npub struct D;\npub struct E;\n",
),
],
"crate::domain",
&["crate::infra"],
)
.unwrap();
assert_eq!(
out,
vec!["crate::infra::Secret exposed by fn crate::domain::leak_wide"],
"wide tuple alias with 6 resolvable targets and forbidden target first must expand without truncation: {out:?}"
);
}
#[test]
fn diamond_alias_expansion_does_not_leak_intermediate_aliases() {
use crate::resolve::{AliasMap, ReexportMap, expand_canonical_paths};
let mut aliases = AliasMap::new();
aliases.insert(
"crate::domain::Mid".to_string(),
vec!["crate::infra::Secret".to_string()],
);
aliases.insert(
"crate::domain::Other".to_string(),
vec!["crate::domain::Mid".to_string()],
);
aliases.insert(
"crate::domain::Diamond".to_string(),
vec![
"crate::domain::Mid".to_string(),
"crate::domain::Other".to_string(),
],
);
let reexports = ReexportMap::new();
let expanded = expand_canonical_paths("crate::domain::Diamond", &aliases, &reexports);
assert_eq!(
expanded,
vec!["crate::infra::Secret"],
"diamond alias expansion must yield strictly terminal target without intermediate alias leakage: {expanded:?}"
);
}