From ab197de272da186e1e1e4a5f3497e8d62925e170 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Sat, 29 Aug 2026 00:03:10 -0400 Subject: [PATCH] Don't suggest `@NonRestartableComposable` on `@Preview`-annotated composables --- CHANGELOG.md | 2 + .../NonRestartableComposableDetector.kt | 2 + .../NonRestartableComposableDetectorTest.kt | 149 ++++++++++-------- 3 files changed, 87 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27304235..61e808dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changelog **Unreleased** -------------- +- **Fix**: Don't suggest `@NonRestartableComposable` on `@Preview`-annotated composables. + 1.6.0 ----- diff --git a/compose-lint-checks/src/main/java/slack/lint/compose/NonRestartableComposableDetector.kt b/compose-lint-checks/src/main/java/slack/lint/compose/NonRestartableComposableDetector.kt index 057243d7..7784437d 100644 --- a/compose-lint-checks/src/main/java/slack/lint/compose/NonRestartableComposableDetector.kt +++ b/compose-lint-checks/src/main/java/slack/lint/compose/NonRestartableComposableDetector.kt @@ -29,6 +29,7 @@ import slack.lint.compose.util.directCall import slack.lint.compose.util.findChildrenByClass import slack.lint.compose.util.isForwardedReceiver import slack.lint.compose.util.isForwardedValue +import slack.lint.compose.util.isPreview import slack.lint.compose.util.returnsUnitOrVoid import slack.lint.compose.util.singleBodyExpression import slack.lint.compose.util.sourceImplementation @@ -63,6 +64,7 @@ class NonRestartableComposableDetector : ComposableFunctionDetector(), SourceCod if (namedFunction.isLocal() || namedFunction.definedInInterface) return if (NON_RESTARTABLE_MODIFIERS.any(namedFunction::hasModifier)) return if (INELIGIBLE_ANNOTATIONS.any(method::hasAnnotation)) return + if (method.isPreview) return val bodyExpression = namedFunction.singleBodyExpression() ?: return // A pass-through body has one call and no lambda body of its own. This also rejects calls used diff --git a/compose-lint-checks/src/test/java/slack/lint/compose/NonRestartableComposableDetectorTest.kt b/compose-lint-checks/src/test/java/slack/lint/compose/NonRestartableComposableDetectorTest.kt index fe889fd4..c0103b2d 100644 --- a/compose-lint-checks/src/test/java/slack/lint/compose/NonRestartableComposableDetectorTest.kt +++ b/compose-lint-checks/src/test/java/slack/lint/compose/NonRestartableComposableDetectorTest.kt @@ -3,6 +3,7 @@ package slack.lint.compose import com.android.tools.lint.checks.infrastructure.TestFiles.binaryStub +import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.intellij.lang.annotations.Language @@ -10,92 +11,87 @@ import org.junit.Test class NonRestartableComposableDetectorTest : BaseComposeLintTest() { - private val compiledRuntime = + private val compiledStubs = binaryStub( - "libs/compiled-runtime.jar", + "libs/compiled-stubs.jar", kotlin( """ package androidx.compose.runtime + @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.TYPE) @Retention(AnnotationRetention.BINARY) - @Target(AnnotationTarget.FUNCTION) annotation class Composable + @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) @Retention(AnnotationRetention.SOURCE) - @Target(AnnotationTarget.FUNCTION) annotation class NonRestartableComposable - class CompiledContent { - @Composable - @NonRestartableComposable - fun render(value: String) {} - } - """ - ) - .indented() - .to("src/androidx/compose/runtime/CompiledRuntime.kt"), - ) - - private val stubs = - kotlin( - """ - package androidx.compose.runtime - - @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.TYPE) - @Retention(AnnotationRetention.BINARY) - annotation class Composable + @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) + @Retention(AnnotationRetention.SOURCE) + annotation class NonSkippableComposable - @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) - @Retention(AnnotationRetention.SOURCE) - annotation class NonRestartableComposable + @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) + @Retention(AnnotationRetention.BINARY) + annotation class ReadOnlyComposable - @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) - @Retention(AnnotationRetention.SOURCE) - annotation class NonSkippableComposable + @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) + @Retention(AnnotationRetention.SOURCE) + annotation class ExplicitGroupsComposable - @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) - @Retention(AnnotationRetention.BINARY) - annotation class ReadOnlyComposable + interface State { + val value: T + } - @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) - @Retention(AnnotationRetention.SOURCE) - annotation class ExplicitGroupsComposable + @Composable + external fun Content(value: String, child: @Composable () -> Unit = {}) - interface State { - val value: T - } + @Composable + external fun VarargContent(vararg values: String) - @Composable - external fun Content(value: String, child: @Composable () -> Unit = {}) + @Composable + external fun ValueContent(): String - @Composable - external fun VarargContent(vararg values: String) + @Composable + external fun DefaultValue(): String - @Composable - external fun ValueContent(): String + @Composable + @ReadOnlyComposable + external fun ReadOnlyContent() - @Composable - external fun DefaultValue(): String + @Composable + @NonRestartableComposable + external fun NonRestartableContent() - @Composable - @ReadOnlyComposable - external fun ReadOnlyContent() + @Composable + @NonSkippableComposable + external fun NonSkippableContent() - @Composable - @NonRestartableComposable - external fun NonRestartableContent() + class CompiledContent { + @Composable + @NonRestartableComposable + fun render(value: String) {} + } + """ + ) + .indented() + .to("src/androidx/compose/runtime/CompiledStubs.kt"), + kotlin( + """ + package androidx.compose.ui.tooling.preview - @Composable - @NonSkippableComposable - external fun NonSkippableContent() - """ - .trimIndent() + annotation class Preview + """ + ) + .indented() + .to("src/androidx/compose/ui/tooling/preview/Preview.kt"), ) override fun getDetector(): Detector = NonRestartableComposableDetector() override fun getIssues(): List = listOf(NonRestartableComposableDetector.ISSUE) + override fun lint(): TestLintTask = super.lint().allowKotlinClassStubs(true) + @Test fun `suggests non-restartable for a direct composable call`() { @Language("kotlin") @@ -110,7 +106,7 @@ class NonRestartableComposableDetectorTest : BaseComposeLintTest() { .trimIndent() lint() - .files(stubs, kotlin(code)) + .files(compiledStubs, kotlin(code)) .run() .expect( """ @@ -155,7 +151,7 @@ class NonRestartableComposableDetectorTest : BaseComposeLintTest() { .trimIndent() lint() - .files(stubs, kotlin(code)) + .files(compiledStubs, kotlin(code)) .run() .expect( """ @@ -188,7 +184,7 @@ class NonRestartableComposableDetectorTest : BaseComposeLintTest() { .trimIndent() lint() - .files(stubs, kotlin(code)) + .files(compiledStubs, kotlin(code)) .run() .expect( """ @@ -217,8 +213,7 @@ class NonRestartableComposableDetectorTest : BaseComposeLintTest() { .trimIndent() lint() - .files(compiledRuntime, kotlin(code)) - .allowKotlinClassStubs(true) + .files(compiledStubs, kotlin(code)) .run() .expect( """ @@ -284,7 +279,7 @@ class NonRestartableComposableDetectorTest : BaseComposeLintTest() { """ .trimIndent() - lint().files(stubs, kotlin(code)).run().expectClean() + lint().files(compiledStubs, kotlin(code)).run().expectClean() } @Test @@ -353,7 +348,7 @@ class NonRestartableComposableDetectorTest : BaseComposeLintTest() { """ .trimIndent() - lint().files(stubs, kotlin(code)).run().expectClean() + lint().files(compiledStubs, kotlin(code)).run().expectClean() } @Test @@ -385,7 +380,7 @@ class NonRestartableComposableDetectorTest : BaseComposeLintTest() { .trimIndent() lint() - .files(stubs, kotlin(code)) + .files(compiledStubs, kotlin(code)) .run() .expect( """ @@ -429,6 +424,28 @@ class NonRestartableComposableDetectorTest : BaseComposeLintTest() { """ .trimIndent() - lint().files(stubs, kotlin(code)).run().expectClean() + lint().files(compiledStubs, kotlin(code)).run().expectClean() + } + + @Test + fun `does not suggest non-restartable on previews`() { + @Language("kotlin") + val code = + """ + import androidx.compose.runtime.Composable + import androidx.compose.ui.tooling.preview.Preview + + @Composable + fun AnotherComposable() {} + + @Preview + @Composable + fun IgnoredValueChild() { + AnotherComposable() + } + """ + .trimIndent() + + lint().files(compiledStubs, kotlin(code)).run().expectClean() } }