Skip to content

Commit a6524c6

Browse files
committed
Add fix for field declaration-site target
1 parent b57ca8d commit a6524c6

1 file changed

Lines changed: 167 additions & 4 deletions

File tree

kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/common/impl/PsiResolutionStrategy.kt

Lines changed: 167 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.google.devtools.ksp.impl.symbol.kotlin.toKSClassDeclaration
3939
import com.google.devtools.ksp.impl.symbol.kotlin.toKSFile
4040
import com.google.devtools.ksp.impl.symbol.kotlin.toKSFunctionDeclaration
4141
import com.google.devtools.ksp.impl.symbol.kotlin.toKSPropertyDeclaration
42+
import com.google.devtools.ksp.impl.symbol.kotlin.toKtClassSymbol
4243
import com.google.devtools.ksp.impl.symbol.kotlin.toLocation
4344
import com.google.devtools.ksp.processing.SymbolProcessor
4445
import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget
@@ -60,13 +61,17 @@ import com.intellij.psi.PsiParameter
6061
import com.intellij.psi.PsiTypeParameter
6162
import com.intellij.psi.PsiTypeParameterList
6263
import com.intellij.util.containers.addIfNotNull
64+
import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotation
65+
import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotationValue
6366
import org.jetbrains.kotlin.analysis.api.resolution.KaAnnotationCall
6467
import org.jetbrains.kotlin.analysis.api.resolution.successfulCallOrNull
6568
import org.jetbrains.kotlin.analysis.api.resolution.symbol
6669
import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
70+
import org.jetbrains.kotlin.analysis.api.symbols.KaClassKind
6771
import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
6872
import org.jetbrains.kotlin.analysis.api.symbols.KaTypeAliasSymbol
6973
import org.jetbrains.kotlin.analysis.api.types.symbol
74+
import org.jetbrains.kotlin.name.CallableId
7075
import org.jetbrains.kotlin.name.ClassId
7176
import org.jetbrains.kotlin.name.FqName
7277
import org.jetbrains.kotlin.name.Name
@@ -77,7 +82,7 @@ import org.jetbrains.kotlin.psi.KtFile
7782
import org.jetbrains.kotlin.psi.KtTypeReference
7883
import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
7984
import org.jetbrains.kotlin.utils.addToStdlib.flatGroupBy
80-
import kotlin.collections.mapValues
85+
import org.jetbrains.kotlin.utils.addToStdlib.getOrPut
8186

