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/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 new file mode 100644 index 0000000000..f1193d29c3 --- /dev/null +++ b/kotlin-analysis-api/testData/annotationTypeArguments.kt @@ -0,0 +1,58 @@ +/* + * 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 +// 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 + +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/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 new file mode 100644 index 0000000000..40510ec1d1 --- /dev/null +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationTypeArgumentsProcessor.kt @@ -0,0 +1,61 @@ +/* + * 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.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)) + } + + 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() +}