Skip to content

Commit a5a85a2

Browse files
Merge pull request #14831 from dotty-staging/fix-14818
Detect clashes involving renamed exports
2 parents 3d1d299 + 8d57aec commit a5a85a2

File tree

4 files changed

+70
-4
lines changed

4 files changed

+70
-4
lines changed

compiler/src/dotty/tools/dotc/typer/Namer.scala

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,17 +1072,34 @@ class Namer { typer: Typer =>
10721072
/** The forwarders defined by export `exp` */
10731073
private def exportForwarders(exp: Export)(using Context): List[tpd.MemberDef] =
10741074
val buf = new mutable.ListBuffer[tpd.MemberDef]
1075-
val Export(expr, selectors) = exp
1075+
val Export(expr, selectors0) = exp
10761076
if expr.isEmpty then
10771077
report.error(em"Export selector must have prefix and `.`", exp.srcPos)
10781078
return Nil
10791079

1080+
val renamed = mutable.Set[Name]()
1081+
val selectors = selectors0 map {
1082+
case sel @ ImportSelector(imported, id @ Ident(alias), bound) if alias != nme.WILDCARD =>
1083+
def noAliasSelector =
1084+
cpy.ImportSelector(sel)(imported, EmptyTree, bound).asInstanceOf[ImportSelector]
1085+
if renamed.contains(alias) then
1086+
report.error(i"duplicate rename target", id.srcPos)
1087+
noAliasSelector
1088+
else if alias == imported.name then
1089+
report.warning(i"redundant rename in export", id.srcPos)
1090+
noAliasSelector
1091+
else
1092+
renamed += alias
1093+
sel
1094+
case sel => sel
1095+
}
1096+
10801097
val path = typedAheadExpr(expr, AnySelectionProto)
10811098
checkLegalExportPath(path, selectors)
10821099
lazy val wildcardBound = importBound(selectors, isGiven = false)
10831100
lazy val givenBound = importBound(selectors, isGiven = true)
10841101

1085-
def canForward(mbr: SingleDenotation): CanForward = {
1102+
def canForward(mbr: SingleDenotation, alias: TermName): CanForward = {
10861103
import CanForward.*
10871104
val sym = mbr.symbol
10881105
if !sym.isAccessibleFrom(path.tpe) then
@@ -1091,6 +1108,8 @@ class Namer { typer: Typer =>
10911108
Skip
10921109
else if cls.derivesFrom(sym.owner) && (sym.owner == cls || !sym.is(Deferred)) then
10931110
No(i"is already a member of $cls")
1111+
else if alias == mbr.name.toTermName && renamed.contains(alias) then
1112+
No(i"clashes with a renamed export")
10941113
else if sym.is(Override) then
10951114
sym.allOverriddenSymbols.find(
10961115
other => cls.derivesFrom(other.owner) && !other.is(Deferred)
@@ -1133,7 +1152,7 @@ class Namer { typer: Typer =>
11331152
case _ =>
11341153
acc.reverse ::: prefss
11351154

1136-
if canForward(mbr) == CanForward.Yes then
1155+
if canForward(mbr, alias) == CanForward.Yes then
11371156
val sym = mbr.symbol
11381157
val hasDefaults = sym.hasDefaultParams // compute here to ensure HasDefaultParams and NoDefaultParams flags are set
11391158
val forwarder =
@@ -1190,7 +1209,7 @@ class Namer { typer: Typer =>
11901209
val mbrs = List(name, name.toTypeName).flatMap(path.tpe.member(_).alternatives)
11911210
mbrs.foreach(addForwarder(alias, _, span))
11921211
if buf.size == size then
1193-
val reason = mbrs.map(canForward).collect {
1212+
val reason = mbrs.map(canForward(_, alias)).collect {
11941213
case CanForward.No(whyNot) => i"\n$path.$name cannot be exported because it $whyNot"
11951214
}.headOption.getOrElse("")
11961215
report.error(i"""no eligible member $name at $path$reason""", ctx.source.atSpan(span))

docs/_docs/reference/other-new-features/export.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ def f: c.T = ...
100100

101101
1. Export clauses can appear in classes or they can appear at the top-level. An export clause cannot appear as a statement in a block.
102102
1. If an export clause contains a wildcard or given selector, it is forbidden for its qualifier path to refer to a package. This is because it is not yet known how to safely track wildcard dependencies to a package for the purposes of incremental compilation.
103+
1. An export renaming hides un-renamed exports matching the target name. For instance, the following
104+
clause would be invalid since `B` is hidden by the renaming `A as B`.
105+
```scala
106+
export {A as B, B} // error: B is hidden
107+
```
108+
109+
1. Renamings in an export clause must have pairwise different target names. For instance, the following clause would be invalid:
110+
```scala
111+
export {A as C, B as C} // error: duplicate renaming
103112

104113
1. Simple renaming exports like
105114
```scala

tests/neg/i14818.check

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- Error: tests/neg/i14818.scala:9:12 ----------------------------------------------------------------------------------
2+
9 | export M.{A, B as A} // error
3+
| ^
4+
| no eligible member A at M
5+
| M.A cannot be exported because it clashes with a renamed export
6+
-- [E050] Type Error: tests/neg/i14818.scala:16:10 ---------------------------------------------------------------------
7+
16 | val x = b(1) // error
8+
| ^
9+
| method b in object T3 does not take parameters
10+
|
11+
| longer explanation available when compiling with `-explain`
12+
-- Error: tests/neg/i14818.scala:19:25 ---------------------------------------------------------------------------------
13+
19 | export M.{A as C, B as C} // error
14+
| ^
15+
| duplicate rename target

tests/neg/i14818.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
object M {
2+
type A
3+
type B
4+
def a = 1
5+
def b(x: Int) = x
6+
}
7+
8+
object T1:
9+
export M.{A, B as A} // error
10+
11+
object T2:
12+
export M.{A as B, *}
13+
14+
object T3:
15+
export M.{a as b, *}
16+
val x = b(1) // error
17+
18+
object T4:
19+
export M.{A as C, B as C} // error
20+
21+
object T5:
22+
export M.{A as B, B as A} // OK
23+

0 commit comments

Comments
 (0)