Skip to content

Commit 97101fb

Browse files
authored
Merge pull request #10600 from dotty-staging/fix-#10527
Fix #10527: Generate `canEqual` test for case class equals
2 parents 26ad497 + 61e9c82 commit 97101fb

File tree

2 files changed

+21
-8
lines changed

2 files changed

+21
-8
lines changed

compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,9 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
7777
def enumValueSymbols(using Context): List[Symbol] = { initSymbols; myEnumValueSymbols }
7878
def nonJavaEnumValueSymbols(using Context): List[Symbol] = { initSymbols; myNonJavaEnumValueSymbols }
7979

80-
private def existingDef(sym: Symbol, clazz: ClassSymbol)(using Context): Symbol = {
80+
private def existingDef(sym: Symbol, clazz: ClassSymbol)(using Context): Symbol =
8181
val existing = sym.matchingMember(clazz.thisType)
82-
if (existing != sym && !existing.is(Deferred)) existing
83-
else NoSymbol
84-
}
82+
if existing != sym && !existing.is(Deferred) then existing else NoSymbol
8583

8684
private def synthesizeDef(sym: TermSymbol, rhsFn: List[List[Tree]] => Context ?=> Tree)(using Context): Tree =
8785
DefDef(sym, rhsFn(_)(using ctx.withOwner(sym))).withSpan(ctx.owner.span.focus)
@@ -236,12 +234,13 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
236234
* def equals(that: Any): Boolean =
237235
* (this eq that) || {
238236
* that match {
239-
* case x$0 @ (_: C @unchecked) => this.x == this$0.x && this.y == x$0.y
237+
* case x$0 @ (_: C @unchecked) => this.x == this$0.x && this.y == x$0.y && that.canEqual(this)
240238
* case _ => false
241239
* }
242240
* ```
243241
*
244-
* If `C` is a value class the initial `eq` test is omitted.
242+
* If `C` is a value class, the initial `eq` test is omitted.
243+
* The `canEqual` test can be omitted if it is known that `canEqual` return always true.
245244
*
246245
* `@unchecked` is needed for parametric case classes.
247246
*
@@ -254,8 +253,14 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
254253
val sortedAccessors = accessors.sortBy(accessor => if (accessor.info.typeSymbol.isPrimitiveValueClass) 0 else 1)
255254
val comparisons = sortedAccessors.map { accessor =>
256255
This(clazz).select(accessor).equal(ref(thatAsClazz).select(accessor)) }
257-
val rhs = // this.x == this$0.x && this.y == x$0.y
258-
if (comparisons.isEmpty) Literal(Constant(true)) else comparisons.reduceLeft(_ and _)
256+
var rhs = // this.x == this$0.x && this.y == x$0.y && that.canEqual(this)
257+
if comparisons.isEmpty then Literal(Constant(true)) else comparisons.reduceLeft(_ and _)
258+
val canEqualMeth = existingDef(defn.Product_canEqual, clazz)
259+
if !clazz.is(Final) || canEqualMeth.exists && !canEqualMeth.is(Synthetic) then
260+
rhs = rhs.and(
261+
ref(thatAsClazz)
262+
.select(canEqualMeth.orElse(defn.Product_canEqual))
263+
.appliedTo(This(clazz)))
259264
val matchingCase = CaseDef(pattern, EmptyTree, rhs) // case x$0 @ (_: C) => this.x == this$0.x && this.y == x$0.y
260265
val defaultCase = CaseDef(Underscore(defn.AnyType), EmptyTree, Literal(Constant(false))) // case _ => false
261266
val matchExpr = Match(that, List(matchingCase, defaultCase))

tests/run/i10527.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
case class C(x: Int)
2+
class CC(x: Int) extends C(x) { override def canEqual(o: Any) = o.isInstanceOf[CC] }
3+
final case class D(x: Int)
4+
final case class E(x: Int) { override def canEqual(o: Any) = false }
5+
@main def Test =
6+
assert(C(1) != new CC(1))
7+
assert(D(1) == D(1))
8+
assert(E(1) != E(1))

0 commit comments

Comments
 (0)