6

Integrating detekt in the Workflow

 3 years ago
source link: https://www.raywenderlich.com/24470020-integrating-detekt-in-the-workflow
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Home Android & Kotlin Tutorials

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 Aug 2 2021 · Article (30 mins) · Intermediate

5/5 3 Ratings

Version

There are several ways to keep your technical debt low throughout the development process. Finding potential problems early — and minimizing them — is important. It’s also a good idea to maintain your team’s code styles and conventions and keep the cognitive complexity of methods within acceptable limits.

detekt, a static code analysis tool for Kotlin, helps you do all of this. It comes with a wide range of rule sets. It provides you with options to configure them, as well as the ability to add your own custom rules. So, adding this as a part of your CI steps can prove to be beneficial. CI stands for Continuous Integration, a process of ensuring features and bug fixes are added to an application regularly.

In this tutorial, you’ll build DetektSampleApp, an app that shows detekt rule sets with their details. During the process you’ll learn about:

  • detekt and its features.
  • Adding detekt to your project.
  • Rule sets available in detekt.
  • Writing custom rules and processors.
  • Integrating detekt into your IDE.
  • Adding detekt to GitHub Actions.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Open Android Studio 4.2.1 or later and import the starter project. Then, build and run the project. You’ll see the following screen:

The app shows a list of detekt rule sets. Each rule set has a short description of what it does. Clicking on any of the rule sets takes you to a view that loads the official documentation from the detekt website.

Understanding detekt

detekt is a static analysis tool for the Kotlin programming language. It tries to improve your codebase by enforcing a set of rules. You can integrate it with your CI to help avoid code smells on your codebase. This is helpful — especially when working with a team of developers.

detekt has several features that make it a worthwhile addition to your project:

  • It offers code-smell analysis for Kotlin projects.
  • It’s highly configurable — you can customize it according to your own needs.
  • You can suppress findings if you don’t want warnings for everything.
  • IDE, SonarQube and GitHub Actions integration.
  • You can specify code-smells threshold to break your builds or print warnings.
  • You can add code-smell baseline and suppression for legacy projects.

You’ll learn about all of these features in this tutorial. Now, it’s time to add detekt to your project.

Adding detekt To Your Project

You can easily add detekt to new or existing Kotlin projects. For existing projects, you’ll add some more customization to prevent many errors.

Adding to New Projects

detekt is available as a Gradle plugin. For new projects, add the plugin via the Gradle files.

Navigate to the project-level build.gradle file. Add the corresponding classpath in the dependencies block:

classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.17.1"

Here, you define the path that Gradle will use to fetch the plugin.

Next, you need to add the plugin to your project.

Below the dependencies block add this:

apply plugin: "io.gitlab.arturbosch.detekt"

With this, you apply the detekt plugin to your project. The final step of adding the plugin is to apply the plugin on the app level Gradle file.

Navigate to the app-level build.gradle file. Add this apply line:

apply plugin: "io.gitlab.arturbosch.detekt"

This enables you to use the plugin in the app module too. With this added, do a Gradle sync. After the sync is complete, your plugin is set and ready to use!

Note: detekt requires Gradle 6.1 or higher. You can check the supported version from the compatibility documentation.

Running detekt Terminal Command

Now that you have the detekt plugin set up, run the following command on your terminal:

./gradlew detekt

Once the task completes, the build will fail with the following results:

There are a few things to notice:

  • At the very top, detekt shows warnings from Gradle. Currently, it shows a deprecation warning.
  • You can see the red text with the message Task :app:detekt FAILED. Below this text is a list of detekt rule sets. In this case, you have naming and style rule sets. In each rule set, you can see the rule that has been violated and the file together with the line. This makes it easier for you to pinpoint where in your code you need to do refactoring. Each rule set has an associated debt.
  • After the rule sets, you have the Overall debt: 1h 25min text. detekt estimates the total time it would take to fix all the code smells in your project. Imagine this is a new project and you already have 1 hour and 25 minutes of debt. And if it were an existing project, you could have days and days of debt. That’s a code smell already!

Congratulations! You’ve set up detekt in your new project successfully. Quite easy, right? :]

Now, you’ll focus on learning how to add it to an ongoing project.

Adding to Existing Projects

Integrating detekt into an ongoing project can result in many warnings and violated rules. The debt can be a lot of hours that you may not have time to resolve.

One option is to ignore the legacy code and instead focus on the code you add after integrating detekt. To do this, detekt allows you to define a baseline property that generates a list of every violated rule that detekt will ignore. Run ./gradlew detektBaseline, and this generates a baseline file for you. Now, when running ./gradlew detekt, detekt will ignore warnings listed in the baseline file. Check out the official documentation to learn more about baseline configuration.

Note: You can remove this baseline whenever you want, and you’ll see all the warnings again. To do so, remove the app/detekt-baseline.xml generated file.

In the next section, you’ll dive deeper into the detekt rule sets to know what they are. You’ll also look at rules in the rule sets.

Looking at detekt Rule Sets

