Skip to content

A new task to build type usage report (ComputeTypeUsageTask).#1639

Open
Laimiux wants to merge 2 commits intoautonomousapps:mainfrom
Laimiux:laimonas/compute-type-usage-task
Open

A new task to build type usage report (ComputeTypeUsageTask).#1639
Laimiux wants to merge 2 commits intoautonomousapps:mainfrom
Laimiux:laimonas/compute-type-usage-task

Conversation

@Laimiux
Copy link

@Laimiux Laimiux commented Feb 10, 2026

Summary

A new task to build type usage report (#1637).

Introducing a new ComputeTypeUsageTask task. It generates type-usage.json reporting which types (classes) your code uses from each dependency. Designed for analyzing module dependency usage and complexity.

{
  "projectPath": ":app",
  "summary": {
    "totalTypes": 245,
    "totalFiles": 67,
    "internalTypes": 12,
    "projectDependencies": 3,
    "libraryDependencies": 18
  },
  "internal": {
    "com.example.MyClass": 5
  },
  "projectDependencies": {
    ":core": {
      "com.example.core.Utils": 2
    }
  },
  "libraryDependencies": {
    "org.apache.commons:commons-collections4": {
      "org.apache.commons.collections4.bag.HashBag": 3
    }
  }
}

Usage:

./gradlew computeTypeUsageMain

Output location:

build/reports/dependency-analysis/main/intermediates/type-usage.json

Configuration:

dependencyAnalysis {
  typeUsage {
    excludePackages("kotlin.jvm.internal")
    excludeTypes("kotlin.Unit")
    excludeRegex(".*_Factory$", ".*Companion$")
  }
}

Key design decisions:

  • Task runs on-demand only (not part of buildHealth or other critical paths)
  • Uses existing intermediate files (synthetic-project.json, exploded-jars.json.gz)
  • Tracks both annotation and non-annotation class usage
  • Sensible defaults exclude common generated/internal types

api/api.txt Outdated
public abstract class AbstractExtension {
ctor @javax.inject.Inject public AbstractExtension(org.gradle.api.model.ObjectFactory objects, org.gradle.api.invocation.Gradle gradle);
method public final org.gradle.api.file.RegularFileProperty adviceOutput();
method public final org.gradle.api.file.RegularFileProperty typeUsageOutput();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide guidance here - I've added this, but I'm not sure what should actually be included here.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whatever the abi generation task requires!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a Gradle task to generate this?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

./gradlew :updateApi


import static com.autonomousapps.kit.gradle.dependencies.Dependencies.*

final class TypeUsageProject extends AbstractProject {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a functional test for this task (tried to follow project conventions)


import static com.autonomousapps.kit.gradle.dependencies.Dependencies.*

final class TypeUsageWithFiltersProject extends AbstractProject {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another functional test for the new task (tried to follow project conventions)

import static com.autonomousapps.utils.Runner.build
import static com.google.common.truth.Truth.assertThat

final class TypeUsageSpec extends AbstractJvmSpec {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following project convention to test computeTypeUsageMain task

// Excluded specific types
internal val excludedTypes: SetProperty<String> = objects.setProperty(String::class.java)
.convention(setOf(
"kotlin.Unit",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if we should provide certain defaults here for exclusion out of the report

* enabling coupling analysis and complexity metrics.
*/
@JsonClass(generateAdapter = false)
public data class ProjectTypeUsage(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Json serialized models for type usage task.

}

// Computes type-level usage statistics for complexity analysis.
val computeTypeUsageTask = tasks.register("computeTypeUsage$taskNameSuffix", ComputeTypeUsageTask::class.java) { t ->
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New task registration

Copy link
Owner

@autonomousapps autonomousapps left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! Left some comments.

then: 'has correct summary'
def usage = project.actualTypeUsage()
assertThat(usage.projectPath).isEqualTo(':proj')
assertThat(usage.summary.totalTypes).isGreaterThan(0)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why a fuzzy match here?

Also in general, I think it might be better to just create an instance of a type usage and check equality directly?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid specs! I'm curious why we can't make more straightforward equality checks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed it


fun shouldExclude(className: String): Boolean {
if (excludedTypes.contains(className)) return true
if (excludedPackages.any { className.startsWith("$it.") }) return true
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we answer one of my questions from earlier about how the packages are treated. I wonder if it might be cleaner if, when accepting the user input, map it and affix a . to what they pass in. Also, we need to handle the case where a user might think they have to pass in a . themselves. I have some functions that do similar stuff:

internal fun String.ensurePrefix(prefix: String = ":"): String {
  return if (startsWith(prefix)) this else "$prefix$this"
}

That function is in another context. For your use-case, please add a similar function named ensureSuffix that ensures all packages end with exactly one ..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, added ensureSuffix

val allUsedClasses = source.usedNonAnnotationClasses + source.usedAnnotationClasses

allUsedClasses.forEach { className ->
if (filter.shouldExclude(className)) return@forEach
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally prefer using filter() to this pattern.

// Determine coordinates: check project classes first, then external
val coords = when {
projectClasses.contains(className) -> project.coordinates.identifier
else -> classToCoords[className] ?: "UNKNOWN"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would UNKNOWN happen? Would !! or error("shouldn't happen") be more appropriate?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen cases with R classes. Instead of failure, I suggest exposing this via separate unknownDependencies property.

private val filter: TypeFilter
) {
fun analyze(): ProjectTypeUsage {
val usageMap = mutableMapOf<String, MutableMap<String, Int>>()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be better off using Coordinates instead of String here for the key.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call

@Laimiux Laimiux force-pushed the laimonas/compute-type-usage-task branch from 840d8ec to b3c127e Compare March 6, 2026 22:24
field public static final com.autonomousapps.Flags INSTANCE;
}

@org.gradle.work.DisableCachingByDefault(because="Writes to console") public abstract class ListSourceFilesTask extends org.gradle.api.DefaultTask {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants