diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/common/IncrementalContext.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/common/IncrementalContext.kt new file mode 100644 index 0000000000..6f8749b11d --- /dev/null +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/common/IncrementalContext.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.ksp.common + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.google.devtools.ksp.symbol.KSNode +import com.intellij.psi.PsiJavaFile +import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol +import org.jetbrains.kotlin.analysis.api.types.KaType +import java.io.File + +/** + * Incremental-processing operations used by symbol processing and resolution. + * + * Cache storage, lookup trackers, and logging implementation details are deliberately excluded, + * so callers do not depend on a particular incremental-context implementation. + */ +interface IncrementalContext { + fun calcDirtyFiles(ksFiles: List): Collection + + fun registerGeneratedFiles(newFiles: Collection) + + fun closeFilesOnException(f: () -> T): T + + fun closeFiles() + + fun updateCachesAndOutputs( + dirtyFiles: Collection, + outputs: Set, + sourceToOutputs: Map>, + ) + + fun recordLookup(psiFile: PsiJavaFile, fqn: String) + + fun recordLookup(type: KaType, context: KSNode?) + + fun recordClassReferenceLookup(type: KaType, declaration: KSDeclaration) + + fun recordClassReferenceLookup(fqn: String, declaration: KSDeclaration) + + fun recordLookupForPropertyOrMethod(declaration: KSDeclaration) + + fun recordLookupForGetAll(supers: List, predicate: (KaSymbol) -> Boolean) + + fun recordLookupWithSupertypes( + ktType: KaType, + visited: MutableSet, + extra: (KaType, PsiJavaFile) -> Unit, + ) + + fun recordGetSealedSubclasses(classDeclaration: KSClassDeclaration) + + /** Lookup traces used by existing debug logging and incremental-processing tests. */ + fun dumpLookupRecords(): Map> +} diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/common/IncrementalContextBase.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/common/IncrementalContextBase.kt index 90004de1e9..3c33c98f79 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/common/IncrementalContextBase.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/common/IncrementalContextBase.kt @@ -70,7 +70,7 @@ abstract class IncrementalContextBase( protected val knownModified: List, protected val knownRemoved: List, protected val changedClasses: List, -) { +) : IncrementalContext { // Symbols defined in changed files. This is used to update symbolsMap in the end. protected val updatedSymbols = MultiMap.createSet() @@ -226,7 +226,7 @@ abstract class IncrementalContextBase( } // Beware: no side-effects here; Caches should only be touched in updateCaches. - fun calcDirtyFiles(ksFiles: List): Collection = closeFilesOnException { + override fun calcDirtyFiles(ksFiles: List): Collection = closeFilesOnException { if (!isIncremental) { return@closeFilesOnException ksFiles } @@ -412,14 +412,14 @@ abstract class IncrementalContextBase( sealedMap.flush() } - fun registerGeneratedFiles(newFiles: Collection) = closeFilesOnException { + override fun registerGeneratedFiles(newFiles: Collection) = closeFilesOnException { if (!isIncremental) return@closeFilesOnException collectDefinedSymbols(newFiles) } - fun closeFilesOnException(f: () -> T): T { + override fun closeFilesOnException(f: () -> T): T { try { return f() } catch (e: Exception) { @@ -428,7 +428,7 @@ abstract class IncrementalContextBase( } } - fun closeFiles() { + override fun closeFiles() { symbolsMap.flush() onDemandImportsCache.clear() sealedMap.flush() @@ -439,7 +439,7 @@ abstract class IncrementalContextBase( } // TODO: add a wildcard for outputs with no source and get rid of the outputs parameter. - fun updateCachesAndOutputs( + override fun updateCachesAndOutputs( dirtyFiles: Collection, outputs: Set, sourceToOutputs: Map>, @@ -516,7 +516,7 @@ abstract class IncrementalContextBase( * - If `sym` is an on-demand import, then `sym` may invalidate the file. * - If `sym` is an implicit import, i.e., from the same package, then `sym` may invalidate the file. */ - fun recordLookup(psiFile: PsiJavaFile, fqn: String) { + override fun recordLookup(psiFile: PsiJavaFile, fqn: String) { if (!isIncremental) return @@ -552,7 +552,7 @@ abstract class IncrementalContextBase( ?.filter { it.isOnDemand } ?: emptyList() - fun recordGetSealedSubclasses(classDeclaration: KSClassDeclaration) { + override fun recordGetSealedSubclasses(classDeclaration: KSClassDeclaration) { if (!isIncremental) return diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/IncrementalContextAA.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/IncrementalContextAA.kt index 587fdb68cb..426495592c 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/IncrementalContextAA.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/IncrementalContextAA.kt @@ -127,7 +127,7 @@ class IncrementalContextAA( override val classLookupCache = LookupStorageWrapperImpl(LookupStorage(classLookupCacheDir, icContext)) // Debugging and testing only. - fun dumpLookupRecords(): Map> { + override fun dumpLookupRecords(): Map> { val map = mutableMapOf>() symbolLookupTracker.lookups.entrySet().forEach { e -> val key = "${e.key.scope}.${e.key.name}" @@ -175,7 +175,7 @@ class IncrementalContextAA( } } - fun recordLookup(type: KaType, context: KSNode?) { + override fun recordLookup(type: KaType, context: KSNode?) { if (!isIncremental) return @@ -191,7 +191,7 @@ class IncrementalContextAA( * @param declaration the closest enclosing declaration where the reference occurs. * Callers can use [KSNode.findParentDeclaration] to obtain the enclosing declaration. */ - fun recordClassReferenceLookup(type: KaType, declaration: KSDeclaration) { + override fun recordClassReferenceLookup(type: KaType, declaration: KSDeclaration) { if (!isIncremental) { return } @@ -210,7 +210,7 @@ class IncrementalContextAA( * @param declaration the closest enclosing declaration where the reference occurs. * Callers can use [KSNode.findParentDeclaration] to obtain the enclosing declaration. */ - fun recordClassReferenceLookup(fqn: String, declaration: KSDeclaration) { + override fun recordClassReferenceLookup(fqn: String, declaration: KSDeclaration) { if (!isIncremental) { return } @@ -260,7 +260,7 @@ class IncrementalContextAA( } } - fun recordLookupForPropertyOrMethod(declaration: KSDeclaration) { + override fun recordLookupForPropertyOrMethod(declaration: KSDeclaration) { if (!isIncremental) return @@ -275,7 +275,7 @@ class IncrementalContextAA( recordLookupForDeclaration(symbol, file) } - internal fun recordLookupForGetAll(supers: List, predicate: (KaSymbol) -> Boolean) { + override fun recordLookupForGetAll(supers: List, predicate: (KaSymbol) -> Boolean) { if (!isIncremental) return @@ -314,7 +314,11 @@ class IncrementalContextAA( } } - fun recordLookupWithSupertypes(ktType: KaType, visited: MutableSet, extra: (KaType, PsiJavaFile) -> Unit) { + override fun recordLookupWithSupertypes( + ktType: KaType, + visited: MutableSet, + extra: (KaType, PsiJavaFile) -> Unit, + ) { if (!isIncremental) return diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt index 1b2c432015..81abaa558b 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt @@ -20,6 +20,7 @@ package com.google.devtools.ksp.impl import com.google.devtools.ksp.common.AnyChanges +import com.google.devtools.ksp.common.IncrementalContext import com.google.devtools.ksp.common.KSObjectCacheManager import com.google.devtools.ksp.common.impl.AAResolutionStrategy import com.google.devtools.ksp.common.impl.CodeGeneratorImpl @@ -556,7 +557,7 @@ class KotlinSymbolProcessing( ) val dualLookupTracker = DualLookupTracker() - val incrementalContext = IncrementalContextAA( + val incrementalContext: IncrementalContext = IncrementalContextAA( kspConfig.incremental, dualLookupTracker, File(anyChangesWildcard.filePath).relativeTo(kspConfig.projectBaseDir), diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt index 5edf17812b..747d950af8 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt @@ -22,6 +22,7 @@ import com.google.devtools.ksp.InternalKSPException import com.google.devtools.ksp.InternalKSPException.Companion.render import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.closestClassDeclaration +import com.google.devtools.ksp.common.IncrementalContext import com.google.devtools.ksp.common.JVM_DEFAULT_ANNOTATION_FQN import com.google.devtools.ksp.common.JVM_DEFAULT_WITHOUT_COMPATIBILITY_ANNOTATION_FQN import com.google.devtools.ksp.common.JVM_STATIC_ANNOTATION_FQN @@ -157,7 +158,7 @@ import org.jetbrains.org.objectweb.asm.Opcodes class ResolverAAImpl( val allKSFiles: List, val project: Project, - val incrementalContext: IncrementalContextAA, + val incrementalContext: IncrementalContext, val resolutionStrategy: AnnotationResolutionStrategy, val processorsRegisteredForNewFeatures: Set, ) : Resolver { diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/IncrementalContextTest.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/IncrementalContextTest.kt new file mode 100644 index 0000000000..fd44eb315a --- /dev/null +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/IncrementalContextTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.ksp.test + +import com.google.devtools.ksp.IncrementalContextLoggingOptions +import com.google.devtools.ksp.common.IncrementalContext +import com.google.devtools.ksp.impl.DualLookupTracker +import com.google.devtools.ksp.impl.IncrementalContextAA +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +class IncrementalContextTest { + @TempDir + lateinit var baseDir: File + + @Test + fun incrementalLifecycleThroughInterface() { + val context = createContext(incremental = true) + assertTrue(context.calcDirtyFiles(emptyList()).isEmpty()) + context.registerGeneratedFiles(emptyList()) + assertTrue(context.dumpLookupRecords().isEmpty()) + context.updateCachesAndOutputs(emptyList(), emptySet(), emptyMap()) + assertTrue(File(baseDir, "caches/caches.uptodate").exists()) + } + + @Test + fun nonIncrementalLifecycleThroughInterface() { + val context = createContext(incremental = false) + assertTrue(context.calcDirtyFiles(emptyList()).isEmpty()) + context.registerGeneratedFiles(emptyList()) + assertTrue(context.dumpLookupRecords().isEmpty()) + context.updateCachesAndOutputs(emptyList(), emptySet(), emptyMap()) + assertFalse(File(baseDir, "caches/caches.uptodate").exists()) + context.closeFiles() + } + + @Test + fun exceptionHandlingThroughInterface() { + val context = createContext(incremental = false) + assertEquals("result", context.closeFilesOnException { "result" }) + val failure = IllegalStateException("processing failed") + val thrown = assertThrows(IllegalStateException::class.java) { + context.closeFilesOnException { throw failure } + } + assertSame(failure, thrown) + } + + private fun createContext(incremental: Boolean): IncrementalContext = IncrementalContextAA( + isIncremental = incremental, + lookupTracker = DualLookupTracker(), + anyChangesWildcard = File(""), + loggingOptions = IncrementalContextLoggingOptions(false, null), + baseDir = baseDir, + cachesDir = File(baseDir, "caches").apply { mkdirs() }, + kspOutputDir = File(baseDir, "output").apply { mkdirs() }, + knownModified = emptyList(), + knownRemoved = emptyList(), + changedClasses = emptyList(), + ) +}