diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/visitor/CollectAnnotatedSymbolsPsiVisitor.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/visitor/CollectAnnotatedSymbolsPsiVisitor.kt index ecb966e48b..380f68db27 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/visitor/CollectAnnotatedSymbolsPsiVisitor.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/visitor/CollectAnnotatedSymbolsPsiVisitor.kt @@ -8,7 +8,10 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod import com.intellij.psi.PsiModifierList import com.intellij.psi.PsiRecursiveElementWalkingVisitor +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeElement import com.intellij.psi.PsiTypeParameter +import org.jetbrains.kotlin.psi.KtAnnotatedExpression import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtDeclarationModifierList import org.jetbrains.kotlin.psi.KtExpression @@ -29,38 +32,80 @@ class CollectAnnotatedSymbolsPsiVisitor : PsiRecursiveElementWalkingVisitor() { return } when (element) { - // For all annotated elements in Kotlin sources, we expect the structure: - // KtAnnotationEntry -> KtDeclarationModifierList / KtFileAnnotationList -> Annotated Element is KtAnnotationEntry -> when (val parent = element.parent) { + // Annotations on declarations. Examples: + // @MyAnnotation class MyClass + // @MyAnnotation val myVal = ""; + // @MyAnnotation fun myFun() {} + // fun myMethodWithParameter(@MyAnnotation myParam: Int) {} is KtDeclarationModifierList -> { result.add(parent.parent) return } + + // Annotations at the file level. Example: + // @file:MyAnnotation is KtFileAnnotationList -> { result.add(parent.parent) return } + + // Annotations on expressions. Example: + // val x = @Suppress("UNCHECKED_CAST") list as List + is KtAnnotatedExpression -> + // Do nothing - KSP does not consider expressions / values + return + else -> // Explicit crash error("Unexpected Kotlin Psi element at ${parent.toLocation()}: ${parent.javaClass}") } - // For Java sources, we expect annotated type parameters to have the structure: - // PsiAnnotation -> Annotated Element - // For all other annotated elements, we expect the structure: - // PsiAnnotation -> PsiModifierList -> Annotated Element - is PsiAnnotation -> when (val parent = element.parent) { + is PsiAnnotation -> when (val owner = element.owner) { + // Annotations on declarations. Examples: + // @MyAnnotation class MyClass {} + // @MyAnnotation String myField = ""; + // @MyAnnotation void myMethod() {} + // void myMethodWithParameter(@MyAnnotation int myParam) {} + is PsiModifierList -> { + result.add(owner.parent) + return + } + + // Type parameter annotations. Examples: + // class MyClass<@MyAnnotation A> + // fun <@MyAnnotation A> myFun() {} is PsiTypeParameter -> { - result.add(parent) + result.add(owner) return } - is PsiModifierList -> { - result.add(parent.parent) + + is PsiTypeElement -> { + result.add(owner) return } + + // Type argument annotation / type application. Example: + // interface Foo {} + // abstract class Abs { + // Foo bar(Foo2<@MyAnnotation Boolean> baz) + // Foo<@MyAnnotation A> baz(Foo2 baz) + // } + is PsiType -> + // Do nothing - Analysis API implementation does not collect these + return + + // Annotations used as values. Example with other annotation used as default value + // @interface MyAnnotation { + // MyOtherAnnotation value() default @MyOtherAnnotation("default value here"); + // } + null -> + // Do nothing - Analysis API implementation does not collect these + return + else -> // Explicit crash - error("Unexpected Java Psi element at ${parent.toLocation()}: ${parent.javaClass}") + error("Unexpected Java Psi element at ${(owner as? PsiElement).toLocation()}: ${owner.javaClass}") } } super.visitElement(element) diff --git a/kotlin-analysis-api/testData/annotationValue/annotationValue_java.kt b/kotlin-analysis-api/testData/annotationValue/annotationValue_java.kt index 22b9b2df08..fc8f00b9d3 100644 --- a/kotlin-analysis-api/testData/annotationValue/annotationValue_java.kt +++ b/kotlin-analysis-api/testData/annotationValue/annotationValue_java.kt @@ -39,6 +39,8 @@ // RGB.G // JavaEnum.ONE // 31 +// IsEmailClass +// user@example.com // [warning1, warning 2] // Sub: [i:42] // TestJavaLib: OtherAnnotation @@ -83,6 +85,11 @@ public @interface JavaAnnotationWithDefaults { OtherAnnotation otherAnnotationVal() default @OtherAnnotation("def"); } +// FILE: Email.java +public @interface Email { + String value (); +} + // FILE: KotlinAnnotationWithDefaults.kt annotation class KotlinAnnotationWithDefaults(val otherAnnotation: OtherAnnotation = OtherAnnotation("hij")) @@ -117,6 +124,11 @@ annotation class Bar( class C { } + +// FILE: IsEmailClass.java +@Email(value = "user@example.com") +class IsEmailClass {} + // FILE: JavaAnnotated.java @Default @Bar(argStr = "Str", diff --git a/kotlin-analysis-api/testData/annotationValue/annotationValue_kt.kt b/kotlin-analysis-api/testData/annotationValue/annotationValue_kt.kt index c42bd41676..8208e100f3 100644 --- a/kotlin-analysis-api/testData/annotationValue/annotationValue_kt.kt +++ b/kotlin-analysis-api/testData/annotationValue/annotationValue_kt.kt @@ -34,6 +34,8 @@ // RGB.G // JavaEnum.ONE // 31 +// IsEmailClass +// user@example.com // Throws // Sub: [i:42] // Cls: argToA: b @@ -77,6 +79,9 @@ annotation class ClsA(val b: ClsB) @ClsA(b = ClsB(i = 42)) class Cls +// FILE: Email.kt +annotation class Email(val value: String) + // FILE: OtherAnnotation.java import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -192,3 +197,7 @@ class TestNestedAnnotationDefaults {} @JavaAnnotationWithDefaults @KotlinAnnotationWithDefaults class TestValueArgEquals {} + +// FILE: IsEmailClass.kt +@Email(value = "user@example.com") +class IsEmailClass diff --git a/kotlin-analysis-api/testData/locations.kt b/kotlin-analysis-api/testData/locations.kt index 543868796e..b24c277d5f 100644 --- a/kotlin-analysis-api/testData/locations.kt +++ b/kotlin-analysis-api/testData/locations.kt @@ -114,3 +114,23 @@ class L { } fun @receiver:Location String.extFun() = Unit + +interface Foo + +abstract class Abs { + fun bar1(baz: Foo<@Location A>): Foo + fun bar2(baz: Foo): Foo<@Location A> +} + +// FILE: J2.java + +interface Foo2 {} + +abstract class Abs2 { + Foo2 bar1(Foo2<@Location Boolean> baz) + Foo2<@Location A> bar2(Foo2 baz) +} + +// FILE: ExpressionAnnotation.kt + +val x = @Location listOf(1, 2, 3) diff --git a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationArgumentProcessor.kt b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationArgumentProcessor.kt index 6891684c2d..b3b2fe989d 100644 --- a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationArgumentProcessor.kt +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationArgumentProcessor.kt @@ -53,6 +53,11 @@ class AnnotationArgumentProcessor : AbstractTestProcessor() { it.annotations.forEach { it.arguments.forEach { it.accept(visitor, Unit) } } } + resolver.getSymbolsWithAnnotation("Email").forEach { + results.add(it.toString()) + it.annotations.forEach { it.arguments.forEach { it.accept(visitor, Unit) } } + } + val C = resolver.getClassDeclarationByName("C") C?.annotations?.first()?.arguments?.forEach { results.add(it.value.toString()) } val ThrowsClass = resolver.getClassDeclarationByName("ThrowsClass")