From 21200df294e456d1508a91f99a88e7b7b74bde19 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Nov 2023 18:52:09 +0100 Subject: [PATCH] Konsist: check that if `sealed interface` is used in parameter of `Composable`, it has the `@Stable` or `@Immutable` annotation --- .../lockscreen/impl/pin/model/PinDigit.kt | 3 +++ .../MessageComposerPresenter.kt | 2 ++ .../components/MessagesReactionButton.kt | 2 ++ .../model/event/TimelineItemStateContent.kt | 3 +++ .../event/TimelineItemTextBasedContent.kt | 2 ++ .../components/button/ButtonVisuals.kt | 4 +++- .../components/list/ListItemContent.kt | 2 ++ .../designsystem/theme/components/Button.kt | 2 ++ .../designsystem/theme/components/ListItem.kt | 2 ++ .../theme/components/ListSupportingText.kt | 2 ++ .../theme/components/SearchBar.kt | 2 ++ libraries/matrix/api/build.gradle.kts | 2 +- .../matrix/api/room/MatrixRoomMembersState.kt | 3 +++ .../libraries/mediapickers/api/PickerType.kt | 2 ++ .../textcomposer/model/MessageComposerMode.kt | 2 ++ .../textcomposer/model/VoiceMessageState.kt | 2 ++ services/apperror/api/build.gradle.kts | 2 +- .../services/apperror/api/AppErrorState.kt | 3 +++ .../tests/konsist/KonsistArchitectureTest.kt | 24 +++++++++++++++++++ 19 files changed, 63 insertions(+), 3 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt index aa3c45e02e..f3af07294a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt @@ -16,6 +16,9 @@ package io.element.android.features.lockscreen.impl.pin.model +import androidx.compose.runtime.Immutable + +@Immutable sealed interface PinDigit { data object Empty : PinDigit data class Filled(val value: Char) : PinDigit diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index b66decd81e..c4b631e4df 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -20,6 +20,7 @@ import android.Manifest import android.annotation.SuppressLint import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -409,6 +410,7 @@ class MessageComposerPresenter @Inject constructor( } } +@Immutable sealed interface RoomMemberSuggestion { data object Room : RoomMemberSuggestion data class Member(val roomMember: RoomMember) : RoomMemberSuggestion diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt index 7e806e624a..357c9b3c20 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -103,6 +104,7 @@ fun MessagesReactionButton( } } +@Immutable sealed interface MessagesReactionsButtonContent { data class Text(val text: String) : MessagesReactionsButtonContent data class Icon(@DrawableRes val resourceId: Int) : MessagesReactionsButtonContent diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt index b136a602b2..7f9bc7973a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt @@ -16,6 +16,9 @@ package io.element.android.features.messages.impl.timeline.model.event +import androidx.compose.runtime.Immutable + +@Immutable sealed interface TimelineItemStateContent : TimelineItemEventContent { val body: String } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt index 10fca53261..5d5f121b75 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt @@ -16,8 +16,10 @@ package io.element.android.features.messages.impl.timeline.model.event +import androidx.compose.runtime.Immutable import org.jsoup.nodes.Document +@Immutable sealed interface TimelineItemTextBasedContent : TimelineItemEventContent { val body: String val htmlDocument: Document? diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonVisuals.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonVisuals.kt index 24f3989f66..3da88c49a4 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonVisuals.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonVisuals.kt @@ -18,14 +18,16 @@ package io.element.android.libraries.designsystem.components.button import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.TextButton /** - * A sealed class that represents the different visual styles that a button can have. + * A sealed interface that represents the different visual styles that a button can have. */ +@Immutable sealed interface ButtonVisuals { val action: () -> Unit diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt index cac8b557ee..a229aba378 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.components.list import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpSize @@ -34,6 +35,7 @@ import io.element.android.libraries.designsystem.theme.components.Text as TextCo /** * This is a helper to set default leading and trailing content for [ListItem]s. */ +@Immutable sealed interface ListItemContent { /** * Default Switch content for [ListItem]. diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt index 5ce528c814..30734f3f12 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -238,6 +239,7 @@ private fun ButtonInternal( } } +@Immutable sealed interface IconSource { val contentDescription: String? diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt index 528aa3cfec..0b0584da98 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -134,6 +135,7 @@ fun ListItem( /** * The style to use for a [ListItem]. */ +@Immutable sealed interface ListItemStyle { data object Default : ListItemStyle data object Primary : ListItemStyle diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt index f76562f0cd..d71073b604 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ExperimentalTextApi @@ -82,6 +83,7 @@ fun ListSupportingText( object ListSupportingTextDefaults { /** Specifies the padding to use for the supporting text. */ + @Immutable sealed interface Padding { /** No padding. */ data object None : Padding diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt index 65d156fbbc..19b73bf46a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt @@ -30,6 +30,7 @@ import androidx.compose.material3.SearchBarColors import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -180,6 +181,7 @@ object ElementSearchBarDefaults { ) } +@Immutable sealed interface SearchBarResultState { /** No search results are available yet (e.g. because the user hasn't entered a search term). */ class NotSearching : SearchBarResultState diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index b52f201458..fd59941ac3 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -15,7 +15,7 @@ */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") id("kotlin-parcelize") alias(libs.plugins.anvil) kotlin("plugin.serialization") version "1.9.10" diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt index 38ce7a03d3..5597aaf1c5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt @@ -16,6 +16,9 @@ package io.element.android.libraries.matrix.api.room +import androidx.compose.runtime.Immutable + +@Immutable sealed interface MatrixRoomMembersState { data object Unknown : MatrixRoomMembersState data class Pending(val prevRoomMembers: List? = null) : MatrixRoomMembersState diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt index de07450eec..9a20421a16 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt @@ -20,8 +20,10 @@ import android.net.Uri import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Immutable import io.element.android.libraries.core.mimetype.MimeTypes +@Immutable sealed interface PickerType { fun getContract(): ActivityResultContract fun getDefaultRequest(): Input diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index 34ab1641f2..9abf6bc52e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -17,11 +17,13 @@ package io.element.android.libraries.textcomposer.model import android.os.Parcelable +import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import kotlinx.parcelize.Parcelize +@Immutable sealed interface MessageComposerMode : Parcelable { @Parcelize data object Normal: MessageComposerMode diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index e418e64a1c..94a854a491 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -16,9 +16,11 @@ package io.element.android.libraries.textcomposer.model +import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.ImmutableList import kotlin.time.Duration +@Immutable sealed interface VoiceMessageState { data object Idle: VoiceMessageState diff --git a/services/apperror/api/build.gradle.kts b/services/apperror/api/build.gradle.kts index 94970d9774..7c776881bf 100644 --- a/services/apperror/api/build.gradle.kts +++ b/services/apperror/api/build.gradle.kts @@ -15,7 +15,7 @@ */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { diff --git a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt index fb5fb9fd76..f370aefa2e 100644 --- a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt @@ -16,6 +16,9 @@ package io.element.android.services.apperror.api +import androidx.compose.runtime.Immutable + +@Immutable sealed interface AppErrorState { data object NoError : AppErrorState diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt index a650eda0cc..5223d89ed0 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt @@ -16,11 +16,16 @@ package io.element.android.tests.konsist +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.constructors import com.lemonappdev.konsist.api.ext.list.modifierprovider.withSealedModifier import com.lemonappdev.konsist.api.ext.list.parameters +import com.lemonappdev.konsist.api.ext.list.withAnnotationOf import com.lemonappdev.konsist.api.ext.list.withNameEndingWith +import com.lemonappdev.konsist.api.ext.list.withoutAnnotationOf import com.lemonappdev.konsist.api.ext.list.withoutConstructors import com.lemonappdev.konsist.api.ext.list.withoutName import com.lemonappdev.konsist.api.ext.list.withoutParents @@ -67,4 +72,23 @@ class KonsistArchitectureTest { .withoutParents() .assertEmpty(additionalMessage = "Sealed class without constructor MUST be sealed interface") } + + @Test + fun `Composable MUST not have sealed interface in parameter`() { + // List all sealed interface without Immutable nor Stable annotation in the project + val forbiddenInterfacesForComposableParameter = Konsist.scopeFromProject() + .interfaces() + .withSealedModifier() + .withoutAnnotationOf(Immutable::class, Stable::class) + .map { it.fullyQualifiedName } + + Konsist.scopeFromProject() + .functions() + .withAnnotationOf(Composable::class) + .assertTrue(additionalMessage = "Consider adding the @Immutable or @Stable annotation to the sealed interface") { + it.parameters.all { param -> + param.type.fullyQualifiedName !in forbiddenInterfacesForComposableParameter + } + } + } }