Skip to content

Commit 69a11c8

Browse files
derekxu16Space Cloud
authored andcommitted
Add a diagnostic to restrict usages of runCatching in @Composable functions
Test: RunCatchingCompsableCheckerTests (which was adapted from TryCatchCompsableCheckerTests) Fixes: 417989445 Relnote: "Added a diagnostic to restrict usages of `runCatching` in `@Composable` functions"
1 parent fb70d2a commit 69a11c8

File tree

4 files changed

+242
-0
lines changed

4 files changed

+242
-0
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.compiler.plugins.kotlin.analysis
18+
19+
import androidx.compose.compiler.plugins.kotlin.AbstractComposeDiagnosticsTest
20+
import org.junit.Test
21+
import org.junit.runner.RunWith
22+
import org.junit.runners.JUnit4
23+
24+
@RunWith(JUnit4::class)
25+
class RunCatchingComposableCheckerTests : AbstractComposeDiagnosticsTest(useFir = true) {
26+
@Test
27+
fun testTryCatchReporting001() {
28+
check(
29+
"""
30+
import androidx.compose.runtime.*;
31+
32+
@Composable fun foo() { }
33+
34+
@Composable fun bar() {
35+
<!ILLEGAL_RUN_CATCHING_AROUND_COMPOSABLE!>runCatching<!> { foo() }
36+
}
37+
"""
38+
)
39+
}
40+
41+
@Test
42+
fun testTryCatchReporting002() {
43+
check(
44+
"""
45+
import androidx.compose.runtime.*;
46+
47+
fun foo() { }
48+
49+
@Composable fun bar() {
50+
runCatching { foo() }
51+
}
52+
"""
53+
)
54+
}
55+
56+
@Test
57+
fun testTryCatchReporting003() {
58+
check(
59+
"""
60+
import androidx.compose.runtime.*;
61+
62+
@Composable fun foo() { }
63+
64+
@Composable fun bar() {
65+
<!ILLEGAL_RUN_CATCHING_AROUND_COMPOSABLE!>runCatching<!> {
66+
(1..10).forEach { foo() }
67+
}
68+
}
69+
"""
70+
)
71+
}
72+
73+
@Test
74+
fun testTryCatchReporting004() {
75+
check(
76+
"""
77+
import androidx.compose.runtime.*
78+
var globalContent = @Composable {}
79+
fun setContent(content: @Composable () -> Unit) {
80+
globalContent = content
81+
}
82+
@Composable fun A() {}
83+
84+
fun test() {
85+
runCatching {
86+
setContent {
87+
A()
88+
}
89+
}
90+
}
91+
"""
92+
)
93+
}
94+
95+
@Test
96+
fun testTryCatchReporting005() {
97+
check(
98+
"""
99+
import androidx.compose.runtime.*
100+
@Composable fun A() {}
101+
102+
@Composable
103+
fun test() {
104+
<!ILLEGAL_RUN_CATCHING_AROUND_COMPOSABLE!>runCatching<!> {
105+
object {
106+
init { A() }
107+
}
108+
}
109+
}
110+
"""
111+
)
112+
}
113+
114+
@Test
115+
fun testTryCatchReporting006() {
116+
check(
117+
"""
118+
import androidx.compose.runtime.*
119+
@Composable fun A() {}
120+
121+
@Composable
122+
fun test() {
123+
<!ILLEGAL_RUN_CATCHING_AROUND_COMPOSABLE!>runCatching<!> {
124+
object {
125+
val x = A()
126+
}
127+
}
128+
}
129+
"""
130+
)
131+
}
132+
133+
@Test
134+
fun testTryCatchReporting007() {
135+
check(
136+
"""
137+
import androidx.compose.runtime.*
138+
139+
@Composable
140+
fun test() {
141+
<!ILLEGAL_RUN_CATCHING_AROUND_COMPOSABLE!>runCatching<!> {
142+
val x by remember { lazy { 0 } }
143+
print(x)
144+
}
145+
}
146+
"""
147+
)
148+
}
149+
150+
@Test
151+
fun testTryCatchReporting008() {
152+
check(
153+
"""
154+
import androidx.compose.runtime.*
155+
@Composable fun A() {}
156+
157+
@Composable
158+
fun test() {
159+
runCatching {
160+
object {
161+
val x: Int
162+
@Composable get() = remember { 0 }
163+
}
164+
}
165+
}
166+
"""
167+
)
168+
}
169+
170+
@Test
171+
fun testTryCatchReporting009() {
172+
check(
173+
"""
174+
import androidx.compose.runtime.*
175+
@Composable fun A() {}
176+
177+
@Composable
178+
fun test() {
179+
runCatching {
180+
class C {
181+
init { <!COMPOSABLE_INVOCATION!>A<!>() }
182+
}
183+
}
184+
}
185+
"""
186+
)
187+
}
188+
189+
@Test
190+
fun testTryCatchReporting010() {
191+
check(
192+
"""
193+
import androidx.compose.runtime.*
194+
@Composable fun A() {}
195+
196+
@Composable
197+
fun test() {
198+
runCatching {
199+
@Composable fun B() {
200+
A()
201+
}
202+
}
203+
}
204+
"""
205+
)
206+
}
207+
}

