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(),
&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(),
&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(),
&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(),
&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(),
&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#"
use crate::application::session::Session;
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(),
&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(),
&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(),
&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,
&HashSet::new(),
);
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(),
&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(),
&HashSet::new(),
)
};
for layer in ["application", "cli", "mcp"] {
assert!(
names_for_layer(&by_layer, layer).is_empty(),
"layer {layer} must be empty"
);
}
}
#[test]
fn test_collect_pub_fns_skips_pub_fn_inside_private_inline_mod() {
let file = parse(
r#"
mod private {
pub fn helper() {}
}
pub fn visible_top() {}
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("visible_top"),
"top-level pub fn must be recorded, got {cli:?}"
);
assert!(
!cli.contains("helper"),
"pub fn inside private inline mod must be skipped, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_treats_pub_self_as_private() {
let file = parse(
r#"
pub(self) fn helper() {}
pub fn visible() {}
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("visible"),
"plain `pub fn` must be recorded, got {cli:?}"
);
assert!(
!cli.contains("helper"),
"`pub(self) fn` is private-equivalent and must be skipped, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_skips_impl_method_on_type_in_private_inline_mod() {
let file = parse(
r#"
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
!cli.contains("op"),
"impl method on type in private mod must be skipped, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_records_impl_in_private_mod_for_public_type() {
let file = parse(
r#"
pub struct Session;
mod methods {
impl super::Session {
pub fn diff(&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(),
&HashSet::new(),
)
};
let app = names_for_layer(&by_layer, "application");
assert!(
app.contains("diff"),
"impl in private mod for public type must be recorded, got {app:?}"
);
}
#[test]
fn test_collect_pub_fns_records_impl_via_nested_pub_use_export_path() {
let file = parse(
r#"
pub mod outer {
mod private {
pub struct Hidden;
}
pub use self::private::Hidden;
}
impl outer::Hidden {
pub fn op(&self) {}
}
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"impl on nested-mod re-export path must be recorded, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_records_impl_via_chained_type_alias() {
let file = parse(
r#"
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
type Inner = private::Hidden;
pub type Public = Inner;
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"alias chain target's impl method must be recorded, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_does_not_promote_bare_local_arc() {
let file = parse(
r#"
mod wrap { pub struct Arc<T>(T); }
use crate::wrap::Arc;
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
pub type Public = Arc<private::Hidden>;
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
!cli.contains("op"),
"bare Arc shadowed by local must not auto-peel, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_peels_qualified_user_wrapper() {
let file = parse(
r#"
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
pub type Public = axum::extract::State<private::Hidden>;
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let mut wrappers = HashSet::new();
wrappers.insert("State".to_string());
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(
&files,
&aliases,
&adapter_layers(),
&HashSet::new(),
&wrappers,
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"fully-qualified user wrapper must peel via leaf, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_does_not_promote_qualified_local_arc() {
let file = parse(
r#"
mod wrap { pub struct Arc<T>(T); }
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
pub type Public = wrap::Arc<private::Hidden>;
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
!cli.contains("op"),
"qualified local Arc must not auto-peel as stdlib Arc, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_does_not_promote_local_wrapper_alias() {
let file = parse(
r#"
use crate::wrap::Arc as Shared;
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
pub type Public = Shared<private::Hidden>;
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
!cli.contains("op"),
"local wrapper alias must not auto-peel as stdlib Arc, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_records_impl_via_renamed_stdlib_wrapper() {
let file = parse(
r#"
use std::sync::Arc as Shared;
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
pub type Public = Shared<private::Hidden>;
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"renamed stdlib wrapper alias must peel in visibility pass, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_records_impl_via_pub_type_alias_through_user_wrapper() {
let file = parse(
r#"
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
pub type Public = State<private::Hidden>;
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let mut wrappers = HashSet::new();
wrappers.insert("State".to_string());
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(
&files,
&aliases,
&adapter_layers(),
&HashSet::new(),
&wrappers,
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"user-wrapper alias target's impl method must be recorded, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_records_impl_via_pub_type_alias_through_wrapper() {
let file = parse(
r#"
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
pub type Public = Box<private::Hidden>;
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"wrapper-alias target's impl method must be recorded, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_records_impl_via_pub_type_alias() {
let file = parse(
r#"
mod private {
pub struct Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
pub type Public = private::Hidden;
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"impl on pub-type-alias target must be recorded, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_records_renamed_reexport_impl_methods() {
let file = parse(
r#"
mod private {
pub struct Hidden;
}
impl private::Hidden {
pub fn op(&self) {}
}
pub use private::Hidden as PublicHidden;
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"renamed re-export must still expose impl method, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_records_pub_use_reexport_with_qualified_impl() {
let file = parse(
r#"
mod private {
pub struct Hidden;
}
impl private::Hidden {
pub fn op(&self) {}
}
pub use private::Hidden;
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"re-exported type with file-level impl must be recorded, got {cli:?}"
);
}
#[test]
fn test_collect_pub_fns_skips_impl_methods_under_short_name_collision() {
let file = parse(
r#"
pub mod api {
pub struct Session;
impl Session {
pub fn run(&self) {}
}
}
mod internal {
pub struct Session;
impl Session {
pub fn cleanup(&self) {}
}
}
"#,
);
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(),
&HashSet::new(),
)
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("run"),
"public-mod impl method must be recorded, got {cli:?}"
);
assert!(
!cli.contains("cleanup"),
"private-mod impl method must not leak via short-name collision, got {cli:?}"
);
}