You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Built from source, #2361 head (8116eaaa, on main @ 2278498, v0.11.0-186)
Platform / Install channel / Binary variant
Linux (x64) / Built from source / standard
What happened, and what did you expect?
Follow-up to #2154 / #2361, as agreed in the review there. #2361 gives a Scala companion object the Foo$ QN, and the import resolver / registry accept Foo$ for Foo — but only for the top-level owner of a member path. When the companion is an inner owner, import a.Outer.Inner.member and Outer.Inner.member(...) fail closed (per #2360's member rule) instead of binding Inner$.member.
Expected: import pkg.Outer.Inner.member → IMPORTS edge to pkg.Outer.Inner$.member, and Outer.Inner.member() → CALLS edge to the same node, i.e. the $ tolerance applied at every segment of the owner path, not just the first.
Measured on twitter/finagle (ca472de) in #2361: exactly two IMPORTS edges are lost to this, import Types.Name.Unnamed (ClientDispatcherSpec.scala → postgresql.Types.Name$.Unnamed) and import Mux.Server.SessionF (NonNegotiatingServer.scala → Mux.Server$.SessionF). Both were bound before #2361 only because the object's members collided into the class QN. Small on that corpus, but any codebase that nests object inside object and imports through the pair hits it.
Command: codebase-memory-mcp cli index_repository --repo-path /tmp/repro, then read the IMPORTS / CALLS edges of the project (search_graph for unit, or the SQLite file directly).
Expected: IMPORTS Use.scala → Shapes.Circle$.unit, and both CALLS at import_map confidence to Shapes.Circle$.unit.
Where it is
pass_pkgmap.c, member-import lookup (import pkg.Owner.rest): the top-level Owner comes from the index, then <Owner QN>$.<rest> and <Owner QN>.<rest> are tried, with $ only on that top-level segment; for Shapes.Circle.unit the node that exists is Shapes.Circle$.unit, and neither candidate names it.
registry.c, resolve_same_module / resolve_import_map: the companion probe suffixes the owner prefix once (companion_known(owner) → <owner>$.<rest>); qn_seg_matches already compares segment by segment and would accept Inner$ for Inner, but the two exact-lookup probes above do not enumerate the 2^k owner-segment combinations (in practice k ≤ 2–3, and only segments that are registered companions need the $ variant, so the enumeration is small and gated).
Proposed fix
In both places, walk the owner segments left to right and at each segment take the $ form when companion_known() says the prefix so far names a registered Scala companion (one hash probe per segment); the non-$ form otherwise. That keeps the gate from #2361 (nothing changes for Java/JS/TS) and adds no work for paths without companions. Tests: an edge_imports case for the snippet above (IMPORTS + CALLS), and a registry case for Outer.Inner.member with Outer.Inner$ registered from a .scala file.
I can send this as a small PR stacked on #2361 if wanted.
Confirmations
I searched existing issues and this is not a duplicate.
My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.
Version
Built from source, #2361 head (
8116eaaa, onmain@ 2278498, v0.11.0-186)Platform / Install channel / Binary variant
Linux (x64) / Built from source / standard
What happened, and what did you expect?
Follow-up to #2154 / #2361, as agreed in the review there. #2361 gives a Scala companion object the
Foo$QN, and the import resolver / registry acceptFoo$forFoo— but only for the top-level owner of a member path. When the companion is an inner owner,import a.Outer.Inner.memberandOuter.Inner.member(...)fail closed (per #2360's member rule) instead of bindingInner$.member.Expected:
import pkg.Outer.Inner.member→ IMPORTS edge topkg.Outer.Inner$.member, andOuter.Inner.member()→ CALLS edge to the same node, i.e. the$tolerance applied at every segment of the owner path, not just the first.Measured on
twitter/finagle(ca472de) in #2361: exactly two IMPORTS edges are lost to this,import Types.Name.Unnamed(ClientDispatcherSpec.scala→postgresql.Types.Name$.Unnamed) andimport Mux.Server.SessionF(NonNegotiatingServer.scala→Mux.Server$.SessionF). Both were bound before #2361 only because the object's members collided into the class QN. Small on that corpus, but any codebase that nestsobjectinsideobjectand imports through the pair hits it.Reproduction
Command:
codebase-memory-mcp cli index_repository --repo-path /tmp/repro, then read theIMPORTS/CALLSedges of the project (search_graphforunit, or the SQLite file directly).Result on the fix(scala): keep companion objects distinct from their classes (#2154) #2361 head (
8116eaaa): the nodet.Shapes.Shapes.Circle$.unitexists (the companion QN is right), butUse.scalahas no IMPORTS edge forimport geo.Shapes.Circle.unit(on the feat(scala): parse import selectors and resolve package-aware imports (#2153) #2360 head it boundShapes.Circle.unit, i.e. the pre-$collided node);Use.go → Circle$.unitandUse2.go → Circle$.unitstill exist, but only throughunique_name— on the feat(scala): parse import selectors and resolve package-aware imports (#2153) #2360 head both wereimport_map. They survive becauseunitis unique in this three-file repo; in a corpus with severalunitmethods the same calls are either lost or bound to the wrong one (and the fix(scala): suppress weak short-name matches for receiver calls #2366 guard, correctly, does not rescue a weak match forShapes.Circle.unit()).Expected: IMPORTS
Use.scala → Shapes.Circle$.unit, and both CALLS atimport_mapconfidence toShapes.Circle$.unit.Where it is
pass_pkgmap.c, member-import lookup (import pkg.Owner.rest): the top-levelOwnercomes from the index, then<Owner QN>$.<rest>and<Owner QN>.<rest>are tried, with$only on that top-level segment; forShapes.Circle.unitthe node that exists isShapes.Circle$.unit, and neither candidate names it.registry.c,resolve_same_module/resolve_import_map: the companion probe suffixes the owner prefix once (companion_known(owner)→<owner>$.<rest>);qn_seg_matchesalready compares segment by segment and would acceptInner$forInner, but the two exact-lookup probes above do not enumerate the 2^k owner-segment combinations (in practice k ≤ 2–3, and only segments that are registered companions need the$variant, so the enumeration is small and gated).Proposed fix
In both places, walk the owner segments left to right and at each segment take the
$form whencompanion_known()says the prefix so far names a registered Scala companion (one hash probe per segment); the non-$form otherwise. That keeps the gate from #2361 (nothing changes for Java/JS/TS) and adds no work for paths without companions. Tests: anedge_importscase for the snippet above (IMPORTS + CALLS), and aregistrycase forOuter.Inner.memberwithOuter.Inner$registered from a.scalafile.I can send this as a small PR stacked on #2361 if wanted.
Confirmations