@@ -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