Skip to content

Hande clashes between same-named exports #14967

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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 ""
Expand All @@ -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 = ""
}
Expand Down
59 changes: 58 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tests/neg/i14966.check
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions tests/neg/i14966.scala
Original file line number Diff line number Diff line change
@@ -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

10 changes: 10 additions & 0 deletions tests/neg/i14966a.check
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions tests/neg/i14966a.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Test:
def f[X <: String](x: List[X]): String = ???
def f(x: List[Int]): String = ??? // error

13 changes: 13 additions & 0 deletions tests/pos/i14966.scala
Original file line number Diff line number Diff line change
@@ -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.*

2 changes: 2 additions & 0 deletions tests/pos/i14966a.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class B[T](val s: Set[T]):
export s.*