Skip to content

Commit c3ba62b

Browse files
authored
Merge pull request #7652 from dotty-staging/fix-#7650b
Fix #7650: Drop stale toplevel definitions
2 parents 4c76479 + d150664 commit c3ba62b

File tree

25 files changed

+193
-79
lines changed

25 files changed

+193
-79
lines changed

Diff for: compiler/src/dotty/tools/dotc/core/Denotations.scala

+6
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ object Denotations {
116116
*/
117117
def filterWithFlags(required: FlagSet, excluded: FlagSet)(implicit ctx: Context): PreDenotation
118118

119+
/** Map `f` over all single denotations and aggregate the results with `g`. */
120+
def aggregate[T](f: SingleDenotation => T, g: (T, T) => T): T
121+
119122
private var cachedPrefix: Type = _
120123
private var cachedAsSeenFrom: AsSeenFromResult = _
121124
private var validAsSeenFrom: Period = Nowhere
@@ -1132,6 +1135,7 @@ object Denotations {
11321135
if (denots.exists && denots.matches(this)) NoDenotation else this
11331136
def filterWithFlags(required: FlagSet, excluded: FlagSet)(implicit ctx: Context): SingleDenotation =
11341137
if (required.isEmpty && excluded.isEmpty || compatibleWith(required, excluded)) this else NoDenotation
1138+
def aggregate[T](f: SingleDenotation => T, g: (T, T) => T): T = f(this)
11351139

11361140
type AsSeenFromResult = SingleDenotation
11371141
protected def computeAsSeenFrom(pre: Type)(implicit ctx: Context): SingleDenotation = {
@@ -1286,6 +1290,8 @@ object Denotations {
12861290
derivedUnion(denot1 filterDisjoint denot, denot2 filterDisjoint denot)
12871291
def filterWithFlags(required: FlagSet, excluded: FlagSet)(implicit ctx: Context): PreDenotation =
12881292
derivedUnion(denot1.filterWithFlags(required, excluded), denot2.filterWithFlags(required, excluded))
1293+
def aggregate[T](f: SingleDenotation => T, g: (T, T) => T): T =
1294+
g(denot1.aggregate(f, g), denot2.aggregate(f, g))
12891295
protected def derivedUnion(denot1: PreDenotation, denot2: PreDenotation) =
12901296
if ((denot1 eq this.denot1) && (denot2 eq this.denot2)) this
12911297
else denot1 union denot2

Diff for: compiler/src/dotty/tools/dotc/core/SymDenotations.scala

+65-11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import annotation.tailrec
1616
import util.SimpleIdentityMap
1717
import util.Stats
1818
import java.util.WeakHashMap
19+
import scala.util.control.NonFatal
1920
import config.Config
2021
import reporting.diagnostic.Message
2122
import reporting.diagnostic.messages.BadSymbolicReference
@@ -625,6 +626,10 @@ object SymDenotations {
625626
def isPackageObject(implicit ctx: Context): Boolean =
626627
name.isPackageObjectName && owner.is(Package) && this.is(Module)
627628

629+
/** Is this symbol a toplevel definition in a package object? */
630+
def isWrappedToplevelDef(given Context): Boolean =
631+
!isConstructor && owner.isPackageObject
632+
628633
/** Is this symbol an abstract type? */
629634
final def isAbstractType(implicit ctx: Context): Boolean = this.is(DeferredType)
630635

@@ -1527,6 +1532,14 @@ object SymDenotations {
15271532
myBaseTypeCachePeriod = Nowhere
15281533
}
15291534

1535+
def invalidateMemberCaches(sym: Symbol)(given Context): Unit =
1536+
if myMemberCache != null then myMemberCache.invalidate(sym.name)
1537+
if !sym.flagsUNSAFE.is(Private) then
1538+
invalidateMemberNamesCache()
1539+
if sym.isWrappedToplevelDef then
1540+
val outerCache = sym.owner.owner.asClass.classDenot.myMemberCache
1541+
if outerCache != null then outerCache.invalidate(sym.name)
1542+
15301543
override def copyCaches(from: SymDenotation, phase: Phase)(implicit ctx: Context): this.type = {
15311544
from match {
15321545
case from: ClassDenotation =>
@@ -1726,11 +1739,9 @@ object SymDenotations {
17261739
}
17271740

17281741
/** Enter a symbol in given `scope` without potentially replacing the old copy. */
1729-
def enterNoReplace(sym: Symbol, scope: MutableScope)(implicit ctx: Context): Unit = {
1742+
def enterNoReplace(sym: Symbol, scope: MutableScope)(given Context): Unit =
17301743
scope.enter(sym)
1731-
if (myMemberCache != null) myMemberCache.invalidate(sym.name)
1732-
if (!sym.flagsUNSAFE.is(Private)) invalidateMemberNamesCache()
1733-
}
1744+
invalidateMemberCaches(sym)
17341745

17351746
/** Replace symbol `prev` (if defined in current class) by symbol `replacement`.
17361747
* If `prev` is not defined in current class, do nothing.
@@ -2071,6 +2082,7 @@ object SymDenotations {
20712082

20722083
private var packageObjsCache: List[ClassDenotation] = _
20732084
private var packageObjsRunId: RunId = NoRunId
2085+
private var ambiguityWarningIssued: Boolean = false
20742086

20752087
/** The package objects in this class */
20762088
def packageObjs(implicit ctx: Context): List[ClassDenotation] = {
@@ -2122,19 +2134,61 @@ object SymDenotations {
21222134
case pcls :: pobjs1 =>
21232135
if (pcls.isCompleting) recur(pobjs1, acc)
21242136
else {
2125-
// A package object inherits members from `Any` and `Object` which
2126-
// should not be accessible from the package prefix.
21272137
val pmembers = pcls.computeNPMembersNamed(name).filterWithPredicate { d =>
2138+
// Drop members of `Any` and `Object`
21282139
val owner = d.symbol.maybeOwner
21292140
(owner ne defn.AnyClass) && (owner ne defn.ObjectClass)
21302141
}
21312142
recur(pobjs1, acc.union(pmembers))
21322143
}
21332144
case nil =>
21342145
val directMembers = super.computeNPMembersNamed(name)
2135-
if (acc.exists) acc.union(directMembers.filterWithPredicate(!_.symbol.isAbsent()))
2136-
else directMembers
2146+
if !acc.exists then directMembers
2147+
else acc.union(directMembers.filterWithPredicate(!_.symbol.isAbsent())) match
2148+
case d: DenotUnion => dropStale(d)
2149+
case d => d
21372150
}
2151+
2152+
def dropStale(multi: DenotUnion): PreDenotation =
2153+
val compiledNow = multi.filterWithPredicate(d =>
2154+
d.symbol.isDefinedInCurrentRun || d.symbol.associatedFile == null
2155+
// if a symbol does not have an associated file, assume it is defined
2156+
// in the current run anyway. This is true for packages, and also can happen for pickling and
2157+
// from-tasty tests that generate a fresh symbol and then re-use it in the next run.
2158+
)
2159+
if compiledNow.exists then compiledNow
2160+
else
2161+
val assocFiles = multi.aggregate(d => Set(d.symbol.associatedFile), _ union _)
2162+
if assocFiles.size == 1 then
2163+
multi // they are all overloaded variants from the same file
2164+
else
2165+
// pick the variant(s) from the youngest class file
2166+
val lastModDate = assocFiles.map(_.lastModified).max
2167+
val youngest = assocFiles.filter(_.lastModified == lastModDate)
2168+
val chosen = youngest.head
2169+
def ambiguousFilesMsg(f: AbstractFile) =
2170+
em"""Toplevel definition $name is defined in
2171+
| $chosen
2172+
|and also in
2173+
| $f"""
2174+
if youngest.size > 1 then
2175+
throw TypeError(i"""${ambiguousFilesMsg(youngest.tail.head)}
2176+
|One of these files should be removed from the classpath.""")
2177+
2178+
// Warn if one of the older files comes from a different container.
2179+
// In that case picking the youngest file is not necessarily what we want,
2180+
// since the older file might have been loaded from a jar earlier in the
2181+
// classpath.
2182+
def sameContainer(f: AbstractFile): Boolean =
2183+
try f.container == chosen.container catch case NonFatal(ex) => true
2184+
if !ambiguityWarningIssued then
2185+
for conflicting <- assocFiles.find(!sameContainer(_)) do
2186+
ctx.warning(i"""${ambiguousFilesMsg(conflicting)}
2187+
|Keeping only the definition in $chosen""")
2188+
ambiguityWarningIssued = true
2189+
multi.filterWithPredicate(_.symbol.associatedFile == chosen)
2190+
end dropStale
2191+
21382192
if (symbol `eq` defn.ScalaPackageClass) {
21392193
val denots = super.computeNPMembersNamed(name)
21402194
if (denots.exists) denots
@@ -2154,17 +2208,17 @@ object SymDenotations {
21542208
recur(packageObjs, super.memberNames(keepOnly))
21552209
}
21562210

2157-
/** If another symbol with the same name is entered, unlink it,
2158-
* and, if symbol is a package object, invalidate the packageObj cache.
2211+
/** If another symbol with the same name is entered, unlink it.
2212+
* If symbol is a package object, invalidate the packageObj cache.
21592213
* @return `sym` is not already entered
21602214
*/
21612215
override def proceedWithEnter(sym: Symbol, mscope: MutableScope)(implicit ctx: Context): Boolean = {
21622216
val entry = mscope.lookupEntry(sym.name)
21632217
if (entry != null) {
21642218
if (entry.sym == sym) return false
21652219
mscope.unlink(entry)
2166-
if (sym.name.isPackageObjectName) packageObjsRunId = NoRunId
21672220
}
2221+
if (sym.name.isPackageObjectName) packageObjsRunId = NoRunId
21682222
true
21692223
}
21702224

Diff for: compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala

+7-2
Original file line numberDiff line numberDiff line change
@@ -1908,10 +1908,15 @@ object messages {
19081908
}
19091909

19101910
case class PackageNameAlreadyDefined(pkg: Symbol)(implicit ctx: Context) extends Message(PackageNameAlreadyDefinedID) {
1911-
val msg: String = em"${pkg} is already defined, cannot be a ${hl("package")}"
1911+
val (where, or) =
1912+
if pkg.associatedFile == null then ("", "")
1913+
else (s" in ${pkg.associatedFile}", " or delete the containing class file")
1914+
val msg: String = em"""${pkg.name} is the name of $pkg$where.
1915+
|It cannot be used at the same time as the name of a package."""
19121916
val kind: String = "Naming"
19131917
val explanation: String =
1914-
em"An ${hl("object")} cannot have the same name as an existing ${hl("package")}. Rename either one of them."
1918+
em"""An ${hl("object")} or other toplevel definition cannot have the same name as an existing ${hl("package")}.
1919+
|Rename either one of them$or."""
19151920
}
19161921

19171922
case class UnapplyInvalidNumberOfArguments(qual: untpd.Tree, argTypes: List[Type])(implicit ctx: Context)

Diff for: compiler/src/dotty/tools/dotc/typer/Namer.scala

+43-16
Original file line numberDiff line numberDiff line change
@@ -302,21 +302,48 @@ class Namer { typer: Typer =>
302302

303303
typr.println(i"creating symbol for $tree in ${ctx.mode}")
304304

305-
def checkNoConflict(name: Name): Name = {
306-
def errorName(msg: => String) = {
307-
ctx.error(msg, tree.sourcePos)
308-
name.freshened
309-
}
310-
def preExisting = ctx.effectiveScope.lookup(name)
311-
if (ctx.owner.is(PackageClass))
312-
if (preExisting.isDefinedInCurrentRun)
313-
errorName(s"${preExisting.showLocated} has already been compiled once during this run")
314-
else name
305+
/** Check that a new definition with given name and privacy status
306+
* in current context would not conflict with existing currently
307+
* compiled definitions.
308+
* The logic here is very subtle and fragile due to the fact that
309+
* we are not allowed to force anything.
310+
*/
311+
def checkNoConflict(name: Name, isPrivate: Boolean): Name =
312+
val owner = ctx.owner
313+
var conflictsDetected = false
314+
315+
def conflict(conflicting: Symbol) =
316+
val where: String =
317+
if conflicting.owner == owner then ""
318+
else if conflicting.owner.isPackageObject then i" in ${conflicting.associatedFile}"
319+
else i" in ${conflicting.owner}"
320+
ctx.error(i"$name is already defined as $conflicting$where", tree.sourcePos)
321+
conflictsDetected = true
322+
323+
def checkNoConflictIn(owner: Symbol) =
324+
val preExisting = owner.unforcedDecls.lookup(name)
325+
if (preExisting.isDefinedInCurrentRun || preExisting.lastKnownDenotation.is(Package))
326+
&& (!preExisting.lastKnownDenotation.is(Private) || preExisting.owner.is(Package))
327+
then conflict(preExisting)
328+
329+
def pkgObjs(pkg: Symbol) =
330+
pkg.denot.asInstanceOf[PackageClassDenotation].packageObjs.map(_.symbol)
331+
332+
if owner.is(PackageClass) then
333+
checkNoConflictIn(owner)
334+
for pkgObj <- pkgObjs(owner) do
335+
checkNoConflictIn(pkgObj)
315336
else
316-
if ((!ctx.owner.isClass || name.isTypeName) && preExisting.exists)
317-
errorName(i"$name is already defined as $preExisting")
318-
else name
319-
}
337+
def preExisting = ctx.effectiveScope.lookup(name)
338+
if (!owner.isClass || name.isTypeName) && preExisting.exists then
339+
conflict(preExisting)
340+
else if owner.isPackageObject && !isPrivate && name != nme.CONSTRUCTOR then
341+
checkNoConflictIn(owner.owner)
342+
for pkgObj <- pkgObjs(owner.owner) if pkgObj != owner do
343+
checkNoConflictIn(pkgObj)
344+
345+
if conflictsDetected then name.freshened else name
346+
end checkNoConflict
320347

321348
/** Create new symbol or redefine existing symbol under lateCompile. */
322349
def createOrRefine[S <: Symbol](
@@ -348,17 +375,17 @@ class Namer { typer: Typer =>
348375

349376
tree match {
350377
case tree: TypeDef if tree.isClassDef =>
351-
val name = checkNoConflict(tree.name).asTypeName
352378
val flags = checkFlags(tree.mods.flags &~ GivenOrImplicit)
379+
val name = checkNoConflict(tree.name, flags.is(Private)).asTypeName
353380
val cls =
354381
createOrRefine[ClassSymbol](tree, name, flags, ctx.owner,
355382
cls => adjustIfModule(new ClassCompleter(cls, tree)(ctx), tree),
356383
ctx.newClassSymbol(ctx.owner, name, _, _, _, tree.nameSpan, ctx.source.file))
357384
cls.completer.asInstanceOf[ClassCompleter].init()
358385
cls
359386
case tree: MemberDef =>
360-
val name = checkNoConflict(tree.name)
361387
var flags = checkFlags(tree.mods.flags)
388+
val name = checkNoConflict(tree.name, flags.is(Private))
362389
tree match
363390
case tree: ValOrDefDef =>
364391
if tree.unforcedRhs == EmptyTree

Diff for: compiler/src/dotty/tools/dotc/typer/Typer.scala

+7-7
Original file line numberDiff line numberDiff line change
@@ -1877,22 +1877,22 @@ class Typer extends Namer
18771877
assignType(cpy.Import(imp)(expr1, selectors1), sym)
18781878
}
18791879

1880-
def typedPackageDef(tree: untpd.PackageDef)(implicit ctx: Context): Tree = {
1880+
def typedPackageDef(tree: untpd.PackageDef)(implicit ctx: Context): Tree =
18811881
val pid1 = typedExpr(tree.pid, AnySelectionProto)(ctx.addMode(Mode.InPackageClauseName))
18821882
val pkg = pid1.symbol
1883-
pid1 match {
1884-
case pid1: RefTree if pkg.exists =>
1885-
if (!pkg.is(Package)) ctx.error(PackageNameAlreadyDefined(pkg), tree.sourcePos)
1883+
pid1 match
1884+
case pid1: RefTree if pkg.is(Package) =>
18861885
val packageCtx = ctx.packageContext(tree, pkg)
18871886
var stats1 = typedStats(tree.stats, pkg.moduleClass)(packageCtx)._1
18881887
if (!ctx.isAfterTyper)
18891888
stats1 = stats1 ++ typedBlockStats(MainProxies.mainProxies(stats1))(packageCtx)._1
18901889
cpy.PackageDef(tree)(pid1, stats1).withType(pkg.termRef)
18911890
case _ =>
18921891
// Package will not exist if a duplicate type has already been entered, see `tests/neg/1708.scala`
1893-
errorTree(tree, i"package ${tree.pid.name} does not exist")
1894-
}
1895-
}
1892+
errorTree(tree,
1893+
if pkg.exists then PackageNameAlreadyDefined(pkg)
1894+
else i"package ${tree.pid.name} does not exist")
1895+
end typedPackageDef
18961896

18971897
def typedAnnotated(tree: untpd.Annotated, pt: Type)(implicit ctx: Context): Tree = {
18981898
val annot1 = typedExpr(tree.annot, defn.AnnotationClass.typeRef)

Diff for: docs/docs/reference/dropped-features/package-objects.md

+3
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ in a source file `src.scala`, it could be invoked from the command line using a
4242
"program name" is mangled it is recommended to always put `main` methods in explicitly named objects.
4343

4444
**Note 3:** The notion of `private` is independent of whether a definition is wrapped or not. A `private` toplevel definition is always visible from everywhere in the enclosing package.
45+
46+
**Note 4:** If several toplevel definitions are overloaded variants with the same name,
47+
they must all come from the same source file.

Diff for: tests/neg/toplevel-doubledef/Test_2.scala

-3
This file was deleted.

Diff for: tests/neg/toplevel-doubledef/moredefs_1.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
def hello(x: String): Unit =
1+
def hello(x: String): Unit = // error: has already been compiled
22
println(s"hi, $x")

Diff for: tests/neg/toplevel-overload/Test_3.scala

-6
This file was deleted.

Diff for: tests/neg/toplevel-overload/defs_1.scala

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
trait A
22

33
def f(x: A) = s"A"
4+
5+
def g(): Unit = ()

Diff for: tests/neg/toplevel-overload/defs_2.scala

-3
This file was deleted.

Diff for: tests/neg/toplevel-overload/moredefs_1.scala

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
trait B
2+
3+
def f(x: B) = s"B" // error: has already been compiled
4+
5+
private def g(): Unit = () // OK, since it is private

Diff for: tests/neg/toplevel-package/defs_1.scala

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package foo
2+
package bar
3+
class C
4+
def hello(x: String): Unit =
5+
println(s"hello: $x")
6+

Diff for: tests/neg/toplevel-package/moredefs_2.scala

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
package foo
2+
def bar = 2 // error: bar is a package

Diff for: tests/pos/i7650/Test_1.scala

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@main def Test() = println("hi")

Diff for: tests/pos/i7650/Test_2.scala

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
object Test

Diff for: tests/run/toplevel-mixed/B_1.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11

22
class x(val s: String)
33

4-
object y {
4+
object yy {
55
def foo = 3
66
}
77

88
object X2 {
99
def bar = "hi"
1010
}
1111

12-
class X3
12+
class X4
1313

Diff for: tests/run/toplevel-mixed/Test_2.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ object Test extends App {
44
assert((new x("abc")).s == "abc")
55

66
assert(y("abc") == 3)
7-
assert(y.foo == 3)
7+
assert(yy.foo == 3)
88

99
val x2: X2 = "abc"
1010
assert(X2.bar == "hi")

Diff for: tests/run/toplevel-overloads.check

-4
This file was deleted.

Diff for: tests/run/toplevel-overloads/Test_2.scala

-7
This file was deleted.

0 commit comments

Comments
 (0)