|
| 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. |
0 commit comments