Does UAST support Kotlin?

Answered

I thought UAST supported Kotlin but I'm trying to do something fairly simple which suggests it doesn't. I'm trying to write code that extracts the methods of source code files using UAST. I am able to successfully do so with Java and Groovy but Kotlin doesn't seem to correctly detect the classes/methods even though it successfully turns the KtFile into a KotlinUFile.

Here is the code I'm using which demonstrates the issue:

import com.intellij.ide.highlighter.JavaFileType
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiJavaFile
import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.plugins.groovy.GroovyFileType
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile
import org.jetbrains.uast.UFile
import org.jetbrains.uast.toUElement
import org.junit.Test

class UGetMethodTest : LightPlatformCodeInsightFixture4TestCase() {

@Test
fun `get java method`() {
@Language("Java") val code = """
public class GetterMethod {
private String str;
public String getStr() {
return str;
}
}
""".trimIndent()
val sourceFile = PsiFileFactory.getInstance(project).createFileFromText(
"GetterMethod.java", JavaFileType.INSTANCE, code
) as PsiJavaFile

var foundFunction = false
val uFile = sourceFile.toUElement() as UFile
assertEquals(1, uFile.classes.count())
uFile.classes.forEach {
assertEquals(1, it.methods.count())
foundFunction = true
}
assertTrue(foundFunction)
}

@Test
fun `get groovy method`() {
@Language("Groovy") val code = """
class GetterMethod {
private String str
String getStr() {
return str
}
}
""".trimIndent()
val sourceFile = PsiFileFactory.getInstance(project).createFileFromText(
"GetterMethod.groovy", GroovyFileType.GROOVY_FILE_TYPE, code
) as GroovyFile

var foundFunction = false
val uFile = sourceFile.toUElement() as UFile
assertEquals(1, uFile.classes.count())
uFile.classes.forEach {
assertEquals(1, it.methods.count())
foundFunction = true
}
assertTrue(foundFunction)
}

@Test
fun `get kotlin method`() {
@Language("Kt") val code = """
class GetterMethod(private val str: String) {
fun getStr(): String {
return str
}
}
""".trimIndent()
val sourceFile = PsiFileFactory.getInstance(project).createFileFromText(
"GetterMethod.kt", KotlinFileType.INSTANCE, code
) as KtFile

var foundFunction = false
val uFile = sourceFile.toUElement() as UFile
assertEquals(1, uFile.classes.count())
uFile.classes.forEach {
assertEquals(1, it.methods.count())
foundFunction = true
}
assertTrue(foundFunction)
}
}

 

I've also tried using UastVisitor to no avail.

8 comments
Comment actions Permalink
Official comment

Hello! Kotlin resolving doesn't work properly for files created "in the air" (I mean created by the PsiFileFactory and not connected to the project and module), which is necessary to build a proper UAST tree.

The correct way to create real physical files in tests is to use the `myFixture` field:

val sourceFile = myFixture.configureByText("GetterMethod.kt", code) as KtFile

And If you still want to have a file "in the air" there is a specific method in Kotlin for it (but you still need to have a physical file to make the resolve work):

val contextFile = myFixture.configureByText("emptyButPhisical.kt", "")
val sourceFile = KtPsiFactory(project).createAnalyzableFile("GetterMethod.kt", code, contextFile)
Comment actions Permalink

I see this post is marked as planned. Is there something I can do to keep track of whatever is being planned?

 

Also, I wanted to add that the following:

val sourceFile = PsiFileFactory.getInstance(project).createFileFromText(
"GetterMethod.kt", KotlinFileType.INSTANCE, code
) as KtFile

println((sourceFile as PsiClassOwner).classes)

Produces this error:

java.lang.Throwable: Could not find correct module information.
Reason: Analyzing element of type class org.jetbrains.kotlin.psi.KtFile in non-physical file KtFile: GetterMethod.kt of type class org.jetbrains.kotlin.psi.KtFile
Text:
class GetterMethod(private val str: String) {
fun getStr(): String {
return str
}
}

