use super::super::*;
use super::helpers::*;
use super::impl_trait::impl_trait_subtree;
use super::unsafe_confinement::unsafe_labels;
pub(super) 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]
pub(super) 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]
pub(super) fn must_not_acquire_rejects_a_malformed_colon_operand() {
let files: &[(&str, &str)] = &[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"#[derive(serde::Serialize)]\npub struct Order;\n",
),
];
for bad in [
"::serde::Serialize",
"serde::Serialize::",
"::serde::Serialize::",
] {
let err = marker_findings("marker-malformed", files, "crate::domain", &[bad]).unwrap_err();
assert!(
err.contains(bad),
"constitution error must name the malformed operand {bad:?}: {err}"
);
}
let empty_err =
marker_findings("marker-malformed-empty", files, "crate::domain", &[""]).unwrap_err();
assert!(
empty_err.contains("is empty"),
"constitution error must flag the empty operand: {empty_err}"
);
let clean = marker_findings(
"marker-malformed-control",
files,
"crate::domain",
&["serde::Serialize"],
)
.unwrap();
assert_eq!(clean, ["derive serde::Serialize on crate::domain::Order"]);
}
#[test]
pub(super) 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]
pub(super) 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]
pub(super) fn forbidden_derive_leaf_reacts_when_the_forbidden_alias_is_declared_first() {
let out = marker_findings(
"derive-cfg-forbidden-first",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"#[cfg(unix)]\nuse bad::Marker as M;\n#[cfg(not(unix))]\nuse good::NotBad as M;\n#[derive(M)]\npub struct Order;\n",
),
],
"crate::domain",
&["bad::Marker"],
)
.unwrap();
assert_eq!(out, ["derive M on crate::domain::Order"]);
}
#[test]
pub(super) fn forbidden_derive_leaf_reacts_when_the_forbidden_alias_is_declared_second() {
let out = marker_findings(
"derive-cfg-forbidden-second",
&[
("lib.rs", "pub mod domain;\n"),
(
"domain.rs",
"#[cfg(not(unix))]\nuse good::NotBad as M;\n#[cfg(unix)]\nuse bad::Marker as M;\n#[derive(M)]\npub struct Order;\n",
),
],
"crate::domain",
&["bad::Marker"],
)
.unwrap();
assert_eq!(out, ["derive M on crate::domain::Order"]);
}
#[test]
pub(super) fn forbidden_impl_trait_leaf_reacts_when_the_forbidden_alias_is_declared_first() {
let out = marker_findings(
"impl-cfg-forbidden-first",
&[
("lib.rs", "pub mod domain;\npub mod wire;\n"),
("domain.rs", "pub struct Order;\n"),
(
"wire.rs",
"#[cfg(unix)]\nuse bad::Marker as M;\n#[cfg(not(unix))]\nuse good::NotBad as M;\nimpl M for crate::domain::Order {}\n",
),
],
"crate::domain",
&["bad::Marker"],
)
.unwrap();
assert_eq!(out, ["impl M for crate::domain::Order in crate::wire"]);
}
#[test]
pub(super) fn forbidden_impl_trait_leaf_reacts_when_the_forbidden_alias_is_declared_second() {
let out = marker_findings(
"impl-cfg-forbidden-second",
&[
("lib.rs", "pub mod domain;\npub mod wire;\n"),
("domain.rs", "pub struct Order;\n"),
(
"wire.rs",
"#[cfg(not(unix))]\nuse good::NotBad as M;\n#[cfg(unix)]\nuse bad::Marker as M;\nimpl M for crate::domain::Order {}\n",
),
],
"crate::domain",
&["bad::Marker"],
)
.unwrap();
assert_eq!(out, ["impl M for crate::domain::Order in crate::wire"]);
}
#[test]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) fn a_deeply_nested_acyclic_module_tree_is_a_scan_error_not_a_stack_overflow() {
let depth = 60;
let source = format!(
"{}pub struct Leaf;{}\n",
"pub mod a{".repeat(depth),
"}".repeat(depth)
);
let tree = TempSrcTree::new("deep-acyclic-walk-module");
tree.write("lib.rs", &source);
let result = forbidden_marker_findings(tree.src(), &tree.root(), "crate", &[], "x");
let err = result.expect_err(
"a deeply nested but acyclic module tree must be a scan error, not a hang/overflow",
);
assert!(
err.contains("depth bound"),
"the error must name the depth bound it could not judge past: {err}"
);
}
#[test]
pub(super) fn a_deeply_nested_acyclic_subtree_walk_is_a_scan_error_not_a_stack_overflow() {
let depth = 60;
let source = format!(
"{}pub struct Leaf;{}\n",
"pub mod a{".repeat(depth),
"}".repeat(depth)
);
let files = &[("lib.rs", source.as_str())];
let err = impl_trait_subtree("deep-acyclic-subtree", files, "crate")
.expect_err("a deeply nested but acyclic subtree walk must be a scan error, not a hang");
assert!(
err.contains("depth bound"),
"the error must name the depth bound it could not judge past: {err}"
);
}
#[test]
pub(super) fn a_deeply_nested_acyclic_unsafe_walk_is_a_scan_error_not_a_stack_overflow() {
let depth = 60;
let source = format!(
"{}pub struct Leaf;{}\n",
"pub mod a{".repeat(depth),
"}".repeat(depth)
);
let err = unsafe_labels(
"deep-acyclic-unsafe",
&[("lib.rs", &source)],
&["crate::ffi"],
)
.expect_err("a deeply nested but acyclic unsafe walk must be a scan error, not a hang");
assert!(
err.contains("depth bound"),
"the error must name the depth bound it could not judge past: {err}"
);
}
#[test]
pub(super) fn a_moderately_nested_module_tree_still_observes_a_real_violation() {
let depth = 20;
let source = format!(
"{}#[derive(serde::Serialize)]\npub struct Order;{}\n",
"pub mod a{".repeat(depth),
"}".repeat(depth)
);
let out = marker_findings(
"moderate-depth",
&[("lib.rs", &source)],
"crate",
&["serde::Serialize"],
)
.expect("nesting well under the depth bound must still be judged, not error");
assert_eq!(out.len(), 1, "{out:?}");
assert!(out[0].contains("Order"), "{out:?}");
}
#[test]
pub(super) 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]
pub(super) 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]
pub(super) 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);
}
#[test]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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:?}"
);
}