Skip to content

Commit da5f363

Browse files
authored
Strict mode (#243)
1 parent 3bab585 commit da5f363

File tree

28 files changed

+1853
-23
lines changed

28 files changed

+1853
-23
lines changed

compiler-plugin/compiler-plugin-cli/src/main/latest/kotlinx/rpc/codegen/RpcCompilerPlugin.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package kotlinx.rpc.codegen
66

77
import kotlinx.rpc.codegen.extension.RpcIrExtension
88
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
9-
import org.jetbrains.kotlin.compiler.plugin.CliOption
9+
import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption
1010
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
1111
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
1212
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
@@ -15,9 +15,25 @@ import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter
1515

1616
@OptIn(ExperimentalCompilerApi::class)
1717
class RpcCommandLineProcessor : CommandLineProcessor {
18-
override val pluginId = "kotlinx.rpc.compiler-plugin"
19-
20-
override val pluginOptions = emptyList<CliOption>()
18+
override val pluginId = "kotlinx-rpc"
19+
20+
override val pluginOptions = listOf(
21+
StrictModeCliOptions.STATE_FLOW,
22+
StrictModeCliOptions.SHARED_FLOW,
23+
StrictModeCliOptions.NESTED_FLOW,
24+
StrictModeCliOptions.STREAM_SCOPED_FUNCTIONS,
25+
StrictModeCliOptions.SUSPENDING_SERVER_STREAMING,
26+
StrictModeCliOptions.NOT_TOP_LEVEL_SERVER_FLOW,
27+
StrictModeCliOptions.FIELDS,
28+
)
29+
30+
override fun processOption(
31+
option: AbstractCliOption,
32+
value: String,
33+
configuration: CompilerConfiguration,
34+
) {
35+
option.processAsStrictModeOption(value, configuration)
36+
}
2137
}
2238

2339
@OptIn(ExperimentalCompilerApi::class)

compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.rpc.codegen.common
66

7+
import org.jetbrains.kotlin.name.CallableId
78
import org.jetbrains.kotlin.name.ClassId
89
import org.jetbrains.kotlin.name.FqName
910
import org.jetbrains.kotlin.name.Name
@@ -21,6 +22,16 @@ object RpcClassId {
2122
val stateFlow = ClassId(FqName("kotlinx.coroutines.flow"), Name.identifier("StateFlow"))
2223
}
2324

25+
object RpcCallableId {
26+
val streamScoped = CallableId(FqName("kotlinx.rpc.krpc"), Name.identifier("streamScoped"))
27+
val withStreamScope = CallableId(FqName("kotlinx.rpc.krpc"), Name.identifier("withStreamScope"))
28+
val StreamScope = CallableId(FqName("kotlinx.rpc.krpc"), Name.identifier("StreamScope"))
29+
val invokeOnStreamScopeCompletion = CallableId(
30+
FqName("kotlinx.rpc.krpc"),
31+
Name.identifier("invokeOnStreamScopeCompletion"),
32+
)
33+
}
34+
2435
object RpcNames {
2536
val SERVICE_STUB_NAME: Name = Name.identifier("\$rpcServiceStub")
2637

compiler-plugin/compiler-plugin-k2/build.gradle.kts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
56
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
67
import util.enableContextReceivers
78
import util.otherwise
@@ -10,6 +11,49 @@ import util.whenForIde
1011
plugins {
1112
alias(libs.plugins.conventions.jvm)
1213
alias(libs.plugins.compiler.specific.module)
14+
alias(libs.plugins.shadow.jar)
15+
}
16+
17+
/*
18+
This is a hack to solve the next problem:
19+
20+
There is PsiElement class that is used in compiler and plugin.
21+
22+
If we use kotlin-compiler-embeddable.jar to compile the module,
23+
PsiElement is org.jetbrains.kotlin.com.jetbrains.intellij.PsiElement.
24+
And it works ok in the user projects!
25+
26+
But.
27+
28+
If we run tests, which use kotlin-compiler.jar, we run into ClassNotFoundException.
29+
Because the class it has in th classpath is com.jetbrains.intellij.PsiElement
30+
31+
- Alright, we can use kotlin-compiler.jar to compile the plugin.
32+
- No, because the same error now will occur in the user projects,
33+
but it is com.jetbrains.intellij.PsiElement that is not found.
34+
35+
- Ok, we can use kotlin-compiler-embeddable.jar to compile tests.
36+
- Then we ran into java.lang.VerifyError: Bad type on operand stack, which I have no idea how to fix.
37+
38+
This solution replaces org.jetbrains.kotlin.com.jetbrains.intellij.PsiElement usages in plugin
39+
with com.jetbrains.intellij.PsiElement only for the tests, fixing both use cases.
40+
It is basically a reverse engineering of what Kotlin does for the embedded jar.
41+
*/
42+
val shadowJar = tasks.named<ShadowJar>("shadowJar") {
43+
configurations = listOf(project.configurations.compileClasspath.get())
44+
relocate("org.jetbrains.kotlin.com.intellij.psi", "com.intellij.psi")
45+
46+
exclude("javaslang/**")
47+
exclude("kotlin/**")
48+
exclude("messages/**")
49+
exclude("misc/**")
50+
exclude("org/**")
51+
52+
archiveFileName.set("plugin-k2-for-tests.jar")
53+
}
54+
55+
tasks.jar {
56+
finalizedBy(shadowJar)
1357
}
1458

1559
kotlin {

compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcCheckers.kt renamed to compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package kotlinx.rpc.codegen
77
import kotlinx.rpc.codegen.checkers.FirCheckedAnnotationHelper
88
import kotlinx.rpc.codegen.checkers.FirRpcDeclarationCheckers
99
import kotlinx.rpc.codegen.checkers.FirRpcExpressionCheckers
10+
import kotlinx.rpc.codegen.checkers.diagnostics.FirRpcStrictModeDiagnostics
1011
import org.jetbrains.kotlin.fir.FirSession
1112
import org.jetbrains.kotlin.fir.analysis.checkers.declaration.DeclarationCheckers
1213
import org.jetbrains.kotlin.fir.analysis.checkers.expression.ExpressionCheckers
@@ -16,19 +17,29 @@ import org.jetbrains.kotlin.fir.caches.firCachesFactory
1617
import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar
1718
import org.jetbrains.kotlin.fir.symbols.impl.FirTypeParameterSymbol
1819

19-
class FirRpcCheckers(session: FirSession, serializationIsPresent: Boolean) : FirAdditionalCheckersExtension(session) {
20+
class FirRpcAdditionalCheckers(
21+
session: FirSession,
22+
serializationIsPresent: Boolean,
23+
modes: StrictModeAggregator,
24+
) : FirAdditionalCheckersExtension(session) {
2025
override fun FirDeclarationPredicateRegistrar.registerPredicates() {
2126
register(FirRpcPredicates.rpc)
2227
register(FirRpcPredicates.checkedAnnotationMeta)
2328
}
2429

25-
private val ctx = FirCheckersContext(session, serializationIsPresent)
30+
private val ctx = FirCheckersContext(session, serializationIsPresent, modes)
2631

2732
override val declarationCheckers: DeclarationCheckers = FirRpcDeclarationCheckers(ctx)
2833
override val expressionCheckers: ExpressionCheckers = FirRpcExpressionCheckers(ctx)
2934
}
3035

31-
class FirCheckersContext(private val session: FirSession, val serializationIsPresent: Boolean) {
36+
class FirCheckersContext(
37+
private val session: FirSession,
38+
val serializationIsPresent: Boolean,
39+
modes: StrictModeAggregator,
40+
) {
41+
val strictModeDiagnostics = FirRpcStrictModeDiagnostics(modes)
42+
3243
val typeParametersCache = session.firCachesFactory.createCache { typeParameter: FirTypeParameterSymbol ->
3344
FirCheckedAnnotationHelper.checkedAnnotations(session, typeParameter)
3445
}

compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcExtensionRegistrar.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.jetbrains.kotlin.fir.extensions.FirSupertypeGenerationExtension.Facto
1616
class FirRpcExtensionRegistrar(private val configuration: CompilerConfiguration) : FirExtensionRegistrar() {
1717
override fun ExtensionRegistrarContext.configurePlugin() {
1818
val logger = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)
19+
val modes = configuration.strictModeAggregator()
1920

2021
val serializationIsPresent = try {
2122
Class.forName("org.jetbrains.kotlinx.serialization.compiler.fir.SerializationFirResolveExtension")
@@ -29,7 +30,7 @@ class FirRpcExtensionRegistrar(private val configuration: CompilerConfiguration)
2930
false
3031
}
3132

32-
+CFactory { FirRpcCheckers(it, serializationIsPresent) }
33+
+CFactory { FirRpcAdditionalCheckers(it, serializationIsPresent, modes) }
3334

3435
+SFactory { FirRpcSupertypeGenerator(it, logger) }
3536
}

compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcServiceGenerator.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,10 @@ class FirRpcServiceGenerator(
187187
owner: FirClassSymbol<*>,
188188
name: Name,
189189
rpcServiceStubKey: RpcGeneratedStubKey,
190-
): FirClassLikeSymbol<*> {
190+
): FirClassLikeSymbol<*>? {
191191
val methodName = name.rpcMethodName
192-
val rpcMethod = rpcServiceStubKey.functions.single { it.name == methodName }
192+
val rpcMethod = rpcServiceStubKey.functions.singleOrNull { it.name == methodName }
193+
?: return null
193194
val rpcMethodClassKey = RpcGeneratedRpcMethodClassKey(rpcMethod)
194195
val classKind = if (rpcMethodClassKey.isObject) ClassKind.OBJECT else ClassKind.CLASS
195196

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.codegen
6+
7+
import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption
8+
import org.jetbrains.kotlin.compiler.plugin.CliOption
9+
import org.jetbrains.kotlin.config.CompilerConfiguration
10+
import org.jetbrains.kotlin.config.CompilerConfigurationKey
11+
import kotlin.text.lowercase
12+
13+
enum class StrictMode {
14+
NONE, WARNING, ERROR;
15+
16+
companion object {
17+
fun fromCli(value: String): StrictMode? {
18+
return when (value.lowercase()) {
19+
"none" -> NONE
20+
"warning" -> WARNING
21+
"error" -> ERROR
22+
else -> null
23+
}
24+
}
25+
}
26+
}
27+
28+
data class StrictModeAggregator(
29+
val stateFlow: StrictMode,
30+
val sharedFlow: StrictMode,
31+
val nestedFlow: StrictMode,
32+
val streamScopedFunctions: StrictMode,
33+
val suspendingServerStreaming: StrictMode,
34+
val notTopLevelServerFlow: StrictMode,
35+
val fields: StrictMode,
36+
)
37+
38+
object StrictModeConfigurationKeys {
39+
val STATE_FLOW = CompilerConfigurationKey.create<StrictMode>("state flow rpc mode")
40+
val SHARED_FLOW = CompilerConfigurationKey.create<StrictMode>("shared flow rpc mode")
41+
val NESTED_FLOW = CompilerConfigurationKey.create<StrictMode>("nested flow rpc mode")
42+
val STREAM_SCOPED_FUNCTIONS = CompilerConfigurationKey.create<StrictMode>("stream scoped rpc mode")
43+
val SUSPENDING_SERVER_STREAMING = CompilerConfigurationKey.create<StrictMode>(
44+
"suspending server streaming rpc mode"
45+
)
46+
val NOT_TOP_LEVEL_SERVER_FLOW = CompilerConfigurationKey.create<StrictMode>("not top level server flow rpc mode")
47+
val FIELDS = CompilerConfigurationKey.create<StrictMode>("fields rpc mode")
48+
}
49+
50+
fun CompilerConfiguration.strictModeAggregator(): StrictModeAggregator {
51+
return StrictModeAggregator(
52+
stateFlow = get(StrictModeConfigurationKeys.STATE_FLOW, StrictMode.WARNING),
53+
sharedFlow = get(StrictModeConfigurationKeys.SHARED_FLOW, StrictMode.WARNING),
54+
nestedFlow = get(StrictModeConfigurationKeys.NESTED_FLOW, StrictMode.WARNING),
55+
streamScopedFunctions = get(StrictModeConfigurationKeys.STREAM_SCOPED_FUNCTIONS, StrictMode.NONE),
56+
suspendingServerStreaming = get(StrictModeConfigurationKeys.SUSPENDING_SERVER_STREAMING, StrictMode.NONE),
57+
notTopLevelServerFlow = get(StrictModeConfigurationKeys.NOT_TOP_LEVEL_SERVER_FLOW, StrictMode.WARNING),
58+
fields = get(StrictModeConfigurationKeys.FIELDS, StrictMode.WARNING),
59+
)
60+
}
61+
62+
object StrictModeCliOptions {
63+
val STATE_FLOW = CliOption(
64+
optionName = "strict-stateFlow",
65+
valueDescription = VALUE_DESCRIPTION,
66+
description = description("StateFlow"),
67+
required = false,
68+
allowMultipleOccurrences = false,
69+
)
70+
71+
val SHARED_FLOW = CliOption(
72+
optionName = "strict-sharedFlow",
73+
valueDescription = VALUE_DESCRIPTION,
74+
description = description("SharedFlow"),
75+
required = false,
76+
allowMultipleOccurrences = false,
77+
)
78+
79+
val NESTED_FLOW = CliOption(
80+
optionName = "strict-nested-flow",
81+
valueDescription = VALUE_DESCRIPTION,
82+
description = description("Nested flows"),
83+
required = false,
84+
allowMultipleOccurrences = false,
85+
)
86+
87+
val STREAM_SCOPED_FUNCTIONS = CliOption(
88+
optionName = "strict-stream-scope",
89+
valueDescription = VALUE_DESCRIPTION,
90+
description = description("Stream Scopes"),
91+
required = false,
92+
allowMultipleOccurrences = false,
93+
)
94+
95+
val SUSPENDING_SERVER_STREAMING = CliOption(
96+
optionName = "strict-suspending-server-streaming",
97+
valueDescription = VALUE_DESCRIPTION,
98+
description = description("suspending server streaming methods"),
99+
required = false,
100+
allowMultipleOccurrences = false,
101+
)
102+
103+
val NOT_TOP_LEVEL_SERVER_FLOW = CliOption(
104+
optionName = "strict-not-top-level-server-flow",
105+
valueDescription = VALUE_DESCRIPTION,
106+
description = description("not top-level server streaming declarations"),
107+
required = false,
108+
allowMultipleOccurrences = false,
109+
)
110+
111+
val FIELDS = CliOption(
112+
optionName = "strict-fields",
113+
valueDescription = VALUE_DESCRIPTION,
114+
description = description("fields"),
115+
required = false,
116+
allowMultipleOccurrences = false,
117+
)
118+
119+
const val VALUE_DESCRIPTION = "none, warning or error"
120+
121+
fun description(entity: String): String {
122+
return "Diagnostic level for $entity in @Rpc services."
123+
}
124+
125+
val configurationMapper = mapOf(
126+
STATE_FLOW to StrictModeConfigurationKeys.STATE_FLOW,
127+
SHARED_FLOW to StrictModeConfigurationKeys.SHARED_FLOW,
128+
NESTED_FLOW to StrictModeConfigurationKeys.NESTED_FLOW,
129+
STREAM_SCOPED_FUNCTIONS to StrictModeConfigurationKeys.STREAM_SCOPED_FUNCTIONS,
130+
SUSPENDING_SERVER_STREAMING to StrictModeConfigurationKeys.SUSPENDING_SERVER_STREAMING,
131+
NOT_TOP_LEVEL_SERVER_FLOW to StrictModeConfigurationKeys.NOT_TOP_LEVEL_SERVER_FLOW,
132+
FIELDS to StrictModeConfigurationKeys.FIELDS,
133+
)
134+
}
135+
136+
fun AbstractCliOption.processAsStrictModeOption(value: String, configuration: CompilerConfiguration): Boolean {
137+
val key = StrictModeCliOptions.configurationMapper[this] ?: return false
138+
val mode = StrictMode.fromCli(value) ?: return false
139+
140+
configuration.put(key, mode)
141+
return true
142+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import org.jetbrains.kotlin.fir.analysis.checkers.expression.ExpressionCheckers
1414
import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirFunctionCallChecker
1515

1616
class FirRpcDeclarationCheckers(ctx: FirCheckersContext) : DeclarationCheckers() {
17-
override val regularClassCheckers: Set<FirRegularClassChecker> = setOf(
17+
override val regularClassCheckers: Set<FirRegularClassChecker> = setOfNotNull(
1818
FirRpcAnnotationChecker(ctx),
19+
if (ctx.serializationIsPresent) FirRpcStrictModeClassChecker(ctx) else null,
20+
FirRpcServiceDeclarationChecker(ctx),
1921
)
2022

2123
override val classCheckers: Set<FirClassChecker> = setOf(
@@ -34,5 +36,6 @@ class FirRpcDeclarationCheckers(ctx: FirCheckersContext) : DeclarationCheckers()
3436
class FirRpcExpressionCheckers(ctx: FirCheckersContext) : ExpressionCheckers() {
3537
override val functionCallCheckers: Set<FirFunctionCallChecker> = setOf(
3638
FirCheckedAnnotationFunctionCallChecker(ctx),
39+
FirRpcStrictModeExpressionChecker(ctx),
3740
)
3841
}

0 commit comments

Comments
 (0)