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::type_infer::{
CanonicalType, WorkspaceTypeIndex,
};
use crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::collect_local_symbols;
use crate::adapters::shared::use_tree::{
gather_alias_map, gather_alias_map_scoped, ScopedAliasMap,
};
use std::collections::{HashMap, HashSet};
const SESSION_PATH: &str = "crate::app::session::Session";
const CTX_PATH: &str = "crate::app::Ctx";
struct RegFixture {
file: syn::File,
alias_map: HashMap<String, Vec<String>>,
local_symbols: HashSet<String>,
crate_roots: HashSet<String>,
}
fn parse(src: &str) -> RegFixture {
let file: syn::File = syn::parse_str(src).expect("parse fixture");
let alias_map = gather_alias_map(&file);
let local_symbols = collect_local_symbols(&file);
RegFixture {
file,
alias_map,
local_symbols,
crate_roots: HashSet::new(),
}
}
fn rlm_index() -> WorkspaceTypeIndex {
let session = CanonicalType::path(["crate", "app", "session", "Session"]);
let response = CanonicalType::path(["crate", "app", "Response"]);
let error = CanonicalType::path(["crate", "app", "Error"]);
let mut index = WorkspaceTypeIndex::new();
index.insert_method_return(
SESSION_PATH,
"open",
CanonicalType::Result(Box::new(session.clone())),
);
index.insert_method_return(
SESSION_PATH,
"open_cwd",
CanonicalType::Result(Box::new(session.clone())),
);
index.insert_method_return(SESSION_PATH, "diff", response.clone());
index.insert_method_return(SESSION_PATH, "files", response.clone());
index.insert_method_return(
SESSION_PATH,
"insert",
CanonicalType::Result(Box::new(response.clone())),
);
index.insert_struct_field(CTX_PATH, "session", session);
index.fn_returns.insert(
"crate::app::make_session".to_string(),
CanonicalType::Result(Box::new(CanonicalType::path([
"crate", "app", "session", "Session",
]))),
);
let _ = error; index
}
fn find_fn<'a>(file: &'a syn::File, name: &str) -> &'a syn::ItemFn {
file.items
.iter()
.find_map(|item| match item {
syn::Item::Fn(f) if f.sig.ident == name => Some(f),
_ => None,
})
.unwrap_or_else(|| panic!("fn {name} not in fixture"))
}
fn sig_params(sig: &syn::Signature) -> Vec<(String, &syn::Type)> {
sig.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Typed(pt) => match pt.pat.as_ref() {
syn::Pat::Ident(pi) => Some((pi.ident.to_string(), pt.ty.as_ref())),
_ => None,
},
_ => None,
})
.collect()
}
fn run(fx: &RegFixture, index: &WorkspaceTypeIndex, fn_name: &str) -> HashSet<String> {
let f = find_fn(&fx.file, fn_name);
run_with_self(fx, index, &f.sig, &f.block, None)
}
fn run_impl_method(
fx: &RegFixture,
index: &WorkspaceTypeIndex,
type_name: &str,
fn_name: &str,
self_segs: Vec<String>,
) -> HashSet<String> {
let (_, f) = find_impl_method(&fx.file, type_name, fn_name);
run_with_self(fx, index, &f.sig, &f.block, Some(self_segs))
}
fn run_with_self(
fx: &RegFixture,
index: &WorkspaceTypeIndex,
sig: &syn::Signature,
body: &syn::Block,
self_type: Option<Vec<String>>,
) -> HashSet<String> {
let ctx = FnContext {
file: &FileScope {
path: "src/cli/handlers.rs",
alias_map: &fx.alias_map,
aliases_per_scope: &ScopedAliasMap::new(),
local_symbols: &fx.local_symbols,
local_decl_scopes: &HashMap::new(),
crate_root_modules: &fx.crate_roots,
},
mod_stack: &[],
body,
signature_params: sig_params(sig),
generic_params: vec![],
self_type,
workspace_index: Some(index),
workspace_files: None,
};
collect_canonical_calls(&ctx)
}
fn find_impl_method<'a>(
file: &'a syn::File,
type_name: &str,
fn_name: &str,
) -> (Vec<String>, &'a syn::ImplItemFn) {
file.items
.iter()
.filter_map(|item| match item {
syn::Item::Impl(i) => Some(i),
_ => None,
})
.find_map(|item_impl| {
let segs = impl_self_segments(item_impl)?;
if segs.last().map(String::as_str) != Some(type_name) {
return None;
}
item_impl.items.iter().find_map(|it| match it {
syn::ImplItem::Fn(f) if f.sig.ident == fn_name => Some((segs.clone(), f)),
_ => None,
})
})
.unwrap_or_else(|| panic!("impl {type_name}::{fn_name} not in fixture"))
}
fn impl_self_segments(item: &syn::ItemImpl) -> Option<Vec<String>> {
let syn::Type::Path(p) = item.self_ty.as_ref() else {
return None;
};
Some(
p.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect(),
)
}
#[test]
fn rlm_group2_open_map_err_unwrap() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
let s = Session::open().map_err(handle).unwrap();
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(
calls.contains("crate::app::session::Session::diff"),
"expected Session::diff edge, got {calls:?}"
);
}
#[test]
fn rlm_group2_open_cwd_map_err_try() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
let s = Session::open_cwd().map_err(map_err)?;
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(
calls.contains("crate::app::session::Session::diff"),
"expected Session::diff edge, got {calls:?}"
);
}
#[test]
fn rlm_group2_plain_unwrap() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
let s = Session::open().unwrap();
s.files();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(calls.contains("crate::app::session::Session::files"));
}
#[test]
fn rlm_group2_expect_message() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
let s = Session::open().expect("session must open");
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(calls.contains("crate::app::session::Session::diff"));
}
#[test]
fn rlm_group2_unwrap_or_else_closure() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
let s = Session::open().unwrap_or_else(|e| fallback(e));
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(calls.contains("crate::app::session::Session::diff"));
}
#[test]
fn rlm_group2_chained_inline() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
Session::open().unwrap().diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(calls.contains("crate::app::session::Session::diff"));
}
#[test]
fn rlm_group2_insert_returns_result_chained() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
Session::open().unwrap().insert();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(calls.contains("crate::app::session::Session::insert"));
}
#[test]
fn rlm_group3_ctx_field_access() {
let fx = parse(
r#"
use crate::app::Ctx;
pub fn handle(ctx: &Ctx) {
ctx.session.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "handle");
assert!(calls.contains("crate::app::session::Session::diff"));
}
#[test]
fn rlm_group3_ctx_field_access_via_let() {
let fx = parse(
r#"
use crate::app::Ctx;
pub fn handle(ctx: &Ctx) {
let s = &ctx.session;
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "handle");
assert!(calls.contains("crate::app::session::Session::diff"));
}
#[test]
fn self_method_call_resolves_via_impl_type() {
let fx = parse(
r#"
impl Session {
pub fn run(&self) {
self.diff();
}
}
"#,
);
let self_segs = vec![
"crate".to_string(),
"app".to_string(),
"session".to_string(),
"Session".to_string(),
];
let calls = run_impl_method(&fx, &rlm_index(), "Session", "run", self_segs);
assert!(
calls.contains("crate::app::session::Session::diff"),
"self.diff() must route through workspace_index, got {calls:?}"
);
}
#[test]
fn self_field_access_resolves_via_impl_type() {
let fx = parse(
r#"
impl Ctx {
pub fn run(&self) {
self.session.diff();
}
}
"#,
);
let self_segs = vec!["crate".to_string(), "app".to_string(), "Ctx".to_string()];
let calls = run_impl_method(&fx, &rlm_index(), "Ctx", "run", self_segs);
assert!(
calls.contains("crate::app::session::Session::diff"),
"self.session.diff() must chain through field type, got {calls:?}"
);
}
#[test]
fn signature_param_typed_self_resolves() {
let fx = parse(
r#"
impl Session {
pub fn merge(&self, other: Self) {
other.diff();
}
}
"#,
);
let self_segs = vec![
"crate".to_string(),
"app".to_string(),
"session".to_string(),
"Session".to_string(),
];
let calls = run_impl_method(&fx, &rlm_index(), "Session", "merge", self_segs);
assert!(
calls.contains("crate::app::session::Session::diff"),
"param `other: Self` must resolve to Session, got {calls:?}"
);
}
#[test]
fn let_annotation_self_resolves() {
let fx = parse(
r#"
impl Session {
pub fn run(&self) {
let other: Self = make();
other.diff();
}
}
"#,
);
let self_segs = vec![
"crate".to_string(),
"app".to_string(),
"session".to_string(),
"Session".to_string(),
];
let calls = run_impl_method(&fx, &rlm_index(), "Session", "run", self_segs);
assert!(
calls.contains("crate::app::session::Session::diff"),
"`let other: Self = …` must bind to Session, got {calls:?}"
);
}
#[test]
fn turbofish_self_inside_impl_resolves() {
let fx = parse(
r#"
impl Session {
pub fn run(&self) {
let s = get::<Self>();
s.diff();
}
}
"#,
);
let self_segs = vec![
"crate".to_string(),
"app".to_string(),
"session".to_string(),
"Session".to_string(),
];
let calls = run_impl_method(&fx, &rlm_index(), "Session", "run", self_segs);
assert!(
calls.contains("crate::app::session::Session::diff"),
"`get::<Self>()` turbofish must resolve to Session, got {calls:?}"
);
}
#[test]
fn annotated_destructuring_self_resolves() {
let fx = parse(
r#"
impl Session {
pub fn run(&self) {
let Some(other): Option<Self> = maybe() else { return; };
other.diff();
}
}
"#,
);
let self_segs = vec![
"crate".to_string(),
"app".to_string(),
"session".to_string(),
"Session".to_string(),
];
let calls = run_impl_method(&fx, &rlm_index(), "Session", "run", self_segs);
assert!(
calls.contains("crate::app::session::Session::diff"),
"annotated destructuring with Self must bind to Session, got {calls:?}"
);
}
#[test]
fn cast_as_self_resolves() {
let fx = parse(
r#"
impl Session {
pub fn run(&self) {
let s = (raw() as Self);
s.diff();
}
}
"#,
);
let self_segs = vec![
"crate".to_string(),
"app".to_string(),
"session".to_string(),
"Session".to_string(),
];
let calls = run_impl_method(&fx, &rlm_index(), "Session", "run", self_segs);
assert!(
calls.contains("crate::app::session::Session::diff"),
"`as Self` cast must resolve to Session, got {calls:?}"
);
}
#[test]
fn qualified_path_does_not_alias_promote_through_leaf() {
let fx = parse(
r#"
use std::sync::Arc as Shared;
use crate::app::session::Session;
pub fn handle(s: wrap::Shared<Session>) {
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "handle");
assert!(
!calls.contains("crate::app::session::Session::diff"),
"qualified path must not be alias-promoted, got {calls:?}"
);
}
#[test]
fn qualified_local_arc_does_not_auto_peel() {
let fx = parse(
r#"
use crate::app::session::Session;
mod wrap { pub struct Arc<T>(T); }
pub fn handle(s: wrap::Arc<Session>) {
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "handle");
assert!(
!calls.contains("crate::app::session::Session::diff"),
"qualified local Arc must not auto-peel as stdlib Arc, got {calls:?}"
);
}
#[test]
fn bare_local_arc_does_not_auto_peel() {
let fx = parse(
r#"
use crate::wrap::Arc;
use crate::app::session::Session;
pub fn handle(s: Arc<Session>) {
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "handle");
assert!(
!calls.contains("crate::app::session::Session::diff"),
"bare Arc shadowed by local must not auto-peel, got {calls:?}"
);
}
#[test]
fn fully_qualified_user_wrapper_peels_via_leaf_match() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn handle(s: axum::extract::State<Session>) {
s.diff();
}
"#,
);
let mut index = rlm_index();
index.transparent_wrappers.insert("State".to_string());
let calls = run(&fx, &index, "handle");
assert!(
calls.contains("crate::app::session::Session::diff"),
"fully-qualified user wrapper must peel via leaf-name, got {calls:?}"
);
}
#[test]
fn renamed_external_user_wrapper_peels_via_user_config() {
let fx = parse(
r#"
use axum::extract::State as ExtractState;
use crate::app::session::Session;
pub fn handle(s: ExtractState<Session>) {
s.diff();
}
"#,
);
let mut index = rlm_index();
index.transparent_wrappers.insert("State".to_string());
let calls = run(&fx, &index, "handle");
assert!(
calls.contains("crate::app::session::Session::diff"),
"renamed external user wrapper must peel, got {calls:?}"
);
}
#[test]
fn aliased_local_wrapper_does_not_auto_peel() {
let fx = parse(
r#"
use crate::wrap::Arc as Shared;
use crate::app::session::Session;
pub fn handle(s: Shared<Session>) {
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "handle");
assert!(
!calls.contains("crate::app::session::Session::diff"),
"aliased local wrapper must not auto-peel as stdlib Arc, got {calls:?}"
);
}
#[test]
fn aliased_stdlib_wrapper_inside_inline_mod_peels_to_inner() {
let fx = parse(
r#"
mod inner {
use std::sync::Arc as Shared;
use crate::app::session::Session;
pub fn handle(s: Shared<Session>) {
s.diff();
}
}
"#,
);
let f = find_fn_in_mod(&fx.file, "inner", "handle");
let ctx = FnContext {
file: &FileScope {
path: "src/cli/handlers.rs",
alias_map: &fx.alias_map,
aliases_per_scope: &gather_alias_map_scoped(&fx.file),
local_symbols: &fx.local_symbols,
local_decl_scopes: &HashMap::new(),
crate_root_modules: &fx.crate_roots,
},
mod_stack: &["inner".to_string()],
body: &f.block,
signature_params: sig_params(&f.sig),
generic_params: vec![],
self_type: None,
workspace_index: Some(&rlm_index()),
workspace_files: None,
};
let calls = collect_canonical_calls(&ctx);
assert!(
calls.contains("crate::app::session::Session::diff"),
"scoped Arc-alias inside inline mod must peel to Session, got {calls:?}"
);
}
fn find_fn_in_mod<'a>(file: &'a syn::File, mod_name: &str, fn_name: &str) -> &'a syn::ItemFn {
file.items
.iter()
.find_map(|item| match item {
syn::Item::Mod(m) if m.ident == mod_name => m.content.as_ref(),
_ => None,
})
.and_then(|(_, items)| {
items.iter().find_map(|i| match i {
syn::Item::Fn(f) if f.sig.ident == fn_name => Some(f),
_ => None,
})
})
.unwrap_or_else(|| panic!("fn {mod_name}::{fn_name} not found"))
}
#[test]
fn aliased_stdlib_wrapper_peels_to_inner() {
let fx = parse(
r#"
use std::sync::Arc as Shared;
use crate::app::session::Session;
pub fn handle(s: Shared<Session>) {
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "handle");
assert!(
calls.contains("crate::app::session::Session::diff"),
"aliased Arc wrapper must peel to Session, got {calls:?}"
);
}
#[test]
fn free_fn_result_chain() {
let fx = parse(
r#"
pub fn cmd() {
let s = crate::app::make_session().unwrap();
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(calls.contains("crate::app::session::Session::diff"));
}
#[test]
fn fast_path_signature_param_resolves() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn handle(s: &Session) {
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "handle");
assert!(calls.contains("crate::app::session::Session::diff"));
}
#[test]
fn fast_path_let_type_annotation() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
let s: Session = make_it();
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(calls.contains("crate::app::session::Session::diff"));
}
#[test]
fn fast_path_direct_constructor() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
let s = Session::open_cwd();
// No unwrap — s is Result<Session, _>, not Session.
// Fast path on the bare-ident fails; inference fallback on
// `s.diff()` receiver infers Result<Session>, which doesn't
// have `diff` in the combinator table → <method>:diff.
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(
calls.contains("<method>:diff") || calls.contains("crate::app::session::Session::diff"),
"pathological Result<T>.method() must either fall back or correctly unwrap, got {calls:?}"
);
}
#[test]
fn negative_external_type_method_is_bare() {
let fx = parse(
r#"
pub fn cmd() {
let x: u32 = 42;
x.custom_method();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(
calls.contains("<method>:custom_method"),
"expected <method>:custom_method fallback, got {calls:?}"
);
assert!(
!calls.iter().any(|c| c.contains("u32::custom_method")),
"must not fabricate stdlib method edges, got {calls:?}"
);
}
#[test]
fn negative_unannotated_generic_stays_unresolved() {
let fx = parse(
r#"
pub fn cmd() {
let x = get();
x.some_method();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(calls.contains("<method>:some_method"));
}
#[test]
fn negative_stdlib_map_closure_is_unresolved() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
Session::open().map(|r| r.diff());
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(
calls.iter().any(|c| c == "<method>:diff"),
"closure-body call should stay <method>:diff without binding, got {calls:?}"
);
}
#[test]
fn negative_tuple_destructuring_is_limit() {
let fx = parse(
r#"
pub fn cmd() {
let (a, s) = setup();
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(
calls.contains("<method>:diff"),
"tuple destructuring is a Stage 1 limit — expected <method>:diff, got {calls:?}"
);
}
#[test]
fn trait_dispatch_emits_trait_method_anchor() {
let fx = parse(
r#"
use crate::ports::Handler;
pub fn dispatch(h: &dyn Handler) {
h.handle();
}
"#,
);
let mut index = WorkspaceTypeIndex::new();
index.trait_methods.insert(
"crate::ports::Handler".to_string(),
std::iter::once("handle".to_string()).collect(),
);
index.trait_impls.insert(
"crate::ports::Handler".to_string(),
vec![
"crate::app::LoggingHandler".to_string(),
"crate::app::MetricsHandler".to_string(),
"crate::app::AuditHandler".to_string(),
],
);
let calls = run(&fx, &index, "dispatch");
assert!(
calls.contains("crate::ports::Handler::handle"),
"expected single trait-method anchor edge, got {calls:?}"
);
assert!(
!calls.contains("crate::app::LoggingHandler::handle"),
"must not emit per-impl fanout edges from dispatch (Check C false-positive source), got {calls:?}"
);
assert!(!calls.contains("crate::app::MetricsHandler::handle"));
assert!(!calls.contains("crate::app::AuditHandler::handle"));
}
#[test]
fn trait_dispatch_skips_unrelated_methods() {
let fx = parse(
r#"
use crate::ports::Handler;
pub fn dispatch(h: &dyn Handler) {
h.unrelated();
}
"#,
);
let mut index = WorkspaceTypeIndex::new();
index.trait_methods.insert(
"crate::ports::Handler".to_string(),
std::iter::once("handle".to_string()).collect(),
);
index.trait_impls.insert(
"crate::ports::Handler".to_string(),
vec!["crate::app::X".to_string()],
);
let calls = run(&fx, &index, "dispatch");
assert!(
calls.contains("<method>:unrelated"),
"unrelated method on trait must fall through, got {calls:?}"
);
assert!(
!calls.contains("crate::app::X::unrelated"),
"must not fabricate edge for non-trait method, got {calls:?}"
);
}
#[test]
fn trait_dispatch_emits_anchor_regardless_of_default_status() {
let fx = parse(
r#"
use crate::ports::Handler;
pub fn dispatch(h: &dyn Handler) {
h.handle();
}
"#,
);
let mut index = WorkspaceTypeIndex::new();
index.trait_methods.insert(
"crate::ports::Handler".to_string(),
std::iter::once("handle".to_string()).collect(),
);
index.trait_impls.insert(
"crate::ports::Handler".to_string(),
vec!["crate::app::AppHandler".to_string()],
);
let mut by_impl: std::collections::HashMap<String, std::collections::HashSet<String>> =
std::collections::HashMap::new();
by_impl.insert(
"crate::app::AppHandler".to_string(),
std::collections::HashSet::new(),
);
index
.trait_impl_overrides
.insert("crate::ports::Handler".to_string(), by_impl);
let calls = run(&fx, &index, "dispatch");
assert!(
calls.contains("crate::ports::Handler::handle"),
"expected trait-method anchor edge, got {calls:?}"
);
assert!(
!calls.contains("crate::app::AppHandler::handle"),
"must not fabricate impl-method edge from dispatch, got {calls:?}"
);
}
#[test]
fn trait_dispatch_with_send_marker_emits_anchor() {
let fx = parse(
r#"
use crate::ports::Handler;
pub fn dispatch(h: &(dyn Handler + Send)) {
h.handle();
}
"#,
);
let mut index = WorkspaceTypeIndex::new();
index.trait_methods.insert(
"crate::ports::Handler".to_string(),
std::iter::once("handle".to_string()).collect(),
);
index.trait_impls.insert(
"crate::ports::Handler".to_string(),
vec!["crate::app::X".to_string()],
);
let calls = run(&fx, &index, "dispatch");
assert!(
calls.contains("crate::ports::Handler::handle"),
"expected anchor edge, got {calls:?}"
);
assert!(!calls.contains("crate::app::X::handle"));
}
#[test]
fn trait_dispatch_box_dyn_emits_anchor() {
let fx = parse(
r#"
use crate::ports::Handler;
pub fn dispatch(h: Box<dyn Handler>) {
h.handle();
}
"#,
);
let mut index = WorkspaceTypeIndex::new();
index.trait_methods.insert(
"crate::ports::Handler".to_string(),
std::iter::once("handle".to_string()).collect(),
);
index.trait_impls.insert(
"crate::ports::Handler".to_string(),
vec!["crate::app::Y".to_string()],
);
let calls = run(&fx, &index, "dispatch");
assert!(
calls.contains("crate::ports::Handler::handle"),
"Box<dyn Trait> must be peeled and emit anchor, got {calls:?}"
);
assert!(!calls.contains("crate::app::Y::handle"));
}
#[test]
fn user_wrapper_is_peeled_on_signature_param() {
let fx = parse(
r#"
use crate::app::Db;
pub fn handle(db: State<Db>) {
db.query();
}
"#,
);
let db = CanonicalType::path(["crate", "app", "Db"]);
let mut index = WorkspaceTypeIndex::new();
index.insert_method_return(
"crate::app::Db",
"query",
CanonicalType::path(["crate", "app", "Rows"]),
);
index.transparent_wrappers.insert("State".to_string());
let calls = run(&fx, &index, "handle");
let _ = db;
assert!(
calls.contains("crate::app::Db::query"),
"user-wrapper State<Db> should peel to Db, got {calls:?}"
);
}
#[test]
fn user_wrapper_unconfigured_stays_unresolved() {
let fx = parse(
r#"
use crate::app::Db;
pub fn handle(db: State<Db>) {
db.query();
}
"#,
);
let index = WorkspaceTypeIndex::new();
let calls = run(&fx, &index, "handle");
assert!(
calls.contains("<method>:query"),
"unconfigured wrapper must not be peeled, got {calls:?}"
);
}
#[test]
fn type_alias_expands_to_target_via_signature_param() {
let fx = parse(
r#"
type DbRef = std::sync::Arc<Store>;
pub fn handle(db: DbRef) {
db.read();
}
"#,
);
let store = CanonicalType::path(["crate", "cli", "handlers", "Store"]);
let mut index = WorkspaceTypeIndex::new();
let aliased: syn::Type = syn::parse_str("std::sync::Arc<Store>").expect("parse alias target");
index.type_aliases.insert(
"crate::cli::handlers::DbRef".to_string(),
crate::adapters::analyzers::architecture::call_parity_rule::type_infer::workspace_index::AliasDef {
params: Vec::new(),
target: aliased,
decl_file: "src/cli/handlers.rs".to_string(),
decl_mod_stack: Vec::new(),
},
);
index.insert_method_return(
"crate::cli::handlers::Store",
"read",
CanonicalType::path(["crate", "cli", "handlers", "Data"]),
);
let mut fx = fx;
fx.local_symbols.insert("DbRef".to_string());
fx.local_symbols.insert("Store".to_string());
let calls = run(&fx, &index, "handle");
let _ = store;
assert!(
calls.contains("crate::cli::handlers::Store::read"),
"type-alias should expand DbRef → Store, got {calls:?}"
);
}
#[test]
fn turbofish_gives_concrete_return_type() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
let s = get::<Session>();
s.diff();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(
calls.contains("crate::app::session::Session::diff"),
"turbofish should resolve generic-ctor return type, got {calls:?}"
);
}
#[test]
fn turbofish_on_type_method_is_not_overridden() {
let fx = parse(
r#"
pub fn cmd() {
let v = Vec::<u32>::new();
v.custom_method();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(
calls.contains("<method>:custom_method"),
"Vec::<T>::new() turbofish must not override, got {calls:?}"
);
}
#[test]
fn mixed_resolutions_in_single_body() {
let fx = parse(
r#"
use crate::app::session::Session;
pub fn cmd() {
let s = Session::open().unwrap();
s.diff();
let x: u32 = 0;
x.random();
crate::app::make_session().unwrap().files();
}
"#,
);
let calls = run(&fx, &rlm_index(), "cmd");
assert!(
calls.contains("crate::app::session::Session::diff"),
"resolved: Session::diff missing, got {calls:?}"
);
assert!(
calls.contains("crate::app::session::Session::files"),
"resolved: Session::files missing, got {calls:?}"
);
assert!(
calls.contains("<method>:random"),
"unresolved: <method>:random expected, got {calls:?}"
);
}