8287
/**
8388
* An [AnnotationResolutionStrategy] that uses a combination of Psi and Kotlin's Analysis API to resolve
@@ -90,6 +95,8 @@ class PsiResolutionStrategy(
9095
override val deferredSymbols: Map<SymbolProcessor, List<Restorable>>
9196
) : AnnotationResolutionStrategy {
9297

98+
private val targetFieldFix = AnnotationDeclarationTargetFieldFix()
99+
93100
/**
94101
* Returns all symbols annotated with [annotationName].
95102
*/
@@ -280,10 +287,11 @@ class PsiResolutionStrategy(
280287
// so we should only cache for known unique declarations in the file.
281288
// See test shadowingAnnotations.kt
282289
val fileCache = fileCaches.getOrPut(file) { FileCache(file) }
283-
val classId = resolveAnnotationEntryClassId(annotationEntry, fileCache)
284290
// N.B.: Skip nullable qualified names without error. This is what the AA-based implementation does.
285291
// For now, this is the intended behavior. If it is changed in the future, then it is an API change.
286-
val fqn = classId?.asFqNameString() ?: return@forEach
292+
val classId = resolveAnnotationEntryClassId(annotationEntry, fileCache) ?: return@forEach
293+
targetFieldFix.recordClassId(annotationEntry, classId)
294+
val fqn = classId.asFqNameString()
287295
getOrPut(fqn, ::mutableListOf)
288296
.add(annotated to annotationEntry)
289297
}
@@ -427,7 +435,14 @@ class PsiResolutionStrategy(
427435
private fun KtDeclaration.resolve(annotationEntry: KtAnnotationEntry): Collection<KSAnnotated> {
428436
// TODO: This should perform case distinction instead of getTargetedSymbol
429437
val ksSym = analyze { symbol.toKSAnnotated() }
430-
return ksSym.findTargetedSymbol(annotationEntry.ksUseSiteTarget)
438+
return when (ksSym) {
439+
is KSPropertyDeclaration -> targetFieldFix.resolveKSPropertyDeclaration(
440+
ksSym,
441+
annotationEntry
442+
)
443+
444+
else -> ksSym.findTargetedSymbol(annotationEntry.ksUseSiteTarget)
445+
}
431446
}
432447

433448
/**
@@ -839,4 +854,152 @@ class PsiResolutionStrategy(
839854
*/
840855
private val KtAnnotationEntry.ksUseSiteTarget: AnnotationUseSiteTarget?
841856
get() = useSiteTarget?.getAnnotationUseSiteTarget()?.toKSAnnotationUseSiteTarget()
857+
858+
859+
/**
860+
* This object represents a temporary fix for https://github.com/google/ksp/issues/2987.
861+
* It decides if an annotation is annotated with the FIELD annotation target,
862+
* and if so, applies to the field instead of the property.
863+
*/
864+
private inner class AnnotationDeclarationTargetFieldFix {
865+
866+
/**
867+
* Stores the [ClassId]s for the [KtAnnotationEntry].
868+
*/
869+
private val annotationClassIds: MutableMap<KtAnnotationEntry, ClassId> = mutableMapOf()
870+
871+
/**
872+
* Stores the key-value pair [annotation] -> [classId]. This mapping is used by [resolveKSPropertyDeclaration].
873+
*/
874+
fun recordClassId(annotation: KtAnnotationEntry, classId: ClassId) {
875+
annotationClassIds.putIfAbsent(annotation, classId)
876+
}
877+
878+
/**
879+
* Resolves [ksPropDecl] with respect to the declaration-site target [annotation].
880+
* The use-site target of [annotation] takes precedence over the declaration-site target.
881+
*/
882+
fun resolveKSPropertyDeclaration(
883+
ksPropDecl: KSPropertyDeclaration,
884+
annotation: KtAnnotationEntry
885+
): Collection<KSAnnotated> =
886+
when {
887+
annotation.ksUseSiteTarget != null -> ksPropDecl.findTargetedSymbol(annotation.ksUseSiteTarget)
888+
canBeAppliedTo(
889+
annotation,
890+
ANNOTATION_TARGET_PROPERTY_ENTRY
891+
) && canBeAppliedTo(
892+
annotation,
893+
ANNOTATION_TARGET_FIELD_ENTRY
894+
) -> {
895+
listOfNotNull(ksPropDecl, ksPropDecl.backingField)
896+
}
897+
898+
canBeAppliedTo(annotation, ANNOTATION_TARGET_FIELD_ENTRY) -> {
899+
listOfNotNull(ksPropDecl, ksPropDecl.backingField)
900+
}
901+
902+
canBeAppliedTo(annotation, ANNOTATION_TARGET_PROPERTY_ENTRY) -> {
903+
listOfNotNull(ksPropDecl)
904+
}
905+
906+
else -> emptyList()
907+
}
908+
909+
/**
910+
* Returns `true` if [annotation] can be applied to [entry], which is a [CallableId] for
911+
* the FIELD or PROPERTY [AnnotationTarget].
912+
* @see [ANNOTATION_TARGET_FIELD_ENTRY]
913+
* @see [ANNOTATION_TARGET_PROPERTY_ENTRY]
914+
*/
915+
private fun canBeAppliedTo(annotation: KtAnnotationEntry, entry: CallableId): Boolean =
916+
applicationCache.getOrPut(annotation to entry) {
917+
val targetAnnotation = findTargetAnnotation(getCachedClassId(annotation)) ?: return@getOrPut true
918+
val args = targetAnnotationArguments(targetAnnotation)
919+
args.contains(entry)
920+
}
921+
922+
/**
923+
* Private cache for [canBeAppliedTo]
924+
*/
925+
private val applicationCache: MutableMap<Pair<KtAnnotationEntry, CallableId>, Boolean> = mutableMapOf()
926+
927+
private fun getCachedClassId(annotation: KtAnnotationEntry): ClassId =
928+
annotationClassIds[annotation] ?: throw InternalKSPException(
929+
"Unexpected null class id for annotation entry: $annotation",
930+
annotation.psiOrParent.toLocation(),
931+
annotation.javaClass
932+
)
933+
934+
private val TARGET_ANNOTATION = ClassId(
935+
FqName("kotlin.annotation"),
936+
Name.identifier("Target")
937+
)
938+
private val ANNOTATION_TARGET_FIELD_ENTRY: CallableId = annotationTargetEntry("FIELD")
939+
private val ANNOTATION_TARGET_PROPERTY_ENTRY: CallableId = annotationTargetEntry("PROPERTY")
940+
941+
private fun annotationTargetEntry(entry: String) = ClassId(
942+
FqName("kotlin.annotation"),
943+
Name.identifier("AnnotationTarget")
944+
).let {
945+
CallableId(it, Name.identifier(entry))
946+
}
947+
948+
private fun isAnnotationClass(kaClassSym: KaClassSymbol?): Boolean =
949+
kaClassSym?.classKind == KaClassKind.ANNOTATION_CLASS
950+
951+
/**
952+
* Returns the `@Target` annotation if it exists.
953+
*/
954+
private fun findTargetAnnotation(kaClassSym: KaClassSymbol?): KaAnnotation? {
955+
if (!isAnnotationClass(kaClassSym)) {
956+
return null
957+
}
958+
return kaClassSym?.annotations?.find { it.classId == TARGET_ANNOTATION }
959+
}
960+
961+
private fun findTargetAnnotation(classId: ClassId): KaAnnotation? =
962+
findTargetAnnotation(classId.toKtClassSymbol())
963+
964+
/**
965+
* Returns the arguments of the target annotation. Throws an error if the annotation
966+
* has more than one non-null argument.
967+
*/
968+
private fun targetAnnotationArguments(targetAnnotation: KaAnnotation): List<CallableId> {
969+
val args = targetAnnotation.arguments
970+
if (args.size != 1) {
971+
throw InternalKSPException(
972+
"Unexpected number of arguments passed to @Target annotation",
973+
targetAnnotation.psi.toLocation(),
974+
targetAnnotation.javaClass
975+
)
976+
}
977+
return args.firstOrNull()?.let { flattenAnnotationArgs(it.expression) } ?: emptyList()
978+
}
979+
980+
/**
981+
* Returns a list of [CallableId] where each member is the concrete type of the argument passed to
982+
* the [Target] annotation.
983+
* The [Target] annotation is defined as:
984+
* ```kt
985+
* annotation class Target(val allowedTarget: AnnotationTarget)
986+
* ```
987+
* However, you can pass an array of [AnnotationTarget] to [Target], so
988+
* both of the following constructors are allowed, hence the flattening:
989+
* ```kt
990+
* @Target(AnnotationTarget.FIELD)
991+
* @Target({AnnotationTarget.FIELD, AnnotationTarget.CLASS})
992+
* ```
993+
*/
994+
private fun flattenAnnotationArgs(annotationValue: KaAnnotationValue): List<CallableId> =
995+
when (annotationValue) {
996+
is KaAnnotationValue.EnumEntryValue -> listOfNotNull(annotationValue.callableId)
997+
is KaAnnotationValue.ArrayValue -> annotationValue.values.flatMap(::flattenAnnotationArgs)
998+
else -> throw InternalKSPException(
999+
"Unexpected argument type to @Target annotation",
1000+
annotationValue.sourcePsi.toLocation(),
1001+
annotationValue.javaClass,
1002+
)
1003+
}
1004+
}
8421005
}

0 commit comments

Comments
 (0)