{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreigqd6nposko54qlqrijugjndg37sd47ibkv5aonmw5762pg4frwwa",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpk7g4mqxqt2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiguknyt7taoy6a6gvjnihsqzkozj2o57imijxdtur57uulxeuiwju"
},
"mimeType": "image/webp",
"size": 147398
},
"path": "/willyshakes/detekt-explained-enhance-kotlin-projects-with-static-analysis-part-1-165k",
"publishedAt": "2026-06-30T23:23:41.000Z",
"site": "https://dev.to",
"tags": [
"kotlin",
"detekt",
"staticanalysis",
"wilfried.hashnode.dev",
"https://github.com/detekt/detekt/blob/main/detekt-core/src/main/resources/default-detekt-config.yml",
"PMD 6.10.0 release notes — Kotlin CPD support",
"PMD 7.0.0-rc1 release notes — experimental Kotlin support",
"Gradle issue #7028",
"detekt GitHub repository",
"@Suppress"
],
"textContent": "This article was originally published on wilfried.hashnode.dev\n\nStatic analysis isn't about making your code \"look nice.\" It's about catching the kind of problems that quietly slow teams down: hidden complexity, fragile logic, and maintainability debt.\n\nIn most Android codebases, quality doesn't degrade because engineers don't care—it degrades because there's no consistent, automated feedback loop.\n\nThat's where detekt comes in.\n\nThis article kicks off a practical series on using detekt in real projects—from zero setup to custom rules and CI enforcement.\n\nIn this first part, we'll cover:\n\n * Why Detekt Exists\n\n * What Makes Detekt Different\n\n * How Detekt Works\n\n * Rules and Rule Sets\n\n * Extensibility\n\n * Production-Ready Setup\n\n\n\n\n## Why Detekt Exists\n\nWhen Kotlin 1.0 was released in 2016, it solved many problems—null safety, conciseness, better readability.\n\nBut it created a new one: tooling.\n\nJava teams relied on Checkstyle, PMD, and FindBugs — none of which translated to Kotlin. Checkstyle physically can't parse .kt files. PMD took seven years to add experimental Kotlin support, held back by compiler instability and the cost of building custom ANTLR parsers. FindBugs died in 2015, before Kotlin 1.0 was released.\n\nFindBugs could technically handle Kotlin because it operates on Java bytecode which has the same format as Kotlin bytecode. The problem is FindBugs will give false positives because Kotlin provides advanced structural features that do not natively exist in Java (null safety, data classes, coroutines…)\n\nLet's take the example of a Kotlin data class.\n\nKotlin:\n\n\n\n data class User(val name: String, val age: Int)\n\n\nJava bytecode representation:\n\n\n\n public final class User {\n private final String name;\n private final int age;\n public User(String name, int age) {\n this.name = name;\n this.age = age;\n }\n // Synthesized component methods for destructuring\n public final String component1() { return this.name; }\n public final int component2() { return this.age; }\n // Synthesized copy mechanism\n public final User copy(String name, int age) { return new User(name, age); }\n }\n\n\nFindBugs sees `component1()`, `component2()`, `copy()` as suspicious methods with no obvious call sites in the code — because they're used via Kotlin destructuring which compiles away. It would flag them as dead code or unused methods. That's the false positive.\n\n### The gap Artur Bosch filled\n\nKotlin 1.0 launched in 2016. Detekt also started in 2016. In 2016, the Android and Kotlin ecosystem was at a massive turning point. The landscape was predominantly Java-based, but the official release of Kotlin 1.0 in February sparked a quiet revolution among developers eager to escape Java's verbosity and NullPointerExceptions. It was the right time for a tool like detekt to emerge — it was a bet. If Kotlin became a success, it would become the first Kotlin static analyzer tool in the space.\n\nJetBrains was focused on adoption, not tooling — which left room for the community to fill the gap. Detekt was created by Artur Bosch in 2016 to fill that gap: a static analysis tool built specifically for Kotlin.\n\nToday:\n\n * It has hundreds of contributors\n\n * It's actively maintained\n\n * It's widely adopted across serious Kotlin codebases\n\n\n\n\nSponsors include Emerge, CodeRabbit, PostHog, American Express, and Doist.\n\nMore importantly, it has survived Kotlin's evolution — which is the real signal of maturity.\n\n### How detekt survived for 10 years\n\nKotlin has evolved from a niche JVM language designed to fix Java's stagnation into a mature, multiplatform ecosystem backed by Google and JetBrains. Detekt emerged in 2016 with the first stable release of Kotlin and survived 10 years by adapting to Kotlin changes. It evolved from a simple standalone code smell scanner into a deeply integrated, highly extensible compiler plugin that enforces best practices, security, and architectural rules in both pure Kotlin and Android projects.\n\nHere are the major moves detekt made to adapt:\n\n**Google endorsing Kotlin in 2019 → detekt became a Gradle plugin.** Android developers were forced to migrate from Java to Kotlin. Before the Gradle plugin, the only way to run detekt on Android was via the command line or manual Gradle task wiring. That was a barrier. A first-class Gradle plugin meant detekt could plug directly into the Android build lifecycle — run automatically on every build, integrate with lint reports, fit the workflow Android developers already had.\n\n**Kotlin went multiplatform with KMP → detekt added KMP support.** A tool that only worked on Android/JVM would have become irrelevant to half the Kotlin community overnight.\n\n**Kotlin introduces Kotlin compiler plugin → Detekt 2.0 brings native compilation with a Kotlin compiler plugin.** Detekt now leverages the speed of the compiler for the analysis.\n\n## What Makes Detekt Different\n\nDetekt doesn't operate on raw text. It uses the Kotlin compiler's PSI (Program Structure Interface).\n\nIn practical terms: it understands your code the same way the compiler does.\n\nThat enables deeper analysis:\n\n * Detecting complex functions\n\n * Identifying large classes doing too much work\n\n * Spotting unsafe patterns, not just style violations\n\n\n\n\nThis is a key distinction from formatting tools like ktlint.\n\n * ktlint → enforces how your code looks\n\n * detekt → evaluates how your code behaves and evolves over time\n\n\n\n\nWe will use the rule `ComplexMethod`. `ComplexMethod` is a rule of the complexity rule set. It measures the cyclomatic complexity of a function or constructor — the number of independent branching paths. It matters because a complex method is hard to maintain in a codebase. A text-based tool can count lines of code but cannot understand the structure of the code like PSI can.\n\nLet's see this snippet:\n\n\n\n private fun processItems(items: List<Item>) {\n items.forEach { item ->\n if (item.isActive) {\n if (item.type == Type.A) {\n if (item.value > 100) {\n compute()\n } else if (item.value > 50) {\n computeFallback()\n }\n } else if (item.type == Type.B) {\n when (item.priority) {\n Priority.HIGH -> handleHigh()\n Priority.LOW -> handleLow()\n else -> handleDefault()\n }\n }\n } else {\n handleInactive(item)\n }\n }\n }\n\n\nktlint: ✅ perfectly formatted detekt: ❌ flags `ComplexMethod` — cyclomatic complexity: 11\n\nIn this context, the issue isn't formatting — it's structural complexity that no text-based tool can measure.\n\n## How Detekt Works\n\nDetekt cannot analyze raw source code because it is just lines and keywords without structure to analyse.\n\nThat's why detekt first parses and converts raw source code into an AST (Abstract Syntax Tree). The AST gives detekt a structured representation of your code as nodes — conditions, classes, methods, variables.\n\nThe parsing creates a queryable structure the rules can traverse node by node and measure: count expressions, check names, depths, compare to thresholds.\n\nEach violation found by rules during node traversal becomes an issue. At the end of the process, detekt produces a report with all found issues. Reports are produced in many formats:\n\n * HTML for humans\n\n * SARIF for CI/code scanning\n\n * Markdown for lightweight reporting\n\n\n\n\n## Rules and Rule Sets\n\nRules are grouped into rule sets. Each targets a specific category of issues.\n\nCommon rule sets include complexity, style, performance, potential bugs and coroutines.\n\n**Complexity** is a rule set about complexity that can be found in the code.\n\n`ComplexCondition` checks if a condition has more than 3 expressions, to ensure simplicity.\n\n\n\n if (temperature < 32 && pression < 100 && weight > 200) {\n println(\"this is the right combination\")\n }\n\n\nDetekt flags this as `ComplexCondition`.\n\n\n\n if (temperature < 32 && pression < 100) {\n println(\"this is the right combination\")\n }\n\n\nDetekt finds no issues.\n\n**Style** is a rule set about code style.\n\n`FunctionNaming` checks if a function name complies with the Kotlin style guide.\n\n\n\n fun IsTheCarNew() {\n }\n\n\nDetekt flags this as bad naming.\n\n\n\n fun isTheCarNew() {\n }\n\n\nDetekt finds no issues.\n\n**Performance** is a rule set about performance issues detectable from static analysis.\n\n`UnnecessaryInitOnArray` flags useless initialization of arrays.\n\n\n\n val list = IntArray(5) { 0 }\n\n\nDetekt flags this as unnecessary initialization.\n\n\n\n val list = IntArray(5)\n\n\nDetekt finds no issues.\n\n**Potential bugs** is a rule set about bugs that can be caught by looking at the code.\n\n`EqualsAlwaysReturnsTrueOrFalse` checks if an `equals` method always returns `true` or `false`.\n\n\n\n val isRunning = true\n\n override fun equals(other: Any?): Boolean {\n return isRunning\n }\n\n\nDetekt flags this as `EqualsAlwaysReturnsTrueOrFalse`.\n\n\n\n val isCar = true\n var isRunning = true\n\n override fun equals(other: Any?): Boolean {\n if (isCar) { isRunning = false }\n return isRunning\n }\n\n\nDetekt finds no issues.\n\n**Coroutines** is a rule set about the correct use of coroutines.\n\n`GlobalCoroutineUsage` flags coroutines launched with `GlobalScope`, which bypasses structured concurrency.\n\n\n\n GlobalScope.launch {\n val record = withContext(Dispatchers.IO) {\n fetchLastRecordFromServer()\n }\n withContext(Dispatchers.Main) {\n component.displayLastRecord(record)\n }\n }\n\n\nDetekt flags this as `GlobalCoroutineUsage`.\n\n\n\n val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)\n\n applicationScope.launch {\n // your coroutine work here\n }\n\n\nDetekt finds no issues.\n\n## Extensibility\n\nOut of the box, detekt rules are useful. But custom rules are where detekt becomes a real engineering tool.\n\nDetekt lets you encode your team's architecture decisions as automated guardrails. This is done by creating custom rules.\n\nHere are 3 examples of architecture decisions you can encode with custom rules:\n\n * No ViewModel should exceed a certain complexity threshold\n\n * Repository methods must return `Result<T>`\n\n * No direct Retrofit usage outside the data layer\n\n\n\n\nLet's see what those check at the AST level to act as guardrails.\n\nFor the ViewModel rule, we go to the class node, check if it extends `ViewModel`, then check cyclomatic complexity, raise an issue if above threshold. The threshold can be a parameter of the rule.\n\nFor the Repository rule, we go to the class node, check if it contains the keyword \"repository\", iterate the methods and check return type, raise issue if not `Result<T>`.\n\nFor the Retrofit rule, we visit call expression nodes, check if the call references an API service interface, then verify the containing package. If the package is outside the data layer, raise an issue.\n\nFor some detail about custom rule creation: extend the `Rule` class, implement the visitor methods you need, register the custom rule via `RuleSetProvider`, declare in `META-INF`, activate in `detekt.yml`.\n\nThis is what turns detekt from a linter into an architectural safety net.\n\n## Production-Ready Setup\n\nLet's set up detekt in a real Kotlin or Android project.\n\n### Step 1: Add the plugin\n\n\n plugins {\n id(\"io.gitlab.arturbosch.detekt\") version(\"1.23.8\")\n }\n\n repositories {\n mavenCentral()\n }\n\n\nQuick note about the detekt plugin ID: a new plugin ID was added in version 2 (currently in alpha) — `dev.detekt`. For this production-grade setup, use the stable ID above.\n\nThis gives you tasks depending on your project variants. For Android developers, by default you get `detektDebug` and `detektRelease`.\n\n### Step 2: Add configuration\n\nCreate a `detekt.yml` file. This file controls:\n\n * Enabled or disabled rules\n\n * Thresholds, such as max function length\n\n * Build failure conditions\n\n * Report formats\n\n\n\n\nStart from the default config:\n\nhttps://github.com/detekt/detekt/blob/main/detekt-core/src/main/resources/default-detekt-config.yml\n\nThen customize minimally. A common mistake is over-configuring too early. Start strict on a few rules, then expand.\n\n### Step 3: Run analysis\n\nDetekt gives you multiple tasks depending on what level of analysis you need.\n\nYou can run the analysis without type resolution:\n\n\n\n ./gradlew detekt\n\n\nYou can also run the analysis with type resolution. Type resolution gives detekt one more capability — the ability to know exactly the type and symbols across your codebase. This is particularly useful when spotting bugs or code smells.\n\nLet's see an example. There is a rule called `UnnecessarySafeCall` that detects when you use the safe call operator unnecessarily.\n\n\n\n // getVersionFromDb() returns String, not String?\n fun getVersionFromDb(): String = \"1.0.0\"\n\n // detekt without type resolution: silent\n // detekt with type resolution: flags UnnecessarySafeCall\n getVersionFromDb()?.let { print(\"Version: $it\") }\n\n\nTo run with type resolution, use `detektDebug` or `detektRelease` for Android projects.\n\n### Step 3.1: Activate your baseline\n\nWith detekt you can ignore legacy debt initially with a baseline file. Here is the command to generate one:\n\n\n\n ./gradlew detektBaseline\n\n\nLet's look at the format:\n\n\n\n <SmellBaseline>\n <ManuallySuppressedIssues>\n <ID>CatchRuntimeException:Junk.kt$e: RuntimeException</ID>\n </ManuallySuppressedIssues>\n <CurrentIssues>\n <ID>NestedBlockDepth:Indentation.kt$Indentation$override fun procedure(node: ASTNode)</ID>\n <ID>TooManyFunctions:LargeClass.kt$dev.detekt.rules.complexity.LargeClass.kt</ID>\n <ID>ComplexMethod:DetektExtension.kt$DetektExtension$fun convertToArguments(): MutableList<String></ID>\n </CurrentIssues>\n </SmellBaseline>\n\n\nThe baseline saves all current issues in `baseline.xml`, letting your team move forward while keeping the codebase clean from that point in time.\n\nThe workflow to progressively shrink the list: remove one issue at a time from the baseline, fix it, run detekt again. If the issue no longer appears, it's resolved. If it does, keep fixing.\n\nQuick note on `ManuallySuppressedIssues`: these are issues you voluntarily suppress with `@Suppress` because you accept them based on your context. This tag helps detekt avoid false positives.\n\n### Step 4: Fail the build when ready\n\nOnce the configuration is stable, add the following to your Gradle project build file:\n\n\n\n detekt {\n buildUponDefaultConfig = true\n allRules = false\n config.setFrom(files(\"detekt.yml\"))\n ignoreFailures = true\n }\n\n\nSet `ignoreFailures = true` during initial rollout so detekt reports issues without blocking the build. Once your baseline is stable and the team is ready, flip it to `false` to enforce failures in CI.\n\n## What's Coming Next\n\nIn Part 2, we'll go deeper into:\n\n * Writing custom rules\n\n * Integrating detekt with CI/CD properly\n\n * Avoiding rule overload\n\n * Scaling detekt across large Android codebases\n\n\n\n\n## Sources\n\n * PMD 6.10.0 release notes — Kotlin CPD support\n\n * PMD 7.0.0-rc1 release notes — experimental Kotlin support\n\n * Gradle issue #7028\n\n * detekt GitHub repository\n\n\n",
"title": "Detekt Explained: Enhance Kotlin Projects with Static Analysis - part 1"
}