| name | detekt-custom-rules |
| description | Custom detekt rules, DI/privacy/suppression guardrails, detekt.yml configuration, and false-positive triage. |
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
| Rule | What It Catches | Why |
|---|
InjectConstructorDefaultParameter | Default values on @Inject constructor params | Hilt ignores defaults, causing silent behavior mismatch |
HiltViewModelApplicationContext | @ApplicationContext in @HiltViewModel constructors | Forces narrower abstractions, improves testability |
DisallowNewSuppression | @Suppress without an adjacent ROADMAP-architecture-refactor allowlist comment | Prevents new lint debt from becoming invisible |
NoResolverIpInLogs | Resolver or upstream IP identifiers interpolated into log calls | Keeps 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)
if (!klass.hasAnnotation("SomeAnnotation")) return
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),
),
)
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:
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:
klass.hasAnnotation("HiltViewModel")
constructor.hasAnnotation("Inject")
parameter.findAnnotation("ApplicationContext")
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
| Method | When 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
./gradlew detektDebug
Common Mistakes
| Mistake | Fix |
|---|
Forgetting to register in RipDpiRuleSetProvider | Rule exists but never runs. Add to the listOf(...) in instance(). |
| Using FQN for annotation matching | hasAnnotation("javax.inject.Inject") won't match. Use simple name: "Inject". |
Not activating in detekt.yml | Rule is registered but inactive. Add YourRuleName: { active: true } under diGuardrails:. |
| Testing only primary constructors | Many rules apply to both primary and secondary. Override both visitor methods. |
| Reporting on wrong entity | Use Entity.from(specificElement) not Entity.from(constructor) -- highlights the exact problem location. |
Missing super.visitX() call | Always 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