How to share and publish Detekt config?

Alex Hrindii
6 min readDec 12, 2023

--

Hello! I’m Alex Hrindii, and I’d like to share my experience and solutions for eliminating code duplication in development when using the Detekt library. Let’s explore effective methods together.

Introduction

Meet Detekt, a static code analysis tool for the Kotlin programming language. Visit the project website for installation guides, rule descriptions, configuration options, and more.

If you investigate the Detekt library documentation, you’ll find information stating that Detekt doesn’t support and lacks any methods for remote uploading. The first time I encountered this issue was when my team attempted to optimize a large legacy project by publishing core modules to remote artifacts, which significantly reduced redundant build time. However, a major challenge arose as we needed to share lint and detekt conventions with repositories. This was crucial, as every repository and codebase inherently adhered to common code style rules and development approaches, including custom rules. Let’s delve into this challenge and explore potential solutions.

When might this solution be needed?

In my experience, it can be helpful in three cases:

  1. You are developing a few local projects and want to use common code analysis.
  2. In my case, you have a huge project that is separated into parts and teams. As a team lead, you want to ensure that all members adhere to the rules.
  3. You want to provide access not only to local developers but also to publish the configuration to a remote source.

Let’s attempt to address the first and third requirements.

Let’s imagine that you have a simple project without Kotlin conventions plugins. The first thing you do during the integration of Detekt is creating a custom task for configuration based on your environment. For example, you might have a basic task named detektAll.

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
id("io.gitlab.arturbosch.detekt") version("1.23.3")
}

buildscript {
repositories {
mavenCentral()
}
}

tasks.register("detektAll", io.gitlab.arturbosch.detekt.Detekt::class) {

description = "DETEKT build for all modules"
parallel = true
ignoreFailures = false
autoCorrect = false
buildUponDefaultConfig = true
setSource(project.file(projectDir))
baseline.set(project.file("$rootDir/detekt/config/baseline.xml"))
config.setFrom(files("$rootDir/detekt/config/config.yml"))
include("**/*.kt", "**/*.kts")
exclude("**/resources/**", "**/build/**")

reports {
html.required.set(true)
html.outputLocation.set(project.file("build/reports/detekt.html"))
}
}

dependencies {
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.3")
}

As you may have noticed, baseline.set and setSource refer to some local file. The main keyword is 'file'. Detekt allows providing files, and we can determine which source will be used for providing this file (local store, remote, etc.).

Let’s add a custom Gradle task that will download our configuration from a remote source, such as your own GitHub repository

tasks.register("fetchDetektConfig") {
val detektConfigUrl =
"https://github.com/impossible1770/.../config.yml"
// Replace with the actual URL

inputs.property("configUrl", detektConfigUrl)

val configFile = file("$buildDir/custom-detekt-config.yml")
val url = java.net.URL(detektConfigUrl)
val connection = url.openConnection()
connection.connect()

val inputStream = connection.getInputStream()
val outputStream = FileOutputStream(configFile)

try {
inputStream.copyTo(outputStream)
} finally {
inputStream.close()
outputStream.close()
}
}

Take into account that our constant path for saving config is $buildDir/custom-detekt-config.yml . Don’t forget to change the path inside detektAll task.

Let’s attempt to start detektAll without config.

 What went wrong:
Execution failed for task ':detektAll'.
> Provided path '/Users/../TestProject/build/custom-detekt-config.yml' does not exist!

It happens because detektAll was executed before our configuration was downloaded to the buildDir. To resolve the problem, we must declare a strict Gradle rule that ensures fetchDetektConfig is executed before detektAll

tasks.getByName("detektAll").dependsOn(tasks.named("fetchDetektConfig"))

Let’s try again, and now it is successful, go fix detekt mistakes)

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':detektAll'.
> Analysis failed with 35 weighted issues.
$buildDir/custom-detekt-config.yml

The current approach enables you to share your configuration with a remote source and integrate it into the ecosystem of projects where a common Detekt configuration is required.

Using Kotlin conventions plugins

What about cases when we don’t require as much flexibility and don’t need to upload the configuration to a separate remote source?

Then, your choice is to use conventional plugins. I believe it doesn’t matter to describe what a Build-logic module and composite projects/modules are. You can research the internet if you need to. However, if you’re not familiar with these terms, I would recommend taking the time to investigate and truly understand if it’s necessary

class DetektConventionPlugin : Plugin<Project> {

@Throws(URISyntaxException::class)
fun Any.getFileFromResource(fileName: String): File {
val classLoader: ClassLoader = javaClass.classLoader
val resource: URL? = classLoader.getResource(fileName)
return if (resource == null) {
throw IllegalArgumentException("file not found! $fileName")
} else {
val file: File = File.createTempFile("config", ".yml")
val inputStream = resource.openStream()
inputStream.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
file
}
}

@Suppress("SpreadOperator")
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("io.gitlab.arturbosch.detekt")
}

val parallelOpt = true
val sourceOpt = projectDir
val includeOpt = arrayOf("**/*.kt", "**/*.kts")
val excludeOpt = arrayOf("**/resources/**", "**/build/**")
val baseLineOpt = file("$rootDir/detekt/config/baseline.xml")
val configOpt = this@DetektConventionPlugin.getFileFromResource("detekt/config.yml")
val ignoreFailuresOpt = false
val buildUponDefaultConfigOpt = true
val autoCorrectOpt = true

tasks.register<Detekt>("detektAll") {
description = "Runs Detekt on the whole project at once."
parallel = parallelOpt
setSource(sourceOpt)
config.setFrom(configOpt)
baseline.set(baseLineOpt)
autoCorrect = autoCorrectOpt
include(*includeOpt)
exclude(*excludeOpt)
}

tasks.register<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask>("updateDetektBaseline") {
description = "Overrides current baseline for all modules"
parallel.set(parallelOpt)
ignoreFailures.set(ignoreFailuresOpt)
autoCorrect.set(autoCorrectOpt)
buildUponDefaultConfig.set(buildUponDefaultConfigOpt)
setSource(sourceOpt)
baseline.set(baseLineOpt)
config.from(configOpt)
include(*includeOpt)
exclude(*excludeOpt)
}

dependencies {
add("detektPlugins", libsDependency("detektFormatting"))
}
}
}
}

As you may have noticed, it’s a Kotlin convention plugin for configuring Detekt. It represents a more modern approach for setting up environments and building configurations. It is also utilized to configure some libraries instead of using Gradle tasks.

The main part of this code is the function getFileFromResource. It allows us to get a file from the Android resource folder. The folder is attached with the artifact when you try to build your own artifact and integrate it into some project.

All you have to do is simply connect your plugin to the root build of Gradle

plugins {
id("com.impossible.kotlin.detekt")
}

If the article garners interest within the development community, the next one will delve into more detail on how you can publish your plugins in Maven Local or in your own Artifactory. This will ensure that the size of the current article is not unduly extended.

Conclusion:

The article dives into practical solutions for mitigating code duplication challenges by sharing Detekt conventions across diverse repositories.
By addressing the complexities of configuring Detekt in a shared environment, the article offers insights into creating a seamless workflow. Developers are guided through the implementation of custom Gradle tasks, such as fetchDetektConfig, ensuring efficient sharing of configurations from remote sources like GitHub repositories.
For those seeking a more straightforward solution, the article introduces Kotlin convention plugins, providing a modern and efficient approach to setting up environments, building configurations, and handling library configurations within the Detekt framework.

--

--