0 = {StackTraceElement@15762} "com.intellij.testFramework.LoggedErrorProcessor.processError(LoggedErrorProcessor.java:56)"
1 = {StackTraceElement@15763} "com.intellij.testFramework.TestLogger.error(TestLogger.java:26)"
2 = {StackTraceElement@15764} "com.intellij.openapi.diagnostic.Logger.error(Logger.java:143)"
3 = {StackTraceElement@15765} "org.jetbrains.kotlin.idea.caches.project.ModuleInfoCollector$NotNullTakeFirst$2.invoke(getModuleInfo.kt:100)"
4 = {StackTraceElement@15766} "org.jetbrains.kotlin.idea.caches.project.ModuleInfoCollector$NotNullTakeFirst$2.invoke(getModuleInfo.kt:97)"
5 = {StackTraceElement@15767} "org.jetbrains.kotlin.idea.caches.project.GetModuleInfoKt.collectInfos(getModuleInfo.kt:184)"
6 = {StackTraceElement@15768} "org.jetbrains.kotlin.idea.caches.project.GetModuleInfoKt.getModuleInfo(getModuleInfo.kt:41)"
7 = {StackTraceElement@15769} "org.jetbrains.kotlin.idea.caches.resolve.KtFileClassProviderImpl.getFileClasses(KtFileClassProviderImpl.kt:40)"
8 = {StackTraceElement@15770} "org.jetbrains.kotlin.psi.KtFile.getClasses(KtFile.kt:197)"
0
Comment actions Permalink

That solved my issue. Thank you, Nicolay.

I can create a new topic if necessary but decided to follow up on this topic as the issue I'm facing now seems related. Given this code:

@Test
fun `get kotlin method`() {
@Language("Kt") val code = """
class GetterMethod(private val str: String) {
@Test
fun getStr(): String {
return str
}
}
""".trimIndent()
val sourceFile = myFixture.configureByText("GetterMethod.kt", code) as KtFile

var foundAnnotation = false
sourceFile.accept(object : KtTreeVisitorVoid() {
override fun visitAnnotation(annotation: KtAnnotation) {
foundAnnotation = true
}
})
assertTrue(foundAnnotation)
}

 

Are you aware of why annotations are not found? I've tried it in Java, Groovy, and Scala to no issue.

0
Comment actions Permalink

Where is "@Test" declared/coming from in your test case?

0
Comment actions Permalink

I haven't been able to figure out how to properly declare annotations in a test case. I've been getting around that by using fully qualified annotation names like @org.junit.Test. That allows me to correctly resolve the annotations. If you know how to fix that I would be interested in knowing, but my Kotlin test doesn't work with the simple or qualified name whereas all my other tests do. Here are those tests:

import com.intellij.ide.highlighter.JavaFileType
import com.intellij.psi.JavaRecursiveElementVisitor
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiJavaFile
import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.psi.KtAnnotation
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
import org.jetbrains.plugins.groovy.GroovyFileType
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile
import org.jetbrains.plugins.groovy.lang.psi.GroovyRecursiveElementVisitor
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotation
import org.jetbrains.plugins.scala.ScalaFileType
import org.jetbrains.plugins.scala.lang.psi.api.ScalaFile
import org.jetbrains.plugins.scala.lang.psi.api.ScalaRecursiveElementVisitor
import org.jetbrains.plugins.scala.lang.psi.api.base.ScAnnotation
import org.junit.Test

