Skip to content

Commit e63f083

Browse files
authored
Add exemptions for androidx.lifecycle.compose effects (#449)
1 parent 7639054 commit e63f083

File tree

4 files changed

+121
-5
lines changed

4 files changed

+121
-5
lines changed

rules/common/src/main/kotlin/io/nlopez/compose/core/util/Composables.kt

+3
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ private val RestartableEffects by lazy {
344344
"LaunchedEffect",
345345
"produceState",
346346
"DisposableEffect",
347+
"LifecycleEventEffect", // androidx.lifecycle.compose
348+
"LifecycleResumeEffect",
349+
"LifecycleStartEffect",
347350
"produceRetainedState", // Circuit
348351
)
349352
}

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

+16-5
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,24 @@ class LambdaParameterInRestartableEffect : ComposeKtVisitor {
5252
// Filter out dot receivers e.g. `something.myLambda()`
5353
.filterNot { it.isDotSelector() }
5454

55-
val isDisposableEffect = effect.calleeExpression?.text == "DisposableEffect"
55+
val effectCallee = effect.calleeExpression?.text
56+
val isDisposableEffect = effectCallee == "DisposableEffect"
57+
val isLifecycleEffect = effectCallee == "LifecycleStartEffect" ||
58+
effectCallee == "LifecycleResumeEffect"
5659

5760
// Lambdas used directly: myLambda()
5861
val invoked = callExpressions
5962
.let { expressions ->
60-
if (isDisposableEffect) {
61-
expressions.filter { it.calleeExpression?.text != "onDispose" }
62-
} else {
63-
expressions
63+
when {
64+
isDisposableEffect -> {
65+
expressions.filter { it.calleeExpression?.text != "onDispose" }
66+
}
67+
isLifecycleEffect -> {
68+
expressions.filter { it.calleeExpression?.text !in LifecycleEffectScopeFunctions }
69+
}
70+
else -> {
71+
expressions
72+
}
6473
}
6574
}
6675
.mapNotNull { it.calleeExpression?.text }
@@ -120,5 +129,7 @@ class LambdaParameterInRestartableEffect : ComposeKtVisitor {
120129
However, if the effect is not to be restarted, you will need to use `rememberUpdatedState` on the parameter and use its result in the effect.
121130
See https://mrmans0n.github.io/compose-rules/rules/#be-mindful-of-the-arguments-you-use-inside-of-a-restarting-effect for more information.
122131
""".trimIndent()
132+
133+
private val LifecycleEffectScopeFunctions = setOf("onStopOrDispose", "onPauseOrDispose")
123134
}
124135
}

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

+48
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,18 @@ class LambdaParameterInRestartableEffectCheckTest {
136136
onDispose(onDispose)
137137
}
138138
}
139+
@Composable
140+
fun Something(onDispose: () -> Unit) {
141+
LifecycleStartEffect(Unit) {
142+
onStopOrDispose(onDispose)
143+
}
144+
}
145+
@Composable
146+
fun Something(onDispose: () -> Unit) {
147+
LifecycleResumeEffect(Unit) {
148+
onPauseOrDispose(onDispose)
149+
}
150+
}
139151
140152
// TODO ideally these would also be caught, but may require type resolution
141153
@Composable
@@ -157,6 +169,8 @@ class LambdaParameterInRestartableEffectCheckTest {
157169
.hasStartSourceLocations(
158170
SourceLocation(2, 15),
159171
SourceLocation(8, 15),
172+
SourceLocation(14, 15),
173+
SourceLocation(20, 15),
160174
)
161175
for (error in errors) {
162176
assertThat(error).hasMessage(LambdaParameterInRestartableEffect.LambdaUsedInRestartableEffect)
@@ -179,4 +193,38 @@ class LambdaParameterInRestartableEffectCheckTest {
179193
val errors = rule.lint(code)
180194
assertThat(errors).isEmpty()
181195
}
196+
197+
@Test
198+
fun `passes when a lambda named onStopOrDispose is present but unused in LifecycleStartEffect`() {
199+
@Language("kotlin")
200+
val code =
201+
"""
202+
@Composable
203+
fun Something(onDispose: () -> Unit) {
204+
val latestOnDispose by rememberUpdatedState(onDispose)
205+
LifecycleStartEffect(Unit) {
206+
onStopOrDispose(latestOnDispose)
207+
}
208+
}
209+
""".trimIndent()
210+
val errors = rule.lint(code)
211+
assertThat(errors).isEmpty()
212+
}
213+
214+
@Test
215+
fun `passes when a lambda named onPauseOrDispose is present but unused in LifecycleResumeEffect`() {
216+
@Language("kotlin")
217+
val code =
218+
"""
219+
@Composable
220+
fun Something(onDispose: () -> Unit) {
221+
val latestOnDispose by rememberUpdatedState(onDispose)
222+
LifecycleResumeEffect(Unit) {
223+
onPauseOrDispose(latestOnDispose)
224+
}
225+
}
226+
""".trimIndent()
227+
val errors = rule.lint(code)
228+
assertThat(errors).isEmpty()
229+
}
182230
}

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

+54
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,18 @@ class LambdaParameterInRestartableEffectCheckTest {
142142
onDispose(onDispose)
143143
}
144144
}
145+
@Composable
146+
fun Something(onDispose: () -> Unit) {
147+
LifecycleStartEffect(Unit) {
148+
onStopOrDispose(onDispose)
149+
}
150+
}
151+
@Composable
152+
fun Something(onDispose: () -> Unit) {
153+
LifecycleResumeEffect(Unit) {
154+
onPauseOrDispose(onDispose)
155+
}
156+
}
145157
146158
// TODO ideally these would also be caught, but may require type resolution
147159
@Composable
@@ -170,6 +182,16 @@ class LambdaParameterInRestartableEffectCheckTest {
170182
col = 15,
171183
detail = LambdaParameterInRestartableEffect.LambdaUsedInRestartableEffect,
172184
),
185+
LintViolation(
186+
line = 14,
187+
col = 15,
188+
detail = LambdaParameterInRestartableEffect.LambdaUsedInRestartableEffect,
189+
),
190+
LintViolation(
191+
line = 20,
192+
col = 15,
193+
detail = LambdaParameterInRestartableEffect.LambdaUsedInRestartableEffect,
194+
),
173195
)
174196
}
175197

@@ -188,4 +210,36 @@ class LambdaParameterInRestartableEffectCheckTest {
188210
""".trimIndent()
189211
ruleAssertThat(code).hasNoLintViolations()
190212
}
213+
214+
@Test
215+
fun `passes when a lambda named onStopOrDispose is present but unused in LifecycleStartEffect`() {
216+
@Language("kotlin")
217+
val code =
218+
"""
219+
@Composable
220+
fun Something(onDispose: () -> Unit) {
221+
val latestOnDispose by rememberUpdatedState(onDispose)
222+
LifecycleStartEffect(Unit) {
223+
onStopOrDispose(latestOnDispose)
224+
}
225+
}
226+
""".trimIndent()
227+
ruleAssertThat(code).hasNoLintViolations()
228+
}
229+
230+
@Test
231+
fun `passes when a lambda named onPauseOrDispose is present but unused in LifecycleResumeEffect`() {
232+
@Language("kotlin")
233+
val code =
234+
"""
235+
@Composable
236+
fun Something(onDispose: () -> Unit) {
237+
val latestOnDispose by rememberUpdatedState(onDispose)
238+
LifecycleResumeEffect(Unit) {
239+
onPauseOrDispose(latestOnDispose)
240+
}
241+
}
242+
""".trimIndent()
243+
ruleAssertThat(code).hasNoLintViolations()
244+
}
191245
}

0 commit comments

Comments
 (0)