diff --git a/AGENTS.md b/AGENTS.md
index 81f9a49f..1f9387aa 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -185,6 +185,14 @@ declaring done.
(same rule as `lint-baseline.xml`). To grandfather something deliberately:
`make detekt-baseline MODULE=:foo`, or dispatch `detekt_baseline.yml` against the branch — the
committed diff is the review. Test and `androidTest` sources are scanned too.
+ Compose-specific rules (`Compose:` block in `config/detekt/detekt.yml`) come from
+ [`mrmans0n/compose-rules`](https://github.com/mrmans0n/compose-rules), the actively
+ maintained continuation of `twitter/compose-rules` (frozen since 2023 — `slackhq/compose-lints`
+ is a different project, an Android Lint port, not a Detekt ruleset). Pinned to `0.4.x` because
+ `0.5.0+` requires Detekt 2.0.0-alpha, which would invalidate every `detekt-baseline.xml`.
+ `UnstableCollections`, `Material2`, `ComposableNestingDepth`, and `PreviewNaming` are
+ deliberately off; `ViewModelInjection` is repointed at `metroViewModel`/`assistedMetroViewModel`
+ since this project is on Metro, not Hilt.
- `make android-lint` is **Android Lint** (`:app:lintDebug`, checkDependencies across the whole
graph), gated by `app/lint-baseline.xml`. A string added to `values/strings.xml` without its
`values-fr` / `values-es` siblings passes every other rung and fails CI with
diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index eb5785bf..56784efe 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -4,5 +4,7 @@
EmptyFunctionBlock:FakeSplitInstallManager.kt$FakeSplitInstallManager${}
MatchingDeclarationName:LocalDataSourceTest2.kt$LocalDataSourceTest
+ MutableParams:DynamicFeatureNavigator.kt$backStack
+ RememberMissing:DynamicFeatureNavigator.kt$mutableStateOf<DynamicFeatureKey?>(null)
diff --git a/build-logic/convention/src/main/kotlin/billionbeers.detekt.gradle.kts b/build-logic/convention/src/main/kotlin/billionbeers.detekt.gradle.kts
index 25b6d1ba..a5a47cd0 100644
--- a/build-logic/convention/src/main/kotlin/billionbeers.detekt.gradle.kts
+++ b/build-logic/convention/src/main/kotlin/billionbeers.detekt.gradle.kts
@@ -7,6 +7,10 @@ plugins {
val libs = the()
+dependencies {
+ "detektPlugins"(libs.compose.rules.detekt)
+}
+
configure {
toolVersion = libs.versions.detekt.get()
// Test sources are scanned too. They were excluded, so a third of the repo's Kotlin - the part
diff --git a/catalog/detekt-baseline.xml b/catalog/detekt-baseline.xml
index 136cf64c..4743ec8d 100644
--- a/catalog/detekt-baseline.xml
+++ b/catalog/detekt-baseline.xml
@@ -2,6 +2,7 @@
+ ModifierMissing:MainActivity.kt$CatalogApp
SwallowedException:MainActivity.kt$e: Throwable
TooGenericExceptionCaught:MainActivity.kt$e: Throwable
diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml
index 54df37b0..40e383ea 100644
--- a/config/detekt/detekt.yml
+++ b/config/detekt/detekt.yml
@@ -63,6 +63,95 @@ complexity:
LongMethod:
ignoreAnnotated: ['Composable']
+Compose:
+ # Rules ported from twitter/compose-rules, continued at mrmans0n/compose-rules since Twitter
+ # stopped maintaining the original. Version pinned to the last release built against Detekt
+ # 1.23.x (0.5.0+ requires Detekt 2.0.0-alpha, which we are not adopting - it would invalidate
+ # every per-module detekt-baseline.xml).
+ ComposableAnnotationNaming:
+ active: true
+ ComposableNaming:
+ active: true
+ ComposableNestingDepth:
+ active: false # Opt-in; we don't enforce a max nesting depth today.
+ ComposableParamOrder:
+ active: true
+ CompositionLocalAllowlist:
+ active: true
+ allowedCompositionLocals: LocalSpacing,LocalColors,LocalTypography,LocalDebugDrawerToggle,LocalSplitInstallManager
+ CompositionLocalNaming:
+ active: true
+ ConditionHoist:
+ active: true
+ ContentEmitterReturningValues:
+ active: true
+ ContentTrailingLambda:
+ active: true
+ ContentSlotReused:
+ active: true
+ DefaultsVisibility:
+ active: true
+ InvalidReadOnlyComposable:
+ active: true
+ LambdaParameterEventTrailing:
+ active: true
+ LambdaParameterInRestartableEffect:
+ active: true
+ Material2:
+ active: false # Opt-in; the design system deliberately still uses material.icons.core (M2 icons).
+ MissingReadOnlyComposable:
+ active: true
+ ModifierClickableOrder:
+ active: true
+ ModifierComposed:
+ active: true
+ ModifierMissing:
+ active: true
+ ModifierNaming:
+ active: true
+ ModifierNotUsedAtRoot:
+ active: true
+ ModifierReused:
+ active: true
+ ModifierWithoutDefault:
+ active: true
+ MultipleEmitters:
+ active: true
+ MutableParams:
+ active: true
+ MutableStateAutoboxing:
+ active: true
+ MutableStateParam:
+ active: true
+ ParameterNaming:
+ active: true
+ PreviewAnnotationNaming:
+ active: true
+ PreviewNaming:
+ active: false # Opt-in; @LightDarkPreviews (see :core:designsystem) doesn't follow the suffix convention this expects.
+ PreviewPublic:
+ active: true
+ RememberContentMissing:
+ active: true
+ RememberMissing:
+ active: true
+ StaleRememberUpdatedStateInRemember:
+ active: true
+ StateParam:
+ active: true
+ UnnecessaryComposable:
+ active: true
+ UnstableCollections:
+ active: false # Opt-in; only relevant with strong skipping disabled, and compose-stability.conf already covers our stability gaps.
+ VarsWithoutStateBacking:
+ active: true
+ ViewModelForwarding:
+ active: true
+ ViewModelInjection:
+ active: true
+ # Default factory names are Hilt-shaped (hiltViewModel); this project is on Metro DI.
+ viewModelFactories: metroViewModel,assistedMetroViewModel
+
style:
WildcardImport:
excludeImports:
diff --git a/core/designsystem/detekt-baseline.xml b/core/designsystem/detekt-baseline.xml
index 9bacb7c6..003e0570 100644
--- a/core/designsystem/detekt-baseline.xml
+++ b/core/designsystem/detekt-baseline.xml
@@ -23,6 +23,12 @@
MagicNumber:DialogWithProgressBar.kt$500
MagicNumber:DialogWithProgressBar.kt$DialogProgressProvider$0.5f
MatchingDeclarationName:Spacing.kt$BillionBeersSpacing
+ ModifierComposed:ComposeExtensions.kt$noRippleClickable
+ ModifierMissing:CatalogDemos.kt$ColorCatalogDemo
+ ModifierMissing:CatalogDemos.kt$TypographyCatalogDemo
+ ModifierMissing:DialogWithProgressBar.kt$DialogWithProgressBarDemo
+ MutableStateAutoboxing:DialogWithProgressBar.kt$mutableStateOf(number)
+ PreviewPublic:DialogWithProgressBar.kt$DialogContentPreview
SwallowedException:ComposeExtensions.kt$e: Exception
TooGenericExceptionCaught:ComposeExtensions.kt$e: Exception
diff --git a/feature/beerbrowse/detekt-baseline.xml b/feature/beerbrowse/detekt-baseline.xml
index 6d56e2b3..d979aeb4 100644
--- a/feature/beerbrowse/detekt-baseline.xml
+++ b/feature/beerbrowse/detekt-baseline.xml
@@ -6,5 +6,9 @@
LongParameterList:BrowseBeersScreen.kt$( title: String, viewState: CommonUiState<PagedListUiModel<Beer>>, onBack: () -> Unit, onBeerClick: (Beer) -> Unit, onScrollToBottom: () -> Unit, onRetryLoadMore: () -> Unit, // Serves both first-page reloads: the full-screen error retry and the pull-to-refresh gesture. onRetryFirstPage: () -> Unit, )
LongParameterList:BrowseHomeScreen.kt$( styles: CommonUiState<List<BeerStyle>>, breweries: CommonUiState<List<Brewery>>, selectedTab: Int, onTabSelected: (Int) -> Unit, onStyleClick: (BeerStyle) -> Unit, onBreweryClick: (Brewery) -> Unit, onBack: () -> Unit, onRetryStyles: () -> Unit, onRetryBreweries: () -> Unit, )
MatchingDeclarationName:BeerBrowseScreenImpl.kt$BrowseSelection
+ ParameterNaming:BrowseHomeScreen.kt$onTabSelected
+ PreviewPublic:BrowseBeersScreen.kt$BrowseBeersScreenPreview
+ PreviewPublic:BrowseHomeScreen.kt$BrowseHomeScreenPreview
+ ViewModelInjection:BrowseBeersScreen.kt$viewModel
diff --git a/feature/beerdetail/detekt-baseline.xml b/feature/beerdetail/detekt-baseline.xml
new file mode 100644
index 00000000..f99815e2
--- /dev/null
+++ b/feature/beerdetail/detekt-baseline.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ ModifierMissing:BeerDetailScreen.kt$BeerDetailScreenImpl
+ ModifierMissing:ComposeBeerDetail.kt$ComposeBeerDetail
+ ModifierMissing:ComposeBeerDetail.kt$StatCard
+ MultipleEmitters:ComposeBeerDetail.kt$BeerDetailBulletSection
+ PreviewPublic:ComposeBeerDetail.kt$ComposeBeerDetailPreview
+ PreviewPublic:ComposeBeerDetail.kt$ComposeBeerDetailWithoutEnrichedFieldsPreview
+ ViewModelInjection:BeerDetailScreen.kt$viewModel
+
+
diff --git a/feature/beersearch/detekt-baseline.xml b/feature/beersearch/detekt-baseline.xml
index c7612e7d..689205f9 100644
--- a/feature/beersearch/detekt-baseline.xml
+++ b/feature/beersearch/detekt-baseline.xml
@@ -4,5 +4,7 @@
EmptyFunctionBlock:BeersSearchViewModelTest.kt$BeersSearchViewModelTest${}
LongParameterList:BeersSearchScreen.kt$( viewState: CommonUiState<PagedListUiModel<Beer>>, query: String, onQueryChange: (String) -> Unit, onBeerClick: (Beer) -> Unit, onBack: () -> Unit, onScrollToBottom: () -> Unit, onRetryLoadMore: () -> Unit, onRetrySearch: () -> Unit, autoFocus: Boolean = true, )
+ ModifierMissing:BeersSearchScreen.kt$BeersSearchContent
+ PreviewPublic:BeersSearchScreen.kt$BeersSearchScreenPreview
diff --git a/feature/beerslist/detekt-baseline.xml b/feature/beerslist/detekt-baseline.xml
index ed22813b..4d9c9ec7 100644
--- a/feature/beerslist/detekt-baseline.xml
+++ b/feature/beerslist/detekt-baseline.xml
@@ -3,5 +3,8 @@
LongParameterList:BeersListScreen.kt$( viewState: CommonUiState<PagedListUiModel<Beer>>, onBeerClick: (Beer) -> Unit, onSearchClick: () -> Unit, onBrowseClick: () -> Unit, onScrollToBottom: () -> Unit, onRefresh: () -> Unit, onRetry: () -> Unit, onRetryLoadMore: () -> Unit, )
+ ModifierMissing:BeersListScreen.kt$BeersListContent
+ ModifierMissing:BeersListScreen.kt$BeersListItemSkeleton
+ PreviewPublic:BeersListScreen.kt$BeersListScreenPreview
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d0e4433f..3edfe319 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -50,6 +50,7 @@ kotlinx-serialization-json = "1.11.0"
# Tools
spotless = "8.9.0"
detekt = "1.23.8"
+compose-rules = "0.4.23"
dependency-guard = "0.5.0"
kotlinpoet = "2.3.0"
@@ -166,6 +167,7 @@ kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin
ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "com-google-devtools-ksp" }
spotless-gradlePlugin = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
+compose-rules-detekt = { module = "io.nlopez.compose.rules:detekt", version.ref = "compose-rules" }
android-junit5-plugin = { module = "de.mannodermaus.gradle.plugins:android-junit5", version.ref = "androidJunit5" }
paparazzi-plugin = { module = "app.cash.paparazzi:paparazzi-gradle-plugin", version.ref = "paparazzi" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index a29a6e4d..54cad4ce 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -5891,6 +5891,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -7352,6 +7368,11 @@
+
+
+
+
+
@@ -8103,6 +8124,14 @@
+
+
+
+
+
+
+
+
@@ -8203,6 +8232,11 @@
+
+
+
+
+
diff --git a/presentation_utils/detekt-baseline.xml b/presentation_utils/detekt-baseline.xml
index 864dc8d9..98a617a2 100644
--- a/presentation_utils/detekt-baseline.xml
+++ b/presentation_utils/detekt-baseline.xml
@@ -2,7 +2,19 @@
+ ComposableParamOrder:ComposeErrorView.kt$ComposeErrorView
+ LambdaParameterInRestartableEffect:DynamicFeatureLoader.kt$onCancelled
+ LambdaParameterInRestartableEffect:InfiniteListHandler.kt$onLoadMore
LongParameterList:DynamicFeatureInstallerTest.kt$DynamicFeatureInstallerTest$( status: Int, sessionId: Int = SESSION_ID, errorCode: Int = SplitInstallErrorCode.NO_ERROR, downloaded: Long = 0, total: Long = 0, modules: List<String> = listOf(MODULE), )
MatchingDeclarationName:InfiniteListHandler.kt$ListPosition
+ ModifierMissing:ComposeBeersListItem.kt$BeerChip
+ ModifierMissing:ComposeBeersListItem.kt$BeerImage
+ ModifierMissing:ComposeBeersListItem.kt$ComposeBeersListItem
+ ModifierMissing:ComposeTitle.kt$ComposeTitle
+ ParameterNaming:DynamicFeatureLoader.kt$onCancelled
+ PreviewPublic:ComposeBeersListItem.kt$ComposeBeersListItemPreview
+ PreviewPublic:ComposeErrorView.kt$ComposeErrorViewPreview
+ PreviewPublic:ComposeTitle.kt$ComposeTitlePreview
+ PreviewPublic:DynamicFeatureLoader.kt$InstallFailedContentPreview