Enhancing code quality with detekt for static analysis

In the 2nd part of the series, you will learn to integrate detekt for static code analysis in your Kotlin projects, optimize it for Jetpack Compose, and implement it in pre-commit hooks for better code quality.

Reading Time10 min

In the first part of our 3 blog series, I shared our experience on the benefits of code formatting and how to integrate tools like ktfmt into your project. I wrote about how to set up your ktfmt, configure it and run it in your project. In this part, I will provide you with a step-by-step guide on how to use detekt for static code analysis for Kotlin projects - so let’s start!

Our Focus 

When our team began migrating to Jetpack Compose, we wanted to avoid common mistakes due to our limited expertise. We discovered detekt, a tool that provides static code analysis for Kotlin projects, and Compose-rules, which integrates Jetpack Compose-specific checks into detekt. We also wanted to implement these tools into the pre-commit hook we implemented in the previous blog post (dev.to, Medium).

This blog will guide you through the steps to integrate detekt into your projects and fine-tune its configuration to optimize performance for Jetpack Compose projects.

Let's start with a short introduction to static code analysis and its purpose.

Introduction to static code analysis

Static code analysis helps to identify potential issues, enforce coding standards, and ensure code quality and maintainability. By catching issues early in the development process, it can save time and effort, ultimately leading to more resilient and reliable software.


Our tool of choice was detekt, and here's a quick overview of it.

Introduction to detekt

detekt is a static code analysis tool for Kotlin projects. We chose it for our projects due to its comprehensive set of rules, easy integration, and active community support. It offers features such as customizable rules, reporting capabilities, and integration with popular build tools, making it a valuable addition to our development workflow.

Let's set it up in our project!

Setting up detekt


To implement detekt into your projects, simply add the detekt dependency to your Top-level build.gradle or build.gradle.kts file:

plugins {
    ...
    id("io.gitlab.arturbosch.detekt") version("1.23.6")  // Replace with latest version
}

After adding the detekt Gradle plugin, here's how you can configure it to better match your requirements.

Configuration

detekt offers a lot of customizability, which allowed us to use it effectively on our Jetpack Compose projects. You can fine-tune the rules, console reports, output reports, and similar.

When you use it on Jetpack Compose projects, some additional configuration is required via the detekt-config.yml file. While researching detekt and its configuration, we also came across Compose-rules which came to our rescue when we were migrating from XML to Jetpack Compose. From their overview:

The Compose Rules is a set of custom Ktlint / detekt rules to assure that your composables avoid common pitfalls that might be easy to miss in code reviews.

For detekt to work properly and efficiently on Jetpack Compose projects, please follow these steps:

  • Navigate to your project's root directory, e.g., ~/projects/SampleProject/
  • Create a new directory named configs, and inside it, create a new file named detekt-config.yml

Your file tree should look like this when viewing your project from the Project view:

  • Open the file with a text editor and paste the configuration found here
  • Add the Jetpack Compose-specific detekt configuration at the end of the file

...
naming:
  FunctionNaming:
    ignoreAnnotated: [ 'Composable' ]
  TopLevelPropertyNaming:
    constantPattern: '[A-Z][A-Za-z0-9]*'

complexity:
  LongParameterList:
    functionThreshold: 15
    ignoreDefaultParameters: true

  LongMethod:
    active: false

style:
  MagicNumber:
    active: false
  UnusedPrivateMember:
    ignoreAnnotated: [ 'Preview' ]

This configuration is added to avoid false positives, for example, the default functionThreshold value inside the LongParameterList rule is 6, which can easily be exceeded with complex Composable functions.

The complete detekt-config.yml should look like this:

