Fix undercompilation when a case class field type changes and the class is used in a pattern match#26262
Conversation
When a case class field type is changed, files that pattern-match on it are not recompiled by Zinc, leading to NoSuchMethodError at runtime. Root cause: Scala 3 generates `def unapply(x: C): C = x` whose signature is stable regardless of field types. ExtractDependencies records `unapply` as a used name but never `_1`, which is the method the generated bytecode actually calls. Since `unapply`'s name hash never changes, Zinc skips recompilation of the dependent file. The three new tests document: - `_1` API hash changes when field type changes (expected) - `unapply` API hash does NOT change (the stable signature hiding the change) - `_1` is absent from the used names recorded for a pattern-matching file (the bug) Fixes scala#26231 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…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>
a68989d to
b9fafe3
Compare
| // | ||
| // fun.tpe is a TermRef; widen first to reach the underlying MethodType, | ||
| // then take finalResultType to get the case class type (e.g. Customer2). | ||
| val selectors = productSelectors(fun.tpe.widen.finalResultType) |
There was a problem hiding this comment.
Pessimistically assumes that all selectors are relevant, this could be refined to exclude those that are not bound by patterns.
|
I suspected this is just one of a class of bugs: =========== CLAUDE =========== Fix for #26231 — undercompilation when a Summary of the changeScala 3 generates Verdict✅ Correct for the targeted scenario.
Nits
Related bugs found (all confirmed empirically on Scala 3.7.3, not covered by this fix)Repro harness: one sbt module, upstream
Repro pitfall worth documentingAny downstream file using string interpolation appears to be invalidated correctly — Suggested follow-ups
|
Fixes #26231
Problem
Changing a case class field type causes a
NoSuchMethodErrorat runtime because the file pattern-matching on it is not recompiled by Zinc.Scala 3 generates def
unapply(x: C): C = xfor case classes — its signature never changes regardless of field types. ExtractDependencies runs before PatternMatcher, so the_1(),_2(), … calls that PatternMatcher will emit are not yet in the tree. Since_1is never recorded as a used name, Zinc doesn't recompile the dependent file when_1's return type changes.Fix
Add a case
UnApply(fun, ...)branch torecordTreethat records each product selector (_1,_2, …) as a used name viaaddMemberRefDependency. Note:fun.tpeis aTermRef, so.widenis needed before.finalResultTypeto reach the underlyingMethodTypeand then the case class type.How much have you relied on LLM-based tools in this contribution?
Extensively, for investigation and implementation.
How was the solution tested?
New automated tests
Three new assertions in
ExtractAPISpecification: that_1's API hash changes when the field type changes; thatunapply's hash does not (documenting why the old code failed); and that_1now appears in the used names for a file that pattern-matches on the case class.