The problem
KTIJ-408: Show "run test" gutter icon before semantic analysisGoal
to show run test gutter icons as fast as it is possible and don't wait till entire analysis is done.Background
For Kotlin language (as for any other programming language) we have our own, Kotlin specific, AST (Abstract Syntax Tree) representation. IntelliJ platfrom uses PSI (Program Structure Interface) , for the sake of simplicity it could be considered as AST on steroids. Actually it works in AST-mode when you open a file in an editor.Kotlin syntax significantly differs from Java syntax, so it should not surprise that there is a Kotlin PSI instead of Java PSI:
fun foo() = "foo"
E.g. Kotlin syntax allows to skip a return type when it could be inferred from an expression. For Kotlin there is a
KtFile
(that is surprise, descendant of PsiFile
) but KtClass
and KtNamedFunction
are not descendants of PsiClass
and PsiMethod
correspondingly.Light Classes
Kotlin, to look like Java in IntelliJ, uses a mimic technique known as Light Classes:Basic idea of LightClasses is to wrap Kotlin PSI into Java PSI. You can turn a
KtClass
into a PsiClass
with PsiMethod
s. The hidden elephant is in that some types have to be inferred from a code.
Like in example above: the return type of
foo()
function is String
. To get it we have to perform an analysis (resolve types
or just simple run resolve
). I.e. to build Light Classes we have to have fully resolved code.
Actually, IntelliJ Kotlin plugin has a Top-Down single threaded resolution. It is quite expensive and usually it is performed during syntax highlighting with extensions used by
GeneralHighlightingPass
(Note: gonna describe highlighting process and highlight passes linearization more detailed). In short: GeneralHighlightingPass runs resolve (that is a single threaded). Anyone, who wants to get resolve, has to wait. When resolve is done, result of resolve is cached and reused by all other requesters like Light Class builder.
To rephrase:
you cannot get LightClasses earlier than resolve i.e. syntax highlighting.
Note: to be honest you get a lazy LightClass but in a general case to iterate over all methods or super types it relies on resolve.
Run test gutter icons
To show run test gutter icons (e.g. for JUnit4) IntelliJ Java plugin usesJUnitUtil#isJUnit4TestClass(PsiClass,boolean)
(there are similar methods for JUnit3, JUnit5, TestNG etc). Instead of reinventing the wheel IntelliJ Kotlin plugin utilize a huge and rich legacy of its Java brother.
Now, when you know some background of IntelliJ Kotlin plugin, you know why do you have quite noticeable delay between opening a file and seeing test run gutter icons: to use
JUnitUtil#isJUnit4TestClass(PsiClass,boolean)
we have to convert Kotlin PSI into Java PSI using LightClasses that is built on top of resolve.Let's have a look on how this method is implemented:
for (final PsiMethod method : psiClass.getAllMethods()) {
ProgressManager.checkCanceled();
if (TestUtils.isExplicitlyJUnit4TestAnnotated(method) ||
JUnitRecognizer.willBeAnnotatedAfterCompilation(method))
return true;
}
I.e. a class is considered as a test class if there is at least one method annotated with
@org.junit.Test
. But, It is possible to figure out from Kotlin PSI if a class has at least one
@org.junit.Test
. Well, there is a small problem: usually people use
@Test
annotation rather fully qualified name as @org.junit.Test
or even Kotlin alias @kotlin.test.Test
that points to the same.It is possible to resolve fqName looking into a import list of a file. Again it is just a PSI operation.
The same approach could be applied for test
@org.junit.Test
, it has public
visibility etc.Sounds great, but there are some pitfalls: e.g. what if class extends some other class that could have test functions ?
Indeed, in this case this approach does not work and we have to fallback to the LightClasses solution.
Shall we fallback in all cases if we can't say is it a test class (or test function) or not ?
No. There are several cases when we know that class could not be a test class (besides the case when we don't have a junit4 dependency for the module):
- Class does not have any super classes (except
Any
i.e.Object
) - Data class
- Private class
- class without any public functions
before change is on the left - after is on the right
Комментариев нет:
Отправить комментарий