Skip to content
cccskills
FreePublished on GitHub

detekt-custom-rules

Register, activate, and test a custom detekt rule in :quality:detekt-rules. Use when adding a DI/privacy/suppression guardrail rule or debugging a false positive.

The English summary is being prepared. Showing the original description.

Works withGitLab
See how to install

Files included (1)

  • SKILL.md7.8 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Custom Detekt Rules

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

Existing Rules

scripts/ci/check_harness_manifests.py (validate_factual_ground_truth) mechanically checks that every rule registered in RipDpiRuleSetProvider.kt appears in this table -- omitting a rule here fails CI rather than silently drifting.

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/<YourRuleName>Test.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

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

RIPDPI's own Compose conventions: ViewModel pattern, Route/Screen split, DataStore->StateFlow, RipDpiThemeTokens. Use for how this app does Compose, not generic Compose API questions (see compose).

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 21, 2026

ADB-based RIPDPI device/emulator debugging: build/install/launch, logcat filtering, fixture port-forwarding, instrumented tests, crash/ANR triage. Use when debugging on a real device or emulator.

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 21, 2026

RIPDPI Appium launch-contract reference: start routes and permission/service/data presets. Use when a test launches to the wrong screen or a new automation route is added. General flakiness: appium-test-debug.

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 21, 2026

RIPDPI Appium test authoring: page objects, resource-id locators, assertions, wait tiers. Use when writing a new Appium test or page object, or adding coverage for a new screen.

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 21, 2026

RIPDPI Appium failure triage: flaky tests, locator/session/wait issues, screenshot and element-tree debugging. Use when an Appium test fails or is flaky.

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 21, 2026

Add or modify a GitHub Actions job. Use when editing a file under .github/workflows/. Not for running a workflow locally (local-ci-act) or release signing (release-signing).

The English summary is being prepared. Showing the original description.

po4yka/RIPDPI81Updated Oct 21, 2026

See all skills by po4yka

Report a problem with this skill