πŸš€ UllrichLumina

How to findremove unused dependencies in Gradle

How to findremove unused dependencies in Gradle

πŸ“… | πŸ“‚ Category: Java

Optimizing your software projects is crucial for efficiency, and one often overlooked area is dependency management. Over time, Gradle projects can accumulate a significant number of unused dependencies, leading to slower build times, increased artifact sizes, and potential security vulnerabilities. Understanding how to find and remove unused dependencies in Gradle is a vital skill for any developer aiming to maintain a lean, performant, and secure codebase. This guide will walk you through various methods and best practices to identify and eliminate these redundant libraries, ensuring your build processes are as efficient as possible and your project remains agile.

Why Unused Dependencies Are a Problem

Unused dependencies, often referred to as ‘dead code’ in the context of libraries, can silently degrade your project’s health. Their presence impacts several key aspects of software development, from daily build cycles to long-term maintenance and security. Firstly, they directly contribute to slower build times. Each dependency, even if unused, must be downloaded, resolved, and processed by Gradle during the build phase. This overhead can significantly increase the time it takes to compile and package your application, especially in large-scale projects or CI/CD pipelines where builds run frequently.

Beyond performance, unused libraries inflate the final size of your application artifact. Whether it’s a JAR, WAR, or APK, including unnecessary code bloats the package, leading to longer download times for users and increased resource consumption during deployment. For mobile applications, this can directly impact user adoption and device storage. Furthermore, a cluttered build.gradle file makes the project harder to understand and maintain. Developers waste time sifting through irrelevant entries, increasing the cognitive load and the risk of introducing new dependency conflicts.

Perhaps most critically, unused dependencies pose a significant security risk. Every library you include in your project is a potential attack vector. If an unused dependency contains a known vulnerability (CVE), your project remains susceptible even if the code path for that dependency is never executed. Regular build script cleanup is essential to mitigate these risks. As reported by Snyk, a leading developer security platform, a substantial percentage of known vulnerabilities reside in transitive dependencies, making it even harder to track and manage without proper tools and processes. Source: Snyk

Identifying Unused Dependencies in Gradle

The first step to a cleaner project is identifying what’s no longer needed. Fortunately, there are several effective methods and tools available to help you pinpoint those elusive, unused libraries within your Gradle setup. This process often involves a combination of manual review and automated analysis, catering to different levels of project complexity and developer preference. Understanding the nature of your project’s dependencies – whether they are direct, transitive, or runtime-only – is key to accurate identification.

Manual Review of Your Build Script

While less scalable for large projects, a manual review of your build.gradle or build.gradle.kts files is always a good starting point. Examine each dependencies { } block. Ask yourself: Is this dependency explicitly used in the code? Is it required for a specific feature that is still active? Pay close attention to dependencies that were added for temporary testing or debugging purposes and might have been forgotten. This method is effective for direct dependencies that are obviously not used or for understanding the overall dependency management structure. However, it falls short when dealing with transitive dependencies, which are dependencies brought in by your direct dependencies.

Leveraging Gradle Plugins for Analysis

For a more robust and automated approach, specialized Gradle plugins are invaluable. These tools can analyze your project’s classpath and source code to deduce which dependencies are genuinely used and which are redundant. The dependency-analysis-gradle-plugin is an excellent example. It provides tasks that can report unused declared dependencies and even suggest moving dependencies from api to implementation scope, further optimizing your build. Another popular option is the gradle-lint-plugin, which offers various linting checks, including identifying unused dependencies and enforcing best practices for your build scripts.

These plugins typically work by analyzing compiled class files and comparing them against the declared dependencies. They can often differentiate between compile-time and runtime requirements, giving a more accurate picture of actual usage. For instance, the dependency-analysis plugin provides tasks like dependencies:buildHealth which can reveal a wealth of information about your project’s dependency graph, including potential conflicts and unused artifacts. Integrating such plugins into your CI/CD pipeline ensures continuous Gradle build optimization and prevents dependency bloat from accumulating unnoticed.

Static Analysis Tools (e.g., ProGuard/R8)

For Android projects, tools like ProGuard and R8 (the default code shrinker in Android Gradle Plugin 3.4.0 and higher) are primarily designed for code shrinking and obfuscation, but they also effectively identify and remove unused code, including unused libraries. They perform a deep analysis of your application’s bytecode, stripping away classes, fields, methods, and attributes that are not needed. This process is particularly powerful because it can detect code that is unreachable or simply not invoked during the application’s runtime. While they don’t directly modify your build.gradle file, the output of these tools can strongly indicate which dependencies are truly essential for your application’s functionality. For more details on R8, refer to the official Android Developers documentation: Source: Android Developers

Infographic: The Dependency Cleanup Cycle - Identify, Analyze, Remove, Prevent
Removing Unused Dependencies: A Step-by-Step Guide --------------------------------------------------

