@@ -13,7 +13,12 @@ impl SymbolExtractor for RustExtractor {
1313 walk_tree ( & tree. root_node ( ) , source, & mut symbols, match_rust_node) ;
1414 walk_ast_nodes_with_config ( & tree. root_node ( ) , source, & mut symbols. ast_nodes , & RUST_AST_CONFIG ) ;
1515 walk_tree ( & tree. root_node ( ) , source, & mut symbols, match_rust_type_map) ;
16+ walk_tree ( & tree. root_node ( ) , source, & mut symbols, match_rust_return_type_map) ;
17+ // Must run after type_map is populated — resolves `receiver.method()` call
18+ // assignments against locally-typed receivers (mirrors javascript.rs's ordering).
19+ walk_tree ( & tree. root_node ( ) , source, & mut symbols, match_rust_call_assignments) ;
1620 dedup_type_map ( & mut symbols. type_map ) ;
21+ dedup_type_map ( & mut symbols. return_type_map ) ;
1722 symbols
1823 }
1924}
@@ -77,9 +82,10 @@ fn handle_function_item(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
7782
7883fn handle_struct_item ( node : & Node , source : & [ u8 ] , symbols : & mut FileSymbols ) {
7984 if let Some ( name_node) = node. child_by_field_name ( "name" ) {
85+ let struct_name = node_text ( & name_node, source) . to_string ( ) ;
8086 let children = extract_rust_struct_fields ( node, source) ;
8187 symbols. definitions . push ( Definition {
82- name : node_text ( & name_node , source ) . to_string ( ) ,
88+ name : struct_name . clone ( ) ,
8389 kind : "struct" . to_string ( ) ,
8490 line : start_line ( node) ,
8591 end_line : Some ( end_line ( node) ) ,
@@ -88,6 +94,27 @@ fn handle_struct_item(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
8894 cfg : None ,
8995 children : opt_children ( children) ,
9096 } ) ;
97+ seed_rust_struct_field_types ( node, & struct_name, source, symbols) ;
98+ }
99+ }
100+
101+ /// Seed `${StructName}.${fieldName}` → field-type entries in `symbols.type_map`
102+ /// so `self.field.method()` inside the struct's own impl methods resolves via
103+ /// the class-scoped receiver lookup — mirrors JS's `this.field` class-scoped
104+ /// typing (issues #1323, #1458) and fixes #1876's `self.field` false negatives.
105+ fn seed_rust_struct_field_types ( node : & Node , struct_name : & str , source : & [ u8 ] , symbols : & mut FileSymbols ) {
106+ let Some ( body) = node. child_by_field_name ( "body" ) else { return } ;
107+ for i in 0 ..body. child_count ( ) {
108+ let Some ( field) = body. child ( i) else { continue } ;
109+ if field. kind ( ) != "field_declaration" { continue }
110+ let Some ( field_name) = field. child_by_field_name ( "name" ) else { continue } ;
111+ let Some ( type_node) = field. child_by_field_name ( "type" ) else { continue } ;
112+ let Some ( type_name) = extract_rust_type_name ( & type_node, source) else { continue } ;
113+ push_type_map_entry (
114+ symbols,
115+ format ! ( "{}.{}" , struct_name, node_text( & field_name, source) ) ,
116+ type_name,
117+ ) ;
91118 }
92119}
93120
@@ -370,6 +397,11 @@ fn extract_scoped_use_list(node: &Node, source: &[u8]) -> Vec<(String, Vec<Strin
370397 vec ! [ ( prefix, names) ]
371398}
372399
400+ /// True if `name` matches a struct defined in this file (match_rust_node runs before this).
401+ fn is_known_unit_struct ( name : & str , symbols : & FileSymbols ) -> bool {
402+ symbols. definitions . iter ( ) . any ( |d| d. kind == "struct" && d. name == name)
403+ }
404+
373405fn extract_rust_type_name < ' a > ( type_node : & Node < ' a > , source : & ' a [ u8 ] ) -> Option < & ' a str > {
374406 match type_node. kind ( ) {
375407 "type_identifier" | "identifier" | "scoped_type_identifier" => Some ( node_text ( type_node, source) ) ,
@@ -401,6 +433,28 @@ fn match_rust_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _d
401433 confidence : 0.9 ,
402434 } ) ;
403435 }
436+ } else if let Some ( value_node) = node. child_by_field_name ( "value" ) {
437+ // let x = TypeName; — a bare capitalized identifier value binds
438+ // a unit-struct instance (e.g. `let v = NameValidator;` for
439+ // `struct NameValidator;`), not a reference to another variable (#1876).
440+ // Requiring a same-file `struct` definition excludes unit enum variants
441+ // like `None`/`Ok` (Option/Result, always in scope) and any custom
442+ // fieldless variant brought into scope via `use Enum::Variant` — those
443+ // also parse as a bare capitalized identifier but are values, not types
444+ // (Greptile review). A struct defined elsewhere in the crate is missed,
445+ // same as every other same-file-only heuristic in this extractor.
446+ if value_node. kind ( ) == "identifier" {
447+ let type_name = node_text ( & value_node, source) ;
448+ if type_name. starts_with ( |c : char | c. is_uppercase ( ) )
449+ && is_known_unit_struct ( type_name, symbols)
450+ {
451+ symbols. type_map . push ( TypeMapEntry {
452+ name : node_text ( & pattern, source) . to_string ( ) ,
453+ type_name : type_name. to_string ( ) ,
454+ confidence : 0.7 ,
455+ } ) ;
456+ }
457+ }
404458 }
405459 }
406460 }
@@ -427,6 +481,105 @@ fn match_rust_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _d
427481 }
428482}
429483
484+ // ── Return-type map extraction (Phase 8.2 parity, #1876) ────────────────────
485+
486+ /// Populate `symbols.return_type_map` with declared `-> ReturnType` return
487+ /// types for free functions and impl methods, resolving `Self` to the
488+ /// enclosing impl's type name. Mirrors `extractRustReturnTypeMap` in
489+ /// `src/extractors/rust.ts`. Consumed by `propagate_return_types_across_files`
490+ /// (Phase 8.2) — the same generic cross-file mechanism the JS/TS extractor
491+ /// feeds — so a local var typed from a cross-file call's return value
492+ /// (`let service = build_service();`) resolves without any Rust-specific
493+ /// propagation logic.
494+ fn match_rust_return_type_map ( node : & Node , source : & [ u8 ] , symbols : & mut FileSymbols , _depth : usize ) {
495+ if node. kind ( ) != "function_item" { return }
496+ // Skip default-impl functions inside traits, matching handle_function_item —
497+ // their return type is not tied to a concrete implementing type.
498+ if node. parent ( ) . and_then ( |p| p. parent ( ) ) . map_or ( false , |gp| gp. kind ( ) == "trait_item" ) {
499+ return ;
500+ }
501+ let Some ( name_node) = node. child_by_field_name ( "name" ) else { return } ;
502+ let Some ( return_type_node) = node. child_by_field_name ( "return_type" ) else { return } ;
503+ let Some ( raw_type) = extract_rust_type_name ( & return_type_node, source) else { return } ;
504+ let impl_type = find_current_impl ( node, source) ;
505+ // `-> Self` inside an impl block returns the concrete implementing type.
506+ let type_name = if raw_type == "Self" {
507+ impl_type. as_deref ( ) . unwrap_or ( raw_type)
508+ } else {
509+ raw_type
510+ } ;
511+ let full_name = match & impl_type {
512+ Some ( t) => format ! ( "{}.{}" , t, node_text( & name_node, source) ) ,
513+ None => node_text ( & name_node, source) . to_string ( ) ,
514+ } ;
515+ let existing_confidence = symbols. return_type_map . iter ( )
516+ . find ( |e| e. name == full_name)
517+ . map ( |e| e. confidence ) ;
518+ if existing_confidence. map_or ( true , |c| c < 1.0 ) {
519+ symbols. return_type_map . push ( TypeMapEntry {
520+ name : full_name,
521+ type_name : type_name. to_string ( ) ,
522+ confidence : 1.0 ,
523+ } ) ;
524+ }
525+ }
526+
527+ // ── Call-assignment extraction (Phase 8.2 parity, #1876) ─────────────────────
528+
529+ /// Record `let x = callee(...);` bindings into `symbols.call_assignments` so
530+ /// `propagate_return_types_across_files` can type `x` from `callee`'s
531+ /// declared return type. Mirrors `extractRustCallAssignments` in
532+ /// `src/extractors/rust.ts` — see that function's doc comment for the three
533+ /// call shapes handled (bare function call, `Type::assoc_fn()`, and method
534+ /// call on a locally-typed receiver).
535+ fn match_rust_call_assignments ( node : & Node , source : & [ u8 ] , symbols : & mut FileSymbols , _depth : usize ) {
536+ if node. kind ( ) != "let_declaration" { return }
537+ let Some ( pattern) = node. child_by_field_name ( "pattern" ) else { return } ;
538+ if pattern. kind ( ) != "identifier" { return }
539+ let Some ( value) = node. child_by_field_name ( "value" ) else { return } ;
540+ if value. kind ( ) != "call_expression" { return }
541+ let Some ( fn_node) = value. child_by_field_name ( "function" ) else { return } ;
542+ let var_name = node_text ( & pattern, source) . to_string ( ) ;
543+
544+ match fn_node. kind ( ) {
545+ "identifier" => {
546+ symbols. call_assignments . push ( NativeCallAssignment {
547+ var_name,
548+ callee_name : node_text ( & fn_node, source) . to_string ( ) ,
549+ receiver_type_name : None ,
550+ } ) ;
551+ }
552+ "scoped_identifier" => {
553+ let name = fn_node. child_by_field_name ( "name" ) ;
554+ let path = fn_node. child_by_field_name ( "path" ) ;
555+ if let ( Some ( name) , Some ( path) ) = ( name, path) {
556+ symbols. call_assignments . push ( NativeCallAssignment {
557+ var_name,
558+ callee_name : node_text ( & name, source) . to_string ( ) ,
559+ receiver_type_name : Some ( node_text ( & path, source) . to_string ( ) ) ,
560+ } ) ;
561+ }
562+ }
563+ "field_expression" => {
564+ let field = fn_node. child_by_field_name ( "field" ) ;
565+ let receiver = fn_node. child_by_field_name ( "value" ) ;
566+ if let ( Some ( field) , Some ( receiver) ) = ( field, receiver) {
567+ if receiver. kind ( ) == "identifier" {
568+ let receiver_type = symbols. type_map . iter ( )
569+ . find ( |e| e. name == node_text ( & receiver, source) )
570+ . map ( |e| e. type_name . clone ( ) ) ;
571+ symbols. call_assignments . push ( NativeCallAssignment {
572+ var_name,
573+ callee_name : node_text ( & field, source) . to_string ( ) ,
574+ receiver_type_name : receiver_type,
575+ } ) ;
576+ }
577+ }
578+ }
579+ _ => { }
580+ }
581+ }
582+
430583#[ cfg( test) ]
431584mod tests {
432585 use super :: * ;
@@ -492,4 +645,76 @@ mod tests {
492645 assert_eq ! ( children. len( ) , 1 ) ;
493646 assert_eq ! ( children[ 0 ] . name, "x" ) ;
494647 }
648+
649+ // ── #1876: receiver-typed locals + self.field type map ──────────────────
650+
651+ #[ test]
652+ fn seeds_struct_field_type_map ( ) {
653+ let s = parse_rust ( "struct UserService { repo: UserRepository }" ) ;
654+ let entry = s. type_map . iter ( ) . find ( |e| e. name == "UserService.repo" ) . unwrap ( ) ;
655+ assert_eq ! ( entry. type_name, "UserRepository" ) ;
656+ }
657+
658+ #[ test]
659+ fn seeds_unit_struct_value_type_map ( ) {
660+ let s = parse_rust ( "struct NameValidator;\n fn f() { let v = NameValidator; }" ) ;
661+ let entry = s. type_map . iter ( ) . find ( |e| e. name == "v" ) . unwrap ( ) ;
662+ assert_eq ! ( entry. type_name, "NameValidator" ) ;
663+ }
664+
665+ #[ test]
666+ fn does_not_type_unit_enum_variant_as_unit_struct ( ) {
667+ // `None` (Option::None) parses identically to a unit-struct reference — a bare
668+ // capitalized identifier — but is an enum variant, not a struct. Without a same-file
669+ // `struct` definition for the name, it must not be typed (#1876 review).
670+ let s = parse_rust ( "fn f() { let x = None; }" ) ;
671+ assert ! ( s. type_map. iter( ) . all( |e| e. name != "x" ) ) ;
672+ }
673+
674+ #[ test]
675+ fn does_not_type_lowercase_bare_identifier_binding ( ) {
676+ let s = parse_rust ( "fn f() { let a = 1; let b = a; }" ) ;
677+ assert ! ( s. type_map. iter( ) . all( |e| e. name != "b" ) ) ;
678+ }
679+
680+ #[ test]
681+ fn stores_return_type_for_free_function ( ) {
682+ let s = parse_rust ( "fn build_service() -> UserService { todo!() }" ) ;
683+ let entry = s. return_type_map . iter ( ) . find ( |e| e. name == "build_service" ) . unwrap ( ) ;
684+ assert_eq ! ( entry. type_name, "UserService" ) ;
685+ assert_eq ! ( entry. confidence, 1.0 ) ;
686+ }
687+
688+ #[ test]
689+ fn resolves_self_return_type_to_impl_type ( ) {
690+ let s = parse_rust ( "struct UserRepository;\n impl UserRepository {\n fn new() -> Self { UserRepository }\n }" ) ;
691+ let entry = s. return_type_map . iter ( ) . find ( |e| e. name == "UserRepository.new" ) . unwrap ( ) ;
692+ assert_eq ! ( entry. type_name, "UserRepository" ) ;
693+ }
694+
695+ #[ test]
696+ fn records_call_assignment_for_bare_function_call ( ) {
697+ let s = parse_rust ( "fn f() { let service = build_service(); }" ) ;
698+ let ca = s. call_assignments . iter ( ) . find ( |c| c. var_name == "service" ) . unwrap ( ) ;
699+ assert_eq ! ( ca. callee_name, "build_service" ) ;
700+ assert_eq ! ( ca. receiver_type_name, None ) ;
701+ }
702+
703+ #[ test]
704+ fn records_call_assignment_for_associated_function_call ( ) {
705+ let s = parse_rust ( "fn f() { let repo = UserRepository::new(); }" ) ;
706+ let ca = s. call_assignments . iter ( ) . find ( |c| c. var_name == "repo" ) . unwrap ( ) ;
707+ assert_eq ! ( ca. callee_name, "new" ) ;
708+ assert_eq ! ( ca. receiver_type_name. as_deref( ) , Some ( "UserRepository" ) ) ;
709+ }
710+
711+ #[ test]
712+ fn records_call_assignment_for_method_call_on_typed_receiver ( ) {
713+ let s = parse_rust (
714+ "fn f() {\n let repo: UserRepository = make();\n let user = repo.find_by_id(1);\n }" ,
715+ ) ;
716+ let ca = s. call_assignments . iter ( ) . find ( |c| c. var_name == "user" ) . unwrap ( ) ;
717+ assert_eq ! ( ca. callee_name, "find_by_id" ) ;
718+ assert_eq ! ( ca. receiver_type_name. as_deref( ) , Some ( "UserRepository" ) ) ;
719+ }
495720}
0 commit comments