class GetMethodTest : LightPlatformCodeInsightFixture4TestCase() {

@Test
fun `get java method`() {
@Language("Java") val code = """
public class GetterMethod {
private String str;
@Test
public String getStr() {
return str;
}
}
""".trimIndent()
val sourceFile = PsiFileFactory.getInstance(project).createFileFromText(
"GetterMethod.java", JavaFileType.INSTANCE, code
) as PsiJavaFile

var foundAnnotation = false
sourceFile.accept(object : JavaRecursiveElementVisitor() {
override fun visitAnnotation(annotation: PsiAnnotation?) {
foundAnnotation = true
}
})
assertTrue(foundAnnotation)
}

@Test
fun `get groovy method`() {
@Language("Groovy") val code = """
class GetterMethod {
private String str
@Test
String getStr() {
return str
}
}
""".trimIndent()
val sourceFile = PsiFileFactory.getInstance(project).createFileFromText(
"GetterMethod.groovy", GroovyFileType.GROOVY_FILE_TYPE, code
) as GroovyFile

var foundAnnotation = false
sourceFile.accept(object : GroovyRecursiveElementVisitor() {
override fun visitAnnotation(annotation: GrAnnotation) {
foundAnnotation = true
}
})
assertTrue(foundAnnotation)
}

@Test
fun `get scala method`() {
@Language("Scala") val code = """
class GetterMethod {
private var str: String = _
@Test
def getStr(): String = {
str
}
}
""".trimIndent()
val sourceFile = PsiFileFactory.getInstance(project).createFileFromText(
"GetterMethod.scala", ScalaFileType.INSTANCE, code
) as ScalaFile

var foundAnnotation = false
sourceFile.accept(object : ScalaRecursiveElementVisitor() {
override fun visitAnnotation(annotation: ScAnnotation?) {
foundAnnotation = true
}
})
assertTrue(foundAnnotation)
}

@Test
fun `get kotlin method`() {
@Language("Kt") val code = """
class GetterMethod(private val str: String) {
@Test
fun getStr(): String {
return str
}
}
""".trimIndent()
val sourceFile = myFixture.configureByText("GetterMethod.kt", code) as KtFile

var foundAnnotation = false
sourceFile.accept(object : KtTreeVisitorVoid() {
override fun visitAnnotation(annotation: KtAnnotation) {
foundAnnotation = true
}
})
assertTrue(foundAnnotation)
}
}

 

P.s. Any clue why @Language("Scala") doesn't work?

0
Comment actions Permalink

Please try visitAnnotationEntry instead for Kotlin.

What does "@Language(Scala)" does not work" mean? You mean the language ID is not resolved? It will resolve only to languages from plugins enabled in current IDE instance.

0
Comment actions Permalink

Thanks, Yann. That does indeed work. If you don't mind I would like to ask one more question. When I started this post my goal was to get the annotations from JVM source code without writing language-specific code. You guys have successfully helped me get to parsing methods via UAST and parsing Kotlin annotations via PSI. The final step I'm trying to accomplish is to get to parsing method annotations via UAST. Adding the following code to the code I posted above works in all languages except Kotlin:

var foundAnnotation = false
val uFile = sourceFile.toUElement() as UFile
uFile.classes.forEach {
it.methods.forEach {
if (!foundAnnotation) {
foundAnnotation = it.uAnnotations.isNotEmpty()
}
}
}
assertTrue(foundAnnotation)

 

I've searched around for a UAST implementation of finding Kotlin annotations and the closest I could find is this:

val annotation = uFile.findElementByTextFromPsi<UAnnotation>("@Test")

 

It's promising that I can get UAnnotations from Kotlin, but that method requires me to know what I'm looking for upfront. Is there a way to get all the UAnnotations for a Kotlin method without Kotlin specific code or code which is looking for specific text?

 

P.s. By @Language("Scala") not working I just meant that it's not doing the syntax highlighting on the string like all the other strings benefit from.

0
Comment actions Permalink

Brandon, if you want to collect all annotations from the file, you could use following options, please note also that there could be nested classes and classes defined in methods and so on. So one option is to use the UastVisitor:

 val annotations1 = ArrayList<UAnnotation>()

uFile.accept(object: AbstractUastVisitor(){
override fun visitAnnotation(node: UAnnotation): Boolean {
annotations1.add(node)
return super.visitAnnotation(node)
}
})

println("annotations1 = $annotations1")

 

Other is to use  a `PsiVisitor` it is better approach it is less resource consuming and could find annotation is some compliated cases like nested annotations:

val annotations2 = ArrayList<UAnnotation>()

psiFile.accept(object: PsiRecursiveElementVisitor(){
override fun visitElement(element: PsiElement) {
annotations2.addIfNotNull(element.toUElementOfType<UAnnotation>())
super.visitElement(element)
}
})

println("annotations2 = $annotations2")

Also instead of visitors the `SyntaxTraverser` could be used:

val annotations3 = SyntaxTraverser.psiTraverser(psiFile).asSequence()
.mapNotNull { it.toUElementOfType<UAnnotation>() }
.toList()

println("annotations3 = $annotations3")

If you need only method annotation you could check for uastParent of annotation. or seach for UMethods insdeaf of UAnnotations and then check for annotations

1

Please sign in to leave a comment.