The DetektSampleApp already includes a list of the rule sets and their descriptions. Rule sets contain a group of rules that check compliance with your code to improve the code quality. The rules don’t affect the functionality of your app. Here are the common rule sets that exist:

  1. Comments: It provides rules that address issues in comments and documentation of the code. It checks header files, comments on private methods and undocumented classes, properties or methods.
  2. Complexity: This set contains rules that report complex code. It checks for complex conditions, methods, expressions and classes, as well as reports long methods and long parameter lists.
  3. Coroutines: The coroutines rule set analyzes code for potential coroutines problems.
  4. Empty-Blocks: It contains rules that report empty blocks of code. Examples include empty catch blocks, empty class blocks, empty function and conditional function blocks.
  5. Exceptions: It reports issues related to how code throws and handles exceptions. For example, it has rules if you’re catching generic exceptions among other issues related to handling exceptions.
  6. Formatting: This checks if your codebase follows a specific formatting rule set. It allows checking indentation, spacing, semicolons or even import ordering, among other things.
  7. Naming: It contains rules which assert the naming of different parts of the codebase. It checks how you name your classes, packages, functions and variables. It reports the errors in case you’re not following the set conventions.
  8. Performance: It analyzes code for potential performance problems. Some of the issues it reports include the use of ArrayPrimitives or misuse of forEach loop, for instance.
  9. Potential-Bugs: The potential-bugs rule set provides rules that detect potential bugs.
  10. Style: The style rule set provides rules that assert the style of the code. This will help keep code in line with the given code style guidelines.

Each rule set contains a vast number of rules. This tutorial covers the most common rules, but don’t hesitate to check the official documentation to learn more about the rules.

Some of the rules are not active by default. You have to activate them yourself. How do you do that, you might ask? You’ll look into that in the next section.

Configuring detekt

One of the features of detekt is the high ability to customize it to your own needs. Moreover, it gives you the ability to easily enable or disable rules in your project.

detekt uses a YAML style configuration file for setting up reports, processors, build failures, rule set and rule properties.

In the starter app, switch to project view and you’ll see detekt.yml as shown below:

Open detekt.yml. It contains some rule sets and rules properties — for example, the comments section:

comments:
  active: true
  excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
  CommentOverPrivateFunction:
    active: false
  CommentOverPrivateProperty:
    active: false
  EndOfSentenceFormat:
    active: false
    endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$)
  UndocumentedPublicClass:
    active: false
    searchInNestedClass: true
    searchInInnerClass: true
    searchInInnerObject: true
    searchInInnerInterface: true
  UndocumentedPublicFunction:
    active: false

As you can see, in order to enable or disable a given rule, you just have to set the boolean value active: true/false.

In the code above, you have the configurations for the comments rule set, which is set to be active. There's an extra property for listing the files you'd want to exclude while reporting the issues. In this example, you exclude test and androidTest directories. Below this, you further set the properties for the individual rules in the rule sets. For instance, UndocumentedPublicFunction is not active. detekt won't report this in your codebase.

You can customize detekt.yml according to your project requirements.

For this specific configuration to be read by detekt, you'll need to add this file to your plugin configuration. To do this, navigate to the project-level build.gradle file. Add this below your apply line:

subprojects {
 apply plugin: "io.gitlab.arturbosch.detekt"
 detekt {
   config = files("${project.rootDir}/detekt.yml")
   parallel = true
 }
}

Here, you specify the configuration file for detekt. It will no longer use the default configurations. You can enable the rules or disable them in detekt.yml. This configuration applies to all sub-projects in your project.

You've seen available configuration options for detekt. Next, you'll add more methods that violate rule sets. You'll see how detekt reports these violations.

Adding a Rule Set

Locate the MainActivity class. Add the following coroutineTestRules function below onCreate:

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

private suspend fun coroutineTestRules() {
  GlobalScope.launch {
    delay(2000)
  }
}

Be sure to add the imports above at the top of the file or when the IDE prompts you to do so. This method tests the coroutines rule set.

It's a bad practice to use GlobalScope in an activity. detekt will raise an issue on this.

Finally, call this function at the end of the onCreate method:

runBlocking { coroutineTestRules() }

Resolve the imports once the IDE prompts you. Here, you call your coroutineTestRules() method inside runBlocking. You have to call suspend methods inside coroutines or other suspend methods.

Note: Some of the coroutines rules are not active by default. To activate this rule, add the following code to detekt.yml above the comments section:
coroutines:
  active: true
  GlobalCoroutineUsage:
    active: true

Here, you set the coroutines rule set to be active. You also enable the GlobalCoroutineUsage rule in the process. Now, run the ./gradlew detekt command on your terminal. Your results will be as follows:

In the image, you can see the report from the terminal now includes the coroutines rule set. It shows the debt and the file with the GlobalCoroutineUsage. The report has additional details at the bottom as shown:

detekt.yml has additional configuration at the top. These settings specify the reports you'll get and the project complexity report from detekt. You'll look at this later on in this tutorial.

