skip to content

What is type resolution in detekt, why do some rules require it, and how do you enable it (and how does it differ from a plain `detekt` run)?

level: seniorimportance: should knowfreq 40%

answer

  1. Default detekt = untyped (PSI/AST only); no types
  2. Type resolution runs compiler frontend -> BindingContext
  3. Rules tagged @RequiresTypeResolution are skipped without it
  4. Enable: supply classpath + jvmTarget (detektMain/detektTest)
  5. Typed = slower, needs successful compile

basics

~20 s

Some detekt rules need to know the real types of expressions, not just the text. That extra information is type resolution. You enable it by giving detekt the compiled classpath, which makes those advanced rules work.

solid answer

~50 s

By default `detekt` does **untyped** (lexical/syntactic) analysis — it sees the AST/PSI but not resolved types, so it can't tell what `x.flatMap { }` actually operates on. **Type resolution** runs the Kotlin compiler frontend to build the `BindingContext`, giving rules access to declared and inferred types, supertypes, and symbol resolution. Rules that need it are tagged `@RequiresTypeResolution` (e.g. several `coroutines`, `potential-bugs`, and `RedundantSuspendModifier`, `IgnoredReturnValue` rules). Without it, those rules are silently skipped. In Gradle you enable it by configuring the typed tasks: use `detektMain`/`detektTest` (the source-set-aware tasks the plugin creates when you apply it alongside the Kotlin plugin) and supply `classpath` + `jvmTarget`, or set the `classpath`/`jdkHome` on a `Detekt` task. Via CLI: `detekt --classpath <cp> --jvm-target 17`. Typed runs are slower and need a successful compile, which is why the default `detekt` task stays untyped.

code

kotlin · 9 lines
kotlin
import io.gitlab.arturbosch.detekt.Detekt

// Typed run: detektMain picks up the source set's compileClasspath.
tasks.withType<Detekt>().configureEach {
    jvmTarget = "17"
    reports { sarif.required.set(true) }
}
// CLI equivalent:
// detekt --classpath "build/classes/kotlin/main:libs/*" --jvm-target 17

go deeper

for a junior

Aware that some rules need extra info but may not name type resolution precisely.

for a middle

Knows type resolution exists and that a classpath enables advanced rules.

for a senior

Explains BindingContext, @RequiresTypeResolution, source-set tasks, and the compile/perf trade-offs.

for a principal

Decides where typed runs belong in CI vs local, balancing rule coverage against build time and flakiness.

## Untyped vs typed analysis A plain `./gradlew detekt` does **syntactic** analysis: detekt parses each file into a **PSI** (Program Structure Interface) tree and inspects shapes — "is this method longer than N lines?", "is there an empty catch?". It does **not** know the *types* involved. It sees the token `flatMap` but cannot tell whether the receiver is a `List`, a `Flow`, or your own class. **Type resolution** changes that. detekt invokes the Kotlin compiler's **frontend** (analysis phase) to produce a **`BindingContext`** — the compiler's table mapping each expression to its resolved type and symbol. With it, rules can ask "what is the static type of this receiver?", "does this override a `suspend` function?", "is this return value ignored?". ## Why some rules need it Rules that reason about semantics, not just syntax, are annotated `@RequiresTypeResolution`. Examples: - `coroutines` rules like `SuspendFunWithFlowReturnType`, `RedundantSuspendModifier`. - `potential-bugs` like `IgnoredReturnValue`, `UnconditionalJumpStatementInLoop` variants, `MissingPackageDeclaration`-adjacent typed checks. - `CastToNullableType`, `UselessCallOnNotNull`. **Without type resolution these rules are skipped silently.** A common surprise: "why isn't my coroutine rule firing?" — because the run was untyped. ## Enabling it in Gradle When you apply the detekt plugin together with the Kotlin plugin, detekt creates **source-set-aware tasks** `detektMain`, `detektTest`, etc. These can be wired with the compile classpath: ```kotlin tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach { jvmTarget = "17" } // detektMain/detektTest get classpath from the Kotlin source sets; // you can also set it explicitly on a custom Detekt task: val detektTyped by tasks.registering(io.gitlab.arturbosch.detekt.Detekt::class) { setSource(files("src/main/kotlin")) classpath.setFrom(configurations.getByName("compileClasspath")) jvmTarget = "17" } ``` The essentials a typed run needs: the **`classpath`** (dependencies + compiled output so types resolve) and the **`jvmTarget`/`jdkHome`**. ## CLI ```bash detekt --input src/main/kotlin \ --classpath "libs/*.jar:build/classes/kotlin/main" \ --jvm-target 17 ``` ## Trade-offs - Typed runs are **slower** and require the code to **compile successfully** (the frontend must resolve everything). - That cost is why the default `detekt` task remains untyped; teams often run typed `detektMain`/`detektTest` in CI to get the full rule coverage. ## Mental model Untyped = grammar check on the text. Typed = the compiler hands detekt a fully annotated map of "what every expression really is," unlocking semantic rules.

  • Why might a coroutine-specific detekt rule never report anything even though it's 'active'?
    Because it's annotated @RequiresTypeResolution and the run was untyped (no classpath supplied), so detekt skipped it.
  • Why isn't type resolution on by default?
    It runs the compiler frontend, so it's slower and requires the code to compile fully; the default task stays fast and lexical.

Untyped detekt reads your essay for grammar; typed detekt also has the dictionary and footnotes, so it understands what each word actually refers to.

saying these in an interview costs you the question

  • Claiming all detekt rules work without a classpath
  • Not knowing the BindingContext / compiler frontend is involved
  • Thinking type resolution just makes detekt 'more strict' rather than enabling specific rules
  • Believing typed analysis works even when the project doesn't compile

context