# Code Quality (Detekt)

Required: Detekt on every module via the `build-logic` convention plugin. Single rules source (`plugins/detekt.yml`) with optional per-module overrides; type-resolution enabled on Android modules; Compose ruleset; no legacy `buildscript` block.

## Table of Contents

1. [Version Catalog](#version-catalog)
2. [Detekt Convention Plugin (Build Logic)](#detekt-convention-plugin-build-logic)
3. [Apply in Modules](#apply-in-modules)
4. [Running Detekt](#running-detekt)
5. [Baselines & CI](#baselines--ci)
6. [Compose Rules](#compose-rules)
7. [Suppressing Violations](#suppressing-violations)
8. [Suppression rules](#suppression-rules)

## Version Catalog
Use `assets/libs.versions.toml.template` as the source of truth for:
- The Detekt plugin version and plugin ID.
- The Compose detekt rules dependency (`compose-rules-detekt`).

Use `assets/detekt.yml.template` as the baseline rules file; copy it to
`plugins/detekt.yml` and customize it there (modules can optionally provide
a local `detekt.yml` override).

### Detekt 2.x coordinates and pairing

Detekt 2.x moved to a **new Maven group**. Getting this wrong yields an unresolvable plugin, not a version conflict:

| | Detekt 1.23.x | Detekt 2.x (template pin `2.0.0-alpha.5`) |
|---|---|---|
| Plugin ID   | `io.gitlab.arturbosch.detekt`              | **`dev.detekt`**                     |
| Plugin artifact | `io.gitlab.arturbosch.detekt:detekt-gradle-plugin` | **`dev.detekt:detekt-gradle-plugin`** |
| Task classes | `io.gitlab.arturbosch.detekt.Detekt`       | **`dev.detekt.gradle.Detekt`**        |

**`composeRules` and `detekt` are a matched pair.** `io.nlopez.compose.rules:detekt` compiles against a specific `dev.detekt:detekt-core`, so bumping one alone breaks rule loading at execution time. The template pairs `composeRules = "0.6.3"` with `detekt = "2.0.0-alpha.5"`; verify the pairing in the compose-rules POM before changing either.

Detekt 2.x is still on the alpha line, which is why it is listed under [dependencies.md → Pinned prerelease](dependencies.md#pinned-prerelease-required-for-feature-parity). Re-run `./gradlew detekt` after every bump of either pin.

## Detekt Convention Plugin (Build Logic)

The `DetektConventionPlugin` is available in `assets/convention/DetektConventionPlugin.kt`.

Copy it to `build-logic/convention/src/main/kotlin/DetektConventionPlugin.kt` in your project.

Key features:
- Applies Detekt plugin from version catalog
- Adds Compose rules automatically
- Configures central config file (`config/detekt.yml`)
- Supports module-specific overrides
- Enables type resolution for Android modules
- Generates XML, HTML, and SARIF reports

### Build Logic Registration

The Detekt plugin is already registered in the build script available at `assets/convention/build.gradle.kts`. 

When you copy the build script to your project's `build-logic/convention/build.gradle.kts`, the Detekt plugin registration is included:

```kotlin
register("detekt") {
    id = "app.detekt"
    implementationClass = "DetektConventionPlugin"
}
```

## Apply in Modules

Apply the convention plugin in every module:
```kotlin
plugins {
    alias(libs.plugins.app.detekt)
}
```

## Running Detekt

### Local Development

Run detekt for all modules:
```bash
./gradlew detekt
```

Run for specific module:
```bash
./gradlew :app:detekt
./gradlew :feature-auth:detekt
```

Run with type resolution (slower, more accurate):
```bash
./gradlew detektMain
```

### Excluding Generated Code

Add to `plugins/detekt.yml`:
```yaml
build:
  excludes:
    - '**/build/**'
    - '**/generated/**'
    - '**/*.kts'
    - '**/resources/**'
```

## Baselines & CI

### Detekt baseline routing

**Use baselines when:**

- Adopting detekt in an existing project with many violations.
- New violations must fail CI while legacy debt is scheduled.
- Rules roll out gradually behind a baseline.

**Forbidden:**

- Greenfield projects - fix findings instead of freezing debt.
- Active refactors that need signal - baselines mask regressions.

### Creating Per-Module Baselines

Generate baseline for a specific module:
```bash
./gradlew :app:detektBaseline
```

This creates `app/detekt-baseline.xml` which suppresses existing issues in that module only.

Commit the baseline:
```bash
git add app/detekt-baseline.xml
git commit -m "Add detekt baseline for app module"
```

### CI Integration

**GitHub Actions example:**

```yaml
name: Code Quality

on:
  pull_request:
    branches: [ main ]
  push:
    branches: [ main ]

jobs:
  detekt:
    name: Detekt Check
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: '17'
      
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3
      
      - name: Run Detekt
        run: ./gradlew detekt
      
      - name: Upload SARIF to GitHub Security
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: build/reports/detekt/detekt.sarif
      
      - name: Upload HTML Reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: detekt-reports
          path: '**/build/reports/detekt/'
```

**Key CI considerations:**
- Use `if: always()` to upload reports even on failure
- Upload SARIF for GitHub Security tab integration
- Fail the build if issues are found (default behavior)
- Cache Gradle dependencies for faster builds

If the project uses Gradle toolchains, Detekt will resolve the proper JDK automatically.

## Compose Rules
The Compose detekt ruleset is configured in `assets/detekt.yml.template`. Use that template as-is.
For compatibility information and latest rules, see: [Compose rules + detekt compatibility](https://mrmans0n.github.io/compose-rules/detekt/)

The template's `Compose:` block is kept in sync with the ruleset's own default config. To verify after
a `composeRules` bump, diff the rule keys against the `config/config.yml` bundled inside the
`io.nlopez.compose.rules:detekt` jar - a rule absent from your config is **silently not run**, so a
bump can quietly reduce coverage rather than fail.

Two rules are deliberately turned off in the template, both because enabling them demands refactoring
rather than a fix:

| Rule                     | Off because                                                                 |
|--------------------------|-----------------------------------------------------------------------------|
| `ModifierComposed`       | Migrating `Modifier.composed` to `Modifier.Node` is a significant refactor    |
| `ComposableNestingDepth` | The default threshold flags legitimately nested layouts; opt in per project    |

`PreviewNaming` and `UnstableCollections` are also off by default in the template; enable
`UnstableCollections` when adopting `kotlinx.collections.immutable` ([compose-patterns.md](compose-patterns.md)).

## Suppressing Violations

### Acceptable Suppressions for Compose

The following suppressions are acceptable on `@Composable` functions only.

#### `@Suppress("LongMethod")`
Composable UI functions declare layout trees and are naturally longer than business logic functions.

```kotlin
@Suppress("LongMethod")
@Composable
fun ProductDetailScreen(
    product: Product,
    onAddToCart: () -> Unit,
    onNavigateBack: () -> Unit
) {
    Scaffold(
        topBar = { /* AppBar with 10+ lines */ },
        bottomBar = { /* Actions with 10+ lines */ }
    ) { padding ->
        LazyColumn(Modifier.padding(padding)) {
            item { /* Hero image section 15+ lines */ }
            item { /* Title and price section 10+ lines */ }
            item { /* Description section 10+ lines */ }
            item { /* Specifications section 20+ lines */ }
            item { /* Reviews section 15+ lines */ }
            // Total: 80+ lines is normal for a detail screen
        }
    }
}
```

#### `@Suppress("LongParameterList")`
Route/Screen composables accept ViewModels, callbacks, modifiers, and navigation arguments.

```kotlin
@Suppress("LongParameterList")
@Composable
fun ProductDetailRoute(
    productId: String,
    viewModel: ProductDetailViewModel = hiltViewModel(),
    navigator: ProductNavigator,
    onAddToCart: (Product) -> Unit,
    onShareProduct: (Product) -> Unit,
    modifier: Modifier = Modifier
) {
    // 6+ parameters is normal for feature entry points
}
```

#### `@Suppress("CyclomaticComplexMethod")`
Composables handling UI state often have multiple `when` branches for different states.

```kotlin
@Suppress("CyclomaticComplexMethod")
@Composable
fun ProductsContent(state: ProductsUiState) {
    when (state) {
        is Loading -> LoadingIndicator()
        is Empty -> EmptyState()
        is Error -> ErrorMessage(state.message)
        is Success -> when {
            state.products.isEmpty() -> EmptyProductsList()
            state.isFiltered -> FilteredProductsList(state.products, state.filter)
            else -> ProductsList(state.products)
        }
    }
    // Multiple when branches for UI states is normal
}
```

**Placement:** Place `@Suppress` directly above `@Composable`:
```kotlin
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun MyScreen() { /* ... */ }
```

### Targeted Suppressions

#### `@Suppress` on Catch Parameters
For `TooGenericExceptionCaught`, place `@Suppress` on the catch parameter for maximum precision:

```kotlin
try {
    riskyOperation()
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
    // Sometimes catching Exception is the right choice
    // (e.g., unknown third-party library exceptions)
    crashReporter.recordException(e)
}
```

This is more precise than suppressing the entire function.

#### File-Level Suppressions

Use `@file:Suppress` when an issue affects the entire file:

```kotlin
@file:Suppress("MatchingDeclarationName")
package com.example.ui.view

// File: AnAnimatedComposableExampleView.kt
// Contains helper enum + main composable

enum class CirclePosition { START, END }

@Composable
fun AnAnimatedComposableExampleView(...) { /* ... */ }
```

**Use `@file:Suppress` when:**

- `MatchingDeclarationName`: primary composable plus supporting types (enums, sealed classes, data classes) share one file.
- `TooManyFunctions`: composable files with many tiny helpers.
- `MagicNumber`: UI files dense with layout constants.

### Forbidden suppressions

Do not suppress without fixing the root cause when:
- `ComplexMethod` in ViewModels or business logic → Refactor the code
- `LongParameterList` in data classes → Consider builder pattern or DSL
- `TooGenericExceptionCaught` when you can handle specific exceptions → Use specific catches
- `UnusedPrivateProperty` → Remove the property

## Suppression rules

Required:
- Fix the violation. Suppress only when the rule does not apply (e.g., `LongMethod` on a `@Composable`).
- Add a one-line `// Suppressed because <reason>` comment next to every `@Suppress`.
- Use the narrowest scope: catch parameter > single declaration > `@file:Suppress`.
- Re-audit suppressions on every CI baseline regeneration.

Forbidden:
- Suppressing rules in ViewModel, repository, use case, or other non-`@Composable` code without refactor.
- File-level suppression of `MagicNumber`, `ComplexMethod`, `LongParameterList` in data/domain layers.
- Adding a suppression labeled "temporary" without a tracked follow-up.
