(Kotlin PSI) Check if class is a subclass of a particular class, that works for JVM and Common platforms?

I'm trying to find out if a KtLightClass is a subclass of org.spekframework.spek2.Spek. The code below works if the KtLightClass is a class from a JVM platform module.

fun isSpekSubclass(element: KtLightClass?): Boolean {
if (element != null) {
val superClass = element.superClass

if (superClass != null && superClass is KtLightClass) {
val fqName = superClass.getKotlinFqName()
return if (fqName != null && SPEK_CLASSES.contains(fqName.toString())) {
true
} else {
isSpekSubclass(superClass)
}
}
}

return false
}

It breaks if KtLightClass is a class from a common platform module. For example, say we have a class MySpek: Spek({ ... }). If the class is declared in a JVM platform module then hierarchy seen by the code is MySpek -> org.spekframework.spek2.Spek but when it's declared in a common platform module then the hierarchy is MySpek -> java.lang.Object. Is this expected?

20 comments
Comment actions Permalink
Official comment

Sorry I didn't get that we are talking about resolved elements, I thought it is about elements in sources we analyze. If you want to work with resolved element you should work via "descriptors". for your case it could look something like:

private fun extractPathFromCallExpression(callExpression: KtCallExpression, buffer: List<String> = emptyList()): Path? {

val resultingDescriptor = callExpression.getResolvedCall(callExpression.analyze())?.resultingDescriptor ?: return null
val map = resultingDescriptor.annotations.map { it.allValueArguments.toString() }

You should use `org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall` instead of `resolve` on main reference to get proper descriptor.

 

Comment actions Permalink

Looks like  `KtLightClass` will properly work only for JVM-platforms. You could try using `KtClassOrObject` with `getSuperTypeList` instead.

0
Comment actions Permalink

How would I resolve the actual KtClassOrObject from getSuperTypeList? For example given the hierarchy class MySpec: SomeCustSpek({ ...}), then class SomeCustomSpek(root: Root.() -> Unit): Spek(root). How would I know that MySpec is a subclass of org.spekframework.spek2.Spek? I need to compare it to the qualified name, not just Spek.

0
Comment actions Permalink

It could be impossible to get `KtClassOrObject` from `getSuperTypeList`  but you could rely on `KtTypeReference` and resolve it to a full name. I am sorry I cant provide an exact code right now,  but you could refer  KotlinRedundantOverrideInspection 

0
Comment actions Permalink

I've actually created an issue in the Kotlin bug tracker for your initial problem. But employing `KtClassOrObject` should work anyway. 

0
Comment actions Permalink

Thanks fixed the problem without relying on KtLightClass. Now same problem with KtLightMethod, is there an alternative method that I can use? I need to check for annotations on that method.

0
Comment actions Permalink

You could use KtFunction. But it is not very clear how you actually get Light-elements? If you are writing an inspection you'll get Kotlin source elements (KtClassOrObject, KtFunction etc) and to get Light-elements you need to make additional conversions. What did lead you to Light-elements?

0
Comment actions Permalink

From a KtNamedFunction I use toLightMethods, which works on a JVM module but not a common one.

0
Comment actions Permalink

 You should better operate `KtNamedFunction`, and get annotations via `getAnnotationEntries`. LightMethods were designed to provide in-IDE interop with Java, so yes, they could be not fully supported in non-JVM projects, and also despite "Light" in name could bring some performance overheads

0
Comment actions Permalink

If I remember correctly getAnnotationEntries only gives you a list of annotations but the actual properties of the annotation are undefined.@Synonym(type = SynonymType.Group) for example, will only say the method has the @Synonym annotation but not the value of type.

0
Comment actions Permalink

Annotation entry for Kotlin - is a call expression, so `getValueArguments` should work to get passed arguments

0
Comment actions Permalink

Unfortunately getValueArguments doesn't work either. 

0
Comment actions Permalink

It is very strange. Could you please provide more details about what do you call and what you expect to get?

0
Comment actions Permalink

Essentially I'm trying to extract annotations on some DSL extensions. If it has those annotations, the plugin will add a gutter icon for it (clicking it will run the test at that specific point).

 

@Synonym(SynonymType.GROUP)
@Descriptions(Description(DescriptionLocation.VALUE_PARAMETER, 0))
fun GroupBody.describe(description: String, skip: Skip = Skip.No, body: Suite.() -> Unit) {
createSuite(description, skip, body)
}

The plugin traverses the PSI tree of the test class and tries to find usage of DSL methods (methods with @Synonym and optionally @Descriptions). The following code extracts the annotations:

private fun extractSynonymContext(function: KtNamedFunction): SynonymContext? {
val lightMethod = function.toLightMethods().firstOrNull()
if (lightMethod != null) {
val synonym = lightMethod.annotations
.filter { SYNONYM_CLASSES.contains(it.qualifiedName) }
.map(::PsiSynonym)
.firstOrNull()

if (synonym != null) {
val descriptions = lightMethod.annotations
.filter { DESCRIPTIONS_CLASSES.contains(it.qualifiedName) }
.map(::PsiDescriptions)
.firstOrNull()

if (descriptions != null) {
return SynonymContext(synonym, descriptions)
}
}
}
return null
}

This code works with a jvm module but not a common one. Interestingly when I try the plugin against the spek repo (where the actual source of the DSL methods is) everything magically works.

0
Comment actions Permalink

As we discussed earlier Light-classes will not work in "Common" modules (it is a bug). But through getAnnotationEntries everything should work

0
Comment actions Permalink

I can only get the annotation name, what I need the value arguments as well.

function.annotationEntries.forEach {
}

^ it doesn't even work with JVM modules.

0
Comment actions Permalink

Probably it is really better to send me a whole project and point me to the problematic line. because I still didn't get what is wrong with `getValueArguments` on annotationEntry

0
Comment actions Permalink

As I've said this is what I currently have: https://github.com/spekframework/spek/blob/2.x/spek-ide-plugin/intellij-common/src/main/kotlin/org/spekframework/intellij/util.kt#L159. function.annotationEntries will give you the annotations, but whenever I try to query for the annotation parameters it doesn't give me anything (using getValueArguments). 

0
Comment actions Permalink

Thanks Nicolay! This works, but for some reason it doesn't resolve annotation parameters that are lists on a jvm module - everything works with a common module. 

0

Please sign in to leave a comment.