use crate::adapters::analyzers::architecture::call_parity_rule::pub_fns::{
collect_pub_fns_by_layer, PubFnInfo,
};
use crate::adapters::analyzers::architecture::layer_rule::LayerDefinitions;
use crate::adapters::shared::use_tree::gather_alias_map;
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::collections::{HashMap, HashSet};
fn aliases_from_files(
files: &[(&str, &syn::File)],
) -> HashMap<String, HashMap<String, Vec<String>>> {
files
.iter()
.map(|(p, f)| (p.to_string(), gather_alias_map(f)))
.collect()
}
fn parse(src: &str) -> syn::File {
syn::parse_str(src).expect("parse file")
}
fn globset(patterns: &[&str]) -> GlobSet {
let mut b = GlobSetBuilder::new();
for p in patterns {
b.add(Glob::new(p).unwrap());
}
b.build().unwrap()
}
fn adapter_layers() -> LayerDefinitions {
LayerDefinitions::new(
vec![
"application".to_string(),
"cli".to_string(),
"mcp".to_string(),
],
vec![
("application".to_string(), globset(&["src/application/**"])),
("cli".to_string(), globset(&["src/cli/**"])),
("mcp".to_string(), globset(&["src/mcp/**"])),
],
)
}
fn names_for_layer<'ast>(
by_layer: &std::collections::HashMap<String, Vec<PubFnInfo<'ast>>>,
layer: &str,
) -> HashSet<String> {
by_layer
.get(layer)
.map(|fns| fns.iter().map(|f| f.fn_name.clone()).collect())
.unwrap_or_default()
}
#[test]
fn test_collect_pub_fns_in_layer_free_fn() {
let file = parse(
r#"
pub fn cmd_stats() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
let cli = names_for_layer(&by_layer, "cli");
assert!(cli.contains("cmd_stats"), "cli = {cli:?}");
}
#[test]
fn test_collect_pub_fns_skips_private_fns() {
let file = parse(
r#"
fn helper() {}
pub fn cmd_stats() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
let cli = names_for_layer(&by_layer, "cli");
assert!(cli.contains("cmd_stats"));
assert!(!cli.contains("helper"), "private fn must be skipped");
}
#[test]
fn test_pub_crate_is_treated_as_public_for_intra_crate_layers() {
let file = parse(
r#"
pub(crate) fn cmd_stats() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
let cli = names_for_layer(&by_layer, "cli");
assert!(cli.contains("cmd_stats"), "pub(crate) must be collected");
}
#[test]
fn test_pub_super_and_pub_in_path_treated_as_public() {
let file = parse(
r#"
pub(super) fn cmd_a() {}
pub(in crate::cli) fn cmd_b() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
let cli = names_for_layer(&by_layer, "cli");
assert!(cli.contains("cmd_a"));
assert!(cli.contains("cmd_b"));
}
#[test]
fn test_collect_pub_fns_collects_pub_impl_methods_for_pub_type() {
let file = parse(
r#"
pub struct Session;
impl Session {
pub fn search(&self) {}
fn helper(&self) {}
}
"#,
);
let files = vec![("src/application/session.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
let app = names_for_layer(&by_layer, "application");
assert!(app.contains("search"), "pub impl method must be collected");
assert!(
!app.contains("helper"),
"private impl method must be skipped"
);
}
#[test]
fn test_collect_pub_fns_recognises_impl_across_files() {
let decl_file = parse("pub struct Session;");
let impl_file = parse(
r#"
impl Session {
pub fn search(&self) {}
}
"#,
);
let files = vec![
("src/application/session.rs", &decl_file),
("src/application/session_impls.rs", &impl_file),
];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
let app = names_for_layer(&by_layer, "application");
assert!(
app.contains("search"),
"cross-file impl on pub type must be collected, got {app:?}"
);
}
#[test]
fn test_collect_pub_fns_skips_impl_methods_on_private_type() {
let file = parse(
r#"
struct Session;
impl Session {
pub fn search(&self) {}
}
"#,
);
let files = vec![("src/application/session.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
let app = names_for_layer(&by_layer, "application");
assert!(
!app.contains("search"),
"impl on private type must be skipped"
);
}
#[test]
fn test_collect_pub_fns_groups_by_layer() {
let cli_file = parse("pub fn cmd_stats() {}");
let mcp_file = parse("pub fn handle_stats() {}");
let app_file = parse("pub fn get_stats() {}");
let files = vec![
("src/cli/handlers.rs", &cli_file),
("src/mcp/handlers.rs", &mcp_file),
("src/application/stats.rs", &app_file),
];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
assert_eq!(
names_for_layer(&by_layer, "cli"),
["cmd_stats".to_string()].into()
);
assert_eq!(
names_for_layer(&by_layer, "mcp"),
["handle_stats".to_string()].into()
);
assert_eq!(
names_for_layer(&by_layer, "application"),
["get_stats".to_string()].into()
);
}
#[test]
fn test_collect_pub_fns_skips_cfg_test_files() {
let file = parse("pub fn cmd_stats() {}");
let files = vec![("src/cli/handlers.rs", &file)];
let mut cfg_test = HashSet::new();
cfg_test.insert("src/cli/handlers.rs".to_string());
let aliases = aliases_from_files(&files);
let by_layer = collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &cfg_test);
assert!(
names_for_layer(&by_layer, "cli").is_empty(),
"cfg-test file must be skipped wholesale"
);
}
#[test]
fn test_collect_pub_fns_skips_test_attr_fns() {
let file = parse(
r#"
#[test]
pub fn not_a_handler() {}
pub fn cmd_stats() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
let cli = names_for_layer(&by_layer, "cli");
assert!(cli.contains("cmd_stats"));
assert!(!cli.contains("not_a_handler"), "#[test] fn must be skipped");
}
#[test]
fn test_collect_pub_fns_skips_unmatched_files() {
let file = parse("pub fn free_floating() {}");
let files = vec![("src/utils/misc.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(&files, &aliases, &adapter_layers(), &HashSet::new())
};
for layer in ["application", "cli", "mcp"] {
assert!(
names_for_layer(&by_layer, layer).is_empty(),
"layer {layer} must be empty"
);
}
}