PSI Parsing of Kotlin Files
30 Sep 2018I have written a couple of intellJ plugins that involves parsing PSI, but that’s for Java. Recently, I was writing a plugin to help me in my office work. This time it involved PSI parsing of Kotlin files. I expected it to be the same as Java, but oh my, it was different. So thought of writing a one-pager on it.
Adding Kotlin support for the Plugin
The first thing to do would be to add Kotlin
plugin dependency. This will let to access
the Kotlin Psi classes. The intelliJ document
explains how to add plugin dependencies. But, as of this time, whatever mentioned there does not work (hope they update the page soon).
You need to add the below to your build.gradle
to add Kotlin dependency.
intellij {
plugins "Kotlin" // This is possible because Kotlin is bundled with the IDE, if not you need to mention the exact version of the Kotlin plugin
}
Objective
import org.springframework.stereotype.Service
@Service(value = Constants.SAMPLE_SERVICE)
class SampleService {
//...
}
The goal is to fetch the value
.
Now, value
can be either be Constants.SAMPLE_SERVICE
or simply SAMPLE_SERVICE
(by making a static import).
We need to fetch the fully qualified name of SAMPLE_SERVICE
. In this case, it is com.sample.Constants.SAMPLE_SERVICE
.
The Process
Let me breakdown the steps involved to accomplish this.
- Obtain
PsiAnnotation
val psiAnnotation = (psiClass.modifierList?.annotations?.toList() ?: listOf()) .filter { annotation -> annotation.qualifiedName == "org.springframework.stereotype.Service" }
We first get an instance of the
PsiClass
, then access thePsiModifierList
, and in turn get an array ofPsiAnnotaion
. We then filter the@Service
annotation from it. - Obtain
KtValueArgument
val valueArgument = ((psiAnnotation as KtLightElement<*, *>).kotlinOrigin as KtAnnotationEntry) .findAttributeValue("value") as? KtValueArgument
The code here is straightforward. The key here is the type casting to the right
Kt
element. This is how Psi for Java differs from Kotlin. We have a correspondingKt
class for each of the Java Psi. I use thePsiViewer
plugin to get a general sense of the Psi tree and then use that to parse the Psi class. - Obtain
PsiReference
val expression = valueArgument.lastChild val references = when (expression) { is KtDotQualifiedExpression -> expression.lastChild.references is KtNameReferenceExpression -> expression.references else -> null } val reference = references?.find { it is KtSimpleNameReference }
This is handle the 2 types of occurrences of the
value
. Again, I don’t know these Psi terms on the top of my head, and I often use thePsiViewer
or put a breakpoint and the use theEvaluate Expression
feature to see what I can get out ofKtValueArgument
. Believe me, you can learn a lot about an object in the Debug context. - Obtain Fully qualified name
Just callreference?.resolve()
to get the fully qualified name.
Note:
The findAttributeValue
above is an extension function I wrote for simplification. I love Kotlin!
fun KtAnnotationEntry.findAttributeValue(attributeName: String): ValueArgument? {
return valueArguments.find {
it.getArgumentName()?.asName?.identifier == attributeName
}
}