Integrating detekt in the Workflow

Learn how to integrate the powerful detekt tool in Android app development to help detect and prevent code smells during the development process. By Harun Wangereka.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Writing Custom detekt Rules

As mentioned earlier, detekt allows you to extend its functionality by adding your own rules. Recently, the Kotlin Android Extensions plugin was deprecated. This means using Kotlin Synthetic is no longer recommended. Before the introduction of ViewBinding, this was the go-to way of accessing views in Android apps. This deprecation affects so many projects. Using detekt, you can write a specific rule to check it and fail your build if there is a synthetic import.

Note: You shouldn't be using the Kotlin Android Extensions in your projects anymore. It's used here to demonstrate how you can create custom detekt rules.

First, move to the customRules module and add these dependencies to build.gradle:

// 1
compileOnly "io.gitlab.arturbosch.detekt:detekt-api:1.17.1"
// 2
testImplementation "io.gitlab.arturbosch.detekt:detekt-api:1.17.1"
testImplementation "io.gitlab.arturbosch.detekt:detekt-test:1.17.1"
testImplementation "org.assertj:assertj-core:3.19.0"
testImplementation 'junit:junit:4.13.2'

This code:

  1. Adds the detekt API dependency. You need this for writing the custom rules.
  2. Adds test dependencies. To test your rules you need detekt-test. It also requires assertj-core as a dependency.

Do a Gradle sync to add this dependencies. Next, inside the com.raywenderlich.android.customrules.rules package in the customRules module, add a new file. Name it NoSyntheticImportRule.kt and add the following:

package com.raywenderlich.android.customrules.rules

import io.gitlab.arturbosch.detekt.api.*
import org.jetbrains.kotlin.psi.KtImportDirective
//1
class NoSyntheticImportRule : Rule() {
  //2
  override val issue = Issue("NoSyntheticImport",
      Severity.Maintainability, "Don’t import Kotlin Synthetics "
      + "as it is already deprecated.", Debt.TWENTY_MINS)
  //3
  override fun visitImportDirective(
      importDirective: KtImportDirective
  ) {
    val import = importDirective.importPath?.pathStr
    if (import?.contains("kotlinx.android.synthetic") == true) {
      report(CodeSmell(issue, Entity.from(importDirective),
          "Importing '$import' which is a Kotlin Synthetics import."))
    }
  }
}

Here's what's happening:

  1. NoSyntheticImportRule extends Rule from the detekt API.
  2. This is a method from Rule that defines your issue. In this case, you create an issue named NoSyntheticImport. You specify the debt for the issue, the severity of the issue and the message for detekt to show. The debt represents the time you need to fix the issue.
  3. This method checks imports in your files and classes. Inside here you have a check to see if any of the import contains Kotlin synthetics. If it does, you report the code smell with a message.

Adding Your Own Rule to detekt

With your rule class complete, your need to create a RuleSetProvider. This class lets detekt know about your rule. You create one by implementing RuleSetProvider interface.

On the same package as the file above, create another new file. Name it CustomRuleSetProvider.kt and add the following code:

package com.raywenderlich.android.customrules.rules

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider

class CustomRuleSetProvider : RuleSetProvider {
  override val ruleSetId: String = "synthetic-import-rule"
  override fun instance(config: Config): RuleSet = RuleSet(ruleSetId, listOf(NoSyntheticImportRule()))
}

Implementing this interface provides detekt with a rule set containing your new rule. You also provide an id for your rule set. You can group as many rules as you'd like in this rule set if you have other related rules.

Next, you need to let detekt know about your CustomRuleSetProvider. Navigate to the customRules module. Open src/main/resources/META-INF/services, and you'll find io.gitlab.arturbosch.detekt.api.RuleSetProvider. Inside this file, add:

com.raywenderlich.android.customrules.rules.CustomRuleSetProvider

Here, you add the full qualified name for your CustomRuleSetProvider. detekt can now find the class, instantiate it and retrieve your rule set. This file notifies detekt about your CustomRuleSetProvider.

Woohoo! You've created your first custom rule. It's time to test if your rule is working.

Testing Your Custom Rule

Inside the test directory in the customRules module, add a new Kotlin file to the package and name it NoSyntheticImportTest.kt. Add the following code:

package com.raywenderlich.android.customrules.rules

import io.gitlab.arturbosch.detekt.test.lint
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class NoSyntheticImportTest {
  @Test
  fun noSyntheticImports() {
    // 1
    val findings = NoSyntheticImportRule().lint("""
       import a.b.c
       import kotlinx.android.synthetic.main.activity_synthetic_rule.*
       """.trimIndent())
    // 2
    assertThat(findings).hasSize(1)
    assertThat(findings[0].message).isEqualTo("Importing " +
        "'kotlinx.android.synthetic.main.activity_synthetic_rule.*' which is a Kotlin Synthetics import.")
  }
}

In the code above:

  1. You use lint() extension function, which executes the checks and returns the results. Inside the functions you've added two imports. One is compliant and the other one is non-compliant.
  2. Using the results from above, you do an assertion to check if the findings has a size of one. This is because you have one non-compliant import. You also do an assertion to check for message in your findings.

Click the Run icon on the left side of noSyntheticImports(). You'll see your test passes as shown below:

detekt custom rule test

You can see your test for the custom rule passes. This means you can use your rule in a real project. You'll learn how to do that next.

Note: Don't worry about the WARNING: An illegal reflective access operation has occurred message. It is relative to this test running on itself. This won't happen when using this rule for the app module.

Using the Custom Rule in Your Project

To use this new rule in your project, you'll need to apply the customRules module in your app module. Navigate to the app-level build.gradle file and add the following to the dependencies section:

detekt "io.gitlab.arturbosch.detekt:detekt-cli:1.17.1"
detekt project(":customRules")

The first line is relative to detekt-cli dependency. detekt requires it to run your custom rule. The second line tells detekt to use your module as a dependency in order to be able to use your custom rule.

Have a look at the SyntheticRuleActivity class. As you can see, it has Kotlin Synthetic Import. You'll use this class to test if the rule works.

import kotlinx.android.synthetic.main.activity_synthetic_rule.*

Last, you need to activate your rule set and include your rule in the configuration file. To do this, add the following code to detekt.yml just above the comments section:

synthetic-import-rule:
  active: true
  NoSyntheticImportRule:
    active: true

Run ./gradlew detekt on your terminal. You'll see these results:

Custom Rule Set Report

detekt now reports an issue under synthetic-import-rule. It shows a debt of 20 minutes and points to the class that has the import.

Congratulations! You've made your first custom rule. Next, you'll learn about processors.

Looking at Custom Processors to detekt

detekt uses processors to calculate project metrics. If you enable count processors, for instance, this is what your report looks like:

detekt Processor statistics

detekt is customizable, so you can create your own processor for the statistics that you want to see in your project. Creating a custom processor is very similar to creating a custom rule.

To create a custom processor, your class needs to implement FileProcessListener, which is called when file processing begins. You also need a visitor class that depends on the statistics you want to get for the metrics. The visitor can be for methods, loops and so on. Lastly, you need to register your custom processor qualified name on a file named io.gitlab.arturbosch.detekt.api.FileProcessListener inside src/main/resources/META-INF/services to inform detekt of your processor. That's all you have to do. You won't create a processor in this tutorial, but if you want to learn more about the custom processors, check out the custom processors documentation.

You've seen how to add custom processors to your project. Next, you'll learn how to add detekt on GitHub Actions.