Compose:
  ComposableAnnotationNaming:
    active: true
  ComposableNaming:
    active: true
    # -- You can optionally disable the checks in this rule for regex matches against the composable name (e.g. molecule presenters)
    # allowedComposableFunctionNames: .*Presenter,.*MoleculePresenter
  ComposableParamOrder:
    active: true
    # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically)
    # treatAsLambda: MyLambdaType
  CompositionLocalAllowlist:
    active: true
    allowedCompositionLocals: LocalCustomColorsPalette,LocalCustomTypography
    # -- You can optionally define a list of CompositionLocals that are allowed here
    # allowedCompositionLocals: LocalSomething,LocalSomethingElse
  CompositionLocalNaming:
    active: true
  ContentEmitterReturningValues:
    active: true
    # -- You can optionally add your own composables here
    # contentEmitters: MyComposable,MyOtherComposable
  DefaultsVisibility:
    active: true
  LambdaParameterInRestartableEffect:
    active: true
    # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically)
    # treatAsLambda: MyLambdaType
  ModifierClickableOrder:
    active: true
    # -- You can optionally add your own Modifier types
    # customModifiers: BananaModifier,PotatoModifier
  ModifierComposable:
    active: true
    # -- You can optionally add your own Modifier types
    # customModifiers: BananaModifier,PotatoModifier
  ModifierMissing:
    active: true
    # -- You can optionally control the visibility of which composables to check for here
    # -- Possible values are: `only_public`, `public_and_internal` and `all` (default is `only_public`)
    # checkModifiersForVisibility: only_public
    # -- You can optionally add your own Modifier types
    # customModifiers: BananaModifier,PotatoModifier
  ModifierNaming:
    active: true
    # -- You can optionally add your own Modifier types
    # customModifiers: BananaModifier,PotatoModifier
  ModifierNotUsedAtRoot:
    active: true
    # -- You can optionally add your own composables here
    # contentEmitters: MyComposable,MyOtherComposable
    # -- You can optionally add your own Modifier types
    # customModifiers: BananaModifier,PotatoModifier
  ModifierReused:
    active: true
    # -- You can optionally add your own Modifier types
    # customModifiers: BananaModifier,PotatoModifier
  ModifierWithoutDefault:
    active: true
  MultipleEmitters:
    active: true
    # -- You can optionally add your own composables here that will count as content emitters
    # contentEmitters: MyComposable,MyOtherComposable
    # -- You can add composables here that you don't want to count as content emitters (e.g. custom dialogs or modals)
    # contentEmittersDenylist: MyNonEmitterComposable
  MutableParams:
    active: true
  MutableStateParam:
    active: true
  PreviewAnnotationNaming:
    active: true
  PreviewPublic:
    active: true
  RememberMissing:
    active: true
  RememberContentMissing:
    active: true
  UnstableCollections:
    active: true
  ViewModelForwarding:
    active: true
    # -- You can optionally use this rule on things other than types ending in "ViewModel" or "Presenter" (which are the defaults). You can add your own via a regex here:
    # allowedStateHolderNames: .*ViewModel,.*Presenter
    # -- You can optionally add an allowlist for Composable names that won't be affected by this rule
    # allowedForwarding: .*Content,.*FancyStuff
  ViewModelInjection:
    active: true
    # -- You can optionally add your own ViewModel factories here
    # viewModelFactories: hiltViewModel,potatoViewModel

naming:
  FunctionNaming:
    ignoreAnnotated: [ 'Composable' ]
  TopLevelPropertyNaming:
    constantPattern: '[A-Z][A-Za-z0-9]*'

complexity:
  LongParameterList:
    functionThreshold: 15
    ignoreDefaultParameters: true

  LongMethod:
    active: false

style:
  MagicNumber:
    active: false
  UnusedPrivateMember:
    ignoreAnnotated: [ 'Preview' ]

This configuration enables the Compose-rules and modifies some of detekt's default rule sets and their configuration options to avoid incorrect detection.

  • Save the file and close the editor
  • At your Top-level build.gradle or build.gradle.kts add the following code:

   ...
   allprojects {
       ...
       apply(plugin = "io.gitlab.arturbosch.detekt")
   
       // Configure the detekt plugin
       detekt {
           // Set the detekt configuration from previous steps
           config.setFrom(file("$projectDir/config/detekt-config.yml"))
   
           // Build upon the default detekt configuration, instead of replacing it
           buildUponDefaultConfig = true
   
           // Do not activate all detekt rules
           allRules = false
   
           // Enable automatic correction of issues found by detekt
           autoCorrect = true
   
           // Run detekt in parallel mode for better performance
           parallel = true
       }
   }

The code snippet above configures the detekt plugin to build upon the default configuration and apply the configuration we defined inside the detekt-config.yml file. We set allRules to false so unstable rules aren't applied. autoCorrect is set to true so that if a rule supports auto-correction, it is applied to the code, and parallel ensures parallel compilation and analysis of source files.

