DEV Community

Cover image for Enhancing Code Quality with detekt for Static Analysis
Marko Pavičić
Marko Pavičić

Posted on • Edited on

Enhancing Code Quality with detekt for Static Analysis

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 in identifying potential issues, enforcing coding standards, and ensuring code quality and maintainability. By catching issues early in the development process, it can save time and effort, ultimately leading to more robust and reliable software.

Our tool of choice was detekt, 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, ease of 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
}
Enter fullscreen mode Exit fullscreen mode

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, it requires some additional configuration via the detekt-config.yml file. While researching detekt and its configuration, we also came across compose-rules which came to 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 ensure that your composables don't fall into 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:

  1. Navigate to your project's root directory, e.g., ~/projects/SampleProject/

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

    config_file_tree

  3. Open the file with a text editor and paste the configuration found here

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

  5. Save the file and close the editor

  6. 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, parallel ensures parallel compilation and analysis of source files.

    ...
    // Configure each detekt task
    tasks.withType<Detekt>().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 cofigures all detekt tasks to generate HTML, TXT and Markdown reports and to save them in the build/reports directory.

  7. 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
       }
    }
    
  8. Sync your project with Gradle Files

These steps provide you with somewhat of 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 you implemented and configured detekt to run on your project, it's time to run 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)

gradle_tool_window

  • The terminal:

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

terminal

The task failed because code issues were detected, we will dive deeper into reading these reports in a 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 implemented in the previous blog in the series:

  1. Navigate to your project's root directory, e.g., ~/projects/SampleProject/

  2. Toggle hidden files and folders visibility [Windows, MacOS, Linux]

  3. Navigate to rootProjectDir/.git/hooks and open the file named pre-commit, which we created previously with a text editor

  4. 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
    
  5. Save the file and close the editor

After we ran detekt, we found some issues, now we will dive deeper into reading those reports.

Inspecting detekt reports

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

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

Metrics

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

metrics

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 code written.
  • 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.

complexity_report

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 fixed. This often includes guidance on how to correct the problem.
    • Code Snippet: A portion of the code where the issue is detected is highlighted to give context to the finding.

findings_1

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

findings_2

Some issues detekt reports could be acceptable to you, and you don't want to change your code. To avoid detekt reporting these issues, you can suppress them, which we will describe below.

Suppressing Issues

Since we implemented detekt in a pre-commit hook that will block the commit if there are any issues, you will quickly encounter an issue that blocks the commit, but you and the team have no concern with it.

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") }
}
Enter fullscreen mode Exit fullscreen mode

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") }
}

Enter fullscreen mode Exit fullscreen mode

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.

Conclusion

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

Initially, we implemented detekt on a single project. This setup, described above, involved configuring rules and integrating it into our build process. However, when we moved to implement 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 in this series. In the meantime check out our Barrage blog for more interesting topics.

Top comments (0)