use crate::adapters::analyzers::architecture::trait_contract_rule::{
check_trait_contracts, CompiledTraitContract,
};
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use globset::{Glob, GlobSet, GlobSetBuilder};
fn parse(src: &str) -> syn::File {
syn::parse_str(src).expect("test fixture must parse")
}
fn globset(patterns: &[&str]) -> GlobSet {
let mut b = GlobSetBuilder::new();
for p in patterns {
b.add(Glob::new(p).expect("valid glob"));
}
b.build().expect("valid glob set")
}
fn empty() -> CompiledTraitContract {
CompiledTraitContract {
name: "t".into(),
scope: globset(&["**/*.rs"]),
receiver_may_be: None,
required_param_type_contains: None,
forbidden_return_type_contains: Vec::new(),
forbidden_error_variant_contains: Vec::new(),
error_types: Vec::new(),
methods_must_be_async: None,
must_be_object_safe: None,
required_supertraits_contain: Vec::new(),
}
}
fn run(file: &str, src: &str, rule: &CompiledTraitContract) -> Vec<MatchLocation> {
let ast = parse(src);
check_trait_contracts(&[(file.to_string(), &ast)], std::slice::from_ref(rule))
}
fn checks(hits: &[MatchLocation]) -> Vec<&'static str> {
hits.iter()
.filter_map(|h| match &h.kind {
ViolationKind::TraitContract { check, .. } => Some(*check),
_ => None,
})
.collect()
}
#[test]
fn out_of_scope_file_is_skipped() {
let mut rule = empty();
rule.scope = globset(&["src/ports/**"]);
rule.methods_must_be_async = Some(true);
let src = "pub trait Svc { fn f(&self); }";
let hits = run("src/other/x.rs", src, &rule);
assert!(hits.is_empty());
}
#[test]
fn non_trait_items_are_ignored() {
let mut rule = empty();
rule.methods_must_be_async = Some(true);
let src = r#"
pub fn plain() {}
pub struct S;
impl S { pub fn f(&self) {} }
"#;
assert!(run("any.rs", src, &rule).is_empty());
}
#[test]
fn receiver_shared_ref_only_flags_mut_receivers() {
let mut rule = empty();
rule.receiver_may_be = Some(vec!["shared_ref".into()]);
let src = r#"
pub trait Svc {
fn read(&self);
fn write(&mut self);
fn consume(self);
}
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(checks(&hits), vec!["receiver", "receiver"]);
}
#[test]
fn receiver_any_accepts_all_forms() {
let mut rule = empty();
rule.receiver_may_be = Some(vec!["any".into()]);
let src = r#"
pub trait Svc {
fn read(&self);
fn write(&mut self);
fn consume(self);
}
"#;
assert!(run("any.rs", src, &rule).is_empty());
}
#[test]
fn receiver_associated_fn_without_receiver_not_flagged() {
let mut rule = empty();
rule.receiver_may_be = Some(vec!["shared_ref".into()]);
let src = "pub trait Build { fn make() -> Self where Self: Sized; }";
assert!(run("any.rs", src, &rule).is_empty());
}
#[test]
fn methods_must_be_async_flags_sync_methods() {
let mut rule = empty();
rule.methods_must_be_async = Some(true);
let src = r#"
pub trait Svc {
async fn a(&self);
fn b(&self);
}
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(checks(&hits), vec!["async"]);
}
#[test]
fn forbidden_return_type_matches_substring() {
let mut rule = empty();
rule.forbidden_return_type_contains = vec!["anyhow::".into(), "Box<dyn".into()];
let src = r#"
pub trait Svc {
fn a(&self) -> anyhow::Result<()>;
fn b(&self) -> Result<Box<dyn std::error::Error>, ()>;
fn c(&self) -> Result<(), String>;
}
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(checks(&hits), vec!["return_type", "return_type"]);
}
#[test]
fn required_param_fires_when_none_of_the_params_match() {
let mut rule = empty();
rule.required_param_type_contains = Some("CancellationToken".into());
let src = r#"
pub trait Svc {
fn with_ctx(&self, ctx: CancellationToken);
fn without(&self, path: String);
}
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(checks(&hits), vec!["required_param"]);
}
#[test]
fn required_supertraits_flags_missing_bound() {
let mut rule = empty();
rule.required_supertraits_contain = vec!["Send".into(), "Sync".into()];
let src = r#"
pub trait A: Send + Sync {}
pub trait B: Send {}
pub trait C {}
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(
checks(&hits),
vec!["supertrait", "supertrait", "supertrait"]
);
}
#[test]
fn must_be_object_safe_flags_self_return() {
let mut rule = empty();
rule.must_be_object_safe = Some(true);
let src = r#"
pub trait A { fn clone_box(&self) -> Self; }
pub trait B { fn do_it(&self) -> (); }
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(checks(&hits), vec!["object_safety"]);
}
#[test]
fn must_be_object_safe_flags_generic_method() {
let mut rule = empty();
rule.must_be_object_safe = Some(true);
let src = r#"
pub trait A { fn cast<T>(&self, x: T); }
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(checks(&hits), vec!["object_safety"]);
}
#[test]
fn error_variant_substring_flagged_via_naming() {
let mut rule = empty();
rule.forbidden_error_variant_contains = vec!["syn::".into()];
let src = r#"
pub enum MyError {
Parse(syn::Error),
Other(String),
}
pub trait Svc { fn f(&self) -> Result<(), MyError>; }
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(checks(&hits), vec!["error_variant"]);
}
#[test]
fn fully_compliant_trait_has_no_hits() {
let mut rule = empty();
rule.receiver_may_be = Some(vec!["shared_ref".into()]);
rule.methods_must_be_async = Some(true);
rule.forbidden_return_type_contains = vec!["anyhow::".into()];
rule.required_supertraits_contain = vec!["Send".into(), "Sync".into()];
rule.must_be_object_safe = Some(true);
let src = r#"
pub trait Svc: Send + Sync {
async fn read(&self) -> Result<String, MyError>;
}
"#;
assert!(run("any.rs", src, &rule).is_empty());
}
#[test]
fn trait_inside_inline_module_is_checked() {
let mut rule = empty();
rule.forbidden_return_type_contains = vec!["anyhow::".into()];
let src = r#"
mod inner {
pub trait Svc {
fn read(&self) -> anyhow::Result<()>;
}
}
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(
checks(&hits),
vec!["return_type"],
"trait defined inside `mod inner {{ ... }}` must be checked"
);
}
#[test]
fn trait_in_nested_inline_module_is_checked() {
let mut rule = empty();
rule.forbidden_return_type_contains = vec!["anyhow::".into()];
let src = r#"
mod outer {
pub mod middle {
pub mod inner {
pub trait Svc {
fn read(&self) -> anyhow::Result<()>;
}
}
}
}
"#;
let hits = run("any.rs", src, &rule);
assert_eq!(
checks(&hits),
vec!["return_type"],
"traits inside deeply nested inline modules must be checked"
);
}