...
// Configure each detekt task
tasks.withType().configureEach {
    reports {
        // Enable the generation of an HTML report
        html.required.set(true)
        html.outputLocation.set(file("build/reports/detekt.html"))
    
        // Enable the generation of a TXT report
        txt.required.set(true)
        txt.outputLocation.set(file("build/reports/detekt.txt"))
    
        // Enable the generation of a Markdown (MD) report
        md.required.set(true)
        md.outputLocation.set(file("build/reports/detekt.md"))
    }
}

The code snippet above configures all detekt tasks to generate HTML, TXT, and Markdown reports and to save them in the build/reports directory.

  • At your Top-level build.gradle or build.gradle.kts add the following code to apply the compose-rules detekt rule set:
    allprojects {
    ...
    dependencies {
        ...
        detektPlugins("io.nlopez.compose.rules:detekt:0.4.4") // Replace with latest version
    }
}
  • Sync your project with Gradle Files

These steps provide you with a basic setup when using detekt on Jetpack Compose projects; feel free to modify it and try out different configurations that better suit your needs. For a complete list of configuration fields, please refer to: Options for detekt configuration closure and detekt Configuration File.

After implementing and configuring detekt to run on your project, it's time to initiate running it!

Running detekt

After implementing the necessary dependencies for detekt, applying the configuration, and syncing your project with Gradle Files, you can run the detekt Gradle task. That can be done by:

The Gradle tool window:

  • Open the Gradle tool window
  • Navigate to Tasks/verification
  • Double-click on the detekt task (or right-click and click on Run)

The terminal:

  • Open the Terminal window inside Android Studio
  • Run the following command: ./gradlew detekt

The task failed because code issues were detected - we will dive deeper into reading these reports in the section below.

Since we implemented a pre-commit hook for ktmft, we will make use of it for detekt as well.

Adding detekt to the pre-commit hook

To avoid having to run this task manually, we can make use of the pre-commit hook we already implemented in the previous blog in the series:

  • Navigate to your project's root directory, e.g., ~/projects/SampleProject/
  • Toggle hidden files and folders visibility [Windows, MacOS, Linux]
  • Navigate to rootProjectDir/.git/hooks and open the file named pre-commit, which we created previously with a text editor.
  • Open the file with a text editor and add the following code:

... 
echo "
=======================
|  Running detekt...  |
======================="

# Run detekt static code analysis with Gradle
if ! ./gradlew --quiet --no-daemon detekt --stacktrace -PdisablePreDex; then
    echo "detekt failed"
    exit 1
fi
... 
 

The complete script should now look like this:

#!/bin/bash
    
# Exit immediately if a command exits with a non-zero status
set -e
    
echo "
===================================
|  Formatting code with ktfmt...  |
==================================="
    
# Run ktfmt formatter on the specified files using Gradle
if ! ./gradlew --quiet --no-daemon ktfmtPrecommitFormat; then
    echo "Ktfmt failed"
    exit 1
fi
    
echo "
=======================
|  Running detekt...  |
======================="
    
# Run detekt static code analysis with Gradle
if ! ./gradlew --quiet --no-daemon detekt --stacktrace -PdisablePreDex; then
    echo "detekt failed"
    exit 1
fi
    
# Check if git is available
if ! command -v git &> /dev/null; then
    echo "git could not be found"
    exit 1
fi
    
# Add all updated files to the git staging area
git add -u
    
# Exit the script successfully
exit 0
  • Save the file and close the editor

After we ran detekt, we found some issues, and now we will dig even deeper in the following reports.

Inspecting detekt reports

When we configured detekt, we enabled HTML, text, and Markdown reports. We will now focus on the HTML report because the other output formats are very similar.

detekt found issues when we first ran it in the section above, so let's see that report in HTML:

Metrics

  • number of properties
  • number of functions
  • number of classes
  • number of packages
  • number of kt files

