Skip to content

Fix method overloads being lost in output#16

Merged
rochala merged 1 commit intomainfrom
fix-overloaded-methods
Mar 24, 2026
Merged

Fix method overloads being lost in output#16
rochala merged 1 commit intomainfrom
fix-overloaded-methods

Conversation

@rochala
Copy link
Copy Markdown
Contributor

@rochala rochala commented Mar 24, 2026

Summary

  • Fix collectClassMembers in both GetFormatter and SymbolLister to preserve all method overloads from the same class instead of keeping only the first one per name
  • Add CellarOverloaded test fixture with overloaded methods and corresponding tests for both formatters

Test plan

  • All 374 existing + new tests pass (./mill lib.test)
  • Verified with cellar get-external "com.outr:scribe_3:3.15.3" "scribe.LoggerSupport" — now shows both warn(features: LogFeature*) and warn(message: => String) overloads

Fixes #14

🤖 Generated with Claude Code

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 collectClassMembers in GetFormatter and SymbolLister to preserve multiple declarations with the same name when they come from the same class/trait.
  • Add CellarOverloaded Scala 3 fixture with 3 process overloads.
  • Add tests to verify both listMembers and formatSymbol include 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.

Comment on lines +60 to +62
val newDecls = decls.filter(d => !seen.contains(d.name.toString))
newDecls.foreach(d => seen += d.name.toString)
result ++= newDecls
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +95
val newDecls = decls.filter(d => !seen.contains(d.name.toString))
newDecls.foreach(d => seen += d.name.toString)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
}

Copilot uses AI. Check for mistakes.
}
case other => fail(s"CellarOverloaded not found, got $other")
}
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}
}
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")
}
}

Copilot uses AI. Check for mistakes.
given Context = ctx
val cls = ctx.findStaticClass("cellar.fixture.scala3.CellarOverloaded")
val output = GetFormatter.formatSymbol(cls)
val processCount = output.linesIterator.count(_.contains("def process"))
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
val processCount = output.linesIterator.count(_.contains("def process"))
val processCount = output.linesIterator.count(_.contains("def process("))

Copilot uses AI. Check for mistakes.
Comment on lines 84 to 97
val newDecls = decls.filter(d => !seen.contains(d.name.toString))
newDecls.foreach(d => seen += d.name.toString)
result ++= newDecls
result.result()
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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>
@rochala rochala force-pushed the fix-overloaded-methods branch from 59ee9e1 to d86b9d8 Compare March 24, 2026 15:17
@rochala rochala merged commit 26b06c4 into main Mar 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Method alternatives are lost in output

2 participants