use crate::adapters::analyzers::architecture::call_parity_rule::calls::{
collect_canonical_calls, FnContext,
};
use crate::adapters::analyzers::architecture::call_parity_rule::local_symbols::FileScope;
use crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::{
collect_crate_root_modules, collect_local_symbols,
};
use crate::adapters::shared::use_tree::{gather_alias_map, AliasMap, ScopedAliasMap};
use std::collections::{HashMap, HashSet};
fn parse_file(src: &str) -> syn::File {
syn::parse_str(src).expect("parse file")
}
fn parse_type(src: &str) -> syn::Type {
syn::parse_str(src).expect("parse type")
}
struct FileCtx {
file: syn::File,
alias_map: AliasMap,
aliases_per_scope: ScopedAliasMap,
local_symbols: HashSet<String>,
local_decl_scopes: HashMap<String, Vec<Vec<String>>>,
crate_root_modules: HashSet<String>,
}
impl FileCtx {
fn file_scope<'a>(&'a self, importing_file: &'a str) -> FileScope<'a> {
FileScope {
path: importing_file,
alias_map: &self.alias_map,
aliases_per_scope: &self.aliases_per_scope,
local_symbols: &self.local_symbols,
local_decl_scopes: &self.local_decl_scopes,
crate_root_modules: &self.crate_root_modules,
workspace_module_paths: None,
}
}
}
fn load(src: &str) -> FileCtx {
let file = parse_file(src);
let alias_map = gather_alias_map(&file);
let local_symbols = collect_local_symbols(&file);
FileCtx {
file,
alias_map,
aliases_per_scope: ScopedAliasMap::new(),
local_symbols,
local_decl_scopes: HashMap::new(),
crate_root_modules: HashSet::new(),
}
}
fn load_with_roots(src: &str, roots: &[&str]) -> FileCtx {
let mut fctx = load(src);
fctx.crate_root_modules = roots.iter().map(|s| s.to_string()).collect();
fctx
}
fn roots_from_paths(paths: &[&str]) -> HashSet<String> {
let fake: Vec<(&str, &syn::File)> = Vec::new();
let _ = fake;
let dummy = parse_file("");
let refs: Vec<(&str, &syn::File)> = paths.iter().map(|p| (*p, &dummy)).collect();
collect_crate_root_modules(&refs)
}
fn find_fn<'a>(file: &'a syn::File, name: &str) -> &'a syn::ItemFn {
file.items
.iter()
.find_map(|i| match i {
syn::Item::Fn(f) if f.sig.ident == name => Some(f),
_ => None,
})
.unwrap_or_else(|| panic!("fn {name} not found"))
}
fn impl_self_ty_name(item_impl: &syn::ItemImpl) -> Option<String> {
match item_impl.self_ty.as_ref() {
syn::Type::Path(p) => p.path.segments.last().map(|s| s.ident.to_string()),
_ => None,
}
}
fn find_impl_fn<'a>(
file: &'a syn::File,
type_name: &str,
fn_name: &str,
) -> (&'a syn::ItemImpl, &'a syn::ImplItemFn) {
file.items
.iter()
.filter_map(|item| match item {
syn::Item::Impl(i) if impl_self_ty_name(i).as_deref() == Some(type_name) => Some(i),
_ => None,
})
.find_map(|item_impl| {
item_impl.items.iter().find_map(|it| match it {
syn::ImplItem::Fn(f) if f.sig.ident == fn_name => Some((item_impl, f)),
_ => None,
})
})
.unwrap_or_else(|| panic!("impl {type_name}::{fn_name} not found"))
}
fn sig_params(sig: &syn::Signature) -> Vec<(String, &syn::Type)> {
sig.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Typed(pt) => {
let name = match pt.pat.as_ref() {
syn::Pat::Ident(pi) => pi.ident.to_string(),
_ => return None,
};
Some((name, pt.ty.as_ref()))
}
_ => None,
})
.collect()
}
fn ctx_for_fn<'a>(
fctx: &'a FileCtx,
file_scope: &'a FileScope<'a>,
fn_name: &str,
) -> FnContext<'a> {
let f = find_fn(&fctx.file, fn_name);
FnContext {
file: file_scope,
mod_stack: &[],
body: &f.block,
signature_params: sig_params(&f.sig),
generic_params: std::collections::HashMap::new(),
self_type: None,
workspace_index: None,
workspace_files: None,
}
}
fn canonical_of_impl_self(item: &syn::ItemImpl) -> Option<Vec<String>> {
if let syn::Type::Path(p) = item.self_ty.as_ref() {
Some(
p.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect(),
)
} else {
None
}
}
#[test]
fn test_collect_direct_qualified_call() {
let fctx = load(
r#"
pub fn cmd_search() {
crate::application::stats::get_stats(1);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::application::stats::get_stats"));
}
#[test]
fn test_collect_unqualified_via_use_alias() {
let fctx = load(
r#"
use crate::application::stats::get_stats;
pub fn cmd_search() {
get_stats(1);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::application::stats::get_stats"));
}
#[test]
fn test_collect_unqualified_no_alias_is_bare() {
let fctx = load(
r#"
pub fn cmd_search() {
foo(1);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("<bare>:foo"));
}
#[test]
fn test_collect_in_semicolon_separated_macro_descends() {
let fctx = load(
r#"
pub fn cmd_search() {
let _v = vec![compute(x); 3];
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("<bare>:compute"),
"`;`-separated macro body must still descend, got {calls:?}"
);
}
#[test]
fn test_collect_in_macro_descends() {
let fctx = load(
r#"
pub fn cmd_search() {
debug_assert!(validate(1));
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("<bare>:validate"));
assert!(!calls.contains("<macro>:debug_assert"));
}
#[test]
fn test_collect_self_super_prefix() {
let fctx = load(
r#"
pub fn cmd_search() {
self::helpers::format(1);
}
"#,
);
let fs = fctx.file_scope("src/cli/mod.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::cli::helpers::format"),
"calls = {:?}",
calls
);
}
#[test]
fn test_collect_turbofish_stripped() {
let fctx = load(
r#"
pub fn cmd_search() {
Box::<u32>::new(42);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("<bare>:Box::new"));
}
#[test]
fn test_collect_turbofish_call_resolves_via_use() {
let fctx = load(
r#"
use crate::middleware::record_symbol_query;
pub fn cmd_search() {
record_symbol_query::<RefsQuery>(0);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::middleware::record_symbol_query"),
"turbofish call must resolve through use-alias, got {calls:?}"
);
}
#[test]
fn test_collect_inferred_generic_call_resolves_via_use() {
let fctx = load(
r#"
use crate::middleware::record_operation;
pub fn cmd_search() {
record_operation(0, "x", true);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::middleware::record_operation"),
"inferred-generic call must resolve through use-alias, got {calls:?}"
);
}
#[test]
fn test_turbofish_in_impl_method_body_resolves() {
let fctx = load(
r#"
use crate::middleware::record_symbol_query;
pub struct Session;
impl Session {
pub fn refs(&self, sym: &str) {
record_symbol_query::<RefsQuery>(&self, sym);
}
}
"#,
);
let fs = fctx.file_scope("src/application/session.rs");
let f = find_impl_fn(&fctx.file, "Session", "refs");
let ctx = FnContext {
file: &fs,
mod_stack: &[],
body: &f.1.block,
signature_params: sig_params(&f.1.sig),
generic_params: std::collections::HashMap::new(),
self_type: canonical_of_impl_self(f.0),
workspace_index: None,
workspace_files: None,
};
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::middleware::record_symbol_query"),
"turbofish in impl-method body must resolve via use, got {calls:?}"
);
}
#[test]
fn test_turbofish_via_child_module_qualified_path_resolves() {
let fctx = load_with_roots(
r#"
pub fn cmd_search() {
crate::middleware::record_symbol_query::<RefsQuery>(0);
}
"#,
&["middleware"],
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::middleware::record_symbol_query"),
"qualified-path turbofish must resolve, got {calls:?}"
);
}
#[test]
fn test_multiple_turbofish_calls_to_same_fn_resolve_to_one_canonical() {
let fctx = load(
r#"
use crate::middleware::record_symbol_query;
pub struct Session;
impl Session {
pub fn refs(&self, sym: &str) {
record_symbol_query::<RefsQuery>(&self, sym);
record_symbol_query::<ContextQuery>(&self, sym);
record_symbol_query::<ContextWithGraphQuery>(&self, sym);
}
}
"#,
);
let fs = fctx.file_scope("src/application/session.rs");
let f = find_impl_fn(&fctx.file, "Session", "refs");
let ctx = FnContext {
file: &fs,
mod_stack: &[],
body: &f.1.block,
signature_params: sig_params(&f.1.sig),
generic_params: std::collections::HashMap::new(),
self_type: canonical_of_impl_self(f.0),
workspace_index: None,
workspace_files: None,
};
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::middleware::record_symbol_query"),
"all three turbofish call sites must dedupe to one canonical, got {calls:?}"
);
}
#[test]
fn test_turbofish_unresolved_path_still_bare() {
let fctx = load(
r#"
pub fn cmd_search() {
Box::<u32>::new(42);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("<bare>:Box::new"),
"unresolved turbofish should still bare-key, got {calls:?}"
);
}
#[test]
fn test_collect_closure_body_collected() {
let fctx = load(
r#"
pub fn cmd_search() {
let f = |x: u32| inner_call(x);
f(1);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("<bare>:inner_call"));
}
#[test]
fn test_collect_await_is_not_extra_call() {
let fctx = load(
r#"
pub async fn cmd_search() {
f(1).await;
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("<bare>:f"));
assert!(!calls.iter().any(|c| c.contains("await")));
}
#[test]
fn test_collect_self_dispatch_in_impl() {
let fctx = load(
r#"
pub struct RlmSession;
impl RlmSession {
pub fn search(&self) {
Self::internal_helper();
}
}
"#,
);
let (item, f) = find_impl_fn(&fctx.file, "RlmSession", "search");
let self_ty = canonical_of_impl_self(item);
let ctx = FnContext {
file: &FileScope {
path: "src/application/session.rs",
alias_map: &fctx.alias_map,
aliases_per_scope: &ScopedAliasMap::new(),
local_symbols: &fctx.local_symbols,
local_decl_scopes: &HashMap::new(),
crate_root_modules: &fctx.crate_root_modules,
workspace_module_paths: None,
},
mod_stack: &[],
body: &f.block,
signature_params: sig_params(&f.sig),
generic_params: std::collections::HashMap::new(),
self_type: self_ty,
workspace_index: None,
workspace_files: None,
};
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::application::session::RlmSession::internal_helper"),
"calls = {:?}",
calls
);
}
#[test]
fn test_tracker_let_constructor_binding() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub fn cmd_search(q: u32) {
let s = RlmSession::open_cwd();
s.search(q);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::session::RlmSession::search"),
"calls = {:?}",
calls
);
}
#[test]
fn test_tracker_let_type_annotation() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub fn cmd_search(q: u32) {
let s: RlmSession = make_session();
s.search(q);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::app::session::RlmSession::search"));
}
#[test]
fn test_tracker_fn_param_type() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub fn handle(session: RlmSession) {
session.search(1);
}
"#,
);
let fs = fctx.file_scope("src/mcp/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "handle");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::app::session::RlmSession::search"));
}
#[test]
fn test_tracker_fn_param_ref_type() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub fn handle(session: &RlmSession) {
session.search(1);
}
"#,
);
let fs = fctx.file_scope("src/mcp/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "handle");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::app::session::RlmSession::search"));
}
#[test]
fn test_tracker_fn_param_arc_type() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
use std::sync::Arc;
pub fn handle(session: Arc<RlmSession>) {
session.search(1);
}
"#,
);
let fs = fctx.file_scope("src/mcp/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "handle");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::app::session::RlmSession::search"));
}
#[test]
fn test_tracker_fn_param_box_ref_mut_type() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub fn a(session: Box<RlmSession>) { session.search(1); }
pub fn b(session: &mut RlmSession) { session.search(1); }
"#,
);
let fs = fctx.file_scope("src/mcp/handlers.rs");
for name in &["a", "b"] {
let ctx = ctx_for_fn(&fctx, &fs, name);
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::session::RlmSession::search"),
"fn {name} calls = {:?}",
calls
);
}
}
#[test]
fn test_tracker_alias_resolved_constructor() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub fn cmd_search() {
let s = RlmSession::open();
s.search(1);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::app::session::RlmSession::search"));
}
#[test]
fn test_tracker_shadowing_uses_latest() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
use crate::cli::CliSession;
pub fn cmd_search() {
let s = CliSession::new();
let s = RlmSession::open();
s.search(1);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::app::session::RlmSession::search"));
assert!(!calls.contains("crate::cli::CliSession::search"));
}
#[test]
fn test_tracker_unknown_receiver_falls_back_to_method_shape() {
let fctx = load(
r#"
pub fn cmd_search(x: UnknownType) {
x.search(1);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("<method>:search"));
assert!(!calls.iter().any(|c| c.contains("UnknownType::search")));
}
#[test]
fn test_tracker_closure_inherits_parent_bindings() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub fn cmd_search() {
let s = RlmSession::open();
let f = || s.search(1);
f();
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::app::session::RlmSession::search"));
}
#[test]
fn test_tracker_factory_helper_unresolved_falls_back_to_method_shape() {
let fctx = load(
r#"
pub fn cmd_search() {
let s = helpers::open_session();
s.search(1);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("<method>:search"));
}
#[test]
fn test_tracker_in_async_fn() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub async fn handle(s: RlmSession) {
s.search(1).await;
}
"#,
);
let fs = fctx.file_scope("src/mcp/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "handle");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::app::session::RlmSession::search"));
}
#[test]
fn test_collect_async_block() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub fn cmd_search() {
let s = RlmSession::open();
let _fut = async { s.search(1) };
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::session::RlmSession::search"),
"calls = {:?}",
calls
);
}
#[test]
fn test_empty_body_yields_no_calls() {
let fctx = load("pub fn f() {}");
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "f");
let calls = collect_canonical_calls(&ctx);
assert_eq!(calls, HashSet::<String>::new());
}
#[test]
fn test_local_helper_call_resolves_to_crate_module() {
let fctx = load(
r#"
fn helper() {}
pub fn cmd_foo() {
helper();
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_foo");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::cli::handlers::helper"),
"local helper must resolve via file module, got {calls:?}"
);
assert!(
!calls.contains("<bare>:helper"),
"local helper must not fall back to bare, got {calls:?}"
);
}
#[test]
fn test_external_call_without_use_still_falls_to_bare() {
let fctx = load(
r#"
pub fn cmd_foo() {
not_a_local_symbol();
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_foo");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("<bare>:not_a_local_symbol"),
"unknown fn must stay bare, got {calls:?}"
);
}
#[test]
fn test_super_aliased_call_normalises_to_crate_rooted() {
let fctx = load(
r#"
use super::stats::get_stats;
pub fn cmd_foo() {
get_stats();
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_foo");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::cli::stats::get_stats"),
"super-aliased call must normalise to crate::, got {calls:?}"
);
assert!(
!calls.iter().any(|c| c.starts_with("super::")),
"super-rooted canonical must not leak, got {calls:?}"
);
}
#[test]
fn test_unqualified_local_type_in_signature_resolves() {
let fctx = load(
r#"
pub struct Session;
impl Session {
pub fn search(&self) {}
}
pub fn cmd_foo(s: Session) {
s.search();
}
"#,
);
let fs = fctx.file_scope("src/application/session.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_foo");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::application::session::Session::search"),
"unqualified local-type receiver must resolve, got {calls:?}"
);
assert!(
!calls.contains("<method>:search"),
"must not fall back to <method>:, got {calls:?}"
);
}
#[test]
fn test_rust2018_absolute_call_without_use_resolves_to_crate_rooted() {
let fctx = load_with_roots(
r#"
pub fn cmd_x() {
app::foo();
}
"#,
&["app"],
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_x");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::foo"),
"unaliased Rust 2018+ call must crate-prefix, got {calls:?}"
);
assert!(
!calls.iter().any(|c| c == "<bare>:app::foo"),
"must not fall back to bare, got {calls:?}"
);
}
#[test]
fn test_rust2018_absolute_import_resolves_to_crate_rooted() {
let fctx = load_with_roots(
r#"
use app::foo;
pub fn cmd_x() {
foo();
}
"#,
&["app"],
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_x");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::foo"),
"Rust 2018+ absolute import must normalise to crate::, got {calls:?}"
);
assert!(
!calls.iter().any(|c| c == "app::foo"),
"must not leave unprefixed app::foo, got {calls:?}"
);
}
#[test]
fn test_collect_crate_root_modules_from_paths() {
let roots = roots_from_paths(&[
"src/app/mod.rs",
"src/app/session.rs",
"src/cli/handlers.rs",
"src/lib.rs",
"src/main.rs",
]);
assert!(roots.contains("app"));
assert!(roots.contains("cli"));
assert!(!roots.contains("lib"));
assert!(!roots.contains("main"));
}
#[test]
fn test_top_level_self_as_alias_maps_to_current_file() {
let fctx = load(
r#"
use self as fs;
pub fn cmd_x() {
fs::something();
}
"#,
);
let fs = fctx.file_scope("src/util/fs_helpers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_x");
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::util::fs_helpers::something"),
"top-level self-alias must resolve to the current file's module, got {calls:?}"
);
}
#[test]
fn test_qualified_impl_path_does_not_double_crate() {
let fctx = load(
r#"
impl crate::app::Session {
pub fn search(&self) {
Self::internal_helper();
}
}
"#,
);
let (item, f) = find_impl_fn(&fctx.file, "Session", "search");
let self_ty = canonical_of_impl_self(item);
let ctx = FnContext {
file: &FileScope {
path: "src/other_file.rs",
alias_map: &fctx.alias_map,
aliases_per_scope: &ScopedAliasMap::new(),
local_symbols: &fctx.local_symbols,
local_decl_scopes: &HashMap::new(),
crate_root_modules: &fctx.crate_root_modules,
workspace_module_paths: None,
},
mod_stack: &[],
body: &f.block,
signature_params: sig_params(&f.sig),
generic_params: std::collections::HashMap::new(),
self_type: self_ty,
workspace_index: None,
workspace_files: None,
};
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::Session::internal_helper"),
"qualified impl path must canonicalise as-is, got {calls:?}"
);
assert!(
!calls.iter().any(|c| c.contains("crate::crate::")),
"must not double-crate, got {calls:?}"
);
}
fn ctx_with_index<'a>(
fctx: &'a FileCtx,
file_scope: &'a FileScope<'a>,
fn_name: &str,
index: &'a crate::adapters::analyzers::architecture::call_parity_rule::type_infer::WorkspaceTypeIndex,
) -> FnContext<'a> {
let f = find_fn(&fctx.file, fn_name);
FnContext {
file: file_scope,
mod_stack: &[],
body: &f.block,
signature_params: sig_params(&f.sig),
generic_params: std::collections::HashMap::new(),
self_type: None,
workspace_index: Some(index),
workspace_files: None,
}
}
#[test]
fn test_inference_fallback_resolves_method_chain_ctor_pattern() {
use crate::adapters::analyzers::architecture::call_parity_rule::type_infer::{
CanonicalType, WorkspaceTypeIndex,
};
let fctx = load(
r#"
use crate::app::session::Session;
pub fn cmd_diff() {
let session = Session::open().map_err(handle_err).unwrap();
session.diff();
}
"#,
);
let mut index = WorkspaceTypeIndex::new();
index.insert_method_return(
"crate::app::session::Session",
"open",
CanonicalType::Result(Box::new(CanonicalType::path([
"crate", "app", "session", "Session",
]))),
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_with_index(&fctx, &fs, "cmd_diff", &index);
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::session::Session::diff"),
"inference fallback should resolve session.diff(), got {calls:?}"
);
}
#[test]
fn test_inference_fallback_resolves_field_access() {
use crate::adapters::analyzers::architecture::call_parity_rule::type_infer::{
CanonicalType, WorkspaceTypeIndex,
};
let fctx = load(
r#"
use crate::app::Ctx;
pub fn handle_diff(ctx: &Ctx) {
ctx.session.diff();
}
"#,
);
let mut index = WorkspaceTypeIndex::new();
index.insert_struct_field(
"crate::app::Ctx",
"session",
CanonicalType::path(["crate", "app", "Session"]),
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_with_index(&fctx, &fs, "handle_diff", &index);
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::Session::diff"),
"field-access inference should resolve ctx.session.diff(), got {calls:?}"
);
}
#[test]
fn test_inference_fallback_on_result_unwrap_chain() {
use crate::adapters::analyzers::architecture::call_parity_rule::type_infer::{
CanonicalType, WorkspaceTypeIndex,
};
let fctx = load(
r#"
use crate::app::session::Session;
pub fn cmd_direct() {
Session::open().unwrap().diff();
}
"#,
);
let mut index = WorkspaceTypeIndex::new();
index.insert_method_return(
"crate::app::session::Session",
"open",
CanonicalType::Result(Box::new(CanonicalType::path([
"crate", "app", "session", "Session",
]))),
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_with_index(&fctx, &fs, "cmd_direct", &index);
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::session::Session::diff"),
"combinator chain should resolve Session::open().unwrap().diff(), got {calls:?}"
);
}
#[test]
fn test_existing_fast_path_still_works_without_index() {
let fctx = load(
r#"
use crate::app::session::RlmSession;
pub fn cmd_search(q: u32) {
let s = RlmSession::open_cwd();
s.search(q);
}
"#,
);
let fs = fctx.file_scope("src/cli/handlers.rs");
let ctx = ctx_for_fn(&fctx, &fs, "cmd_search");
let calls = collect_canonical_calls(&ctx);
assert!(calls.contains("crate::app::session::RlmSession::search"));
}