Complexity Report

  • lines of code (loc): The total number of lines in the codebase, including all code, comments, and whitespace. This gives an overall sense of the size of the project.
  • source lines of code (sloc): This metric includes only the lines that contain actual source code, excluding comments and whitespace. It's a more accurate representation of the amount of the written code.
  • logical lines of code (lloc): This includes lines of code that represent executable statements, ignoring comments, whitespace, and block delimiters. It provides a clearer picture of the code's functional size.
  • comment lines of code (cloc): The total number of lines that contain comments. This helps in understanding the documentation level within the code.
  • cyclomatic complexity (mcc): A measure of the code's complexity, calculated by counting the number of linearly independent paths through the code. Higher values indicate more complex code, which is harder to test and maintain.
  • cognitive complexity: This measures how difficult the code is to understand. Unlike cyclomatic complexity, which focuses on the structure, cognitive complexity considers the human aspect of code comprehension.
  • number of total code smells: Code smells are indicators of potential problems in the code. This metric counts all identified code smells, which can help in prioritizing refactoring efforts.
  • comment source ratio: This is the ratio of comment lines to source lines of code (cloc/sloc). A higher ratio typically indicates better-documented code.
  • mcc per 1,000 lloc: This normalizes cyclomatic complexity by logical lines of code, showing how complex the code is on a per 1,000 lines basis. It helps compare complexity across different-sized projects.
  • code smells per 1,000 lloc: This metric normalizes the number of code smells by logical lines of code, providing a density measure that helps in identifying areas that may need more attention regardless of the project's size.

Findings

The Findings section in a detekt report identifies specific issues in the codebase that need attention. These issues are categorized by type and include details such as location, message, and relevant code snippets.

Total: The total number of findings detected by detekt, indicating the overall number of issues in the codebase.

[Category]

Each category represents a specific type of issue detected. Examples of categories include "Compose", "style", "formatting", etc.

RuleName: Each rule represents a specific coding standard or best practice that the code should adhere to.

  • Documentation: A link to detailed documentation about the rule. This documentation provides more in-depth information about why the rule exists, examples of violations, and how to resolve them.
  • Location: This indicates the exact file and line number where the issue is found. It helps developers quickly locate the problematic code.
  • Message: A description of the issue, explaining what is wrong and why it should be corrected. This often includes guidance on how to fix the problem.
  • Code Snippet: A portion of the code where the issue is detected is highlighted to give context to the finding.

The finding above comes from the Compose-rules rule set, and the findings below are from detekt's rule set.

Some issues that detekt reports could be acceptable to you, so you decide not to change your code. To avoid detekt reporting these issues, you can suppress them, which we will describe next.

Suppressing issues

Since we've integrated detekt into our pre-commit hook, any issues it identifies will block the commit process. Detekt will report an issue that isn't really important to you, but it will still block the commit due to the set rules. 

To suppress issues in detekt, you can add the @Suppress annotation. Inside the values field of the @Suppress annotation, you need to write the RuleName.

Here's an example:

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BlogSampleTheme { Greeting("Android") }
}


This block of code produces the following issue:

PreviewPublic: 1 Composables annotated with @Preview that are used only for previewing the UI should not be public.

To suppress this issue, you can add the @Suppress annotation like this:

@Suppress("PreviewPublic")
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BlogSampleTheme { Greeting("Android") }
}

You can also suppress issues at the file level. In the previous example, you could have added the following line at the very top of the file: @file:Suppress("PreviewPublic")

Benefits and challenges

Since integrating detekt, we have observed several benefits:

  • Migration from XML to Jetpack Compose: Our migration process has been greatly impacted by implementing detekt; the migration and learning process were accelerated, and some common mistakes were caught on time.
  • Improved code quality: detekt has helped us identify and address code smells, resulting in cleaner and more maintainable code.
  • Consistent standards: By enforcing coding standards, detekt ensures that all team members adhere to the same guidelines, improving code consistency.
  • Enhanced learning: The feedback provided by detekt has been instrumental in helping developers learn and adopt best practices.

However, we also faced some challenges:

  • Configuration complexity: Initial configuration and customization of detekt to suit our specific needs required some effort and research.
  • False positives: In some cases, detekt flagged issues that were not necessarily problematic, requiring us to fine-tune the rules or suppress them.

Let’s summarize

Thank you for taking the time to read this post!

We hope it has provided valuable insights into the benefits of using detekt and inspired you to implement it in your own projects.

Initially, we implemented the detekt on a single project. This setup, as described above, involved configuring rules and integrating them into our build process. However, when we moved to implement the detekt on another project, we quickly realized we needed to go through the entire configuration process again for each new project. This repetition highlighted the need for a more streamlined and reusable setup method.

We will address this issue in the next blog post of this series. 


Before we publish the third and last blog of this series, take a look at the first part of our blog series!

Hey, you! What do you think?

They say knowledge has power only if you pass it on - we hope our blog post gave you valuable insight.

If you want to share your opinion or you need help with your own projects with codes that need polishing, feel free to contact us.

We'd love to hear what you have to say!