Detekt Convention Plugin

Presumption, prerequisite and good reads πŸ“–

Detekt πŸ”

Detekt is a static code analysis tool, but it does a lot more than that, from being able to add your own custom rule to formatting your code (internally uses ktlint). It’s especially useful tool for when you are working on a large project or with a team and you need to ensure uniformity across code development setup.

Adding detekt βž•

To add detekt to our project we need to do the following things…

[versions]
..
..
detekt = "1.23.1" // Latest as of Oct 2023
detektCompose = "0.3.0" // Latest as of Oct 2023

[libraries]
..
..
detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
detekt-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" }

[plugins]
..
..
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }

We need to define related dependencies in libs.versions.toml

Let’s now add the following to Project’s build.gradle.kts

plugin {
  ..
  ..
  alias(libs.plugins.detekt)
}

And in build.gradle.kts of our convention plugin. This will allow us to use DetektExtension class and other classes in detekt plugin which allows us to configure detekt.

dependencies {
  ..
  ..
  compileOnly(libs.detekt.gradle)
}

Detekt Convention Plugin πŸ‘¨β€πŸ’»

Before we dive into the code, let’s understand what a convention plugin is.

In simple words convention plugin is gradle’s way of abstracting a responsibility and sharing build logic across gradle module, this way we are able to keep build gradle files of our module concise and ensure that we are not writing repetitive code.

Convention plugin can be used for anything, from adding dependencies to customizing build flavours. Now in Android is a great project to explore if you would like to see how convention plugins can make your life easy.

But for now, let’s check our DetektConventionPlugin.kt

class DetektConventionPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    with(project) {
      val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")

      // Apply detekt plugin to module
      pluginManager.apply(libs.findPlugin("detekt").get().get().pluginId)

      // Configure jvmTarget for gradle task `detekt`
      tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
        jvmTarget = JavaVersion.VERSION_17.toString()
      }
      // Configure jvmTarget for gradle task `detektGenerateBaseline`
      tasks.withType<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask>().configureEach {
        jvmTarget = JavaVersion.VERSION_17.toString()
      }

      // Configure detekt
      extensions.getByType<DetektExtension>().apply {
        buildUponDefaultConfig = true // preconfigure defaults.
        allRules = false // activate all available (even unstable) rules.
        autoCorrect = false // To enable or disable auto formatting.
        parallel = true // To enable or disable parallel execution of detekt on multiple submodules.
        config.setFrom("config/detekt/detekt.yml") // point to your custom config defining rules to run, overwriting default behavior.
        baseline = file("config/detekt/detekt-baseline.xml") // a way of suppressing issues before introducing detekt.
      }

      tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
        reports {
          // observe findings in your browser with structure and code snippets
          html.required.set(true)
          // similar to the console output, contains issue signature to manually edit baseline files
          txt.required.set(true)
          // simple Markdown format
          md.required.set(true)
        }
      }

      dependencies.apply {
        // You can add more detektPlugins like shown below.
        add("detektPlugins",  libs.findLibrary("detekt-compose").get()) // Add this in case you want compose rules with detekt
      }
    }
  }
}

We have successfully created our first plugin, but this plugin is not usable yet, one last step before we put it to use.

Registering Plugin βš“

We need to register this plugin, let’s go back to our convention plugin’s build.gradle.kts file and do that.

..
..
gradlePlugin { // Create this block if it doesn't exist
  plugins {
    register("detekt") { // A unique string
      id = "vlr.detekt" // This is the name we'll be using in all our modules to apply this plugin.
      implementationClass = "DetektConventionPlugin" // Class name of the plugin file which implements `Plugin` interface.
    }
  }
}

πŸŽ‰ tada… our plugin is ready for use, let’s add it in project’s build.gradle.kts

plugin {
  ..
  ..
  alias(libs.plugins.detekt)
  id("vlr.detekt") // <-- Add the `id` you've given above in gradle plugin registration.
}

and then add it to all the modules where we want to leverage detekt (for example let’s take app module) so in app/build.gradle.kts

plugin {
  ..
  ..
  id("vlr.detekt")
}

Running detekt ▢️

If we have done the above steps correctly then our ./gradlew tasks should return a task named detekt.

Now let’s run detekt task, we can do that by running the following command and it should run detekt for us.

./gradlew detekt

Note: It is possible while setting up this plugin you may run into some issues around compatibility, make sure you refer this compatibility table

Configuring detekt’s behaviour 🎯

Now let’s generate detekt config file, this file is responsible for detekt’s behaviour in your project. This file contains all the rules and their configuration which detekt is going to abide by. To generate this yml file run the following command

./gradlew detektGenerateConfig

Next step is to ensure the configuration YAML file’s path is known to detekt plugin, we can do that by ensuring that config.setFrom(..<path of config files seperated by ','>) contains that path in our DetektConventionPlugin.

If you run into a lot of warning from detekt and you want to suppress them, you can do that by generating a baseline file

./gradlew detektBaseline

Let’s ensure the baseline file’s path is known to detekt plugin, we can do that by ensuring that baseline = file(<path of baseline file>) has the correct path in our DetektConventionPlugin.

πŸŽ‰With that we have finished the integration of detekt in our project.πŸŽ‰

Closing note 🎀

Detekt is customizable and has a lot of capabilities, with it being actively developed and open source, the future seems bright for Detekt. You can also write a simple workflow file to run detekt on every PR or before generating a build or even add a pre-commit hook.

Detekt documentation is done very well and can be really insightful for someone trying to integrate it or trying to customize it.

You can check out my integration of detekt in this repository