plugins/compose/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/k2/ComposableCallChecker.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ import org.jetbrains.kotlin.fir.resolve.isInvoke
3636
import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
3737
import org.jetbrains.kotlin.fir.types.coneType
3838
import org.jetbrains.kotlin.fir.types.functionTypeKind
39+
import org.jetbrains.kotlin.name.CallableId
40+
import org.jetbrains.kotlin.name.FqName
41+
import org.jetbrains.kotlin.name.Name
3942
import org.jetbrains.kotlin.psi.KtFunction
4043
import org.jetbrains.kotlin.psi.KtFunctionLiteral
4144
import org.jetbrains.kotlin.psi.KtLambdaExpression
@@ -69,13 +72,21 @@ object ComposableFunctionCallChecker : FirFunctionCallChecker(MppCheckerKind.Com
6972
}
7073
}
7174

75+
private val runCatchingCallableId = CallableId(
76+
FqName("kotlin"),
77+
Name.identifier("runCatching"),
78+
);
79+
7280
/**
7381
* Check if `expression` - a call to a composable function or access to a composable property -
7482
* is allowed in the current context. It is allowed if:
7583
*
7684
* - It is executed as part of the body of a composable function.
7785
* - It is not executed as part of the body of a lambda annotated with `@DisallowComposableCalls`.
7886
* - It is not inside of a `try` block.
87+
* - It is not inside of the body of a lambda passed to a `runCatching` call. (As for references to
88+
* `@Composable` functions, the type system prevents those from being passed to `runCatching`
89+
* calls.)
7990
* - It is a call to a readonly composable function if it is executed in the body of a function
8091
* that is annotated with `@ReadOnlyComposable`.
8192
*
@@ -156,6 +167,17 @@ private fun checkComposableCall(
156167
)
157168
}
158169
},
170+
visitFunctionCall = { functionCall ->
171+
if (functionCall.calleeReference.toResolvedCallableSymbol()?.callableId == runCatchingCallableId) {
172+
// If we have reached this point, it means that the composable call under scrutiny
173+
// happens inside of the body of a lambda passed to a `runCatching` call.
174+
reporter.reportOn(
175+
functionCall.source,
176+
ComposeErrors.ILLEGAL_RUN_CATCHING_AROUND_COMPOSABLE,
177+
context
178+
)
179+
}
180+
}
159181
)
160182
reporter.reportOn(
161183
expression.calleeReference.source,
@@ -273,6 +295,7 @@ private inline fun CheckerContext.visitCurrentScope(
273295
visitAnonymousFunction: (FirAnonymousFunction) -> Unit = {},
274296
visitFunction: (FirFunction) -> Unit = {},
275297
visitTryExpression: (FirTryExpression, FirElement) -> Unit = { _, _ -> },
298+
visitFunctionCall: (FirFunctionCall) -> Unit = {},
276299
) {
277300
for ((elementIndex, element) in containingElements.withIndex().reversed()) {
278301
when (element) {
@@ -296,6 +319,9 @@ private inline fun CheckerContext.visitCurrentScope(
296319
?: continue
297320
visitTryExpression(element, container)
298321
}
322+
is FirFunctionCall -> {
323+
visitFunctionCall(element)
324+
}
299325
is FirProperty -> {
300326
// Coming from an initializer or delegate expression, otherwise we'd
301327
// have hit a FirFunction and would already report an error.

plugins/compose/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/k2/ComposeErrorMessages.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ object ComposeErrorMessages : BaseDiagnosticRendererFactory() {
5252
"Try catch is not supported around composable function invocations."
5353
)
5454

55+
map.put(
56+
ComposeErrors.ILLEGAL_RUN_CATCHING_AROUND_COMPOSABLE,
57+
"runCatching call is not allowed to contain @Composable function invocations"
58+
)
59+
5560
map.put(
5661
ComposeErrors.MISSING_DISALLOW_COMPOSABLE_CALLS_ANNOTATION,
5762
"Parameter {0} cannot be inlined inside of lambda argument {1} of {2} " +

plugins/compose/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/k2/ComposeErrors.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.jetbrains.kotlin.fir.symbols.impl.FirValueParameterSymbol
2828
import org.jetbrains.kotlin.fir.symbols.impl.FirVariableSymbol
2929
import org.jetbrains.kotlin.fir.types.ConeKotlinType
3030
import org.jetbrains.kotlin.lexer.KtTokens
31+
import org.jetbrains.kotlin.psi.KtCallExpression
3132
import org.jetbrains.kotlin.psi.KtNamedDeclaration
3233
import org.jetbrains.kotlin.psi.KtTryExpression
3334

@@ -51,6 +52,9 @@ object ComposeErrors : KtDiagnosticsContainer() {
5152
ComposeSourceElementPositioningStrategies.TRY_KEYWORD
5253
)
5354

55+
// error goes on the `runCatching` call
56+
val ILLEGAL_RUN_CATCHING_AROUND_COMPOSABLE by error0<KtCallExpression>(SourceElementPositioningStrategies.REFERENCED_NAME_BY_QUALIFIED)
57+
5458
val MISSING_DISALLOW_COMPOSABLE_CALLS_ANNOTATION by error3<
5559
PsiElement,
5660
FirValueParameterSymbol, // unmarked

0 commit comments

Comments
 (0)