diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/secondary/IncrementalAnnotationArgumentClassReferencesJava.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/secondary/IncrementalAnnotationArgumentClassReferencesJava.kt new file mode 100644 index 0000000000..30fe6a8a9b --- /dev/null +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/secondary/IncrementalAnnotationArgumentClassReferencesJava.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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.secondary + +import com.google.devtools.ksp.test.fixtures.TemporaryTestProject +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.io.File + +@RunWith(Parameterized::class) +class IncrementalAnnotationArgumentClassReferencesJava(experimentalPsiResolution: Boolean) { + + @Rule + @JvmField + val project: TemporaryTestProject = TemporaryTestProject( + "incremental-annotation-argument-class-references-java", + experimentalPsiResolution = experimentalPsiResolution + ) + + companion object { + + @JvmStatic + @Parameterized.Parameters + fun data(): Collection = listOf(true, false) + + private const val TARGET: String = "downstream" + private const val KSP_KOTLIN: String = ":$TARGET:kspKotlin" + private const val ASSEMBLE: String = "assemble" + private const val CLEAN: String = "clean" + private const val PROCESSOR_LABEL: String = "[TestProcessor]" + private const val GENERATED_FILE: String = + "downstream/build/generated/ksp/main/kotlin/DownstreamClassGenerated.kt" + } + + @Test + fun testUpToDate() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + + gradleRunner.withArguments(CLEAN, ASSEMBLE).build().let { result -> + Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":$TARGET:assemble")?.outcome) + } + + gradleRunner.withArguments(ASSEMBLE).build().let { result -> + Assert.assertEquals(TaskOutcome.UP_TO_DATE, result.task(KSP_KOTLIN)?.outcome) + } + } + + @Test + fun testTransitiveJavaClasspathChange() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + // 1. Clean build + gradleRunner.withArguments(CLEAN, ASSEMBLE).build().let { result -> + Assert.assertEquals(TaskOutcome.SUCCESS, result.task(KSP_KOTLIN)?.outcome) + assertProcessed(result.output, "the clean build") + Assert.assertTrue( + "Expected the generated file to record the declarations visited by the processor", + File(project.root, GENERATED_FILE).readText().contains("UpstreamChanges") + ) + } + + // 2. Apply an ABI change to the upstream Java class that only the classpath refers to + val fileToChange = File(project.root, "upstream/src/main/java/com/example/upstream/UpstreamChanges.java") + val addedMember = " public void addedMember() {\n }\n}\n" + fileToChange.writeText(fileToChange.readText().trimEnd().removeSuffix("}") + addedMember) + + // 3. Rebuild + gradleRunner.withArguments(ASSEMBLE).build().let { result -> + Assert.assertEquals(TaskOutcome.SUCCESS, result.task(KSP_KOTLIN)?.outcome) + // Bug: the downstream sources are wrongly considered up to date and nothing is + // reprocessed, because invalidation stops at the class file path of the changed class. + Assert.assertEquals( + emptyList(), + result.output.lines().filter { it.startsWith(PROCESSOR_LABEL) } + ) + Assert.assertFalse( + "Expected the generated file to remain stale", + File(project.root, GENERATED_FILE).readText().contains("addedMember") + ) + } + } + + private fun assertProcessed(output: String, buildDescription: String) { + val processed = output.lines().filter { it.startsWith(PROCESSOR_LABEL) } + listOf("DownstreamClass", "UpstreamEntryPoint", "UpstreamChanges").forEach { simpleName -> + Assert.assertTrue( + "Expected $simpleName to be processed by $buildDescription, but the processor reported:\n" + + processed.joinToString("\n"), + processed.contains("$PROCESSOR_LABEL Processing $simpleName") + ) + } + } +} diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/build.gradle.kts b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/build.gradle.kts new file mode 100644 index 0000000000..17eefd4269 --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +plugins { + kotlin("jvm") +} + +repositories { + maven("https://redirector.kotlinlang.org/maven/bootstrap/") + mavenCentral() +} diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/downstream/build.gradle.kts b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/downstream/build.gradle.kts new file mode 100644 index 0000000000..fcd8f7a4f7 --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/downstream/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +val testRepo: String by project + +plugins { + id("com.google.devtools.ksp") + kotlin("jvm") +} + +version = "1.0-SNAPSHOT" + +repositories { + maven(testRepo) + maven("https://redirector.kotlinlang.org/maven/bootstrap/") + mavenCentral() +} + +dependencies { + implementation(project(":upstream")) + ksp(project(":processor")) +} diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/downstream/src/main/kotlin/com/example/upstream/DownstreamClass.kt b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/downstream/src/main/kotlin/com/example/upstream/DownstreamClass.kt new file mode 100644 index 0000000000..cc4f056742 --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/downstream/src/main/kotlin/com/example/upstream/DownstreamClass.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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.example.upstream + +@ExampleReference(UpstreamEntryPoint::class) +class DownstreamClass diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/build.gradle.kts b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/build.gradle.kts new file mode 100644 index 0000000000..7f80dec6da --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +val kspVersion: String by project +val testRepo: String by project + +plugins { + kotlin("jvm") +} + +group = "com.example" +version = "1.0-SNAPSHOT" + +repositories { + maven(testRepo) + maven("https://redirector.kotlinlang.org/maven/bootstrap/") + mavenCentral() +} + +dependencies { + implementation("com.google.devtools.ksp:symbol-processing-api:$kspVersion") +} + +sourceSets.main { + java.srcDirs("src/main/kotlin") +} diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/src/main/kotlin/TestProcessor.kt b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/src/main/kotlin/TestProcessor.kt new file mode 100644 index 0000000000..33a9c35f4b --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/src/main/kotlin/TestProcessor.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSNode +import com.google.devtools.ksp.symbol.KSType + +class TestProcessor(val environment: SymbolProcessorEnvironment) : SymbolProcessor { + private val createdFiles = mutableSetOf() + override fun process(resolver: Resolver): List { + resolver.getSymbolsWithAnnotation("com.example.upstream.ExampleReference") + .filterIsInstance().forEach { sym -> + // Follow dependencies to let KSP capture dependency graph + val visited = followSym(sym) + + val fileName = "${sym.simpleName.asString()}Generated" + + if (!createdFiles.contains(fileName)) { + createdFiles.add(fileName) + val file = environment.codeGenerator.createNewFile( + Dependencies(false, sym.containingFile!!), + "", + fileName + ) + file.write( + ("// visited: ${visited.joinToString()}\n" + + "class ${sym.simpleName.asString()}Generated").toByteArray() + ) + } + } + // Do not keep KSNodes across rounds + seen.clear() + return emptyList() + } + + private val seen = mutableListOf() + private val queue = mutableListOf() + + private fun followSym(sym: KSAnnotated): List { + val visited = mutableListOf() + if (!seen.contains(sym)) { + queue.add(sym) + } + while (queue.isNotEmpty()) { + val node = queue.removeFirst() + log("Processing $node") + visited.add(node.toString()) + seen.add(node) + when (node) { + is KSDeclaration -> { + followAnnotations(node) + followDeclaration(node) + } + + is KSAnnotated -> { + followAnnotations(node) + } + + else -> Unit + } + } + return visited + } + + private fun followAnnotations(sym: KSAnnotated) { + sym.annotations.forEach { anno -> + anno.arguments.forEach { annoArg -> + annoArg.value?.let { value -> + when (value) { + is List<*> -> { + value.filterIsInstance() + .map { ksType -> ksType.declaration } + .filterNot(seen::contains) + .forEach(queue::add) + } + + is KSType -> { + val decl = value.declaration + if (!seen.contains(decl)) { + queue.add(decl) + } + } + + else -> { + return@forEach + } + } + } + } + } + } + + private fun followDeclaration(ksDecl: KSDeclaration): Unit = when (ksDecl) { + is KSClassDeclaration -> { + ksDecl.declarations + .filterNot(seen::contains) + .filter { it !is KSPropertyDeclaration } + .forEach(queue::add) + } + + is KSFunctionDeclaration -> { + ksDecl.returnType?.resolve()?.declaration?.let { + if (!seen.contains(it)) { + queue.add(it) + } + } + ksDecl.parameters + .map { it.type.resolve().declaration } + .filterNot(seen::contains) + .forEach(queue::add) + } + + else -> Unit + } + + private fun log(msg: Any) { + println("[TestProcessor] $msg") + } +} diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/src/main/kotlin/TestProcessorProvider.kt b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/src/main/kotlin/TestProcessorProvider.kt new file mode 100644 index 0000000000..18511b27bd --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/src/main/kotlin/TestProcessorProvider.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class TestProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return TestProcessor(environment) + } +} diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000000..c91e3e9e0b --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +TestProcessorProvider diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/settings.gradle.kts b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/settings.gradle.kts new file mode 100644 index 0000000000..c4bf6814e9 --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/settings.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +pluginManagement { + val kspVersion: String by settings + val kotlinVersion: String by settings + val testRepo: String by settings + plugins { + id("com.google.devtools.ksp") version kspVersion + kotlin("jvm") version kotlinVersion + } + repositories { + maven(testRepo) + gradlePluginPortal() + maven("https://redirector.kotlinlang.org/maven/bootstrap/") + } +} + +rootProject.name = "incremental-test-annotation-argument-class-references-java" + +include(":downstream") +include(":upstream") +include(":processor") diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/build.gradle.kts b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/build.gradle.kts new file mode 100644 index 0000000000..607d679d55 --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +val testRepo: String by project + +plugins { + kotlin("jvm") +} + +version = "1.0-SNAPSHOT" + +repositories { + maven(testRepo) + maven("https://redirector.kotlinlang.org/maven/bootstrap/") + mavenCentral() +} diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/src/main/java/com/example/upstream/ExampleReference.java b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/src/main/java/com/example/upstream/ExampleReference.java new file mode 100644 index 0000000000..737b2cf403 --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/src/main/java/com/example/upstream/ExampleReference.java @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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.example.upstream; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ExampleReference { + Class value(); +} diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/src/main/java/com/example/upstream/UpstreamChanges.java b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/src/main/java/com/example/upstream/UpstreamChanges.java new file mode 100644 index 0000000000..f69b324339 --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/src/main/java/com/example/upstream/UpstreamChanges.java @@ -0,0 +1,21 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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.example.upstream; + +public class UpstreamChanges { +} diff --git a/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/src/main/java/com/example/upstream/UpstreamEntryPoint.java b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/src/main/java/com/example/upstream/UpstreamEntryPoint.java new file mode 100644 index 0000000000..17faad1564 --- /dev/null +++ b/integration-tests/src/test/resources/incremental-annotation-argument-class-references-java/upstream/src/main/java/com/example/upstream/UpstreamEntryPoint.java @@ -0,0 +1,22 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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.example.upstream; + +@ExampleReference(UpstreamChanges.class) +public class UpstreamEntryPoint { +}