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