diff --git a/build-common/src/org/jetbrains/kotlin/incremental/buildUtil.kt b/build-common/src/org/jetbrains/kotlin/incremental/buildUtil.kt index 312e8e7feb2..32658999660 100644 --- a/build-common/src/org/jetbrains/kotlin/incremental/buildUtil.kt +++ b/build-common/src/org/jetbrains/kotlin/incremental/buildUtil.kt @@ -34,9 +34,6 @@ import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME import org.jetbrains.kotlin.utils.addToStdlib.flattenTo import java.io.File import java.nio.file.Files -import java.util.* -import kotlin.collections.HashSet -import kotlin.collections.LinkedHashSet const val DELETE_MODULE_FILE_PROPERTY = "kotlin.delete.module.file.after.build" @@ -143,7 +140,34 @@ data class DirtyData( val dirtyClassesFqNamesForceRecompile: Collection = emptyList() ) -fun ChangesCollector.getDirtyData( +/** + * Returns changed symbols from the changes collected by this [ChangesCollector]. + * + * If impacted symbols are also needed, use [getChangedAndImpactedSymbols]. + */ +fun ChangesCollector.getChangedSymbols(reporter: ICReporter): DirtyData { + // Caches are used to compute impacted symbols. Set `caches = emptyList()` so that we get changed symbols only, not impacted ones. + return changes().getChangedAndImpactedSymbols(caches = emptyList(), reporter) +} + +/** + * Returns changed and impacted symbols from the changes collected by this [ChangesCollector]. + * + * For example, if `Subclass` extends `Superclass` and `Superclass` has changed, `Subclass` will be impacted. + */ +fun ChangesCollector.getChangedAndImpactedSymbols( + caches: Iterable, + reporter: ICReporter +): DirtyData { + return changes().getChangedAndImpactedSymbols(caches, reporter) +} + +/** + * Returns changed and impacted symbols from this list of changes. + * + * For example, if `Subclass` extends `Superclass` and `Superclass` has changed, `Subclass` will be impacted. + */ +fun List.getChangedAndImpactedSymbols( caches: Iterable, reporter: ICReporter ): DirtyData { @@ -152,7 +176,7 @@ fun ChangesCollector.getDirtyData( val sealedParents = HashSet() - for (change in changes()) { + for (change in this) { reporter.debug { "Process $change" } if (change is ChangeInfo.SignatureChanged) { diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt index 06ec1a75dad..9ac9e1a3712 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt @@ -522,7 +522,7 @@ abstract class IncrementalCompilerRunner< break } - val (dirtyLookupSymbols, dirtyClassFqNames, forceRecompile) = changesCollector.getDirtyData( + val (dirtyLookupSymbols, dirtyClassFqNames, forceRecompile) = changesCollector.getChangedAndImpactedSymbols( listOf(caches.platformCache), reporter ) @@ -598,7 +598,7 @@ abstract class IncrementalCompilerRunner< val changesCollector = ChangesCollector() removedClasses.forEach { changesCollector.collectSignature(FqName(it), areSubclassesAffected = true) } - return changesCollector.getDirtyData(listOf(caches.platformCache), reporter) + return changesCollector.getChangedAndImpactedSymbols(listOf(caches.platformCache), reporter) } open fun runWithNoDirtyKotlinSources(caches: CacheManager): Boolean = false diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJsCompilerRunner.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJsCompilerRunner.kt index fd0f02d40c9..b6833d8f16c 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJsCompilerRunner.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJsCompilerRunner.kt @@ -250,7 +250,8 @@ class IncrementalJsCompilerRunner( val changesCollector = ChangesCollector() // todo: split compare and update (or cache comparing) caches.platformCache.compare(translatedFiles, changesCollector) - val (dirtyLookupSymbols, dirtyClassFqNames) = changesCollector.getDirtyData(listOf(caches.platformCache), reporter) + val (dirtyLookupSymbols, dirtyClassFqNames) = + changesCollector.getChangedAndImpactedSymbols(listOf(caches.platformCache), reporter) // todo unify with main cycle newDirtySources.addAll(mapLookupSymbolsToFiles(caches.lookupCache, dirtyLookupSymbols, reporter, excludes = sourcesToCompile)) newDirtySources.addAll( diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJvmCompilerRunner.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJvmCompilerRunner.kt index 5e9d8174a3e..d62b050e269 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJvmCompilerRunner.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJvmCompilerRunner.kt @@ -38,22 +38,15 @@ import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots -import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.config.IncrementalCompilation -import org.jetbrains.kotlin.config.LanguageVersion -import org.jetbrains.kotlin.config.Services -import org.jetbrains.kotlin.config.languageVersionSettings +import org.jetbrains.kotlin.config.* import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotDisabled import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.IncrementalRun.NoChanges import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.IncrementalRun.ToBeComputedByIncrementalCompiler import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.NotAvailableDueToMissingClasspathSnapshot import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.NotAvailableForNonIncrementalRun import org.jetbrains.kotlin.incremental.ClasspathChanges.NotAvailableForJSCompiler -import org.jetbrains.kotlin.incremental.classpathDiff.AccessibleClassSnapshot +import org.jetbrains.kotlin.incremental.classpathDiff.* import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathChangesComputer.computeClasspathChanges -import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotBuildReporter -import org.jetbrains.kotlin.incremental.classpathDiff.shrinkAndSaveClasspathSnapshot -import org.jetbrains.kotlin.incremental.classpathDiff.toChangesEither import org.jetbrains.kotlin.incremental.components.ExpectActualTracker import org.jetbrains.kotlin.incremental.components.LookupTracker import org.jetbrains.kotlin.incremental.multiproject.EmptyModulesApiHistory @@ -165,8 +158,6 @@ open class IncrementalJvmCompilerRunner( override fun destinationDir(args: K2JVMCompilerArguments): File = args.destinationAsFile - private var dirtyClasspathChanges: Collection = emptySet() - private val messageCollector = BufferingMessageCollector() private val compilerConfiguration: CompilerConfiguration by lazy { val filterMessageCollector = FilteringMessageCollector(messageCollector) { !it.isError } @@ -256,7 +247,7 @@ open class IncrementalJvmCompilerRunner( initDirtyFiles(dirtyFiles, changedFiles) reporter.debug { "Classpath changes info passed from Gradle task: ${classpathChanges::class.simpleName}" } - val classpathChanges = when (classpathChanges) { + val changedAndImpactedSymbols = when (classpathChanges) { // Note: classpathChanges is deserialized, so they are no longer singleton objects and need to be compared using `is` (not `==`) is NoChanges -> ChangesEither.Known(emptySet(), emptySet()) is ToBeComputedByIncrementalCompiler -> reporter.measure(BuildTime.COMPUTE_CLASSPATH_CHANGES) { @@ -267,12 +258,15 @@ open class IncrementalJvmCompilerRunner( currentClasspathSnapshot = currentClasspathSnapshotArg shrunkCurrentClasspathAgainstPreviousLookups = shrunkCurrentClasspathAgainstPreviousLookupsArg } - computeClasspathChanges( + val classpathChanges = computeClasspathChanges( classpathChanges.classpathSnapshotFiles, caches.lookupCache, storeCurrentClasspathSnapshotForReuse, ClasspathSnapshotBuildReporter(reporter) - ).toChangesEither() + ) + // `classpathChanges` contains changed and impacted symbols on the classpath. + // We also need to compute symbols in the current module that are impacted by `classpathChanges`. + classpathChanges.toChangeInfoList().getChangedAndImpactedSymbols(listOf(caches.platformCache), reporter).toChangesEither() } is NotAvailableDueToMissingClasspathSnapshot -> ChangesEither.Unknown(BuildAttribute.CLASSPATH_SNAPSHOT_NOT_FOUND) is NotAvailableForNonIncrementalRun -> ChangesEither.Unknown(BuildAttribute.UNKNOWN_CHANGES_IN_GRADLE_INPUTS) @@ -291,6 +285,7 @@ open class IncrementalJvmCompilerRunner( reporter.debug { "Last Kotlin Build info -- $lastBuildInfo" } val scopes = caches.lookupCache.lookupSymbols.map { it.scope.ifBlank { it.name } }.distinct() + // FIXME The old IC currently doesn't compute impacted symbols getClasspathChanges( args.classpathAsList, changedFiles, lastBuildInfo, modulesApiHistory, reporter, abiSnapshots, withAbiSnapshot, caches.platformCache, scopes @@ -299,20 +294,16 @@ open class IncrementalJvmCompilerRunner( is NotAvailableForJSCompiler -> error("Unexpected type for this code path: ${classpathChanges.javaClass.name}.") } - @Suppress("UNUSED_VARIABLE") // for sealed when - val unused = when (classpathChanges) { + when (changedAndImpactedSymbols) { is ChangesEither.Unknown -> { - reporter.info { - "Could not get classpath's changes: ${classpathChanges.reason}" - } - return CompilationMode.Rebuild(classpathChanges.reason) + reporter.info { "Could not get classpath changes: ${changedAndImpactedSymbols.reason}" } + return CompilationMode.Rebuild(changedAndImpactedSymbols.reason) } - is ChangesEither.Known -> { - dirtyFiles.addByDirtySymbols(classpathChanges.lookupSymbols) - dirtyClasspathChanges = classpathChanges.fqNames - dirtyFiles.addByDirtyClasses(classpathChanges.fqNames) - } - } + is ChangesEither.Known -> Unit + }.forceExhaustiveWhen() + + dirtyFiles.addByDirtySymbols(changedAndImpactedSymbols.lookupSymbols) + dirtyFiles.addByDirtyClasses(changedAndImpactedSymbols.fqNames) reporter.measure(BuildTime.IC_ANALYZE_CHANGES_IN_JAVA_SOURCES) { if (!usePreciseJavaTracking) { @@ -342,6 +333,35 @@ open class IncrementalJvmCompilerRunner( return CompilationMode.Incremental(dirtyFiles) } + private fun ProgramSymbolSet.toChangeInfoList(): List { + val changes = mutableListOf() + classes.forEach { classId -> + // It's important to set `areSubclassesAffected = true` when we don't know + changes.add(ChangeInfo.SignatureChanged(classId.asSingleFqName(), areSubclassesAffected = true)) + } + classMembers.forEach { (classId, members) -> + changes.add(ChangeInfo.MembersChanged(classId.asSingleFqName(), members)) + } + packageMembers.forEach { (packageFqName, members) -> + changes.add(ChangeInfo.MembersChanged(packageFqName, members)) + } + return changes + } + + private fun DirtyData.toChangesEither(): ChangesEither.Known { + return ChangesEither.Known( + lookupSymbols = dirtyLookupSymbols, + fqNames = dirtyClassesFqNames + dirtyClassesFqNamesForceRecompile + ) + } + + /** + * Helper function to force exhaustive when for statements (see https://youtrack.jetbrains.com/issue/KT-47709). + * + * If the current IDE/Kotlin compiler already supports exhaustive when for statements, consider removing this function and its usages. + */ + private fun Any.forceExhaustiveWhen() = this + private fun processChangedJava(changedFiles: ChangedFiles.Known, caches: IncrementalJvmCachesManager): BuildAttribute? { val javaFiles = (changedFiles.modified + changedFiles.removed).filter(File::isJavaFile) diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt index 3b1594fd3b5..98ced528c9b 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt @@ -247,7 +247,7 @@ object ClasspathChangesComputer { incrementalJvmCache.clearCacheForRemovedClasses(changesCollector) // Get the changes and clean up - val dirtyData = changesCollector.getDirtyData(listOf(incrementalJvmCache), DoNothingICReporter) + val dirtyData = changesCollector.getChangedSymbols(DoNothingICReporter) workingDir.deleteRecursively() // Normalize the changes (convert DirtyData to `ProgramSymbol`s) @@ -292,6 +292,10 @@ object ClasspathChangesComputer { it.removeAll(changedFqNames) } + if (unmatchedLookupSymbols.isEmpty() && unmatchedFqNames.isEmpty()) { + return changedProgramSymbols + } + /* When `unmatchedLookupSymbols` or `unmatchedFqNames` is not empty, there are two cases: * 1. The unmatched LookupSymbols/FqNames are redundant. This is not ideal but because it does not cause incremental compilation * to be incorrect, we can fix these issues later if they are not easy to fix immediately. diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/incrementalFirCacheUtils.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/incrementalFirCacheUtils.kt index f8e41fde010..dfa8c975974 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/incrementalFirCacheUtils.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/incrementalFirCacheUtils.kt @@ -132,7 +132,8 @@ internal fun collectNewDirtySources( visitFirFiles(output) } - val (dirtyLookupSymbols, dirtyClassFqNames, forceRecompile) = changesCollector.getDirtyData(listOf(caches.platformCache), reporter) + val (dirtyLookupSymbols, dirtyClassFqNames, forceRecompile) = + changesCollector.getChangedAndImpactedSymbols(listOf(caches.platformCache), reporter) val forceToRecompileFiles = mapClassesFqNamesToFiles(listOf(caches.platformCache), forceRecompile, reporter) diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinBuilder.kt b/jps/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinBuilder.kt index 9954dc9b2f5..df2360696f7 100644 --- a/jps/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinBuilder.kt +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinBuilder.kt @@ -52,9 +52,6 @@ import org.jetbrains.kotlin.utils.KotlinPaths import org.jetbrains.kotlin.utils.KotlinPathsFromHomeDir import org.jetbrains.kotlin.utils.PathUtil import java.io.File -import java.util.* -import kotlin.collections.ArrayList -import kotlin.collections.HashSet import kotlin.system.measureTimeMillis class KotlinBuilder : ModuleLevelBuilder(BuilderCategory.SOURCE_PROCESSOR) { @@ -776,7 +773,7 @@ private fun ChangesCollector.getDirtyFiles( lookupStorageManager: JpsLookupStorageManager ): FilesToRecompile { val reporter = JpsICReporter() - val (dirtyLookupSymbols, dirtyClassFqNames, forceRecompile) = getDirtyData(caches, reporter) + val (dirtyLookupSymbols, dirtyClassFqNames, forceRecompile) = getChangedAndImpactedSymbols(caches, reporter) val dirtyFilesFromLookups = lookupStorageManager.withLookupStorage { mapLookupSymbolsToFiles(it, dirtyLookupSymbols, reporter) } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationBaseIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationBaseIT.kt index 01bab06cd65..7d29c4697c1 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationBaseIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationBaseIT.kt @@ -6,11 +6,11 @@ package org.jetbrains.kotlin.gradle import org.gradle.api.logging.LogLevel -import org.gradle.testkit.runner.BuildResult import org.gradle.util.GradleVersion -import org.jetbrains.kotlin.gradle.testbase.* -import java.nio.file.Path -import kotlin.io.path.relativeTo +import org.jetbrains.kotlin.gradle.testbase.BuildOptions +import org.jetbrains.kotlin.gradle.testbase.KGPBaseTest +import org.jetbrains.kotlin.gradle.testbase.TestProject +import org.jetbrains.kotlin.gradle.testbase.project abstract class IncrementalCompilationBaseIT : KGPBaseTest() { @@ -23,6 +23,7 @@ abstract class IncrementalCompilationBaseIT : KGPBaseTest() { ): TestProject = project( defaultProjectName, gradleVersion, + buildOptions, test = test ) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt index 4ff4a526811..ed27f9bce3b 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt @@ -135,6 +135,20 @@ open class IncrementalCompilationJsMultiProjectIT : BaseIncrementalCompilationMu } } } + + @DisplayName("Lib project classes became final") + @GradleTest + override fun testLibClassBecameFinal(gradleVersion: GradleVersion) { + // `impactedClassInAppIsRecompiled = false` for Kotlin/JS (KT-56197 was fixed for Kotlin/JVM only) + doTestLibClassBecameFinal(gradleVersion, impactedClassInAppIsRecompiled = false) + } + + @DisplayName("KT-56197: Change interface in lib which has subclass in app") + @GradleTest + override fun testChangeInterfaceInLib(gradleVersion: GradleVersion) { + // `impactedClassInAppIsRecompiled = false` for Kotlin/JS (KT-56197 was fixed for Kotlin/JVM only) + doTestChangeInterfaceInLib(gradleVersion, impactedClassInAppIsRecompiled = false) + } } class IncrementalCompilationJsMultiProjectWithPreciseBackupIT : IncrementalCompilationJsMultiProjectIT() { @@ -413,7 +427,6 @@ open class IncrementalCompilationOldICJvmMultiProjectIT : IncrementalCompilation override fun testAbiChangeInLib_afterLibClean_withAbiSnapshot(gradleVersion: GradleVersion) { defaultProject( gradleVersion, - buildOptions = defaultBuildOptions.copy(useGradleClasspathSnapshot = true) ) { build("assemble") @@ -437,6 +450,20 @@ open class IncrementalCompilationOldICJvmMultiProjectIT : IncrementalCompilation } } } + + @DisplayName("Lib project classes became final") + @GradleTest + override fun testLibClassBecameFinal(gradleVersion: GradleVersion) { + // `impactedClassInAppIsRecompiled = false` for the old IC (KT-56197 was fixed for the new IC only) + doTestLibClassBecameFinal(gradleVersion, impactedClassInAppIsRecompiled = false) + } + + @DisplayName("KT-56197: Change interface in lib which has subclass in app") + @GradleTest + override fun testChangeInterfaceInLib(gradleVersion: GradleVersion) { + // `impactedClassInAppIsRecompiled = false` for the old IC (KT-56197 was fixed for the new IC only) + doTestChangeInterfaceInLib(gradleVersion, impactedClassInAppIsRecompiled = false) + } } class IncrementalCompilationOldICJvmMultiProjectWithPreciseBackupIT : IncrementalCompilationOldICJvmMultiProjectIT() { @@ -622,7 +649,11 @@ abstract class BaseIncrementalCompilationMultiProjectIT : IncrementalCompilation @DisplayName("Lib project classes became final") @GradleTest - fun testLibClassBecameFinal(gradleVersion: GradleVersion) { + open fun testLibClassBecameFinal(gradleVersion: GradleVersion) { + doTestLibClassBecameFinal(gradleVersion) + } + + protected fun doTestLibClassBecameFinal(gradleVersion: GradleVersion, impactedClassInAppIsRecompiled: Boolean = true) { defaultProject(gradleVersion) { build("assemble") @@ -633,7 +664,10 @@ abstract class BaseIncrementalCompilationMultiProjectIT : IncrementalCompilation buildAndFail("assemble") { val expectedSources = getExpectedKotlinSourcesForDefaultProject( libSources = listOf("bar/B.kt", "bar/barUseAB.kt", "bar/barUseB.kt"), - appSources = listOf("foo/BB.kt", "foo/fooCallUseAB.kt", "foo/fooUseB.kt") + appSources = listOfNotNull( + "foo/BB.kt", "foo/fooUseB.kt", "foo/fooCallUseAB.kt", + "foo/fooUseBB.kt".takeIf { impactedClassInAppIsRecompiled } + ) ) assertCompiledKotlinSources(expectedSources, output) } @@ -776,6 +810,57 @@ abstract class BaseIncrementalCompilationMultiProjectIT : IncrementalCompilation } } + @DisplayName("KT-56197: Change interface in lib which has subclass in app") + @GradleTest + open fun testChangeInterfaceInLib(gradleVersion: GradleVersion) { + doTestChangeInterfaceInLib(gradleVersion) + } + + protected fun doTestChangeInterfaceInLib(gradleVersion: GradleVersion, impactedClassInAppIsRecompiled: Boolean = true) { + defaultProject(gradleVersion) { + subProject("lib").kotlinSourcesDir().resolve("bar/InterfaceInLib.kt").writeText( + """ + package bar + interface InterfaceInLib { + fun someMethod() {} + } + """.trimIndent() + ) + subProject("app").kotlinSourcesDir().resolve("foo/SubclassInApp.kt").writeText( + """ + package foo + import bar.InterfaceInLib + class SubclassInApp : InterfaceInLib + """.trimIndent() + ) + subProject("app").kotlinSourcesDir().resolve("foo/ClassUsingSubclassInApp.kt").writeText( + """ + package foo + fun main() { + SubclassInApp().someMethod() + } + """.trimIndent() + ) + build(":app:compileKotlin") + + subProject("lib").kotlinSourcesDir().resolve("bar/InterfaceInLib.kt").modify { + it.replace("fun someMethod() {}", "fun someMethod(addedParam: Int = 0) {}") + } + + build(":app:compileKotlin") { + assertIncrementalCompilation( + expectedCompiledKotlinFiles = getExpectedKotlinSourcesForDefaultProject( + libSources = listOf("bar/InterfaceInLib.kt"), + appSources = listOfNotNull( + "foo/SubclassInApp.kt", + "foo/ClassUsingSubclassInApp.kt".takeIf { impactedClassInAppIsRecompiled } + ) + ) + ) + } + } + } + @DisplayName("Test compilation when incremental state is missing") @GradleTest fun testMissingIncrementalState(gradleVersion: GradleVersion) {