Repository navigation
Description
Version
Built from source, main @ 2278498 (v0.11.0-186) and the #2360 head (bb3f1cdd)
Platform / Install channel / Binary variant
Linux (x64) / Built from source / standard
What happened, and what did you expect?
Follow-up to #2153 / #2360, as agreed in the review there. Scala resolves import paths relative to the packages visible at the import site before trying them as absolute paths: inside package a.b.c, import d.E means a.b.c.d.E if that exists, and import Owner.member in the same package means a.b.c.Owner.member. With chained clauses (package a.b / package c) the members of a.b are visible too, so import x.Y may also mean a.b.x.Y. A single package a.b.c clause does not make a.b visible.
The import resolver only knows the absolute form. Every relative import of an in-repo symbol is therefore lost:
- on
main,import Owner.memberin the same package binds through the generic symbol-name fallback to the owner (the owner-fallback shape closed in feat(scala): parse import selectors and resolve package-aware imports (#2153) #2360), andimport subpkg.Nameproduces no edge at all; - on the feat(scala): parse import selectors and resolve package-aware imports (#2153) #2360 head the Scala path is fail-closed, so all three forms produce no IMPORTS edge, and calls through them fall back from
import_maptounique_name, i.e. they survive only while the member name is unique in the repository.
Expected: an IMPORTS edge to the member the relative path names (Addr.State, util.Fmt.Fmt, shared.Log.Log below), and the calls through it at import_map confidence.
Measured on twitter/finagle (ca472de): this is the single edge #2360 loses versus main — StabilizingAddrTest.scala (package com.twitter.finagle.addr) import StabilizingAddr.State._, which main bound to the StabilizingAddr owner and #2360 drops. finagle almost always writes absolute imports, so the corpus under-represents the pattern; codebases that lean on same-package and sub-package imports lose the corresponding share of their Scala IMPORTS edges.
Reproduction
- Dummy snippet, six files under
t/:
// t/Addresses.scala
package net.addr
object Addr {
object State { val Healthy: Int = 1; val Unhealthy: Int = 2 }
def parse(s: String): Addr = new Addr(s)
}
class Addr(val host: String)// t/Watcher.scala — same-package relative member import
package net.addr
import Addr.State
object Watcher { def check(): Int = State.Healthy }// t/util/Fmt.scala
package net.addr.util
object Fmt { def render(a: Addr): String = a.host }// t/Printer.scala — sub-package relative import
package net.addr
import util.Fmt
object Printer { def show(a: Addr): String = Fmt.render(a) }// t/shared/Log.scala
package net.shared
object Log { def info(msg: String): Unit = println(msg) }// t/Report.scala — relative to the outer clause of a chained package
package net
package addr
import shared.Log
object Report { def emit(a: Addr): Unit = Log.info(a.host) }-
Command:
codebase-memory-mcp cli index_repository --repo-path /tmp/repro, then read theIMPORTS/CALLSedges of the project (search_graphforFmt, or the SQLite file directly). -
Result — all four definition nodes exist (
t.Addresses.Addr.State,t.util.Fmt.Fmt,t.shared.Log.Log, …):import main2278498feat(scala): parse import selectors and resolve package-aware imports (#2153) #2360 bb3f1cddexpected import Addr.StateIMPORTS → t.Addresses.Addr(owner, not the member)no edge IMPORTS → t.Addresses.Addr.Stateimport util.Fmtno edge no edge IMPORTS → t.util.Fmt.Fmtimport shared.Logno edge no edge IMPORTS → t.shared.Log.LogFmt.render(a)/Log.info(...)CALLS, unique_nameCALLS, unique_nameCALLS, import_mapNote the file names are deliberately different from the object names (
Addresses.scalaforobject Addr). When they coincide and the two files sit in the same directory, Strategy 1 (path-based module resolution) happens to bind the import to the sibling file's node rather than the member, which masks the defect in the simplest layouts.
Where it is
pass_pkgmap.c,resolve_scala_namespace_import: takesimp->module_pathas an absolute dotted path, walks its prefixes through the namespace map and the(package, top-level name)index, and returnsNULLwhen no prefix is a known package. The importing file's own package is never consulted, although it is already available: the namespace-map walk recordsfile QN → packageand (since feat(scala): parse import selectors and resolve package-aware imports (#2153) #2360) writes it as thepackageproperty on the File node.- Chained clauses are joined into one namespace string (
net.addrforpackage net/package addr), so the resolver cannot tell thatnetis visible as well.
Proposed fix
In resolve_scala_namespace_import, before the absolute attempt, prefix the import path with each package visible at the import site, longest first, and run the same fail-closed lookup on each candidate:
- the file's full package (
net.addr→net.addr.Addr.State,net.addr.util.Fmt); - for chained clauses, each shorter clause prefix (
net→net.shared.Log); a single clausepackage a.b.ccontributes onlya.b.c, nota.b; - the absolute path as today.
First candidate that resolves wins; if none does, NULL as now. Step 2 needs the clause boundaries, which means recording the chain (or the offsets of the joins) next to the joined namespace instead of only the joined string — one extra small field per Scala file. The lookup per candidate is the existing O(1) index probe, so the cost is bounded by the number of clauses. Java and Kotlin are unaffected (they do not enter this resolver).
Tests: edge_imports cases for the three imports above (positive), plus a negative one showing that package a.b.c + import d.E does not bind a.b.d.E, and the incremental-reindex parity check extended to a relative import.
I can send this as a small PR stacked on #2360 if wanted.
Activity
DeusData commentedon Oct 19, 2026
Thank you for the three relative-import forms and the negative single-clause boundary. The distinction between chained and single package clauses is useful to preserve when reviewing a fix. I have not approved a stacked PR or chosen the additional metadata shape yet.
DeusData commentedon Oct 25, 2026
Thank you for your patience. The relative-import clause boundaries and additional Scala metadata still need maintainer direction before a stacked PR is requested. Sorry for the delay; I’ll follow up soon.