diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 1aabb85f5919..0ac5eaefce20 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -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() @@ -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 => @@ -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 diff --git a/sbt-test/tasty-compat/i20010/Repro.scala b/sbt-test/tasty-compat/i20010/Repro.scala new file mode 100644 index 000000000000..392cd009482b --- /dev/null +++ b/sbt-test/tasty-compat/i20010/Repro.scala @@ -0,0 +1 @@ +class MyTest extends child.ValidatingTest diff --git a/sbt-test/tasty-compat/i20010/a/ParsingTest.scala b/sbt-test/tasty-compat/i20010/a/ParsingTest.scala new file mode 100644 index 000000000000..ebddc4634b99 --- /dev/null +++ b/sbt-test/tasty-compat/i20010/a/ParsingTest.scala @@ -0,0 +1,3 @@ +package parent + +trait ParsingTest diff --git a/sbt-test/tasty-compat/i20010/b/ValidatingTest.scala b/sbt-test/tasty-compat/i20010/b/ValidatingTest.scala new file mode 100644 index 000000000000..7bf487d1e313 --- /dev/null +++ b/sbt-test/tasty-compat/i20010/b/ValidatingTest.scala @@ -0,0 +1,3 @@ +package child + +abstract class ValidatingTest extends parent.ParsingTest diff --git a/sbt-test/tasty-compat/i20010/build.sbt b/sbt-test/tasty-compat/i20010/build.sbt new file mode 100644 index 000000000000..d4299b04dbf4 --- /dev/null +++ b/sbt-test/tasty-compat/i20010/build.sbt @@ -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")}" + ) + } + ) diff --git a/sbt-test/tasty-compat/i20010/project/DottyInjectedPlugin.scala b/sbt-test/tasty-compat/i20010/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/tasty-compat/i20010/project/DottyInjectedPlugin.scala @@ -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") + ) +} diff --git a/sbt-test/tasty-compat/i20010/project/FakePrintWriter.scala b/sbt-test/tasty-compat/i20010/project/FakePrintWriter.scala new file mode 100644 index 000000000000..14d3bcdba5b4 --- /dev/null +++ b/sbt-test/tasty-compat/i20010/project/FakePrintWriter.scala @@ -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 +} diff --git a/sbt-test/tasty-compat/i20010/test b/sbt-test/tasty-compat/i20010/test new file mode 100644 index 000000000000..9d72fe641dd0 --- /dev/null +++ b/sbt-test/tasty-compat/i20010/test @@ -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