Skip to content

Commit 23fb9af

Browse files
committed
Add a case-study of rewriting the 'MainFunctionReturnUnitInspection' inspection in K2
1 parent c1154fa commit 23fb9af

File tree

5 files changed

+218
-66
lines changed

5 files changed

+218
-66
lines changed

Writerside/hi.tree

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,9 @@
8383
</toc-element>
8484
<toc-element topic="File-Compilation.md"/>
8585
</toc-element>
86-
<toc-element topic="Migrating-from-K1.md"/>
86+
<toc-element topic="Migrating-from-K1.md">
87+
<toc-element topic="Declaring-K2-Compatibility.md"/>
88+
<toc-element topic="Testing-in-K2-Locally.md"/>
89+
<toc-element topic="Case-Studies.md"/>
90+
</toc-element>
8791
</instance-profile>

Writerside/topics/Case-Studies.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Case Study: Main function should return 'Unit'
2+
3+
## Introduction
4+
5+
The `MainFunctionReturnUnitInspection` from the Kotlin IntelliJ IDEA plugin checks whether the `main()`-like function
6+
has the `Unit` return type. If the return type is implicit, it's easy to return something different by mistake.
7+
The inspection helps to avoid the issue.
8+
9+
In the following function, the author most likely intended to create a `main()` function. But in fact, it returns a
10+
`PrintStream`.
11+
12+
```Kotlin
13+
fun main(args: Array<String>) = System.err.apply {
14+
args.forEach(::println)
15+
}
16+
```
17+
18+
Both the K1 and K2 inspection implementations use a named function visitor.
19+
The difference lies in the `processFunction()` implementation.
20+
21+
```Kotlin
22+
internal class MainFunctionReturnUnitInspection : LocalInspectionTool() {
23+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
24+
return namedFunctionVisitor { processFunction(it, holder) }
25+
}
26+
}
27+
```
28+
29+
## K1 Implementation
30+
31+
Here is the [old](https://github.com/JetBrains/intellij-community/blob/57c570fa9816127f425671605cc390b094e520a0/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/MainFunctionReturnUnitInspection.kt#L27),
32+
K1-based implementation of `processFunction()`.
33+
34+
```Kotlin
35+
if (function.name != "main") return
36+
val descriptor = function.descriptor as? FunctionDescriptor ?: return
37+
val mainFunctionDetector = MainFunctionDetector(function.languageVersionSettings) { it.resolveToDescriptorIfAny() }
38+
if (!mainFunctionDetector.isMain(descriptor, checkReturnType = false)) return
39+
if (descriptor.returnType?.let { KotlinBuiltIns.isUnit(it) } == true) return
40+
holder.registerProblem(...)
41+
```
42+
43+
Let's look at the implementation step by step.
44+
45+
```Kotlin
46+
if (function.name != "main") return
47+
```
48+
49+
Here we check whether the function is named `main` to avoid running further checking logic that depends on semantic code
50+
analysis. It is crucial, as PSI-based checks are much faster than semantic ones.
51+
52+
```Kotlin
53+
val descriptor = function.descriptor as? FunctionDescriptor ?: return
54+
```
55+
56+
Then, by using `function.descriptor` we get a `DeclarationDescriptor`, a container for semantic
57+
declaration information. In the case of a `KtNamedFunction`, it should be a `FunctionDescriptor`, holding the
58+
function's return type.
59+
60+
The `function.descriptor` [helper](https://github.com/JetBrains/intellij-community/blob/76680787081992373bd0029cc54176963adcd858/plugins/kotlin/core/src/org/jetbrains/kotlin/idea/search/usagesSearch/searchHelpers.kt#L50)
61+
calls `resolveToDescriptorIfAny()`, which itself delegates to `analyze()` to get the
62+
`BindingContext`. The obtained `BindingContext` provides bindings between elements in the code and their semantic
63+
representation (such as, `KtDeclaration` to `DeclarationDescriptor`).
64+
65+
```Kotlin
66+
val mainFunctionDetector = MainFunctionDetector(function.languageVersionSettings) { it.resolveToDescriptorIfAny() }
67+
if (!mainFunctionDetector.isMain(descriptor, checkReturnType = false)) return
68+
```
69+
70+
Additionally, we use the `MainFunctionDetector` [compiler utility](https://github.com/JetBrains/kotlin/blob/master/compiler/frontend/src/org/jetbrains/kotlin/idea/MainFunctionDetector.kt#L36)
71+
to check whether a descriptor indeed looks like a `main()` function. Specifically for the IDE use-cases,
72+
`MainFunctionDetector` provides an overload that allows us to skip the return type check. The `MainFunctionDetector`
73+
expects a descriptor, so we pass the one we've obtained from the `KtNamedFunction`.
74+
75+
```Kotlin
76+
if (descriptor.returnType?.let { KotlinBuiltIns.isUnit(it) } == true) return
77+
```
78+
79+
Finally, we check whether the function's return type is `Unit`. We use the `KotlinBuiltIns` class from the compiler
80+
which has a number of static methods which check whether a type is a specific Kotlin built-in type.
81+
82+
If the return type is not `Unit`, the function is not a proper `main()` one, so we register a problem.
83+
84+
## K2 Implementation
85+
86+
Then, let's compare the old implementation with the [K2-based one](https://github.com/JetBrains/intellij-community/blob/9c7e738f1449985836f74ab2d58ee05ddd1a28e2/plugins/kotlin/code-insight/inspections-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/inspections/declarations/MainFunctionReturnUnitInspection.kt#L20).
87+
88+
```Kotlin
89+
val detectorConfiguration = KotlinMainFunctionDetector.Configuration(checkResultType = false)
90+
91+
if (!PsiOnlyKotlinMainFunctionDetector.isMain(function, detectorConfiguration)) return
92+
if (!function.hasDeclaredReturnType() && function.hasBlockBody()) return
93+
if (!KotlinMainFunctionDetector.getInstance().isMain(function, detectorConfiguration)) return
94+
95+
analyze(function) {
96+
if (!function.symbol.returnType.isUnitType) {
97+
holder.registerProblem(...)
98+
}
99+
}
100+
```
101+
102+
Starting from the beginning, one can notice that the K2 implementation is more efficient.
103+
104+
```Kotlin
105+
if (!PsiOnlyKotlinMainFunctionDetector.isMain(function, detectorConfiguration)) return
106+
```
107+
108+
Here we not only check the function name, but we use the [`PsiOnlyKotlinMainFunctionDetector`](https://github.com/JetBrains/intellij-community/blob/db1bf18449fab6d4a3de8576b01ef1e35a4f0ad1/plugins/kotlin/base/code-insight/src/org/jetbrains/kotlin/idea/base/codeInsight/PsiOnlyKotlinMainFunctionDetector.kt#L13)
109+
to check whether the function looks like a `main()` one using only PSI-based checks. In particular, the detector checks
110+
also the presence of type/value parameters and the function location in the file.
111+
112+
```Kotlin
113+
if (!function.hasDeclaredReturnType() && function.hasBlockBody()) return
114+
```
115+
116+
Next, we check whether the function has a declared return type. If it doesn't, and if the function has a block body, we
117+
assume that the return type is `Unit`, so we skip further checks.
118+
119+
```Kotlin
120+
if (!KotlinMainFunctionDetector.getInstance().isMain(function, detectorConfiguration)) return
121+
```
122+
123+
Then, we use another implementation of the `KotlinMainFunctionDetector` – this one performs semantic checks. For the
124+
K2 mode, it will be [`SymbolBasedKotlinMainFunctionDetector`](https://github.com/JetBrains/intellij-community/blob/f0132d1fa64f21db0bd8dd19207a94a90c6ef301/plugins/kotlin/base/fir/code-insight/src/org/jetbrains/kotlin/idea/base/fir/codeInsight/SymbolBasedKotlinMainFunctionDetector.kt#L24).
125+
126+
We use the same `detectorConfiguration` as before to skip the return type check, as we want to catch `main()`-like
127+
functions with a wrong return type.
128+
129+
```Kotlin
130+
analyze(function) {
131+
if (!function.symbol.returnType.isUnitType) {
132+
holder.registerProblem(...)
133+
}
134+
}
135+
```
136+
137+
Finally, we perform the return type check by ourselves using the Analysis API.
138+
139+
The Analysis API requires all work with resolved declarations to be performed inside the `analyze {}` block.
140+
Inside the lambda, we get access to various helpers providing semantic information about declarations visible
141+
from the use-site module. In our case, it would be the module containing the `function` (as we passed it to `analyze()`).
142+
143+
We need to check whether the function has a `Unit` return type. One can get it from a `KaFunctionSymbol`,
144+
a semantic representation of a function, similar to `FunctionDescriptor` in the old compiler.
145+
146+
The `symbol` property available inside the analysis block maps a `KtDeclaration` to a `KaSymbol`. Unlike K1,
147+
the Analysis API guarantees there will be a symbol for every declaration, so we do not need an extra `?: return`.
148+
Also, `symbol` is overloaded for different kinds of declarations, so we can avoid explicit casting to a `KaFunctionSymbol`.
149+
150+
Just like in the old implementation, we check whether the return type is `Unit`, and if it's not, we register a problem.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Declaring compatibility with K2
2+
3+
The Kotlin IntelliJ plugin assumes that a dependent third-party plugin *does not* support K2 Kotlin out of the box. Such
4+
incompatible plugins will not be loaded if the K2 mode is currently enabled.
5+
6+
Once a plugin has been migrated to the Analysis API, a setting should be added to its `plugin.xml` to declare its
7+
compatibility with the K2 mode. Even if the plugin does not use any of the old K1 analysis functions and no migration to
8+
the Analysis API is needed, compatibility with the K2 Kotlin plugin should be declared explicitly nonetheless.
9+
10+
Starting from IntelliJ 2024.2.1 (Preview), the following setting in the `plugin.xml` can be used to declare
11+
compatibility with the K2 mode:
12+
13+
```xml
14+
<extensions defaultExtensionNs="org.jetbrains.kotlin">
15+
<supportsKotlinPluginMode supportsK2="true" />
16+
</extensions>
17+
```
18+
19+
It is also possible to declare compatibility with *only* the K2 mode:
20+
21+
```xml
22+
<extensions defaultExtensionNs="org.jetbrains.kotlin">
23+
<supportsKotlinPluginMode supportsK1="false" supportsK2="true" />
24+
</extensions>
25+
```
26+
27+
Currently, the default setting for `supportsK1` is `true` and for `supportsK2` is `false`.
28+
29+
To test it locally when using the [IntelliJ Platform Gradle Plugin](https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html), add a [dependency](https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html) on the IntelliJ IDEA Community 2024.2.1 (`intellijIdeaCommunity("2024.2.1")`) or higher.
30+
31+
A number of third-party plugins may already be enabled in the K2 mode without a `supportsK2` declaration. The IntelliJ
32+
Kotlin plugin keeps an [internal list](https://github.com/JetBrains/intellij-community/blob/master/platform/core-impl/resources/pluginsCompatibleWithK2Mode.txt)
33+
of plugins which are known to be compatible with the K2 mode as they do not use Kotlin analysis. The authors of these
34+
plugins should not be surprised if their plugin already works in the K2 mode. However, it's still advised to declare K2
35+
support explicitly.

Writerside/topics/Migrating-from-K1.md

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -426,68 +426,4 @@ fun hasAnnotation(declaration: KtDeclaration): Boolean {
426426
return SPECIAL_ANNOTATION_CLASS_ID in declaration.symbol.annotations
427427
}
428428
}
429-
```
430-
431-
## Declaring compatibility with the K2 Kotlin mode
432-
433-
The Kotlin IntelliJ plugin assumes that a dependent third-party plugin *does not* support K2 Kotlin out of the box. Such
434-
incompatible plugins will not be loaded if the K2 mode is currently enabled.
435-
436-
Once a plugin has been migrated to the Analysis API, a setting should be added to its `plugin.xml` to declare its
437-
compatibility with the K2 mode. Even if the plugin does not use any of the old K1 analysis functions and no migration to
438-
the Analysis API is needed, compatibility with the K2 Kotlin plugin should be declared explicitly nonetheless.
439-
440-
Starting from IntelliJ 2024.2.1 (Preview), the following setting in the `plugin.xml` can be used to declare
441-
compatibility with the K2 mode:
442-
443-
```xml
444-
<extensions defaultExtensionNs="org.jetbrains.kotlin">
445-
<supportsKotlinPluginMode supportsK2="true" />
446-
</extensions>
447-
```
448-
449-
It is also possible to declare compatibility with *only* the K2 mode:
450-
451-
```xml
452-
<extensions defaultExtensionNs="org.jetbrains.kotlin">
453-
<supportsKotlinPluginMode supportsK1="false" supportsK2="true" />
454-
</extensions>
455-
```
456-
457-
Currently, the default setting for `supportsK1` is `true` and for `supportsK2` is `false`.
458-
459-
To test it locally when using the [IntelliJ Platform Gradle Plugin](https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html), add a [dependency](https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html) on the IntelliJ IDEA Community 2024.2.1 (`intellijIdeaCommunity("2024.2.1")`) or higher.
460-
461-
A number of third-party plugins may already be enabled in the K2 mode without a `supportsK2` declaration. The IntelliJ
462-
Kotlin plugin keeps an [internal list](https://github.com/JetBrains/intellij-community/blob/master/platform/core-impl/resources/pluginsCompatibleWithK2Mode.txt)
463-
of plugins which are known to be compatible with the K2 mode as they do not use Kotlin analysis. The authors of these
464-
plugins should not be surprised if their plugin already works in the K2 mode. However, it's still advised to declare K2
465-
support explicitly.
466-
467-
## Testing the plugin in K2 mode locally
468-
469-
To test the plugin in K2 mode locally, the `-Didea.kotlin.plugin.use.k2=true` VM option should be passed to the running
470-
IntelliJ IDEA or test process.
471-
472-
When using [IntelliJ Platform Gradle Plugin](https://github.com/JetBrains/intellij-platform-gradle-plugin), you can
473-
modify the `build.gradle.kts` build script to enable K2 mode in different Gradle tasks:
474-
475-
For running in a debug IntelliJ IDEA instance:
476-
477-
```kotlin
478-
tasks.named<RunIdeTask>("runIde") {
479-
jvmArgumentProviders += CommandLineArgumentProvider {
480-
listOf("-Didea.kotlin.plugin.use.k2=true")
481-
}
482-
}
483-
```
484-
485-
Or for running tests:
486-
487-
```kotlin
488-
tasks.test {
489-
jvmArgumentProviders += CommandLineArgumentProvider {
490-
listOf("-Didea.kotlin.plugin.use.k2=true")
491-
}
492-
}
493-
```
429+
```
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Testing in K2 Locally
2+
3+
To test the plugin in K2 mode locally, pass the `-Didea.kotlin.plugin.use.k2=true` VM option to the IntelliJ IDEA
4+
run test, or to the test task.
5+
6+
When using the [IntelliJ Platform Gradle Plugin](https://github.com/JetBrains/intellij-platform-gradle-plugin), you can specify the option directly in the `build.gradle.kts`
7+
file.
8+
9+
To run in a debug IntelliJ IDEA instance, submit the option to the `RunIdeTask`:
10+
11+
```kotlin
12+
tasks.named<RunIdeTask>("runIde") {
13+
jvmArgumentProviders += CommandLineArgumentProvider {
14+
listOf("-Didea.kotlin.plugin.use.k2=true")
15+
}
16+
}
17+
```
18+
19+
To run tests against the Kotlin IntelliJ IDEA plugin in the K2 mode, add the option to the `test` task:
20+
21+
```kotlin
22+
tasks.test {
23+
jvmArgumentProviders += CommandLineArgumentProvider {
24+
listOf("-Didea.kotlin.plugin.use.k2=true")
25+
}
26+
}
27+
```

0 commit comments

Comments
 (0)