Skip to content

Commit fd62fe8

Browse files
fix(code-index): restore Type::method recall for trait-impl UFCS
Dual-index `<Type as Trait>::method` under its type-path alias so calls like `WalkEventIter::from` bind again without renaming UFCS definitions. Prefer inherent methods when both match; ambiguous trait impls still abstain. Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
1 parent e2f4dbc commit fd62fe8

3 files changed

Lines changed: 441 additions & 88 deletions

File tree

‎crates/tracedecay-code-index/src/chunks.rs‎

Lines changed: 222 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,62 @@ pub(crate) fn cross_file_reference_name_is_blocklisted(reference_name: &str) ->
20972097
|| CROSS_FILE_REFERENCE_BLOCKLIST.contains(&owner)
20982098
}
20992099

2100+
/// Map a Rust UFCS trait-impl method path `<Type as Trait>::method` to the
2101+
/// type-path form `Type::method` that call sites write (`WalkEventIter::from`,
2102+
/// `Builder::default`). Keeps the intentional `<Type as Trait>` definition
2103+
/// name while restoring same-file / seal recall for those calls. `None` when
2104+
/// `path` is not a well-formed UFCS trait-impl method.
2105+
pub(crate) fn rust_type_path_alias_for_trait_impl_method(path: &str) -> Option<String> {
2106+
if !path.starts_with('<') {
2107+
return None;
2108+
}
2109+
let mut depth = 0_i32;
2110+
let mut as_split = None;
2111+
let mut close = None;
2112+
for (index, character) in path.char_indices() {
2113+
match character {
2114+
'<' => depth += 1,
2115+
'>' => {
2116+
depth -= 1;
2117+
if depth == 0 {
2118+
close = Some(index);
2119+
break;
2120+
}
2121+
}
2122+
_ => {
2123+
if depth == 1
2124+
&& as_split.is_none()
2125+
&& path[index..].starts_with(" as ")
2126+
{
2127+
as_split = Some(index);
2128+
}
2129+
}
2130+
}
2131+
}
2132+
let as_split = as_split?;
2133+
let close = close?;
2134+
let type_name = path.get(1..as_split)?.trim();
2135+
let trait_name = path.get(as_split + " as ".len()..close)?.trim();
2136+
let method = path.get(close + 1..)?.strip_prefix("::")?;
2137+
if type_name.is_empty()
2138+
|| trait_name.is_empty()
2139+
|| method.is_empty()
2140+
|| method.contains(':')
2141+
|| method.contains('<')
2142+
{
2143+
return None;
2144+
}
2145+
Some(format!("{type_name}::{method}"))
2146+
}
2147+
2148+
/// Whether `qualified_name`'s file-relative path is a UFCS trait-impl method
2149+
/// (`file.rs::<Type as Trait>::method`).
2150+
pub(crate) fn rust_qualified_name_is_ufcs_trait_impl(qualified_name: &str) -> bool {
2151+
qualified_name
2152+
.split_once("::")
2153+
.is_some_and(|(_, relative)| rust_type_path_alias_for_trait_impl_method(relative).is_some())
2154+
}
2155+
21002156
/// Resolve same-file symbol references (calls and other extractor reference
21012157
/// kinds) into relation edges, and retain the references this file cannot
21022158
/// bind as typed cross-file candidates. Only an UNAMBIGUOUS kind-compatible
@@ -2153,7 +2209,8 @@ fn resolve_file_references(
21532209
Vec<CodeIndexUnresolvedReferenceV1>,
21542210
) {
21552211
let mut by_name: BTreeMap<&str, Vec<&SymbolRow>> = BTreeMap::new();
2156-
let mut by_file_relative_name: BTreeMap<&str, Vec<&SymbolRow>> = BTreeMap::new();
2212+
let mut by_file_relative_name: BTreeMap<String, Vec<&SymbolRow>> = BTreeMap::new();
2213+
let mut type_path_aliases: Vec<(String, &SymbolRow)> = Vec::new();
21572214
for symbol in symbols {
21582215
by_name
21592216
.entry(symbol.name.as_str())
@@ -2164,9 +2221,42 @@ fn resolve_file_references(
21642221
.split_once("::")
21652222
.map_or(symbol.qualified_name.as_str(), |(_, name)| name);
21662223
by_file_relative_name
2167-
.entry(relative_name)
2224+
.entry(relative_name.to_owned())
21682225
.or_default()
21692226
.push(symbol);
2227+
// Dual-index `<Type as Trait>::method` under `Type::method` so
2228+
// type-path calls bind without renaming the definition. Collected
2229+
// first so an inherent `Type::method` already in the map keeps the
2230+
// path and trait-impl aliases do not steal it.
2231+
if let Some(alias) = rust_type_path_alias_for_trait_impl_method(relative_name) {
2232+
type_path_aliases.push((alias.clone(), symbol));
2233+
if let Some((type_name, method)) = alias.rsplit_once("::") {
2234+
if let Some(simple) = type_name.rsplit("::").next() {
2235+
if simple != type_name {
2236+
type_path_aliases.push((format!("{simple}::{method}"), symbol));
2237+
}
2238+
}
2239+
}
2240+
}
2241+
}
2242+
for (alias, symbol) in type_path_aliases {
2243+
let bucket = by_file_relative_name.entry(alias).or_default();
2244+
if bucket.iter().any(|existing| {
2245+
let relative = existing
2246+
.qualified_name
2247+
.split_once("::")
2248+
.map_or(existing.qualified_name.as_str(), |(_, name)| name);
2249+
rust_type_path_alias_for_trait_impl_method(relative).is_none()
2250+
}) {
2251+
continue;
2252+
}
2253+
if bucket
2254+
.iter()
2255+
.any(|existing| existing.node_id == symbol.node_id)
2256+
{
2257+
continue;
2258+
}
2259+
bucket.push(symbol);
21702260
}
21712261
let mut references_by_site: HashMap<(&str, EdgeKind, u32, u32), Vec<&UnresolvedRef>> =
21722262
HashMap::new();
@@ -4282,6 +4372,136 @@ pub fn real_symbol() {}
42824372
assert_ne!(calls[0].evidence_span, calls[1].evidence_span);
42834373
}
42844374

4375+
#[test]
4376+
fn rust_type_path_alias_parses_ufcs_trait_impl_methods() {
4377+
assert_eq!(
4378+
rust_type_path_alias_for_trait_impl_method(
4379+
"<WalkEventIter as From<WalkDir>>::from"
4380+
)
4381+
.as_deref(),
4382+
Some("WalkEventIter::from")
4383+
);
4384+
assert_eq!(
4385+
rust_type_path_alias_for_trait_impl_method(
4386+
"<crate::Builder as crate::First>::build"
4387+
)
4388+
.as_deref(),
4389+
Some("crate::Builder::build")
4390+
);
4391+
assert_eq!(
4392+
rust_type_path_alias_for_trait_impl_method("WalkEventIter::from"),
4393+
None
4394+
);
4395+
assert!(rust_qualified_name_is_ufcs_trait_impl(
4396+
"crates/ignore/src/walk.rs::<WalkEventIter as From<WalkDir>>::from"
4397+
));
4398+
assert!(!rust_qualified_name_is_ufcs_trait_impl(
4399+
"crates/ignore/src/walk.rs::WalkEventIter::from"
4400+
));
4401+
}
4402+
4403+
#[test]
4404+
fn type_path_call_binds_unique_trait_impl_method() {
4405+
let source = concat!(
4406+
"struct WalkEventIter;\n",
4407+
"struct WalkDir;\n",
4408+
"impl From<WalkDir> for WalkEventIter {\n",
4409+
" fn from(it: WalkDir) -> WalkEventIter { WalkEventIter }\n",
4410+
"}\n",
4411+
"fn build(wd: WalkDir) {\n",
4412+
" let _ = WalkEventIter::from(wd);\n",
4413+
"}\n",
4414+
);
4415+
let file = validated_file("src/walk.rs", source.as_bytes());
4416+
let batch = batch_for(&file, ParseOutcomeV1::Complete);
4417+
let artifacts = chunker()
4418+
.index_file(&file, &batch, &rust_descriptor(), &NeverCancelled)
4419+
.expect("indexing succeeds");
4420+
let from_method = artifacts
4421+
.symbols
4422+
.iter()
4423+
.find(|symbol| {
4424+
symbol.qualified_name
4425+
== "src/walk.rs::<WalkEventIter as From<WalkDir>>::from"
4426+
})
4427+
.expect("UFCS From::from method");
4428+
let build = artifacts
4429+
.symbols
4430+
.iter()
4431+
.find(|symbol| symbol.qualified_name == "src/walk.rs::build")
4432+
.expect("build function");
4433+
assert!(
4434+
artifacts.edges.iter().any(|edge| {
4435+
edge.from_occurrence == build.occurrence
4436+
&& edge.to_occurrence == from_method.occurrence
4437+
&& edge.kind == RelationEdgeKindV1::Calls
4438+
&& edge.authority == EdgeAuthorityV1::SyntaxExact
4439+
}),
4440+
"WalkEventIter::from must bind to <WalkEventIter as From<WalkDir>>::from"
4441+
);
4442+
assert!(
4443+
!artifacts.unresolved_references.iter().any(|reference| {
4444+
reference.reference_name == "WalkEventIter::from"
4445+
}),
4446+
"type-path call must resolve same-file rather than remain for sealing"
4447+
);
4448+
}
4449+
4450+
#[test]
4451+
fn type_path_alias_does_not_steal_inherent_method() {
4452+
let source = concat!(
4453+
"struct Builder;\n",
4454+
"trait First { fn build(&self); }\n",
4455+
"impl Builder {\n",
4456+
" fn build(&self) {}\n",
4457+
"}\n",
4458+
"impl First for Builder {\n",
4459+
" fn build(&self) {}\n",
4460+
"}\n",
4461+
"fn assemble() {\n",
4462+
" Builder::build(&Builder);\n",
4463+
"}\n",
4464+
);
4465+
let file = validated_file("src/lib.rs", source.as_bytes());
4466+
let batch = batch_for(&file, ParseOutcomeV1::Complete);
4467+
let artifacts = chunker()
4468+
.index_file(&file, &batch, &rust_descriptor(), &NeverCancelled)
4469+
.expect("indexing succeeds");
4470+
let inherent = artifacts
4471+
.symbols
4472+
.iter()
4473+
.find(|symbol| symbol.qualified_name == "src/lib.rs::Builder::build")
4474+
.expect("inherent Builder::build");
4475+
let trait_impl = artifacts
4476+
.symbols
4477+
.iter()
4478+
.find(|symbol| {
4479+
symbol.qualified_name == "src/lib.rs::<Builder as First>::build"
4480+
})
4481+
.expect("trait-impl build");
4482+
let assemble = artifacts
4483+
.symbols
4484+
.iter()
4485+
.find(|symbol| symbol.qualified_name == "src/lib.rs::assemble")
4486+
.expect("assemble");
4487+
assert!(
4488+
artifacts.edges.iter().any(|edge| {
4489+
edge.from_occurrence == assemble.occurrence
4490+
&& edge.to_occurrence == inherent.occurrence
4491+
&& edge.kind == RelationEdgeKindV1::Calls
4492+
}),
4493+
"Builder::build must keep the inherent method when both exist"
4494+
);
4495+
assert!(
4496+
artifacts.edges.iter().all(|edge| {
4497+
edge.from_occurrence != assemble.occurrence
4498+
|| edge.to_occurrence != trait_impl.occurrence
4499+
|| edge.kind != RelationEdgeKindV1::Calls
4500+
}),
4501+
"trait-impl alias must not steal the inherent type-path binding"
4502+
);
4503+
}
4504+
42854505
#[test]
42864506
fn field_type_resolves_to_declaration_when_an_impl_has_the_same_name() {
42874507
let source = concat!(

0 commit comments

Comments
 (0)