Skip to content
Draft
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
36 changes: 35 additions & 1 deletion compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1879,6 +1879,13 @@ object SymDenotations {
private var baseDataCache: BaseData = BaseData.None
private var memberNamesCache: MemberNames = MemberNames.None

/** Set of parent types for which a `BadSymbolicReference` has already
* been reported by `computeBaseData`. Used so the same diagnostic is
* not emitted multiple times when `baseData` is recomputed (e.g. from
* a separate `derivesFrom` invocation). See scala/scala3#20010.
*/
private var reportedMissingParents: Set[Type] = Set.empty

private def memberCache(using Context): EqHashMap[Name, PreDenotation] = {
if (myMemberCachePeriod != ctx.period) {
myMemberCache = EqHashMap()
Expand Down Expand Up @@ -2061,7 +2068,29 @@ object SymDenotations {
case p :: parents1 =>
p.classSymbol match {
case pcls: ClassSymbol => builder.addAll(pcls.baseClasses)
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive) || ctx.tolerateErrorsForBestEffort, s"$this has non-class parent: $p")
case _ =>
// The parent type couldn't be resolved to a class, e.g.
// because a transitive dependency was removed from the
// classpath. Report a BadSymbolicReference-style error
// rather than crashing with an internal assertion. See
// scala/scala3#20010.
if p.typeSymbol == NoSymbol && !isRefinementClass && !p.isError
&& !ctx.mode.is(Mode.Interactive) && !ctx.tolerateErrorsForBestEffort
then
if !reportedMissingParents.contains(p) then
reportedMissingParents = reportedMissingParents + p
val file = symbol.associatedFile
val (location, src) =
if file != null then (i" in $file", file.toString)
else ("", "the signature")
report.error(
em"""Bad symbolic reference. A signature$location
|refers to ${p.show} as a parent of ${symbol.showLocated}, but it is not available.
|It may be completely missing from the current classpath, or the version on
|the classpath might be incompatible with the version used when compiling $src.""",
symbol.srcPos)
else
assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive) || ctx.tolerateErrorsForBestEffort, s"$this has non-class parent: $p")
}
traverse(parents1)
case nil =>
Expand Down Expand Up @@ -2419,6 +2448,11 @@ object SymDenotations {
case pcls: ClassSymbol =>
for name <- pcls.memberNames(keepOnly) do
maybeAdd(name)
case _ =>
// Parent failed to resolve to a class (the missing
// reference has been reported by computeBaseData).
// Skip here to avoid a secondary MatchError.
// See scala/scala3#20010.
val ownSyms =
if (keepOnly eq implicitFilter)
if (this.is(Package)) Iterator.empty
Expand Down
1 change: 1 addition & 0 deletions sbt-test/tasty-compat/i20010/Repro.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class MyTest extends child.ValidatingTest
3 changes: 3 additions & 0 deletions sbt-test/tasty-compat/i20010/a/ParsingTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package parent

trait ParsingTest
3 changes: 3 additions & 0 deletions sbt-test/tasty-compat/i20010/b/ValidatingTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package child

abstract class ValidatingTest extends parent.ParsingTest
56 changes: 56 additions & 0 deletions sbt-test/tasty-compat/i20010/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import sbt.internal.util.ConsoleAppender

// Reproduces https://github.com/scala/scala3/issues/20010
//
// Three modules:
// a : defines `parent.ParsingTest`
// b : defines `child.ValidatingTest extends parent.ParsingTest`,
// compiled with `a` on the classpath; its output ends up in `c-input`
// c : root project that extends `child.ValidatingTest`, but only sees
// `c-input` on its classpath – `a`'s outputs are deliberately not
// exposed, so loading `ValidatingTest`'s TASTy can no longer resolve
// its `parent.ParsingTest` parent.
//
// Before the fix the compiler crashed with
// `java.lang.AssertionError: class ValidatingTest has non-class parent: ...`
// Now it must emit a clean `Bad symbolic reference` error.

lazy val assertCleanMissingRefError = taskKey[Unit](
"checks that compiling c reports `Bad symbolic reference` rather than crashing"
)
lazy val resetMessages = taskKey[Unit]("empties the messages list")

lazy val a = project.in(file("a"))
.settings(
Compile / classDirectory := (ThisBuild / baseDirectory).value / "a-only"
)

lazy val b = project.in(file("b"))
.settings(
Compile / unmanagedClasspath += (ThisBuild / baseDirectory).value / "a-only",
Compile / classDirectory := (ThisBuild / baseDirectory).value / "c-input"
)

lazy val c = project.in(file("."))
.settings(
// Only `b`'s outputs are visible here – `a-only` is *not* on the classpath.
Compile / unmanagedClasspath += (ThisBuild / baseDirectory).value / "c-input",
Compile / classDirectory := (ThisBuild / baseDirectory).value / "c-output",
extraAppenders := { _ => Seq(ConsoleAppender(FakePrintWriter)) },
resetMessages := { FakePrintWriter.resetMessages },
assertCleanMissingRefError := {
val msgs = FakePrintWriter.messages
assert(
msgs.exists(_.contains("Bad symbolic reference")),
s"expected 'Bad symbolic reference' in compiler output, got: ${msgs.mkString("\n")}"
)
assert(
!msgs.exists(_.contains("non-class parent")),
s"compiler crashed with 'non-class parent' assertion; got: ${msgs.mkString("\n")}"
)
assert(
!msgs.exists(_.contains("java.lang.AssertionError")),
s"compiler crashed with AssertionError; got: ${msgs.mkString("\n")}"
)
}
)
11 changes: 11 additions & 0 deletions sbt-test/tasty-compat/i20010/project/DottyInjectedPlugin.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sbt._
import Keys._

object DottyInjectedPlugin extends AutoPlugin {
override def requires = plugins.JvmPlugin
override def trigger = allRequirements

override val projectSettings = Seq(
scalaVersion := sys.props("plugin.scalaVersion")
)
}
6 changes: 6 additions & 0 deletions sbt-test/tasty-compat/i20010/project/FakePrintWriter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
object FakePrintWriter extends java.io.PrintWriter("fake-print-writer") {
@volatile var messages = List.empty[String]
def resetMessages = messages = List.empty[String]
override def println(x: String): Unit = messages = x :: messages
override def print(x: String): Unit = messages = x :: messages
}
10 changes: 10 additions & 0 deletions sbt-test/tasty-compat/i20010/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# compile library a (defines parent.ParsingTest)
> a/compile
# compile library b (defines child.ValidatingTest extends parent.ParsingTest)
> b/compile
# compile c without a on the classpath.
# This used to crash the compiler with an internal `non-class parent` assertion.
# After the fix it must fail with a clean `Bad symbolic reference` error.
> resetMessages
-> c/compile
> assertCleanMissingRefError
Loading