2626import re
2727from datetime import datetime
2828from pathlib import Path
29- from typing import Any , Dict , List , Optional , Tuple
29+ from typing import Any , Dict , List , Optional , Set , Tuple
3030
3131from utilities .file_io import write_json
3232
@@ -75,6 +75,17 @@ def _load_rust_language() -> Language:
7575# `impl` block, or as the RHS of a `let x: <type> = ...` annotation.
7676_TYPE_NODE_KINDS = ("type_identifier" , "generic_type" , "scoped_type_identifier" )
7777
78+ # Non-nominal Self-type node kinds that are absent from _TYPE_NODE_KINDS but CAN be
79+ # an impl target: `impl Trait for u32 / [u8;4] / (i32,i32) / () / &T / *const T /
80+ # dyn X`. Collected in _handle_impl so their methods are extracted; for the
81+ # genuinely non-nominal ones _bare_type_name returns None and _handle_impl falls
82+ # back to the raw type text, while reference/dynamic types unwrap to their nominal
83+ # base as usual.
84+ _IMPL_SELF_EXTRA_KINDS = (
85+ "primitive_type" , "array_type" , "tuple_type" , "unit_type" ,
86+ "reference_type" , "pointer_type" , "dynamic_type" ,
87+ )
88+
7889
7990def _bare_type_name (node : Optional [Node ], source : bytes ) -> Optional [str ]:
8091 """Reduce a type node to its bare (unqualified, non-generic) name.
@@ -106,16 +117,16 @@ def _bare_type_name(node: Optional[Node], source: bytes) -> Optional[str]:
106117 return _text (last , source ) if last is not None else None
107118 if t == "reference_type" :
108119 # `&Point` / `&mut Point` / `&'a Point` -> unwrap to Point;
109- # `&dyn Shape` -> unwrap through the dynamic_type to Shape (val_3_19) .
120+ # `&dyn Shape` -> unwrap through the dynamic_type to Shape.
110121 for child in node .children :
111122 if child .type in _TYPE_NODE_KINDS or child .type == "dynamic_type" :
112123 return _bare_type_name (child , source )
113124 return None
114125 if t == "dynamic_type" :
115126 # `dyn Shape` / `dyn Shape + Send` -> the trait's bare name, so a
116127 # `&dyn Shape` receiver types as `Shape` and dispatches to Shape's
117- # conformers via trait_impls (val_3_19; `dyn Shape` is a `dynamic_type`
118- # node, verified against the installed grammar per pr_2_1 ).
128+ # conformers via trait_impls (`dyn Shape` is a `dynamic_type`
129+ # node, verified against the installed grammar).
119130 for child in node .children :
120131 r = _bare_type_name (child , source )
121132 if r :
@@ -129,6 +140,53 @@ def _bare_type_name(node: Optional[Node], source: bytes) -> Optional[str]:
129140 return None
130141
131142
143+ def _impl_generic_bounds (impl_node : Node , source : bytes ) -> Dict [str , List [str ]]:
144+ """Map an impl block's OWN generic param letter -> its bound trait(s).
145+
146+ `impl<T: Shape + Draw> Foo<T>` and `impl<T> Foo<T> where T: Shape` both yield
147+ `{"T": ["Shape", ...]}`. Only the impl header's own generics (the `<...>` before
148+ the self type, plus the where-clause) are read. Threaded onto each method so a
149+ receiver typed as the impl's generic param (`x: &T`) dispatches to the bound
150+ trait's conformers -- the SAME reachability-safe closure fn-level bounds use --
151+ instead of falling to a bare lookup on the letter `T` (which a blanket impl's
152+ pseudo-type `T` would poison). Mirrors CallGraphBuilder._collect_type_param_bounds.
153+ """
154+ bounds : Dict [str , List [str ]] = {}
155+
156+ def _traits (tb : Node ) -> List [str ]:
157+ return [_text (c , source ) for c in tb .children if c .type == "type_identifier" ]
158+
159+ def _add (param : Optional [str ], tb : Node ) -> None :
160+ if not param :
161+ return
162+ bounds .setdefault (param , [])
163+ for t in _traits (tb ):
164+ if t not in bounds [param ]:
165+ bounds [param ].append (t )
166+
167+ def _param (node : Node ) -> None :
168+ pid = None
169+ for cc in node .children :
170+ if cc .type == "type_identifier" and pid is None :
171+ pid = _text (cc , source )
172+ elif cc .type == "trait_bounds" :
173+ _add (pid , cc )
174+
175+ seen_for = False
176+ for child in impl_node .children :
177+ if child .type == "for" :
178+ seen_for = True
179+ elif child .type == "type_parameters" and not seen_for :
180+ for tp in child .children :
181+ if tp .type in ("type_parameter" , "constrained_type_parameter" ):
182+ _param (tp )
183+ elif child .type == "where_clause" :
184+ for wp in child .children :
185+ if wp .type == "where_predicate" :
186+ _param (wp )
187+ return bounds
188+
189+
132190def _text (node : Optional [Node ], source : bytes ) -> str :
133191 if node is None :
134192 return ""
@@ -403,7 +461,7 @@ def _handle_impl(
403461 if cc .type == "type_identifier" :
404462 impl_generics .add (_text (cc , source ))
405463 break
406- if child .type in _TYPE_NODE_KINDS :
464+ if child .type in _TYPE_NODE_KINDS or child . type in _IMPL_SELF_EXTRA_KINDS :
407465 type_nodes .append ((child , seen_for ))
408466 elif child .type == "declaration_list" :
409467 body = child
@@ -418,6 +476,14 @@ def _handle_impl(
418476 self_node = type_nodes [0 ][0 ] if type_nodes else None
419477
420478 self_type = _bare_type_name (self_node , source )
479+ if not self_type and self_node is not None :
480+ # Non-nominal Self type (primitive `u32`, array `[u8; 4]`, tuple
481+ # `(i32, i32)`, unit `()`): `_bare_type_name` only names nominal types,
482+ # so `impl Serialize for u32` would be dropped ENTIRELY -- every method
483+ # in the block lost from extraction, the graph, and reachability. Fall
484+ # back to the raw type text so the methods are still extracted (keyed by
485+ # that type spelling).
486+ self_type = _text (self_node , source ).strip ()
421487 trait_name = _bare_type_name (trait_node , source )
422488
423489 if not self_type or body is None :
@@ -439,6 +505,8 @@ def _handle_impl(
439505 "module_path" : ctx ["module_path" ],
440506 "in_test_scope" : ctx ["in_test_scope" ],
441507 "in_trait_impl" : trait_name is not None ,
508+ "impl_trait" : trait_name ,
509+ "impl_type_param_bounds" : _impl_generic_bounds (node , source ),
442510 }
443511 worklist .append ((body , new_ctx ))
444512
@@ -514,7 +582,29 @@ def _handle_function(
514582
515583 module_name = "::" .join (ctx ["module_path" ]) if ctx ["module_path" ] else None
516584
585+ # Bare return-type name (`-> Widget` -> "Widget"), so a binding
586+ # `let w = Type::assoc()` can be typed by the assoc fn's ACTUAL return type
587+ # rather than the constructor-idiom assumption that it returns `Type`.
588+ rt_node = node .child_by_field_name ("return_type" )
589+ return_type = _bare_type_name (rt_node , source ) if rt_node is not None else None
590+
517591 func_id = f"{ file_path } :{ qualified_name } "
592+ if func_id in functions :
593+ # Same qualified_name already taken -- e.g. `impl Display for P` and
594+ # `impl Debug for P` both yield `P.fmt`, or an inherent method plus a
595+ # same-named trait method. Without disambiguation the second silently
596+ # clobbers the first (a whole unit lost from the graph AND reachability).
597+ # Append the trait (or `impl`) so both survive. The FIRST occurrence
598+ # keeps the plain id, so class_name-based resolution and existing
599+ # `Type.method` references are unchanged; only the colliding sibling
600+ # gets the `#trait` suffix.
601+ disc = ctx .get ("impl_trait" ) or "impl"
602+ candidate = f"{ file_path } :{ qualified_name } #{ disc } "
603+ n = 2
604+ while candidate in functions :
605+ candidate = f"{ file_path } :{ qualified_name } #{ disc } { n } "
606+ n += 1
607+ func_id = candidate
518608 functions [func_id ] = {
519609 "name" : name ,
520610 "qualified_name" : qualified_name ,
@@ -529,6 +619,11 @@ def _handle_function(
529619 "is_exported" : is_exported ,
530620 "has_self" : has_self ,
531621 "decorators" : attrs ,
622+ # Bounds of the enclosing impl's own generics (`impl<T: Shape> Foo<T>`),
623+ # so a receiver typed as `T` in this method dispatches to the trait's
624+ # conformers. Empty for free functions / inherent-non-generic impls.
625+ "impl_type_param_bounds" : ctx .get ("impl_type_param_bounds" , {}),
626+ "return_type" : return_type ,
532627 }
533628
534629 block = None
0 commit comments