From 48ea52def141c0412c5c26da2e8f78a0ab90e062 Mon Sep 17 00:00:00 2001 From: Nicolay Mitropolsky Date: Thu, 1 Feb 2018 13:13:05 +0300 Subject: [PATCH] Uast: `KotlinUNestedAnnotation` for processing nested annotations (IDEA-185890) --- .../uast/kotlin/KotlinUastLanguagePlugin.kt | 13 ++++ .../kotlin/declarations/KotlinUAnnotation.kt | 77 ++++++++++++++----- .../uast-kotlin/testData/AnnotationComplex.kt | 17 ++++ .../testData/AnnotationComplex.log.txt | 30 ++++++++ .../testData/AnnotationComplex.render.txt | 26 +++++++ .../uast-kotlin/tests/KotlinUastApiTest.kt | 17 ++++ .../tests/SimpleKotlinRenderLogTest.kt | 3 + .../uast/test/env/AbstractUastTest.kt | 3 +- 8 files changed, 165 insertions(+), 21 deletions(-) create mode 100644 plugins/uast-kotlin/testData/AnnotationComplex.kt create mode 100644 plugins/uast-kotlin/testData/AnnotationComplex.log.txt create mode 100644 plugins/uast-kotlin/testData/AnnotationComplex.render.txt diff --git a/plugins/uast-kotlin/src/org/jetbrains/uast/kotlin/KotlinUastLanguagePlugin.kt b/plugins/uast-kotlin/src/org/jetbrains/uast/kotlin/KotlinUastLanguagePlugin.kt index 20755a3bcfe..3bb6adeb182 100644 --- a/plugins/uast-kotlin/src/org/jetbrains/uast/kotlin/KotlinUastLanguagePlugin.kt +++ b/plugins/uast-kotlin/src/org/jetbrains/uast/kotlin/KotlinUastLanguagePlugin.kt @@ -27,6 +27,8 @@ import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade import org.jetbrains.kotlin.asJava.elements.* import org.jetbrains.kotlin.asJava.toLightClass import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper +import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor +import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.ConstructorDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.idea.KotlinLanguage @@ -206,6 +208,17 @@ class KotlinUastLanguagePlugin : UastLanguagePlugin { is KtFile -> el { KotlinUFile(original, this@KotlinUastLanguagePlugin) } is FakeFileForLightClass -> el { KotlinUFile(original.navigationElement, this@KotlinUastLanguagePlugin) } is KtAnnotationEntry -> el(build(::KotlinUAnnotation)) + is KtCallExpression -> + if (requiredType != null && UAnnotation::class.java.isAssignableFrom(requiredType)) { + el { + val classDescriptor = + (original.getResolvedCall(original.analyze())?.resultingDescriptor as? ClassConstructorDescriptor)?.constructedClass + if (classDescriptor?.kind == ClassKind.ANNOTATION_CLASS) + KotlinUNestedAnnotation(original, givenParent, classDescriptor) + else + null + } + } else null is KtLightAnnotationForSourceEntry -> convertElement(original.kotlinOrigin, givenParent, requiredType) else -> null } diff --git a/plugins/uast-kotlin/src/org/jetbrains/uast/kotlin/declarations/KotlinUAnnotation.kt b/plugins/uast-kotlin/src/org/jetbrains/uast/kotlin/declarations/KotlinUAnnotation.kt index f0babf2c91f..05d8c2af3d8 100644 --- a/plugins/uast-kotlin/src/org/jetbrains/uast/kotlin/declarations/KotlinUAnnotation.kt +++ b/plugins/uast-kotlin/src/org/jetbrains/uast/kotlin/declarations/KotlinUAnnotation.kt @@ -1,37 +1,44 @@ package org.jetbrains.uast.kotlin +import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiClass import org.jetbrains.kotlin.asJava.toLightAnnotation +import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget -import org.jetbrains.kotlin.psi.KtAnnotationEntry -import org.jetbrains.kotlin.psi.KtParameter -import org.jetbrains.kotlin.psi.ValueArgument +import org.jetbrains.kotlin.name.FqNameUnsafe +import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall import org.jetbrains.kotlin.resolve.calls.model.ArgumentMatch import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.descriptorUtil.annotationClass +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe import org.jetbrains.kotlin.resolve.source.getPsi +import org.jetbrains.kotlin.types.ErrorUtils import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import org.jetbrains.uast.* import org.jetbrains.uast.kotlin.declarations.KotlinUMethod -class KotlinUAnnotation( - override val psi: KtAnnotationEntry, - givenParent: UElement? +abstract class KotlinUAnnotationBase( + final override val psi: KtElement, + givenParent: UElement? ) : KotlinAbstractUElement(givenParent), UAnnotation { - override val javaPsi = psi.toLightAnnotation() + abstract override val javaPsi: PsiAnnotation? - override val sourcePsi = psi + final override val sourcePsi = psi - private val resolvedAnnotation: AnnotationDescriptor? by lz { psi.analyze()[BindingContext.ANNOTATION, psi] } + protected abstract fun annotationUseSiteTarget(): AnnotationUseSiteTarget? private val resolvedCall: ResolvedCall<*>? by lz { psi.getResolvedCall(psi.analyze()) } override val qualifiedName: String? - get() = resolvedAnnotation?.fqName?.asString() + get() = annotationClassDescriptor.takeUnless(ErrorUtils::isError) + ?.fqNameUnsafe + ?.takeIf(FqNameUnsafe::isSafe) + ?.toSafe() + ?.toString() override val attributeValues: List by lz { resolvedCall?.valueArguments?.entries?.mapNotNull { @@ -47,13 +54,15 @@ class KotlinUAnnotation( } ?: emptyList() } + protected abstract val annotationClassDescriptor: ClassDescriptor? + override fun resolve(): PsiClass? { - val descriptor = resolvedAnnotation?.annotationClass ?: return null + val descriptor = annotationClassDescriptor ?: return null return descriptor.toSource()?.getMaybeLightElement(this) as? PsiClass } override fun findAttributeValue(name: String?): UExpression? = - findDeclaredAttributeValue(name) ?: findAttributeDefaultValue(name ?: "value") + findDeclaredAttributeValue(name) ?: findAttributeDefaultValue(name ?: "value") fun findAttributeValueExpression(arg: ValueArgument): UExpression? { val mapping = resolvedCall?.getArgumentMapping(arg) @@ -66,17 +75,16 @@ class KotlinUAnnotation( override fun findDeclaredAttributeValue(name: String?): UExpression? { return attributeValues.find { it.name == name || - (name == null && it.name == "value") || - (name == "value" && it.name == null) + (name == null && it.name == "value") || + (name == "value" && it.name == null) }?.expression } private fun findAttributeDefaultValue(name: String): UExpression? { - val parameter = resolvedAnnotation - ?.annotationClass - ?.unsubstitutedPrimaryConstructor - ?.valueParameters - ?.find { it.name.asString() == name } ?: return null + val parameter = annotationClassDescriptor + ?.unsubstitutedPrimaryConstructor + ?.valueParameters + ?.find { it.name.asString() == name } ?: return null val defaultValue = (parameter.source.getPsi() as? KtParameter)?.defaultValue ?: return null return getLanguagePlugin().convertWithParent(defaultValue) @@ -84,7 +92,7 @@ class KotlinUAnnotation( override fun convertParent(): UElement? { val superParent = super.convertParent() ?: return null - if (psi.useSiteTarget?.getAnnotationUseSiteTarget() == AnnotationUseSiteTarget.RECEIVER) { + if (annotationUseSiteTarget() == AnnotationUseSiteTarget.RECEIVER) { (superParent.uastParent as? KotlinUMethod)?.uastParameters?.firstIsInstance()?.let { return it } @@ -93,3 +101,32 @@ class KotlinUAnnotation( } } +class KotlinUAnnotation( + val annotationEntry: KtAnnotationEntry, + givenParent: UElement? +) : KotlinUAnnotationBase(annotationEntry, givenParent), UAnnotation { + + override val javaPsi = annotationEntry.toLightAnnotation() + + private val resolvedAnnotation: AnnotationDescriptor? by lz { annotationEntry.analyze()[BindingContext.ANNOTATION, annotationEntry] } + + override val annotationClassDescriptor: ClassDescriptor? + get() = resolvedAnnotation?.annotationClass + + override fun annotationUseSiteTarget() = annotationEntry.useSiteTarget?.getAnnotationUseSiteTarget() + +} + +class KotlinUNestedAnnotation( + original: KtCallExpression, + givenParent: UElement?, + private val classDescriptor: ClassDescriptor? +) : KotlinUAnnotationBase(original, givenParent) { + override val javaPsi: PsiAnnotation? = null + override val annotationClassDescriptor: ClassDescriptor? + get() = classDescriptor + + override fun annotationUseSiteTarget(): AnnotationUseSiteTarget? = null +} + + diff --git a/plugins/uast-kotlin/testData/AnnotationComplex.kt b/plugins/uast-kotlin/testData/AnnotationComplex.kt new file mode 100644 index 00000000000..6a3e4353072 --- /dev/null +++ b/plugins/uast-kotlin/testData/AnnotationComplex.kt @@ -0,0 +1,17 @@ +annotation class Annotation + +@Annotation +class A + +annotation class AnnotationInner(val value: Annotation) + +@AnnotationArray(Annotation()) +class B1 + +@AnnotationArray(value = Annotation()) +class B2 + +annotation class AnnotationArray(vararg val value: Annotation) + +@AnnotationArray(Annotation()) +class C \ No newline at end of file diff --git a/plugins/uast-kotlin/testData/AnnotationComplex.log.txt b/plugins/uast-kotlin/testData/AnnotationComplex.log.txt new file mode 100644 index 00000000000..671589376f6 --- /dev/null +++ b/plugins/uast-kotlin/testData/AnnotationComplex.log.txt @@ -0,0 +1,30 @@ +UFile (package = ) + UClass (name = Annotation) + UClass (name = A) + UAnnotation (fqName = Annotation) + UAnnotationMethod (name = A) + UClass (name = AnnotationInner) + UAnnotationMethod (name = value) + UClass (name = B1) + UAnnotation (fqName = AnnotationArray) + UNamedExpression (name = value) + UCallExpression (kind = UastCallKind(name='constructor_call'), argCount = 0)) + UIdentifier (Identifier (Annotation)) + USimpleNameReferenceExpression (identifier = ) + UAnnotationMethod (name = B1) + UClass (name = B2) + UAnnotation (fqName = AnnotationArray) + UNamedExpression (name = value) + UCallExpression (kind = UastCallKind(name='constructor_call'), argCount = 0)) + UIdentifier (Identifier (Annotation)) + USimpleNameReferenceExpression (identifier = ) + UAnnotationMethod (name = B2) + UClass (name = AnnotationArray) + UAnnotationMethod (name = value) + UClass (name = C) + UAnnotation (fqName = AnnotationArray) + UNamedExpression (name = value) + UCallExpression (kind = UastCallKind(name='constructor_call'), argCount = 0)) + UIdentifier (Identifier (Annotation)) + USimpleNameReferenceExpression (identifier = ) + UAnnotationMethod (name = C) diff --git a/plugins/uast-kotlin/testData/AnnotationComplex.render.txt b/plugins/uast-kotlin/testData/AnnotationComplex.render.txt new file mode 100644 index 00000000000..8704be645c5 --- /dev/null +++ b/plugins/uast-kotlin/testData/AnnotationComplex.render.txt @@ -0,0 +1,26 @@ +public abstract annotation Annotation { +} + +public final class A { + public fun A() = UastEmptyExpression +} + +public abstract annotation AnnotationInner { + public abstract fun value() : Annotation = UastEmptyExpression +} + +public final class B1 { + public fun B1() = UastEmptyExpression +} + +public final class B2 { + public fun B2() = UastEmptyExpression +} + +public abstract annotation AnnotationArray { + public abstract fun value() : Annotation[] = UastEmptyExpression +} + +public final class C { + public fun C() = UastEmptyExpression +} diff --git a/plugins/uast-kotlin/tests/KotlinUastApiTest.kt b/plugins/uast-kotlin/tests/KotlinUastApiTest.kt index 89f039582f2..5e8fbf581c2 100644 --- a/plugins/uast-kotlin/tests/KotlinUastApiTest.kt +++ b/plugins/uast-kotlin/tests/KotlinUastApiTest.kt @@ -275,6 +275,23 @@ class KotlinUastApiTest : AbstractKotlinUastTest() { } } + + @Test + fun testNestedAnnotation() = doTest("AnnotationComplex") { _, file -> + file.findElementByTextFromPsi("@AnnotationArray(value = Annotation())") + .findElementByTextFromPsi("Annotation()") + .sourcePsiElement + .let { referenceExpression -> + val convertedUAnnotation = referenceExpression + .cast() + .toUElementOfType() + ?: throw AssertionError("haven't got annotation from $referenceExpression(${referenceExpression?.javaClass})") + + assertEquals("Annotation", convertedUAnnotation.qualifiedName) + } + } + + } fun Iterable.assertedFind(value: R, transform: (T) -> R): T = find { transform(it) == value } ?: throw AssertionError("'$value' not found, only ${this.joinToString { transform(it).toString() }}") diff --git a/plugins/uast-kotlin/tests/SimpleKotlinRenderLogTest.kt b/plugins/uast-kotlin/tests/SimpleKotlinRenderLogTest.kt index 18d4d6345b1..217297a5a3a 100644 --- a/plugins/uast-kotlin/tests/SimpleKotlinRenderLogTest.kt +++ b/plugins/uast-kotlin/tests/SimpleKotlinRenderLogTest.kt @@ -72,4 +72,7 @@ class SimpleKotlinRenderLogTest : AbstractKotlinRenderLogTest() { @Test fun testAnonymous() = doTest("Anonymous") + + @Test + fun testAnnotationComplex() = doTest("AnnotationComplex") } diff --git a/plugins/uast-kotlin/tests/org/jetbrains/uast/test/env/AbstractUastTest.kt b/plugins/uast-kotlin/tests/org/jetbrains/uast/test/env/AbstractUastTest.kt index 45d13c4df79..1d9a80c77fb 100644 --- a/plugins/uast-kotlin/tests/org/jetbrains/uast/test/env/AbstractUastTest.kt +++ b/plugins/uast-kotlin/tests/org/jetbrains/uast/test/env/AbstractUastTest.kt @@ -73,7 +73,8 @@ inline fun PsiElement.findUElementByTextFromPsi(refText: ?: throw AssertionError("requested text '$refText' was not found in $this") val uElementContainingText = elementAtStart.parentsWithSelf.let { if (strict) it.dropWhile { !it.text.contains(refText) } else it - }.mapNotNull { it.toUElementOfType() }.first() + }.mapNotNull { it.toUElementOfType() }.firstOrNull() + ?: throw AssertionError("requested text '$refText' not found as '${T::class.java.canonicalName}' in $this") if (strict && uElementContainingText.psi != null && uElementContainingText.psi?.text != refText) { throw AssertionError("requested text '$refText' found as '${uElementContainingText.psi?.text}' in $uElementContainingText") }