use super::support::{borrowed_files, build_workspace, three_layer};
use crate::adapters::analyzers::architecture::call_parity_rule::hint::{
collect_private_candidates, format_hint, PrivateCandidate,
};
use crate::adapters::analyzers::architecture::call_parity_rule::local_symbols::{
collect_workspace_module_paths, WorkspaceLookup,
};
use crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::collect_crate_root_modules;
use std::collections::HashSet;
fn candidates_of(path: &str, src: &str) -> Vec<PrivateCandidate> {
let ws = build_workspace(&[(path, src)]);
let borrowed = borrowed_files(&ws);
let crate_root_modules = collect_crate_root_modules(&borrowed);
let workspace_module_paths = collect_workspace_module_paths(&borrowed);
let cfg_test = HashSet::new();
let workspace = WorkspaceLookup {
cfg_test_files: &cfg_test,
crate_root_modules: &crate_root_modules,
workspace_module_paths: &workspace_module_paths,
};
collect_private_candidates(
&borrowed,
&ws.aliases_per_file,
&three_layer(),
&HashSet::new(),
&workspace,
)
}
fn has_candidate(cands: &[PrivateCandidate], fn_name: &str) -> bool {
cands.iter().any(|c| c.fn_name == fn_name)
}
#[test]
fn free_private_attributed_fn_is_recorded() {
let cands = candidates_of("src/application/h.rs", "#[handler]\nfn make_widget() {}");
assert!(
has_candidate(&cands, "make_widget"),
"free attributed fn collected: {cands:?}"
);
let c = cands
.iter()
.find(|c| c.fn_name == "make_widget")
.expect("present");
assert_eq!(c.attr_names, vec!["handler".to_string()], "attr recorded");
}
#[test]
fn fn_in_pub_inline_mod_is_recorded() {
let cands = candidates_of(
"src/application/h.rs",
"pub mod inner { #[handler] fn nested_fn() {} }",
);
assert!(
has_candidate(&cands, "nested_fn"),
"fn inside pub mod collected: {cands:?}"
);
}
#[test]
fn fn_in_private_inline_mod_is_not_recorded() {
let cands = candidates_of(
"src/application/h.rs",
"#[handler] fn top_fn() {}\nmod inner { #[handler] fn buried_fn() {} }",
);
assert!(
has_candidate(&cands, "top_fn"),
"control present: {cands:?}"
);
assert!(
!has_candidate(&cands, "buried_fn"),
"fn in private mod skipped: {cands:?}"
);
}
#[test]
fn fn_in_invisible_impl_is_not_recorded() {
let cands = candidates_of(
"src/application/h.rs",
"struct Hidden;\nimpl Hidden { #[handler] fn method_fn() {} }\n#[handler] fn free_fn() {}",
);
assert!(
has_candidate(&cands, "free_fn"),
"control present: {cands:?}"
);
assert!(
!has_candidate(&cands, "method_fn"),
"method on private type skipped: {cands:?}"
);
}
#[test]
fn test_attributed_fn_is_not_recorded() {
let cands = candidates_of(
"src/application/h.rs",
"#[test]\n#[handler]\nfn test_like() {}\n#[handler]\nfn real() {}",
);
assert!(has_candidate(&cands, "real"), "control present: {cands:?}");
assert!(
!has_candidate(&cands, "test_like"),
"#[test] fn skipped despite handler attr: {cands:?}"
);
}
fn candidate(file: &str, fn_name: &str) -> PrivateCandidate {
PrivateCandidate {
canonical: format!("crate::cli::{fn_name}"),
file: file.to_string(),
line: 1,
fn_name: fn_name.to_string(),
layer: Some("cli".to_string()),
attr_names: vec!["handler".to_string()],
}
}
#[test]
fn format_hint_uses_singular_vs_plural_per_count() {
let a = candidate("src/cli/a.rs", "alpha");
let b = candidate("src/cli/b.rs", "beta");
let singular = format_hint(&[("cli".to_string(), vec![&a])]);
assert!(
singular.contains("1 private fn in cli") && singular.contains("transitively reaches"),
"singular noun + verb: {singular}"
);
let plural = format_hint(&[("cli".to_string(), vec![&a, &b])]);
assert!(
plural.contains("2 private fns in cli") && plural.contains("transitively reach this"),
"plural noun + verb: {plural}"
);
}