Custom Detekt Rules

SkillDev tools

Custom detekt rules, DI/privacy/suppression guardrails, detekt.yml configuration, and false-positive triage.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Custom Detekt Rules skill

What this skill tells your AI

The instructions your AI receives, as published by po4yka/ripdpi in .agents/skills/detekt-custom-rules/SKILL.md and read by ahel’s review.

Custom rule set diGuardrails in module :quality:detekt-rules. Enforces DI, Hilt, suppression, and resolver-log privacy conventions at compile time.

Existing Rules

RuleWhat It CatchesWhy
InjectConstructorDefaultParameterDefault values on @Inject constructor paramsHilt ignores defaults, causing silent behavior mismatch
HiltViewModelApplicationContext@ApplicationContext in @HiltViewModel constructorsForces narrower abstractions, improves testability
DisallowNewSuppression@Suppress without an adjacent ROADMAP-architecture-refactor allowlist commentPrevents new lint debt from becoming invisible
NoResolverIpInLogsResolver or upstream IP identifiers interpolated into log callsKeeps user-visible network state out of logs

Module Structure

quality/detekt-rules/
  src/main/kotlin/com/poyka/ripdpi/quality/detekt/
    RipDpiRuleSetProvider.kt           -- Registers all rules (id: "diGuardrails")
    AnnotationMatchers.kt              -- Shared helpers: hasAnnotation(), findAnnotation()
    InjectConstructorDefaultParameter.kt
    HiltViewModelApplicationContext.kt
    DisallowNewSuppression.kt
    NoResolverIpInLogs.kt
  src/test/kotlin/com/poyka/ripdpi/quality/detekt/
    InjectConstructorDefaultParameterTest.kt
    HiltViewModelApplicationContextTest.kt
    DisallowNewSuppressionTest.kt
    NoResolverIpInLogsTest.kt

Creating a New Rule

Step 1: Write the Rule Class

Create src/main/kotlin/com/poyka/ripdpi/quality/detekt/YourRuleName.kt:

package com.poyka.ripdpi.quality.detekt

import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.psi.KtClass

class YourRuleName(config: Config) : Rule(config) {
    override val issue: Issue =
        Issue(
            id = "YourRuleName",
            severity = Severity.Defect,
            description = "One-line description of what this catches.",
            debt = Debt.FIVE_MINS,
        )

    override fun visitClass(klass: KtClass) {
        super.visitClass(klass)
        // Use hasAnnotation() from AnnotationMatchers.kt
        if (!klass.hasAnnotation("SomeAnnotation")) return

        // Check condition and report
        report(
            CodeSmell(
                issue = issue,
                entity = Entity.from(klass),
                message = "Actionable message: what to do instead.",
            ),
        )
    }
}

Step 2: Register in RuleSetProvider

Add to RipDpiRuleSetProvider.kt:

override fun instance(config: Config): RuleSet =
    RuleSet(
        ruleSetId,
        listOf(
            InjectConstructorDefaultParameter(config),
            HiltViewModelApplicationContext(config),
            DisallowNewSuppression(config),
            NoResolverIpInLogs(config),
            YourRuleName(config),  // <-- add here
        ),
    )

Step 3: Activate in detekt.yml

Add to config/detekt/detekt.yml:

diGuardrails:
  active: true
  InjectConstructorDefaultParameter:
    active: true
  HiltViewModelApplicationContext:
    active: true
  NoResolverIpInLogs:
    active: true
  YourRuleName:               # <-- add here
    active: true

DisallowNewSuppression is registered in RipDpiRuleSetProvider and inherits the active diGuardrails rule-set setting; it has no per-rule override in the current config.

Step 4: Write Tests

Create src/test/kotlin/com/poyka/ripdpi/quality/detekt/YourRuleNameTest.kt:

package com.poyka.ripdpi.quality.detekt

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.test.compileAndLint
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class YourRuleNameTest {
    private val rule = YourRuleName(Config.empty)

    @Test
    fun `reports violation on annotated class`() {
        val code = """
            @SomeAnnotation
            class Bad { /* violating pattern */ }
        """.trimIndent()
        val findings = rule.compileAndLint(code)
        assertEquals(1, findings.size)
    }

    @Test
    fun `allows non-annotated class`() {
        val code = """
            class Fine { /* same pattern but no annotation */ }
        """.trimIndent()
        val findings = rule.compileAndLint(code)
        assertEquals(0, findings.size)
    }
}

Cover: positive detection, negative (no false positive), edge cases (primary vs secondary constructors, nested classes).

Annotation Matching

Use the shared helpers from AnnotationMatchers.kt:

// Check if a KtAnnotated element has an annotation by simple name
klass.hasAnnotation("HiltViewModel")        // matches @HiltViewModel
constructor.hasAnnotation("Inject")          // matches @Inject

// Find the annotation entry (for Entity.from targeting)
parameter.findAnnotation("ApplicationContext")  // returns KtAnnotationEntry?

Important: These match by simple name, not fully-qualified name. hasAnnotation("Inject") matches both @javax.inject.Inject and @com.google.inject.Inject.

Common PSI Visitor Methods

MethodWhen To Use
visitClass(KtClass)Checking class-level patterns
visitPrimaryConstructor(KtPrimaryConstructor)Constructor parameter rules
visitSecondaryConstructor(KtSecondaryConstructor)Must handle alongside primary
visitNamedFunction(KtNamedFunction)Function-level rules
visitProperty(KtProperty)Property declaration rules

Use getStrictParentOfType<KtClassOrObject>() to navigate from a constructor to its owning class.

Convention Plugin

ripdpi.android.detekt.gradle.kts configures detekt for all modules:

  • Parallel: true
  • Config: config/detekt/detekt.yml
  • Reports: HTML + XML
  • JVM target: 17
  • Excludes: **/build/**, **/generated/**, **/jni/**, **/cpp/**
  • Baseline: Auto-detected from detekt-baseline.xml if present in module
  • Plugin deps: Custom rules (:quality:detekt-rules) + detekt-compose-rules

Run detekt:

./gradlew staticAnalysis          # detekt + ktlint + lint
./gradlew detektDebug             # detekt only (single variant)

Common Mistakes

MistakeFix
Forgetting to register in RipDpiRuleSetProviderRule exists but never runs. Add to the listOf(...) in instance().
Using FQN for annotation matchinghasAnnotation("javax.inject.Inject") won't match. Use simple name: "Inject".
Not activating in detekt.ymlRule is registered but inactive. Add YourRuleName: { active: true } under diGuardrails:.
Testing only primary constructorsMany rules apply to both primary and secondary. Override both visitor methods.
Reporting on wrong entityUse Entity.from(specificElement) not Entity.from(constructor) -- highlights the exact problem location.
Missing super.visitX() callAlways call super.visitPrimaryConstructor(constructor) etc. to continue traversal.

See Also

  • config/detekt/detekt.yml -- Full detekt configuration
  • build-logic/convention/src/main/kotlin/ripdpi.android.detekt.gradle.kts -- Convention plugin

Signals

GitHub stars
69
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
detekt-custom-rules
Source
github.com/po4yka/ripdpi