From 729dd65080b122afec7092a30c1424d6de008103 Mon Sep 17 00:00:00 2001 From: Jon Amireh <2724407+jonamireh@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:38:33 -0700 Subject: [PATCH 1/2] Fix annotation type arguments for resolved source annotations and nested type arguments --- .../resolved/KSAnnotationResolvedImpl.kt | 14 +++- .../testData/annotationTypeArguments.kt | 80 +++++++++++++++++++ .../AnnotationTypeArgumentsProcessor.kt | 68 ++++++++++++++++ 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 kotlin-analysis-api/testData/annotationTypeArguments.kt create mode 100644 test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsProcessor.kt diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/resolved/KSAnnotationResolvedImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/resolved/KSAnnotationResolvedImpl.kt index be613b90cf..370a5c2fa5 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/resolved/KSAnnotationResolvedImpl.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/resolved/KSAnnotationResolvedImpl.kt @@ -24,10 +24,13 @@ import com.intellij.psi.PsiArrayInitializerMemberValue import com.intellij.psi.PsiClass import com.intellij.psi.impl.compiled.ClsClassImpl import org.jetbrains.kotlin.analysis.api.KaImplementationDetail +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.components.resolveCall import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotation import org.jetbrains.kotlin.analysis.api.impl.base.annotations.KaBaseNamedAnnotationValue import org.jetbrains.kotlin.analysis.api.symbols.KaSymbolOrigin import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget.* +import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtFile class KSAnnotationResolvedImpl private constructor( @@ -43,9 +46,18 @@ class KSAnnotationResolvedImpl private constructor( } } + @OptIn(KaExperimentalApi::class) override val annotationType: KSTypeReference by lazy { analyze { - KSTypeReferenceResolvedImpl.getCached( + // Try source-level type reference first (preserves type arguments from source), + // then fall back to resolved call signature, and finally build from classId. + val sourceTypeReference = lazy { (annotationApplication.psi as? KtAnnotationEntry)?.typeReference } + val resolvedReturnType = lazy { annotationApplication.psi?.resolveCall()?.signature?.returnType } + sourceTypeReference.value?.let { + KSTypeReferenceImpl.getCached(it, this@KSAnnotationResolvedImpl) + } ?: resolvedReturnType.value?.let { + KSTypeReferenceResolvedImpl.getCached(it, parent = this@KSAnnotationResolvedImpl) + } ?: KSTypeReferenceResolvedImpl.getCached( buildClassType(annotationApplication.classId!!), parent = this@KSAnnotationResolvedImpl ) diff --git a/kotlin-analysis-api/testData/annotationTypeArguments.kt b/kotlin-analysis-api/testData/annotationTypeArguments.kt new file mode 100644 index 0000000000..ed540bfc12 --- /dev/null +++ b/kotlin-analysis-api/testData/annotationTypeArguments.kt @@ -0,0 +1,80 @@ +/* + * 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. + */ + +// WITH_RUNTIME +// TEST PROCESSOR: AnnotationTypeArgumentsProcessor +// EXPECTED: +// ContainerTarget.nested.annotationType: MapConvert +// ContainerTarget.nested.typeArgCount: 3 +// ContainerTarget.nested.typeArgs: SourceObject, DestinationObject, DestinationConverter +// LibClass.annotationType: LibAnno +// LibClass.typeArgCount: 0 +// NestedTarget.annotationType: MapConvert, INVARIANT Map, INVARIANT NestedDestinationConverter> +// NestedTarget.typeArgCount: 3 +// NestedTarget.typeArgs: kotlin.collections.List, kotlin.collections.Map, NestedDestinationConverter +// Target.annotationType: MapConvert +// Target.typeArgCount: 3 +// Target.typeArgs: SourceObject, DestinationObject, DestinationConverter +// END + +// MODULE: lib +// FILE: LibAnno.kt +package com.example + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class LibAnno + +// FILE: LibAnnoValue.kt +package com.example + +class LibAnnoValue + +// FILE: LibClass.kt +package com.example + +@LibAnno +class LibClass + +// MODULE: main(lib) +interface Converter + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class MapConvert> + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class Container( + val nested: MapConvert>, +) + +class SourceObject +class DestinationObject +class DestinationConverter : Converter +class NestedDestinationConverter : Converter, Map> + +@MapConvert +class Target + +@MapConvert, Map, NestedDestinationConverter> +class NestedTarget + +@Container( + nested = MapConvert() +) +class ContainerTarget diff --git a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsProcessor.kt b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsProcessor.kt new file mode 100644 index 0000000000..bfb258f005 --- /dev/null +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsProcessor.kt @@ -0,0 +1,68 @@ +/* + * 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.processor + +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSDeclaration + +class AnnotationTypeArgumentsProcessor : AbstractTestProcessor() { + private val results = mutableListOf() + + override fun process(resolver: Resolver): List { + resolver.getSymbolsWithAnnotation("MapConvert").forEach { annotated -> + val name = (annotated as KSDeclaration).simpleName.asString() + val annotation = annotated.annotations.single { it.shortName.asString() == "MapConvert" } + results.addAll(renderAnnotation(name, annotation)) + } + + resolver.getSymbolsWithAnnotation("Container").forEach { annotated -> + val name = (annotated as KSDeclaration).simpleName.asString() + val containerAnnotation = annotated.annotations.single { it.shortName.asString() == "Container" } + val nestedAnnotation = containerAnnotation.arguments.single().value as KSAnnotation + results.addAll(renderAnnotation("$name.nested", nestedAnnotation)) + } + + resolver.getClassDeclarationByName("com.example.LibClass")?.let { annotated -> + val name = (annotated as KSDeclaration).simpleName.asString() + val annotation = annotated.annotations.single { it.shortName.asString() == "LibAnno" } + results.addAll(renderAnnotation(name, annotation)) + } + + return emptyList() + } + + private fun renderAnnotation(label: String, annotation: KSAnnotation): List { + val typeArguments = annotation.annotationType.element!!.typeArguments + return buildList { + add("$label.annotationType: ${annotation.annotationType}") + add("$label.typeArgCount: ${typeArguments.size}") + if(typeArguments.isNotEmpty()) { + add("$label.typeArgs: ${ + typeArguments.joinToString { argument -> + argument.type!!.resolve().declaration.qualifiedName!!.asString() + } + }") + } + } + } + + override fun toResult(): List = results.sorted() +} From c4d5055b25f7380ca00c1006ab918b1bb837d0e6 Mon Sep 17 00:00:00 2001 From: Jon Amireh <2724407+jonamireh@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:53:07 -0700 Subject: [PATCH 2/2] Split library annotation type arguments into their own test --- .../devtools/ksp/test/ConfigurableKSPTest.kt | 12 +++ .../testData/annotationTypeArguments.kt | 22 ------ .../annotationTypeArgumentsInLibrary.kt | 60 +++++++++++++++ ...notationTypeArgumentsInLibraryProcessor.kt | 75 +++++++++++++++++++ .../AnnotationTypeArgumentsProcessor.kt | 9 +-- 5 files changed, 148 insertions(+), 30 deletions(-) create mode 100644 kotlin-analysis-api/testData/annotationTypeArgumentsInLibrary.kt create mode 100644 test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsInLibraryProcessor.kt diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/ConfigurableKSPTest.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/ConfigurableKSPTest.kt index ab4f913dd5..004d1abfe7 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/ConfigurableKSPTest.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/ConfigurableKSPTest.kt @@ -96,6 +96,18 @@ abstract class ConfigurableKSPTest( runTest("../test-utils/testData/api/annotationTargets.kt") } + @TestMetadata("annotationTypeArguments.kt") + @Test + fun testAnnotationTypeArguments() { + runTest("../kotlin-analysis-api/testData/annotationTypeArguments.kt") + } + + @TestMetadata("annotationTypeArgumentsInLibrary.kt") + @Test + fun testAnnotationTypeArgumentsInLibrary() { + runTest("../kotlin-analysis-api/testData/annotationTypeArgumentsInLibrary.kt") + } + @TestMetadata("annotationWithArbitraryClassValue.kt") @Test fun testAnnotationWithArbitraryClassValue() { diff --git a/kotlin-analysis-api/testData/annotationTypeArguments.kt b/kotlin-analysis-api/testData/annotationTypeArguments.kt index ed540bfc12..f1193d29c3 100644 --- a/kotlin-analysis-api/testData/annotationTypeArguments.kt +++ b/kotlin-analysis-api/testData/annotationTypeArguments.kt @@ -21,8 +21,6 @@ // ContainerTarget.nested.annotationType: MapConvert // ContainerTarget.nested.typeArgCount: 3 // ContainerTarget.nested.typeArgs: SourceObject, DestinationObject, DestinationConverter -// LibClass.annotationType: LibAnno -// LibClass.typeArgCount: 0 // NestedTarget.annotationType: MapConvert, INVARIANT Map, INVARIANT NestedDestinationConverter> // NestedTarget.typeArgCount: 3 // NestedTarget.typeArgs: kotlin.collections.List, kotlin.collections.Map, NestedDestinationConverter @@ -31,26 +29,6 @@ // Target.typeArgs: SourceObject, DestinationObject, DestinationConverter // END -// MODULE: lib -// FILE: LibAnno.kt -package com.example - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.BINARY) -annotation class LibAnno - -// FILE: LibAnnoValue.kt -package com.example - -class LibAnnoValue - -// FILE: LibClass.kt -package com.example - -@LibAnno -class LibClass - -// MODULE: main(lib) interface Converter @Target(AnnotationTarget.CLASS) diff --git a/kotlin-analysis-api/testData/annotationTypeArgumentsInLibrary.kt b/kotlin-analysis-api/testData/annotationTypeArgumentsInLibrary.kt new file mode 100644 index 0000000000..1cc9f974f7 --- /dev/null +++ b/kotlin-analysis-api/testData/annotationTypeArgumentsInLibrary.kt @@ -0,0 +1,60 @@ +/* + * 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. + */ + +// WITH_RUNTIME +// TEST PROCESSOR: AnnotationTypeArgumentsInLibraryProcessor +// EXPECTED: +// BinaryRetentionTarget.annotationType: BinaryRetentionAnno +// BinaryRetentionTarget.typeArgCount: 0 +// RuntimeRetentionTarget.annotationType: RuntimeRetentionAnno +// RuntimeRetentionTarget.typeArgCount: 0 +// END + +// MODULE: lib +// FILE: LibraryAnnotations.kt +package com.example + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class SourceRetentionAnno + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class BinaryRetentionAnno + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class RuntimeRetentionAnno + +class SourceRetentionValue +class BinaryRetentionValue +class RuntimeRetentionValue + +@SourceRetentionAnno +class SourceRetentionTarget + +@BinaryRetentionAnno +class BinaryRetentionTarget + +@RuntimeRetentionAnno +class RuntimeRetentionTarget + +// MODULE: main(lib) +// FILE: Main.kt +package com.example.main + +class Main diff --git a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsInLibraryProcessor.kt b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsInLibraryProcessor.kt new file mode 100644 index 0000000000..15f1f2ce0d --- /dev/null +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsInLibraryProcessor.kt @@ -0,0 +1,75 @@ +/* + * 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.processor + +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation + +class AnnotationTypeArgumentsInLibraryProcessor : AbstractTestProcessor() { + private val results = mutableListOf() + + override fun process(resolver: Resolver): List { + renderAnnotationIfVisible( + resolver, + className = "com.example.SourceRetentionTarget", + annotationName = "SourceRetentionAnno", + ) + renderAnnotationIfVisible( + resolver, + className = "com.example.BinaryRetentionTarget", + annotationName = "BinaryRetentionAnno", + ) + renderAnnotationIfVisible( + resolver, + className = "com.example.RuntimeRetentionTarget", + annotationName = "RuntimeRetentionAnno", + ) + return emptyList() + } + + private fun renderAnnotationIfVisible( + resolver: Resolver, + className: String, + annotationName: String, + ) { + val declaration = resolver.getClassDeclarationByName(className) ?: return + val annotation = declaration.annotations.singleOrNull { + it.shortName.asString() == annotationName + } ?: return + results.addAll(renderAnnotation(declaration.simpleName.asString(), annotation)) + } + + private fun renderAnnotation(label: String, annotation: KSAnnotation): List { + val typeArguments = annotation.annotationType.element!!.typeArguments + return buildList { + add("$label.annotationType: ${annotation.annotationType}") + add("$label.typeArgCount: ${typeArguments.size}") + if (typeArguments.isNotEmpty()) { + add("$label.typeArgs: ${ + typeArguments.joinToString { argument -> + argument.type!!.resolve().declaration.qualifiedName!!.asString() + } + }") + } + } + } + + override fun toResult(): List = results.sorted() +} diff --git a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsProcessor.kt b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsProcessor.kt index bfb258f005..40510ec1d1 100644 --- a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsProcessor.kt +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsProcessor.kt @@ -17,7 +17,6 @@ package com.google.devtools.ksp.processor -import com.google.devtools.ksp.getClassDeclarationByName import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSAnnotation @@ -40,12 +39,6 @@ class AnnotationTypeArgumentsProcessor : AbstractTestProcessor() { results.addAll(renderAnnotation("$name.nested", nestedAnnotation)) } - resolver.getClassDeclarationByName("com.example.LibClass")?.let { annotated -> - val name = (annotated as KSDeclaration).simpleName.asString() - val annotation = annotated.annotations.single { it.shortName.asString() == "LibAnno" } - results.addAll(renderAnnotation(name, annotation)) - } - return emptyList() } @@ -54,7 +47,7 @@ class AnnotationTypeArgumentsProcessor : AbstractTestProcessor() { return buildList { add("$label.annotationType: ${annotation.annotationType}") add("$label.typeArgCount: ${typeArguments.size}") - if(typeArguments.isNotEmpty()) { + if (typeArguments.isNotEmpty()) { add("$label.typeArgs: ${ typeArguments.joinToString { argument -> argument.type!!.resolve().declaration.qualifiedName!!.asString()