use super::support::three_layer;
use crate::adapters::analyzers::architecture::call_parity_rule::pub_fns::{
collect_pub_fns_by_layer, PubFnInfo, PubFnInputs,
};
use crate::adapters::shared::use_tree::gather_alias_map;
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 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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &cfg_test,
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &wrappers,
promoted_attributes: &HashSet::new(),
})
};
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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &wrappers,
promoted_attributes: &HashSet::new(),
})
};
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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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_chases_reexported_type_alias_to_target() {
let file = parse(
r#"
mod private {
pub struct Hidden;
pub type Public = Hidden;
impl Hidden {
pub fn op(&self) {}
}
}
pub use private::Public;
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
})
};
let cli = names_for_layer(&by_layer, "cli");
assert!(
cli.contains("op"),
"re-exported type alias must surface its target's impl methods, 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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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_trait_impl_method_on_private_self_type() {
let file = parse(
r#"
pub trait PubTrait {
fn handle(&self);
}
struct Hidden;
impl PubTrait for Hidden {
fn handle(&self) {}
}
"#,
);
let files = vec![("src/application/mod.rs", &file)];
let aliases = aliases_from_files(&files);
let by_layer = collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
});
let app = names_for_layer(&by_layer, "application");
assert!(
!app.contains("handle"),
"trait-impl method on private self type must stay out of pub-fn set; dispatch reaches it via the trait-method anchor, not as a concrete target pub-fn. Got {app:?}"
);
}
#[test]
fn test_collect_pub_fns_records_inherited_trait_impl_methods() {
let file = parse(
r#"
pub trait PubTrait {
fn handle(&self);
}
pub struct X;
impl PubTrait for X {
fn handle(&self) {}
}
"#,
);
let files = vec![("src/application/mod.rs", &file)];
let aliases = aliases_from_files(&files);
let by_layer = collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
});
let app = names_for_layer(&by_layer, "application");
assert!(
app.contains("handle"),
"trait-impl method must be recorded as pub fn even with Inherited vis, got {app:?}"
);
}
#[test]
fn test_collect_pub_fns_excludes_orphan_file_not_declared_in_crate_root() {
let lib = parse("mod cli;");
let cli = parse("pub fn cmd() {}");
let app = parse("pub fn helper() {}");
let files = vec![
("src/lib.rs", &lib),
("src/cli/mod.rs", &cli),
("src/application/mod.rs", &app),
];
let aliases = aliases_from_files(&files);
let by_layer = collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
});
let app_fns = names_for_layer(&by_layer, "application");
assert!(
!app_fns.contains("helper"),
"orphan file (no `mod application;` decl in lib.rs) must not contribute pub fns, got {app_fns:?}"
);
}
#[test]
fn test_collect_pub_fns_unions_lib_and_main_root_trees() {
let lib = parse("mod application;");
let main = parse("mod cli;");
let app = parse("pub fn search() {}");
let cli = parse("pub fn cmd() {}");
let files = vec![
("src/lib.rs", &lib),
("src/main.rs", &main),
("src/application/mod.rs", &app),
("src/cli/mod.rs", &cli),
];
let aliases = aliases_from_files(&files);
let by_layer = collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
});
let app_fns = names_for_layer(&by_layer, "application");
let cli_fns = names_for_layer(&by_layer, "cli");
assert!(
app_fns.contains("search"),
"application module declared in lib.rs root must surface its pub fns, got {app_fns:?}"
);
assert!(
cli_fns.contains("cmd"),
"cli module declared in main.rs root must surface its pub fns, got {cli_fns:?}"
);
}
#[test]
fn test_collect_pub_fns_includes_crate_root_mod_decl_without_pub() {
let lib = parse("mod cli; mod application;");
let cli_mod = parse("pub fn cmd_search() {}");
let app_mod = parse("pub fn search() {}");
let files = vec![
("src/lib.rs", &lib),
("src/cli/mod.rs", &cli_mod),
("src/application/mod.rs", &app_mod),
];
let aliases = aliases_from_files(&files);
let by_layer = collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
});
let cli = names_for_layer(&by_layer, "cli");
let app = names_for_layer(&by_layer, "application");
assert!(
cli.contains("cmd_search"),
"crate-root `mod cli;` (no pub) must still expose adapter pub-fns, got {cli:?}"
);
assert!(
app.contains("search"),
"crate-root `mod application;` (no pub) must still expose target pub-fns, got {app:?}"
);
}
#[test]
fn test_collect_pub_fns_excludes_pub_fn_under_private_ancestor_chain() {
let app = parse("mod internal;");
let internal = parse("pub mod deep;");
let deep = parse("pub fn helper() {}");
let files = vec![
("src/application/mod.rs", &app),
("src/application/internal/mod.rs", &internal),
("src/application/internal/deep.rs", &deep),
];
let aliases = aliases_from_files(&files);
let by_layer = collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
});
let app_fns = names_for_layer(&by_layer, "application");
assert!(
!app_fns.contains("helper"),
"private ancestor `mod internal;` must hide pub fns in deep descendants, got {app_fns:?}"
);
}
#[test]
fn test_collect_pub_fns_excludes_pub_fn_in_file_backed_private_module() {
let parent = parse("mod internal;");
let child = parse("pub fn helper() {}");
let files = vec![
("src/application/mod.rs", &parent),
("src/application/internal.rs", &child),
];
let aliases = aliases_from_files(&files);
let by_layer = collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
});
let app = names_for_layer(&by_layer, "application");
assert!(
!app.contains("helper"),
"pub fn in a file-backed private module must not enter the target-layer surface, got {app:?}"
);
}
#[test]
fn test_collect_pub_fns_includes_pub_fn_in_file_backed_public_module() {
let parent = parse("pub mod internal;");
let child = parse("pub fn helper() {}");
let files = vec![
("src/application/mod.rs", &parent),
("src/application/internal.rs", &child),
];
let aliases = aliases_from_files(&files);
let by_layer = collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
});
let app = names_for_layer(&by_layer, "application");
assert!(
app.contains("helper"),
"pub fn in a file-backed public module must be recorded, got {app:?}"
);
}
#[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(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &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:?}"
);
}
fn deprecated_for_layer<'ast>(
by_layer: &std::collections::HashMap<String, Vec<PubFnInfo<'ast>>>,
layer: &str,
) -> HashMap<String, bool> {
by_layer
.get(layer)
.map(|fns| {
fns.iter()
.map(|f| (f.fn_name.clone(), f.deprecated))
.collect()
})
.unwrap_or_default()
}
#[test]
fn pub_fn_records_deprecated_attribute_bare() {
let file = parse(
r#"
#[deprecated]
pub fn cmd_old() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
})
};
let dep = deprecated_for_layer(&by_layer, "cli");
assert_eq!(dep.get("cmd_old"), Some(&true));
}
#[test]
fn pub_fn_records_deprecated_with_message() {
let file = parse(
r#"
#[deprecated = "use cmd_new instead"]
pub fn cmd_old() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
})
};
assert_eq!(
deprecated_for_layer(&by_layer, "cli").get("cmd_old"),
Some(&true)
);
}
#[test]
fn pub_fn_records_deprecated_with_args() {
let file = parse(
r#"
#[deprecated(since = "1.0", note = "use cmd_new")]
pub fn cmd_old() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
})
};
assert_eq!(
deprecated_for_layer(&by_layer, "cli").get("cmd_old"),
Some(&true)
);
}
#[test]
fn pub_fn_no_attribute_not_deprecated() {
let file = parse(
r#"
pub fn cmd_active() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
})
};
assert_eq!(
deprecated_for_layer(&by_layer, "cli").get("cmd_active"),
Some(&false)
);
}
#[test]
fn pub_fn_other_attribute_not_deprecated() {
let file = parse(
r#"
#[allow(unused)]
pub fn cmd_active() {}
"#,
);
let files = vec![("src/cli/handlers.rs", &file)];
let by_layer = {
let aliases = aliases_from_files(&files);
collect_pub_fns_by_layer(PubFnInputs {
files: &files,
aliases_per_file: &aliases,
layers: &three_layer(),
cfg_test_files: &HashSet::new(),
transparent_wrappers: &HashSet::new(),
promoted_attributes: &HashSet::new(),
})
};
assert_eq!(
deprecated_for_layer(&by_layer, "cli").get("cmd_active"),
Some(&false)
);
}