Skip to content

Scala: companion $ tolerance stops at the top-level owner, so import pkg.Outer.Inner.member / Outer.Inner.member() fail closed #2426

Description

@htarnacki

Version

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.

Reproduction

  1. Dummy snippet, three files in one package:
// t/Shapes.scala
package geo
object Shapes {
  class Circle(val r: Double)
  object Circle { def unit(): Circle = new Circle(1.0) }
}
// t/Use.scala
package geo.app
import geo.Shapes.Circle.unit
object Use { def go(): Unit = unit() }
// t/Use2.scala
package geo.app
import geo.Shapes
object Use2 { def go(): Unit = Shapes.Circle.unit() }
  1. 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).

  2. Result on the fix(scala): keep companion objects distinct from their classes (#2154) #2361 head (8116eaaa): the node t.Shapes.Shapes.Circle$.unit exists (the companion QN is right), but

    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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    parsing/qualityGraph extraction bugs, false positives, missing edges

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions