diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 18e2d234452d..13bf2cf7f580 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -2128,6 +2128,7 @@ import transform.SymUtils._ class DoubleDefinition(decl: Symbol, previousDecl: Symbol, base: Symbol)(using Context) extends NamingMsg(DoubleDefinitionID) { def msg = { def nameAnd = if (decl.name != previousDecl.name) " name and" else "" + def erasedType = if ctx.erasedTypes then i" ${decl.info}" else "" def details(using Context): String = if (decl.isRealMethod && previousDecl.isRealMethod) { import Signature.MatchDegree._ @@ -2155,7 +2156,7 @@ import transform.SymUtils._ |Consider adding a @targetName annotation to one of the conflicting definitions |for disambiguation.""" else "" - i"have the same$nameAnd type after erasure.$hint" + i"have the same$nameAnd type$erasedType after erasure.$hint" } } else "" @@ -2174,10 +2175,12 @@ import transform.SymUtils._ else "Name clash between inherited members" - em"""$clashDescription: - |${previousDecl.showDcl} ${symLocation(previousDecl)} and - |${decl.showDcl} ${symLocation(decl)} - |""" + details + atPhase(typerPhase) { + em"""$clashDescription: + |${previousDecl.showDcl} ${symLocation(previousDecl)} and + |${decl.showDcl} ${symLocation(decl)} + |""" + } + details } def explain = "" } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index aac0288aa771..ac85798cb128 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -24,6 +24,7 @@ import Inferencing._ import transform.ValueClasses._ import transform.TypeUtils._ import transform.SymUtils._ +import TypeErasure.erasure import reporting._ import config.Feature.sourceVersion import config.SourceVersion._ @@ -1237,8 +1238,64 @@ class Namer { typer: Typer => addForwarders(sels1, sel.name :: seen) case _ => + /** Avoid a clash of export forwarder `forwarder` with other forwarders in `forwarders`. + * @return If `forwarder` clashes, a new leading forwarder and trailing forwarders list + * that avoids the clash according to the scheme described in `avoidClashes`. + * If there's no clash, the inputs as they are in a pair. + */ + def avoidClashWith(forwarder: tpd.DefDef, forwarders: List[tpd.MemberDef]): (tpd.DefDef, List[tpd.MemberDef]) = + def clashes(fwd1: Symbol, fwd2: Symbol) = + fwd1.targetName == fwd2.targetName + && erasure(fwd1.info).signature == erasure(fwd2.info).signature + + forwarders match + case forwarders @ ((forwarder1: tpd.DefDef) :: forwarders1) + if forwarder.name == forwarder1.name => + if clashes(forwarder.symbol, forwarder1.symbol) then + val alt1 = tpd.methPart(forwarder.rhs).tpe + val alt2 = tpd.methPart(forwarder1.rhs).tpe + val cmp = alt1 match + case alt1: TermRef => alt2 match + case alt2: TermRef => compare(alt1, alt2) + case _ => 0 + case _ => 0 + if cmp == 0 then + report.error( + ex"""Clashing exports: The exported + | ${forwarder.rhs.symbol}: ${alt1.widen} + |and ${forwarder1.rhs.symbol}: ${alt2.widen} + |have the same signature after erasure and overloading resolution could not disambiguate.""", + exp.srcPos) + avoidClashWith(if cmp < 0 then forwarder1 else forwarder, forwarders1) + else + val (forwarder2, forwarders2) = avoidClashWith(forwarder, forwarders1) + (forwarder2, forwarders.derivedCons(forwarder1, forwarders2)) + case _ => + (forwarder, forwarders) + end avoidClashWith + + /** Avoid clashes of any two export forwarders in `forwarders`. + * A clash is if two forwarders f1 and f2 have the same name and signatures after erasure. + * We try to avoid a clash by dropping one of f1 and f2, keeping the one whose right hand + * side reference would be preferred by overloading resolution. + * If neither of f1 or f2 is preferred over the other, report an error. + * + * The idea is that this simulates the hypothetical case where export forwarders + * are not generated and we treat an export instead more like an import where we + * expand the use site reference. Test cases in {neg,pos}/i14699.scala. + * + * @pre Forwarders with the same name are consecutive in `forwarders`. + */ + def avoidClashes(forwarders: List[tpd.MemberDef]): List[tpd.MemberDef] = forwarders match + case forwarders @ (forwarder :: forwarders1) => + val (forwarder2, forwarders2) = forwarder match + case forwarder: tpd.DefDef => avoidClashWith(forwarder, forwarders1) + case _ => (forwarder, forwarders1) + forwarders.derivedCons(forwarder2, avoidClashes(forwarders2)) + case Nil => forwarders + addForwarders(selectors, Nil) - val forwarders = buf.toList + val forwarders = avoidClashes(buf.toList) exp.pushAttachment(ExportForwarders, forwarders) forwarders end exportForwarders diff --git a/tests/neg/i14966.check b/tests/neg/i14966.check new file mode 100644 index 000000000000..c5227b352b1b --- /dev/null +++ b/tests/neg/i14966.check @@ -0,0 +1,10 @@ +-- Error: tests/neg/i14966.scala:10:9 ---------------------------------------------------------------------------------- +10 | export s.* // error + | ^^^^^^^^^^ + | Clashing exports: The exported + | method f: (x: List[Int]): Int + | and method f²: (x: List[String]): Int + | have the same signature after erasure and overloading resolution could not disambiguate. + | + | where: f is a method in trait S + | f² is a method in trait I diff --git a/tests/neg/i14966.scala b/tests/neg/i14966.scala new file mode 100644 index 000000000000..38c6d99247ea --- /dev/null +++ b/tests/neg/i14966.scala @@ -0,0 +1,11 @@ +trait I[A]: + def f(x: List[String]): A + +trait S: + def f(x: List[Int]): Int + +trait T[A] extends I[A], S + +class Test(s: T[Int]): + export s.* // error + diff --git a/tests/neg/i14966a.check b/tests/neg/i14966a.check new file mode 100644 index 000000000000..777d1ec74955 --- /dev/null +++ b/tests/neg/i14966a.check @@ -0,0 +1,10 @@ +-- [E120] Naming Error: tests/neg/i14966a.scala:3:6 -------------------------------------------------------------------- +3 | def f(x: List[Int]): String = ??? // error + | ^ + | Double definition: + | def f[X <: String](x: List[X]): String in class Test at line 2 and + | def f(x: List[Int]): String in class Test at line 3 + | have the same type (x: scala.collection.immutable.List): String after erasure. + | + | Consider adding a @targetName annotation to one of the conflicting definitions + | for disambiguation. diff --git a/tests/neg/i14966a.scala b/tests/neg/i14966a.scala new file mode 100644 index 000000000000..f2643798f1e5 --- /dev/null +++ b/tests/neg/i14966a.scala @@ -0,0 +1,4 @@ +class Test: + def f[X <: String](x: List[X]): String = ??? + def f(x: List[Int]): String = ??? // error + diff --git a/tests/pos/i14966.scala b/tests/pos/i14966.scala new file mode 100644 index 000000000000..9ed0e6ce3720 --- /dev/null +++ b/tests/pos/i14966.scala @@ -0,0 +1,13 @@ +trait I[+A] extends IOps[A, I[A]] + +trait S[A] extends I[A], SOps[A, S[A]] + +trait IOps[+A, +C <: I[A]]: + def concat[B >: A](other: IterableOnce[B]): C + +trait SOps[A, +C <: S[A]] extends IOps[A, C]: + def concat(other: IterableOnce[A]): C + +class Test(s: S[Int]): + export s.* + diff --git a/tests/pos/i14966a.scala b/tests/pos/i14966a.scala new file mode 100644 index 000000000000..ad426fc721b2 --- /dev/null +++ b/tests/pos/i14966a.scala @@ -0,0 +1,2 @@ +class B[T](val s: Set[T]): + export s.*