From 34c4c6eba537b31e847c5a34167424a7d6494804 Mon Sep 17 00:00:00 2001 From: Jack Koenig Date: Tue, 20 Jan 2026 15:02:40 -0800 Subject: [PATCH] Fix crash from changing abstract class to trait --- .../sbt/internal/inc/IncrementalCommon.scala | 5 +- .../abstract-class-to-trait/{pending => test} | 0 .../false-error/{pending => test} | 0 .../nested-type-params/{pending => test} | 0 .../no-type-annotation/{pending => test} | 0 .../sbt/inc/IncrementalCompilerSpec.scala | 67 +++++++++++++++++++ 6 files changed, 71 insertions(+), 1 deletion(-) rename zinc/src/sbt-test/source-dependencies/abstract-class-to-trait/{pending => test} (100%) rename zinc/src/sbt-test/source-dependencies/false-error/{pending => test} (100%) rename zinc/src/sbt-test/source-dependencies/nested-type-params/{pending => test} (100%) rename zinc/src/sbt-test/source-dependencies/no-type-annotation/{pending => test} (100%) diff --git a/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala b/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala index 15065cc66f..20330d41b3 100644 --- a/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala +++ b/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala @@ -539,7 +539,10 @@ private[inc] abstract class IncrementalCommon( val removedClasses = classNames(removedSrcs) val dependentOnRemovedClasses = removedClasses.flatMap(previous.memberRef.internal.reverse) val modifiedClasses = classNames(modifiedSrcs) - val invalidatedClasses = removedClasses ++ dependentOnRemovedClasses ++ modifiedClasses + val inheritanceInvalidations = modifiedClasses.flatMap(previous.inheritance.internal.reverse) + + val invalidatedClasses = + removedClasses ++ dependentOnRemovedClasses ++ modifiedClasses ++ inheritanceInvalidations val byProduct = changes.removedProducts.flatMap(previous.produced) val byLibraryDep = changes.libraryDeps.flatMap(previous.usesLibrary) diff --git a/zinc/src/sbt-test/source-dependencies/abstract-class-to-trait/pending b/zinc/src/sbt-test/source-dependencies/abstract-class-to-trait/test similarity index 100% rename from zinc/src/sbt-test/source-dependencies/abstract-class-to-trait/pending rename to zinc/src/sbt-test/source-dependencies/abstract-class-to-trait/test diff --git a/zinc/src/sbt-test/source-dependencies/false-error/pending b/zinc/src/sbt-test/source-dependencies/false-error/test similarity index 100% rename from zinc/src/sbt-test/source-dependencies/false-error/pending rename to zinc/src/sbt-test/source-dependencies/false-error/test diff --git a/zinc/src/sbt-test/source-dependencies/nested-type-params/pending b/zinc/src/sbt-test/source-dependencies/nested-type-params/test similarity index 100% rename from zinc/src/sbt-test/source-dependencies/nested-type-params/pending rename to zinc/src/sbt-test/source-dependencies/nested-type-params/test diff --git a/zinc/src/sbt-test/source-dependencies/no-type-annotation/pending b/zinc/src/sbt-test/source-dependencies/no-type-annotation/test similarity index 100% rename from zinc/src/sbt-test/source-dependencies/no-type-annotation/pending rename to zinc/src/sbt-test/source-dependencies/no-type-annotation/test diff --git a/zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala b/zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala index b99ac36cc4..5a6dfc8962 100644 --- a/zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala +++ b/zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala @@ -227,6 +227,73 @@ class IncrementalCompilerSpec extends BaseCompilerSpec { } } + it should "recompile subclass when superclass changes from abstract class to trait" in withTmpDir { + tmp => + val project = VirtualSubproject(tmp.toPath / "p1") + val comp = project.setup.createCompiler() + try { + // Initial sources: A is an abstract class, B extends A + val aAbstractClass = + """abstract class A + |object Test { new B }""".stripMargin + + val aAsTrait = + """trait A + |object Test { new B }""".stripMargin + + val bSource = "class B extends A" + + val f1 = StringVirtualFile("A.scala", aAbstractClass) + val f1Modified = StringVirtualFile("A.scala", aAsTrait) + val f2 = StringVirtualFile("B.scala", bSource) + + val res1 = comp.compile(f1, f2) + val res2 = comp.compile(f1Modified, f2) + + assert(recompiled(res1, res2) == Set("A", "B", "Test")) + } finally { + comp.close() + } + } + + it should "not recompile all files in a cycle for non-API changes" in withTmpDir { tmp => + val project = VirtualSubproject(tmp.toPath / "p1") + val comp = project.setup.createCompiler() + try { + // Create a cyclic dependency: X references Y, Y references X + val xSource = + """class X { + | def x: Y = null + | def impl = 1 + |}""".stripMargin + // Only implementation change, no API change + val xSourceModified = + """class X { + | def x: Y = null + | def impl = 2 + |}""".stripMargin + + val ySource = + """class Y { + | def y: X = null + |}""".stripMargin + + val zSource = "class Z { def z = 1 }" + + val fX = StringVirtualFile("X.scala", xSource) + val fXModified = StringVirtualFile("X.scala", xSourceModified) + val fY = StringVirtualFile("Y.scala", ySource) + val fZ = StringVirtualFile("Z.scala", zSource) + + val res1 = comp.compile(fX, fY, fZ) + val res2 = comp.compile(fXModified, fY, fZ) + + assert(recompiled(res1, res2) == Set("X")) + } finally { + comp.close() + } + } + it should "not throw NullPointerException when passing -Xshow-phases to scalac" in withTmpDir { tmp => val comp = ProjectSetup.simple(