How do you write a custom detekt rule and register it as a `RuleSetProvider`, and what core APIs are involved?
answer
- Extend Rule, declare Issue, override visit<Element> PSI callbacks
- report(CodeSmell(issue, Entity.from(el), msg))
- RuleSetProvider.instance() returns RuleSet(id, rules)
- Register FQN in META-INF/services/...RuleSetProvider (ServiceLoader)
- Add jar via detektPlugins; test with lint/compileAndLint
basics
~20 sYou write a class that extends detekt's Rule, override the visit method for the code you care about, and report findings. Then you bundle it in a RuleSetProvider and put that module on detekt's plugin classpath.
solid answer
~40 sA custom rule extends `Rule` (from `detekt-api`), overrides an `issue` describing the smell (id, `Severity`, description), and overrides PSI visitor callbacks such as `visitNamedFunction`, `visitCallExpression`, or `visitClass`. Inside, you inspect the `KtElement` and call `report(CodeSmell(issue, Entity.from(element), message))` when the smell is found. For semantic checks you set `@RequiresTypeResolution` and read `bindingContext`. You group rules in a class implementing `RuleSetProvider`, which returns a `RuleSet("my-rules", listOf(MyRule(config)))`, and register that provider's fully-qualified name in `META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider` (Java ServiceLoader). You ship it as a jar added to the `detektPlugins` Gradle configuration. Test rules with `detekt-test`'s `lint(...)`/`compileAndLint(...)` helpers and `Case` fixtures. Each rule reads its own config via `valueOrDefault(...)` so users can tune it in `detekt.yml`.
code
kotlin · 12 linesclass NoTodoComment(config: Config) : Rule(config) {
override val issue = Issue("NoTodoComment", Severity.Maintainability, "No TODO comments allowed.", Debt.FIVE_MINS)
override fun visitComment(comment: PsiComment) {
super.visitComment(comment)
if (comment.text.contains("TODO")) report(CodeSmell(issue, Entity.from(comment), "Resolve the TODO."))
}
}
class AcmeProvider : RuleSetProvider {
override val ruleSetId = "acme"
override fun instance(config: Config) = RuleSet(ruleSetId, listOf(NoTodoComment(config)))
}
// META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider -> com.acme.AcmeProvidergo deeper
Aware custom rules are possible but unlikely to know the API surface.
Can sketch extending Rule and reporting a CodeSmell from a visit method.
Knows RuleSetProvider, ServiceLoader registration, detektPlugins wiring, config reading, and testing helpers.
Weighs maintaining custom rules vs config tuning, distribution/versioning of the rules jar, and typed-rule cost.
## The pieces A custom rule has three parts: the **rule class**, a **RuleSetProvider**, and **registration** so detekt discovers it. ## 1. The rule Extend `Rule` from the `detekt-api` artifact. You declare an `Issue` (its identity and metadata) and override one or more **PSI visitor** methods. detekt's PSI is the Kotlin/IntelliJ syntax tree; methods are named `visit<Element>`. ```kotlin import io.gitlab.arturbosch.detekt.api.* import org.jetbrains.kotlin.psi.KtNamedFunction class NoFunctionNamedFoo(config: Config) : Rule(config) { override val issue = Issue( id = "NoFunctionNamedFoo", severity = Severity.Style, description = "Functions must not be named 'foo'.", debt = Debt.FIVE_MINS, ) private val allowed = valueOrDefault("allowedNames", emptyList<String>()) override fun visitNamedFunction(function: KtNamedFunction) { super.visitNamedFunction(function) if (function.name == "foo" && "foo" !in allowed) { report(CodeSmell( issue, Entity.from(function), "Rename 'foo' to something descriptive.", )) } } } ``` Key APIs: `Issue` (id/severity/description/debt), `report(CodeSmell(...))` to emit a finding, `Entity.from(element)` to locate it, and `valueOrDefault(key, default)` to read per-rule config from `detekt.yml`. Newer detekt versions also offer `requiresTypeResolution()` and reading `bindingContext` for semantic rules (mark with `@RequiresTypeResolution`). ## 2. The RuleSetProvider Group rules into a set: ```kotlin import io.gitlab.arturbosch.detekt.api.RuleSet import io.gitlab.arturbosch.detekt.api.RuleSetProvider import io.gitlab.arturbosch.detekt.api.Config class MyRuleSetProvider : RuleSetProvider { override val ruleSetId = "my-rules" override fun instance(config: Config): RuleSet = RuleSet(ruleSetId, listOf(NoFunctionNamedFoo(config))) } ``` ## 3. Registration via ServiceLoader detekt finds providers through Java's **ServiceLoader**. Add a file: ``` src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider ``` containing the FQN of your provider, e.g. `com.acme.detekt.MyRuleSetProvider`. ## 4. Wiring it into a build Publish the jar, then in the target project: ```kotlin dependencies { detektPlugins("com.acme:my-detekt-rules:1.0.0") } ``` The `detektPlugins` configuration puts your jar on detekt's **plugin classpath** so the new rule set loads. Users enable/configure it in `detekt.yml` under `my-rules:`. ## 5. Testing Use the `detekt-test` artifact: `lint(code)` runs a rule on a snippet and returns findings; `compileAndLint(code)` also compiles (needed for typed rules). Assert on `findings.size` and messages. ## Summary mental model Rule = a PSI visitor that `report`s smells. RuleSetProvider = a labeled bundle. ServiceLoader entry = how detekt discovers your bundle. `detektPlugins` = how your jar reaches detekt at build time.
- How does detekt discover your custom rule set at runtime?Via Java's ServiceLoader: it reads META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider for the provider's fully-qualified class name on the detektPlugins classpath.
- How do you let users configure your rule's behavior?Read options with valueOrDefault("key", default) in the rule; users set them under your ruleSetId in detekt.yml.
saying these in an interview costs you the question
- Forgetting the META-INF/services registration so the rule never loads
- Adding the rules jar to the normal classpath instead of detektPlugins
- Not calling super in visit methods, breaking tree traversal
- Hardcoding behavior instead of reading config via valueOrDefault
- Confusing ktlint custom rules (RuleProvider) with detekt's RuleSetProvider API