Skip to content

Commit 123d05e

Browse files
dzirbelmrmans0n
andauthored
Fix LambdaParameterInRestartableEffect conflict with onDispose lambda (#254)
Composables which accept a parameter named `onDispose` will always raise a warning for its use in `DisposableEffect`, from the call to `DisposableEffectScope.onDispose`. This is a rare edge case but doesn't allow any workarounds other than renaming the lambda or suppressing. To fix, I've implemented simple plain text matching to ignore any usages of `onDispose` in a `DisposableEffect`. This may not be perfect (as the TODO in test cases shows) but in the absence of type resolution it might be a reasonable workaround. Very open to more elegant solutions! Co-authored-by: Nacho Lopez <[email protected]>
1 parent 957a8df commit 123d05e

File tree

3 files changed

+141
-5
lines changed

3 files changed

+141
-5
lines changed

rules/common/src/main/kotlin/io/nlopez/compose/rules/LambdaParameterInRestartableEffect.kt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,26 @@ class LambdaParameterInRestartableEffect : ComposeKtVisitor {
3939

4040
if (lambdaParameterNames.isEmpty()) continue
4141

42-
// Then, we just want the lambda parameters that are actually used inside in any of the found effects'
42+
// Then, we just want the lambda parameters that are actually used inside any of the found effects'
4343
// trailing lambda code
44-
val usedLambdaParameterNames = effects.mapNotNull { it.lambdaArguments.lastOrNull() }
45-
.mapNotNull { it.getLambdaExpression()?.bodyExpression }
46-
.flatMap { body ->
44+
val usedLambdaParameterNames = effects
45+
.flatMap { effect ->
46+
val body = effect.lambdaArguments.lastOrNull()?.getLambdaExpression()?.bodyExpression
47+
?: return@flatMap emptySequence()
48+
4749
val callExpressions = body.findChildrenByClass<KtCallExpression>()
50+
val isDisposableEffect = effect.calleeExpression?.text == "DisposableEffect"
4851

4952
// Lambdas used directly: myLambda()
50-
val invoked = callExpressions.mapNotNull { it.calleeExpression?.text }
53+
val invoked = callExpressions
54+
.let { expressions ->
55+
if (isDisposableEffect) {
56+
expressions.filter { it.calleeExpression?.text != "onDispose" }
57+
} else {
58+
expressions
59+
}
60+
}
61+
.mapNotNull { it.calleeExpression?.text }
5162
.filter { it in lambdaParameterNames }
5263

5364
// Lambdas being tossed around to other methods

rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/LambdaParameterInRestartableEffectCheckTest.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,65 @@ class LambdaParameterInRestartableEffectCheckTest {
7878
val errors = rule.lint(code)
7979
assertThat(errors).isEmpty()
8080
}
81+
82+
@Test
83+
fun `error out when detecting a lambda named onDispose used in a non-DisposableEffect`() {
84+
@Language("kotlin")
85+
val code =
86+
"""
87+
@Composable
88+
fun Something(onDispose: () -> Unit) {
89+
LaunchedEffect(Unit) {
90+
onDispose()
91+
}
92+
}
93+
@Composable
94+
fun Something(onDispose: () -> Unit) {
95+
DisposableEffect(Unit) {
96+
onDispose(onDispose)
97+
}
98+
}
99+
100+
// TODO ideally these would also be caught, but may require type resolution
101+
@Composable
102+
fun Something(onDispose: () -> Unit) {
103+
DisposableEffect(Unit) {
104+
onDispose { onDispose() }
105+
}
106+
}
107+
@Composable
108+
fun Something(onDispose: (Int) -> Unit) {
109+
DisposableEffect(Unit) {
110+
onDispose(0)
111+
onDispose {}
112+
}
113+
}
114+
""".trimIndent()
115+
val errors = rule.lint(code)
116+
assertThat(errors)
117+
.hasStartSourceLocations(
118+
SourceLocation(2, 15),
119+
SourceLocation(8, 15),
120+
)
121+
for (error in errors) {
122+
assertThat(error).hasMessage(LambdaParameterInRestartableEffect.LambdaUsedInRestartableEffect)
123+
}
124+
}
125+
126+
@Test
127+
fun `passes when a lambda named onDispose is present but unused in DisposableEffect`() {
128+
@Language("kotlin")
129+
val code =
130+
"""
131+
@Composable
132+
fun Something(onDispose: () -> Unit) {
133+
val latestOnDispose by rememberUpdatedState(onDispose)
134+
DisposableEffect(Unit) {
135+
onDispose(latestOnDispose)
136+
}
137+
}
138+
""".trimIndent()
139+
val errors = rule.lint(code)
140+
assertThat(errors).isEmpty()
141+
}
81142
}

rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/LambdaParameterInRestartableEffectCheckTest.kt

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,68 @@ class LambdaParameterInRestartableEffectCheckTest {
8686
)
8787
.hasNoLintViolations()
8888
}
89+
90+
@Test
91+
fun `error out when detecting a lambda named onDispose used in a non-DisposableEffect`() {
92+
@Language("kotlin")
93+
val code =
94+
"""
95+
@Composable
96+
fun Something(onDispose: () -> Unit) {
97+
LaunchedEffect(Unit) {
98+
onDispose()
99+
}
100+
}
101+
@Composable
102+
fun Something(onDispose: () -> Unit) {
103+
DisposableEffect(Unit) {
104+
onDispose(onDispose)
105+
}
106+
}
107+
108+
// TODO ideally these would also be caught, but may require type resolution
109+
@Composable
110+
fun Something(onDispose: () -> Unit) {
111+
DisposableEffect(Unit) {
112+
onDispose { onDispose() }
113+
}
114+
}
115+
@Composable
116+
fun Something(onDispose: (Int) -> Unit) {
117+
DisposableEffect(Unit) {
118+
onDispose(0)
119+
onDispose {}
120+
}
121+
}
122+
""".trimIndent()
123+
ruleAssertThat(code)
124+
.hasLintViolationsWithoutAutoCorrect(
125+
LintViolation(
126+
line = 2,
127+
col = 15,
128+
detail = LambdaParameterInRestartableEffect.LambdaUsedInRestartableEffect,
129+
),
130+
LintViolation(
131+
line = 8,
132+
col = 15,
133+
detail = LambdaParameterInRestartableEffect.LambdaUsedInRestartableEffect,
134+
),
135+
)
136+
}
137+
138+
@Test
139+
fun `passes when a lambda named onDispose is present but unused in DisposableEffect`() {
140+
@Language("kotlin")
141+
val code =
142+
"""
143+
@Composable
144+
fun Something(onDispose: () -> Unit) {
145+
val latestOnDispose by rememberUpdatedState(onDispose)
146+
DisposableEffect(Unit) {
147+
onDispose(latestOnDispose)
148+
}
149+
}
150+
""".trimIndent()
151+
ruleAssertThat(code).hasNoLintViolations()
152+
}
89153
}

0 commit comments

Comments
 (0)