Breaking More Rule Sets

Add the following code below your coroutineTestRules function in the MainActivity class:

// 1
private fun complexMethod(name: String, email: String, phone: String,
    address: String, zipCode: String, city: String, country: String): String {
  return name
}

// 2
private fun emptyMethod() {}

// 3
override fun toString(): String {
  throw IllegalStateException()
}

// 4
fun performanceIssues() {
  (1..19).forEach {
    print(it.toString())
  }
}

// 5
fun potentialBugs() {
  val test = when ("type") {
    "main" -> 1
    "main" -> 2
    else -> 3
  }
}

Here's a breakdown of the code above:

  1. This is an example of a complex method. The method has seven parameters that exceed the maximum six parameters set by detekt. You can change this number in the configuration file too.
  2. This is an empty method. It falls in the empty-blocks rule set.
  3. This represents the exceptions rule set. This method throws an exception without a cause. The exception is also thrown from an unexpected location.
  4. You have a forEach loop on a range that leads to performance issues. detekt reports this under the performance rule set.
  5. You have a when condition that has two similar states. This can lead to a bug in your app. detekt reports such cases as code smells under the potential-bugs rule set.

Run ./gradlew detekt command on the terminal and you'll see:

You've learned about the rule sets in different scenarios. What about cases when detekt detects too much? At times you may need to disable a certain rule on a specific method and not the whole project. For such cases, you need to suppress the issues. In the next section, you'll look at how to suppress issues.

Suppressing Issues

To prevent detekt from displaying an issue on a specific method, you can use the @Suppress annotation. Above complexMethod() add the following annotation:

@Suppress("LongParameterList")

Run ./gradlew detekt command on the terminal and you'll see the following:

From the image, you can see detekt doesn't complain about the LongParameterList rule anymore. This will only apply to this method. If you had another file or class with a complex method, detekt would still report the issue.

To see this in action, add this new method below potentialBugs():

@Suppress("EmptyFunctionBlock")
private fun suppressedWarning() {
}

In this method, you suppress EmptyFunctionBlock rule. Run ./gradlew detekt command on the terminal and you'll see:

The report doesn't have EmptyFunctionBlock issue on suppressedWarning(), but on emptyMethod() it's still shown.

So far, you've learned how to use the rules available on the detekt plugin. With your projects, you might need to detect more code smells that are not part of detekt. You'll learn how to do that next.

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:

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:

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 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.

Integrating detekt With GitHub Actions

GitHub Actions is GitHub's platform for automation workflows. With GitHub actions, you can add a detekt action. You can set it to run when someone pushes or creates a pull request on your said branches.

To enable actions, go to any of your GitHub projects and navigate to the actions tab as shown below:

Tap Set up a workflow yourself. You'll see the workflow editor. Inside the editor, add:

## 1
name: detekt
## 2
on:
 push:
   branches:
   - main
 pull_request:
   branches:
   - main
## 3
jobs:
 detekt:
   ## 4
   runs-on: ubuntu-latest
   steps:
      - name: "checkout"
        uses: actions/checkout@v2
      - name: "detekt"
        uses: natiginfo/[email protected]

Here's what the code above does:

  1. Creates a workflow named detekt
  2. Specifies when your workflow will run. In this case, the workflow runs when there's a push or a pull request on the main branch.
  3. Defines the jobs to de done by the work flow. You only have detekt as your job.
  4. Specifies a runner and steps for your job. You also add a third-party action, called detekt action. It runs detekt checks to ensure your code follows the set practices before merging to the main branch.

Save your workflow and create a pull request. For this sample project, the pull request fails with the following error:

From the Github Actions you'll see:

The workflow fails and shows the same report as you were seeing on your terminal. This is a very useful feature for detekt, as it ensures any changes added to the main branch don't have code smells. The pull request creator has to first address the issues before merging the pull request.

Note: If you're not familiar with GitHub Actions, take a look at this Continuous Integration for Android tutorial.

You've seen how to add detekt on GitHub Actions. Next, you'll learn how to add detekt on Android Studio.

Integrating detekt With Your IDE

To add the detekt plugin to Android Studio, navigate to Preferences/ Settings ▸ Plugins ▸ Search detekt. You'll see the plugin as in the image below:

Click Install to install the plugin. Once the installation completes, exit this screen. Navigate to Preferences/Settings ▸ Tools ▸ detekt and check Enable Detekt.

detekt IDE plugin also has these optional configuration options:

  • Configuration file path: This is your detekt.yml.
  • Baseline file: Path to your custom baseline.xml.
  • Plugin Jars: Path to jar file that has your custom rules, if you want detekt to report them.

With this, the IDE will detect errors on the go as you code!

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Congratulations! You've learned so much about detekt and its features. Now you know how you can add it to your project, create custom rules or even integrate it on the workflow in GitHub Actions or your IDE. :]

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.

Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!

Average Rating

5/5

Add a rating for this content

Sign in to add a rating
3 ratings

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK