Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
58 changes: 58 additions & 0 deletions kotlin-analysis-api/testData/annotationTypeArguments.kt
Original file line number Diff line number Diff line change
@@ -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<SourceObject, DestinationObject, DestinationConverter>
// ContainerTarget.nested.typeArgCount: 3
// ContainerTarget.nested.typeArgs: SourceObject, DestinationObject, DestinationConverter
// NestedTarget.annotationType: MapConvert<INVARIANT List<INVARIANT SourceObject>, INVARIANT Map<INVARIANT String, INVARIANT DestinationObject>, INVARIANT NestedDestinationConverter>
// NestedTarget.typeArgCount: 3
// NestedTarget.typeArgs: kotlin.collections.List, kotlin.collections.Map, NestedDestinationConverter
// Target.annotationType: MapConvert<INVARIANT SourceObject, INVARIANT DestinationObject, INVARIANT DestinationConverter>
// Target.typeArgCount: 3
// Target.typeArgs: SourceObject, DestinationObject, DestinationConverter
// END

interface Converter<S : Any, D : Any>

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class MapConvert<S : Any, D : Any, C : Converter<S, D>>

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Container(
val nested: MapConvert<out Any, out Any, out Converter<out Any, out Any>>,
)

class SourceObject
class DestinationObject
class DestinationConverter : Converter<SourceObject, DestinationObject>
class NestedDestinationConverter : Converter<List<SourceObject>, Map<String, DestinationObject>>

@MapConvert<SourceObject, DestinationObject, DestinationConverter>
class Target

@MapConvert<List<SourceObject>, Map<String, DestinationObject>, NestedDestinationConverter>
class NestedTarget

@Container(
nested = MapConvert<SourceObject, DestinationObject, DestinationConverter>()
)
class ContainerTarget
Original file line number Diff line number Diff line change
@@ -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<A : Any>

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
annotation class BinaryRetentionAnno<A : Any>

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class RuntimeRetentionAnno<A : Any>

class SourceRetentionValue
class BinaryRetentionValue
class RuntimeRetentionValue

@SourceRetentionAnno<SourceRetentionValue>
class SourceRetentionTarget

@BinaryRetentionAnno<BinaryRetentionValue>
class BinaryRetentionTarget

@RuntimeRetentionAnno<RuntimeRetentionValue>
class RuntimeRetentionTarget

// MODULE: main(lib)
// FILE: Main.kt
package com.example.main

class Main
Original file line number Diff line number Diff line change
@@ -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<String>()

override fun process(resolver: Resolver): List<KSAnnotated> {
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<String> {
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<String> = results.sorted()
}
Original file line number Diff line number Diff line change
@@ -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<String>()

override fun process(resolver: Resolver): List<KSAnnotated> {
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))
}
Comment thread
jonamireh marked this conversation as resolved.

return emptyList()
}

private fun renderAnnotation(label: String, annotation: KSAnnotation): List<String> {
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<String> = results.sorted()
}