Once you’ve identified the unused dependencies, the next critical step is to remove them safely without breaking your project. This process requires careful execution and thorough testing to ensure that no essential functionality is inadvertently removed. It’s not just about deleting lines; it’s about understanding the impact of each removal and verifying your application’s integrity post-cleanup. This systematic approach ensures effective build script cleanup while maintaining project stability.

  1. Isolate and Comment Out: Instead of immediately deleting a dependency, first comment it out in your build.gradle file. This allows you to easily revert the change if you discover it was still needed. It’s a non-destructive way to test the waters before permanent removal.

  2. Clean and Rebuild Your Project: After commenting out, run a clean build (./gradlew clean build or gradlew clean build). This ensures that Gradle rebuilds your project from scratch without the commented-out dependency, forcing any compilation errors to surface immediately.

  3. Run All Tests: Thoroughly execute your project’s test suite (unit tests, integration tests, UI tests). Comprehensive test coverage is your best defense against inadvertently removing a crucial dependency. If tests pass, it’s a strong indicator that the dependency was indeed unused.

  4. Perform Manual QA: Even with automated tests, conduct manual quality assurance checks on key features, especially those that might have relied on the removed dependency or its transitive components. This is particularly important for features related to I/O, networking, or UI components.

  5. Consider implementation vs api: For Java library projects, reassess your dependency configurations. Using implementation instead of api whenever possible prevents transitive dependencies from leaking into your project’s public API, reducing the risk of unnecessary dependencies being pulled in by consumers of your library. This is a crucial aspect of good dependency management.

  6. Remove Exclusions: If you previously used exclude rules within a dependency declaration to prevent transitive dependencies, and you’ve now removed the parent dependency, remember to also remove those exclude rules. They might become redundant or even cause issues if they conflict with other dependencies.

  7. Commit Changes: Once you are confident that the dependency is no longer needed and the project functions correctly, commit your changes to version control. Documenting the removal can also be helpful for future Question & Answer :
    I wanted to find unused dependencies in my project. Is there a feature for this in Gradle, like in Maven?

    UPDATE May 2024: The plugin is no longer being maintained. Authors suggest dependency-analysis-gradle-plugin which supports Java, Kotlin and Android projects, including kts-based projects.

    UPDATE for Kotlin Users: 17 December 2021: Detects missing or superfluous build dependencies in Kotlin projects : Version 1.0.9 (latest)

    I have added 2 types of configuration for Kotlin users.

    • Using the plugins DSL
    • Using legacy plugin application

    Using the plugins DSL:

    plugins { id("com.faire.gradle.analyze") version "1.0.9" } 
    

    Using legacy plugin application:

    buildscript { repositories { maven { url = uri("https://plugins.gradle.org/m2/") } } dependencies { classpath("com.faire.gradle:gradle-kotlin-buildozer:1.0.9") } } apply(plugin = "com.faire.gradle.analyze") 
    

    Resource Link:

    1. https://plugins.gradle.org/plugin/com.faire.gradle.analyze
    2. https://github.com/Faire/gradle-kotlin-buildozer
    3. https://discuss.gradle.org/t/detecting-unused-projects-dependencies/25522

    UPDATE: 28-06-2016: Android support to unused-dependency

    In June, 2017, they have released the 4.0.0 version and renamed the root project name "gradle-lint-plugin" to "nebula-lint-plugin". They have also added Android support to unused-dependency.


    In May 2016 Gradle has implemented the Gradle lint plugin for finding and removing unwanted dependency

    Gradle Lint Plugin: Full Documentation

    The Gradle Lint plugin is a pluggable and configurable linter tool for identifying and reporting on patterns of misuse or deprecations in Gradle scripts and related files.

    This plugin has various rules. Unused Dependency Rule is one of them. It has three specific characteristics.

    1. Removes unused dependencies.
    2. Promotes transitive dependencies that are used directly by your code to explicit first order dependencies.
    3. Relocates dependencies to the ‘correct’ configuration.

    To apply the rule, add:

    gradleLint.rules += 'unused-dependency' 
    

    Details of Unused Dependency Rule is given in the last part.

    To apply the Gradle lint plugin:

    buildscript { repositories { jcenter() } } plugins { id 'nebula.lint' version '0.30.2' } 
    

    Alternatively:

    buildscript { repositories { jcenter() } dependencies { classpath 'com.netflix.nebula:gradle-lint-plugin:latest.release' } } apply plugin: 'nebula.lint' 
    

    Define which rules you would like to lint against:

    gradleLint.rules = ['all-dependency'] // Add as many rules here as you'd like 
    

    For an enterprise build, we recommend defining the lint rules in a init.gradle script or in a Gradle script that is included via the Gradle apply from mechanism.

    For multimodule projects, we recommend applying the plugin in an allprojects block:

    allprojects { apply plugin: 'nebula.lint' gradleLint.rules = ['all-dependency'] // Add as many rules here as you'd like } 
    


    Details of Unused Dependency Rule is given in this part

    To apply the rule, add:

    gradleLint.rules += 'unused-dependency' 
    

    The rule inspects compiled binaries emanating from your project’s source sets looking for class references and matches those references to the dependencies that you have declared in your dependencies block.

    Specifically, the rule makes the following adjustments to dependencies:

    1. Removes unused dependencies

    • Family-style jars like com.amazonaws:aws-java-sdk are removed, as they don’t contain any code

    2. Promotes transitive dependencies that are used directly by your code to explicit first order dependencies

    • This has the side effect of breaking up family style JAR files, like com.amazonaws:aws-java-sdk, into the parts that you are actually using, and adding those as first order dependencies

    3. Relocates dependencies to the ‘correct’ configuration

    • Webjars are moved to the runtime configuration
    • JAR files that don’t contain any classes and content outside of META-INF are moved to runtime
    • ‘xerces’, ‘xercesImpl’, ‘xml-apis’ should always be runtime scoped
    • Service providers (JAR files containing META-INF/services) like mysql-connector-java are moved to runtime if there isn’t any provable compile-time reference
    • Dependencies are moved to the highest source set configuration possible. For example, ‘junit’ is relocated to testCompile unless there is an explicit dependency on it in the main source set (rare).


    UPDATE: Previous plugins

    For your kind information, I want to share about previous plugins

    1. The Gradle plugin that finds unused dependencies, declared and transitive is com.github.nullstress.dependency-analysis

    But its latest version 1.0.3 is created 23 December 2014. After that there aren’t any updates.

    N.B: Many of our engineers are being confused about this plugin as they updated only the version number, nothing else.