Skip to content

Commit a68989d

Browse files
retronymclaude
andcommitted
Fix undercompilation when case class field type changes (issue #26231)
In Scala 3, the compiler generates `def unapply(x: C): C = x` for case classes. Its signature is always `(C): C` regardless of field types, so its API hash never changes when a field is renamed or retyped. The PatternMatcher phase (which runs after ExtractDependencies) lowers `case C(x)` to a direct call to the product selector `_1()`, `_2()`, etc. Because ExtractDependencies ran before PatternMatcher, those selector calls were never in the tree, so `_1` was never recorded as a used name in the dependent file. Zinc therefore saw no reason to recompile the file when the field type changed, producing a NoSuchMethodError at runtime. Fix: in `AbstractExtractDependenciesCollector.recordTree`, add a case for `UnApply` that records each product selector (`_1`, `_2`, …) found on the unapply's result type as a member-reference used name. When the selector's return type changes (because the field type changed) its name hash changes and Zinc correctly invalidates the dependent file. The key detail is that `fun.tpe` in an `UnApply` node is a `TermRef`; we must call `.widen.finalResultType` to reach the underlying case-class type before asking `Applications.productSelectors` for the `_N` members. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0ac3485 commit a68989d

2 files changed

Lines changed: 16 additions & 7 deletions

File tree

compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ package sbt
44
import java.io.File
55
import java.nio.file.Path
66
import java.util.EnumSet
7-
87
import dotty.tools.dotc.ast.tpd
98
import dotty.tools.dotc.classpath.FileUtils.{hasClassExtension, hasTastyExtension}
109
import dotty.tools.dotc.core.Contexts.*
@@ -16,16 +15,15 @@ import dotty.tools.dotc.core.Phases.*
1615
import dotty.tools.dotc.core.Symbols.*
1716
import dotty.tools.dotc.core.Denotations.StaleSymbol
1817
import dotty.tools.dotc.core.Types.*
19-
20-
import dotty.tools.dotc.util.{SrcPos, NoSourcePosition}
18+
import dotty.tools.dotc.typer.Applications.*
19+
import dotty.tools.dotc.util.{NoSourcePosition, SrcPos}
2120
import dotty.tools.io
22-
import dotty.tools.io.{AbstractFile, PlainFile, ZipArchive, NoAbstractFile, FileExtension}
21+
import dotty.tools.io.{AbstractFile, FileExtension, NoAbstractFile, PlainFile, ZipArchive}
2322
import xsbti.UseScope
2423
import xsbti.api.DependencyContext
2524
import xsbti.api.DependencyContext.*
2625

2726
import scala.jdk.CollectionConverters.*
28-
2927
import scala.collection.{Set, mutable}
3028
import scala.compiletime.uninitialized
3129

@@ -265,6 +263,18 @@ trait AbstractExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.
265263
addInheritanceDependencies(t)
266264
case t: Template =>
267265
addInheritanceDependencies(t)
266+
case UnApply(fun, implicits, patterns) =>
267+
// For a case-class unapply of the form `def unapply(x: C): C = x`, the
268+
// compiler emits calls to the product selector methods `_1`, `_2`, … in
269+
// the generated bytecode (via PatternMatcher, which runs after this phase).
270+
// Those selectors are never explicit in the typed tree, so they would not
271+
// normally be recorded as used names. We record them here so that Zinc
272+
// knows to recompile this file if the return type of any selector changes.
273+
//
274+
// fun.tpe is a TermRef; widen first to reach the underlying MethodType,
275+
// then take finalResultType to get the case class type (e.g. Customer2).
276+
val selectors = productSelectors(fun.tpe.widen.finalResultType)
277+
selectors.foreach(addMemberRefDependency)
268278
case _ => ()
269279

270280
/**Reused EqHashSet, safe to use as each TypeDependencyTraverser is used atomically

sbt-bridge/test/xsbt/ExtractAPISpecification.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,7 @@ class ExtractAPISpecification {
258258
usedNames("Test").contains("unapply"))
259259

260260
// `_1` must also be recorded because it is what the generated bytecode calls.
261-
// TODO: this currently FAILS, documenting the undercompilation bug.
262-
assertTrue("_1 must be a used name in Test (currently missing — undercompilation bug)",
261+
assertTrue("_1 must be a used name in Test",
263262
usedNames("Test").contains("_1"))
264263
}
265264

0 commit comments

Comments
 (0)