From fa175bdd95b689597f0305aee5c9ce9c82ec76f6 Mon Sep 17 00:00:00 2001 From: Ignat Beresnev Date: Thu, 7 Jul 2022 13:29:53 +0200 Subject: [PATCH] Improve handling of Java annotation parameters (#2562) Fixes #2509 Fixes #2551 Fixes #2350 (cherry picked from commit 3332f9f95be5cdea153818ae5d965298aa8c82f9) --- .../psi/DefaultPsiToDocumentableTranslator.kt | 11 +- .../model/annotations/JavaAnnotationsTest.kt | 195 ++++++++++++++++++ 2 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt index f64eb261f2..9a23166ebb 100644 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -687,8 +687,15 @@ class DefaultPsiToDocumentableTranslator( is PsiArrayInitializerMemberValue -> ArrayValue(initializers.mapNotNull { it.toValue() }) is PsiReferenceExpression -> psiReference?.let { EnumValue(text ?: "", DRI.from(it)) } is PsiClassObjectAccessExpression -> { - val psiClass = ((type as PsiImmediateClassType).parameters.single() as PsiClassReferenceType).resolve() - psiClass?.let { ClassValue(text ?: "", DRI.from(psiClass)) } + val parameterType = (type as? PsiClassType)?.parameters?.firstOrNull() + val classType = when (parameterType) { + is PsiClassType -> parameterType.resolve() + // Notice: Array::class will be passed down as String::class + // should probably be Array::class instead but this reflects behaviour for Kotlin sources + is PsiArrayType -> (parameterType.componentType as? PsiClassType)?.resolve() + else -> null + } + classType?.let { ClassValue(it.name ?: "", DRI.from(it)) } } is PsiLiteralExpression -> toValue() else -> StringValue(text ?: "") diff --git a/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt b/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt new file mode 100644 index 0000000000..e704bf7124 --- /dev/null +++ b/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt @@ -0,0 +1,195 @@ +package model.annotations + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.* +import org.junit.jupiter.api.Test +import translators.findClasslike +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class JavaAnnotationsTest : BaseAbstractTest() { + + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + } + } + } + + @Test // see https://github.com/Kotlin/dokka/issues/2350 + fun `should hande array used as annotation param value`() { + testInline( + """ + |/src/main/java/annotation/TestClass.java + |package annotation; + |public class TestClass { + | @SimpleAnnotation(clazz = String[].class) + | public boolean simpleAnnotation() { + | return false; + | } + |} + | + |/src/main/java/annotation/SimpleAnnotation.java + |package annotation; + |@Retention(RetentionPolicy.RUNTIME) + |@Target(ElementType.METHOD) + |public @interface SimpleAnnotation { + | Class clazz(); + |} + """.trimIndent(), + configuration + ) { + documentablesTransformationStage = { module -> + val testClass = module.findClasslike("annotation", "TestClass") as DClass + assertNotNull(testClass) + + val annotatedFunction = testClass.functions.single { it.name == "simpleAnnotation" } + val annotation = + annotatedFunction.extra[Annotations]?.directAnnotations?.entries?.single()?.value?.single() + assertNotNull(annotation) { "Expected to find an annotation on simpleAnnotation function, found none" } + assertEquals("annotation", annotation.dri.packageName) + assertEquals("SimpleAnnotation", annotation.dri.classNames) + assertEquals(1, annotation.params.size) + + val param = annotation.params.values.single() + assertTrue(param is ClassValue) + // should probably be Array instead + // String matches parsing of Kotlin sources as of now + assertEquals("String", param.className) + assertEquals("java.lang", param.classDRI.packageName) + assertEquals("String", param.classDRI.classNames) + } + } + } + + @Test // see https://github.com/Kotlin/dokka/issues/2551 + fun `should hande annotation used within annotation params with class param value`() { + testInline( + """ + |/src/main/java/annotation/TestClass.java + |package annotation; + |public class TestClass { + | @XmlElementRefs({ + | @XmlElementRef(name = "NotOffered", namespace = "http://www.gaeb.de/GAEB_DA_XML/DA86/3.3", type = JAXBElement.class, required = false) + | }) + | public List> content; + |} + | + |/src/main/java/annotation/XmlElementRefs.java + |package annotation; + |public @interface XmlElementRefs { + | XmlElementRef[] value(); + |} + | + |/src/main/java/annotation/XmlElementRef.java + |package annotation; + |public @interface XmlElementRef { + | String name(); + | + | String namespace(); + | + | boolean required(); + | + | Class type(); + |} + | + |/src/main/java/annotation/JAXBElement.java + |package annotation; + |public class JAXBElement { + |} + """.trimIndent(), + configuration + ) { + documentablesTransformationStage = { module -> + val testClass = module.findClasslike("annotation", "TestClass") as DClass + assertNotNull(testClass) + + val contentField = testClass.properties.find { it.name == "content" } + assertNotNull(contentField) + + val annotation = contentField.extra[Annotations]?.directAnnotations?.entries?.single()?.value?.single() + assertNotNull(annotation) { "Expected to find an annotation on content field, found none" } + assertEquals("XmlElementRefs", annotation.dri.classNames) + assertEquals(1, annotation.params.size) + + val arrayParam = annotation.params.values.single() + assertTrue(arrayParam is ArrayValue, "Expected single annotation param to be array") + assertEquals(1, arrayParam.value.size) + + val arrayParamValue = arrayParam.value.single() + assertTrue(arrayParamValue is AnnotationValue) + + val arrayParamAnnotationValue = arrayParamValue.annotation + assertEquals(4, arrayParamAnnotationValue.params.size) + assertEquals("XmlElementRef", arrayParamAnnotationValue.dri.classNames) + + val annotationParams = arrayParamAnnotationValue.params.values.toList() + + val nameParam = annotationParams[0] + assertTrue(nameParam is StringValue) + assertEquals("NotOffered", nameParam.value) + + val namespaceParam = annotationParams[1] + assertTrue(namespaceParam is StringValue) + assertEquals("http://www.gaeb.de/GAEB_DA_XML/DA86/3.3", namespaceParam.value) + + val typeParam = annotationParams[2] + assertTrue(typeParam is ClassValue) + assertEquals("JAXBElement", typeParam.className) + assertEquals("annotation", typeParam.classDRI.packageName) + assertEquals("JAXBElement", typeParam.classDRI.classNames) + + val requiredParam = annotationParams[3] + assertTrue(requiredParam is BooleanValue) + assertFalse(requiredParam.value) + } + } + } + + @Test // see https://github.com/Kotlin/dokka/issues/2509 + fun `should handle generic class in annotation`() { + testInline( + """ + |/src/main/java/annotation/Breaking.java + |package annotation; + |public class Breaking { + |} + | + |/src/main/java/annotation/TestAnnotate.java + |package annotation; + |public @interface TestAnnotate { + | Class value(); + |} + | + |/src/main/java/annotation/TestClass.java + |package annotation; + |@TestAnnotate(Breaking.class) + |public class TestClass { + |} + """.trimIndent(), + configuration + ) { + documentablesTransformationStage = { module -> + val testClass = module.findClasslike("annotation", "TestClass") as DClass + assertNotNull(testClass) + + val annotation = testClass.extra[Annotations]?.directAnnotations?.entries?.single()?.value?.single() + assertNotNull(annotation) { "Expected to find an annotation on TestClass, found none" } + + assertEquals("TestAnnotate", annotation.dri.classNames) + assertEquals(1, annotation.params.size) + + val valueParameter = annotation.params.values.single() + assertTrue(valueParameter is ClassValue) + + assertEquals("Breaking", valueParameter.className) + + assertEquals("annotation", valueParameter.classDRI.packageName) + assertEquals("Breaking", valueParameter.classDRI.classNames) + } + } + } +}