Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes member collection in GetFormatter and SymbolLister so that overloaded methods aren’t collapsed to a single entry, and adds a Scala 3 fixture + tests to guard the behavior (issue #14).
Changes:
- Update
collectClassMembersinGetFormatterandSymbolListerto preserve multiple declarations with the same name when they come from the same class/trait. - Add
CellarOverloadedScala 3 fixture with 3processoverloads. - Add tests to verify both
listMembersandformatSymbolinclude all overloads.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/src/cellar/SymbolLister.scala | Adjusts member collection logic to keep same-owner overloads. |
| lib/src/cellar/GetFormatter.scala | Adjusts member collection logic to keep same-owner overloads in formatted output. |
| lib/test/src/cellar/SymbolListerTest.scala | Adds regression test ensuring listMembers returns all overloads. |
| lib/test/src/cellar/GetFormatterTest.scala | Adds regression test ensuring formatted members include all overloads. |
| fixtureScala3/src/cellar/fixture/scala3/CellarOverloaded.scala | Introduces a dedicated overloaded-method fixture. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
lib/src/cellar/SymbolLister.scala
Outdated
| val newDecls = decls.filter(d => !seen.contains(d.name.toString)) | ||
| newDecls.foreach(d => seen += d.name.toString) | ||
| result ++= newDecls |
There was a problem hiding this comment.
collectClassMembers still deduplicates across the linearization by name only (seen is keyed by d.name.toString). This means overloads declared in different parents/traits (e.g., one process(Int) in a base trait and process(String) in a derived trait) will still be dropped once any member with that name is seen, even though both overloads are part of the public API. Consider deduplicating by a signature-level key (e.g., name + parameter types / declaredType, or a stable representation like TypePrinter.printSymbolSignatureSafe) so only true overrides are removed while distinct overloads are preserved across the whole MRO.
lib/src/cellar/GetFormatter.scala
Outdated
| val newDecls = decls.filter(d => !seen.contains(d.name.toString)) | ||
| newDecls.foreach(d => seen += d.name.toString) |
There was a problem hiding this comment.
collectClassMembers uses seen keyed only by d.name.toString, so overloads coming from different parents/traits in the linearization will still be lost as soon as any overload of that name is encountered. If the goal is to preserve all overloads visible on the type, deduplicate by full signature (e.g., name + parameter types / declaredType, or TypePrinter.printSymbolSignatureSafe) rather than by name.
| val newDecls = decls.filter(d => !seen.contains(d.name.toString)) | |
| newDecls.foreach(d => seen += d.name.toString) | |
| val newDecls = decls.filter { d => | |
| val sig = TypePrinter.printSymbolSignatureSafe(d) | |
| !seen.contains(sig) | |
| } | |
| newDecls.foreach { d => | |
| val sig = TypePrinter.printSymbolSignatureSafe(d) | |
| seen += sig | |
| } |
| } | ||
| case other => fail(s"CellarOverloaded not found, got $other") | ||
| } | ||
| } |
There was a problem hiding this comment.
This new test only covers overloads declared within a single trait. Given the name-based MRO deduplication in collectClassMembers, overloads split across inheritance (base trait defines one overload, derived trait adds another) are a likely regression case; adding a fixture/test for that scenario would better validate the intended behavior end-to-end.
| } | |
| } | |
| test("listMembers includes inherited overloaded methods"): | |
| withCtx { ctx => | |
| given Context = ctx | |
| SymbolLister.resolve("cellar.fixture.scala3.CellarOverloadedInherited").flatMap { | |
| case ListResolveResult.Found(target) => | |
| SymbolLister.listMembers(target).compile.toList.map { syms => | |
| val processCount = syms.count(_.name.toString == "process") | |
| assertEquals( | |
| processCount, | |
| 2, | |
| s"Expected 2 inherited process overloads (base + derived), got names: ${syms.map(_.name)}" | |
| ) | |
| } | |
| case other => fail(s"CellarOverloadedInherited not found, got $other") | |
| } | |
| } |
| given Context = ctx | ||
| val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarOverloaded") | ||
| val output = GetFormatter.formatSymbol(cls) | ||
| val processCount = output.linesIterator.count(_.contains("def process")) |
There was a problem hiding this comment.
The overload assertion here counts lines containing "def process", which can be a bit brittle (e.g., matching docstrings or other sections if they ever include that substring). Tightening the match (e.g., "def process(" in the members block) would reduce false positives while still verifying overload preservation.
| val processCount = output.linesIterator.count(_.contains("def process")) | |
| val processCount = output.linesIterator.count(_.contains("def process(")) |
lib/src/cellar/GetFormatter.scala
Outdated
| val newDecls = decls.filter(d => !seen.contains(d.name.toString)) | ||
| newDecls.foreach(d => seen += d.name.toString) | ||
| result ++= newDecls | ||
| result.result() |
There was a problem hiding this comment.
collectClassMembers is now effectively duplicated between GetFormatter and SymbolLister with the same semantics. To avoid future drift (e.g., fixing overload handling in one place but not the other), consider extracting a shared helper (or placing it in a common utility object) and reusing it from both call sites.
Replace name-based deduplication in collectClassMembers with tastyquery's overridingSymbol to properly distinguish overrides from overloads. This preserves all distinct overloads (including those split across inheritance) while still deduplicating true overrides. Extract shared collectClassMembers into SymbolResolver to eliminate duplication between GetFormatter and SymbolLister. Add overload fixtures and tests for Scala 3, Scala 2, and Java. Fixes #14 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
59ee9e1 to
d86b9d8
Compare
Summary
collectClassMembersin bothGetFormatterandSymbolListerto preserve all method overloads from the same class instead of keeping only the first one per nameCellarOverloadedtest fixture with overloaded methods and corresponding tests for both formattersTest plan
./mill lib.test)cellar get-external "com.outr:scribe_3:3.15.3" "scribe.LoggerSupport"— now shows bothwarn(features: LogFeature*)andwarn(message: => String)overloadsFixes #14
🤖 Generated with Claude Code