Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions api/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package com.autonomousapps {
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.

method public final void app();
method public final void registerPostProcessingTask(org.gradle.api.tasks.TaskProvider<? extends com.autonomousapps.AbstractPostProcessingTask> task);
field public static final String NAME = "dependencyAnalysis";
Expand All @@ -28,6 +29,7 @@ package com.autonomousapps {
method public final void issues(org.gradle.api.Action<com.autonomousapps.extension.IssueHandler> action);
method public final void reporting(org.gradle.api.Action<com.autonomousapps.extension.ReportingHandler> action);
method public final void structure(org.gradle.api.Action<com.autonomousapps.extension.DependenciesHandler> action);
method public final void typeUsage(org.gradle.api.Action<com.autonomousapps.extension.TypeUsageHandler> action);
method public final void usage(org.gradle.api.Action<com.autonomousapps.extension.UsageHandler> action);
method @Deprecated public final void usages(org.gradle.api.Action<com.autonomousapps.extension.UsageHandler> action);
method public final void useTypesafeProjectAccessors(boolean enable);
Expand All @@ -44,6 +46,7 @@ package com.autonomousapps {
method public final void abi(org.gradle.api.Action<com.autonomousapps.extension.AbiHandler> action);
method public final void issues(org.gradle.api.Action<com.autonomousapps.extension.ProjectIssueHandler> action);
method public final void structure(org.gradle.api.Action<com.autonomousapps.extension.DependenciesHandler> action);
method public final void typeUsage(org.gradle.api.Action<com.autonomousapps.extension.TypeUsageHandler> action);
}

public final class Flags {
Expand Down Expand Up @@ -207,6 +210,13 @@ package com.autonomousapps.extension {
property @org.gradle.api.tasks.Input public abstract org.gradle.api.provider.Property<java.lang.String> postscript;
}

public abstract class TypeUsageHandler {
ctor @javax.inject.Inject public TypeUsageHandler(org.gradle.api.model.ObjectFactory objects);
method public final void excludePackages(java.lang.String... packages);
method public final void excludeRegex(@org.intellij.lang.annotations.Language("RegExp") java.lang.String... patterns);
method public final void excludeTypes(java.lang.String... types);
}

public abstract class SourceSetsHandler implements org.gradle.api.Named {
ctor @javax.inject.Inject public SourceSetsHandler(String sourceSetName, String projectPath, org.gradle.api.model.ObjectFactory objects);
method public String getName();
Expand Down Expand Up @@ -505,6 +515,48 @@ package com.autonomousapps.model {
property public String identifier;
}

@com.squareup.moshi.JsonClass(generateAdapter=false) public final class ProjectTypeUsage implements java.lang.Comparable<com.autonomousapps.model.ProjectTypeUsage> {
ctor public ProjectTypeUsage(String projectPath, com.autonomousapps.model.TypeUsageSummary summary, java.util.Map<java.lang.String,java.lang.Integer> internal, java.util.Map<java.lang.String,? extends java.util.Map<java.lang.String,java.lang.Integer>> projectDependencies, java.util.Map<java.lang.String,? extends java.util.Map<java.lang.String,java.lang.Integer>> libraryDependencies);
method public int compareTo(com.autonomousapps.model.ProjectTypeUsage other);
method public String component1();
method public com.autonomousapps.model.TypeUsageSummary component2();
method public java.util.Map<java.lang.String,java.lang.Integer> component3();
method public java.util.Map<java.lang.String,java.util.Map<java.lang.String,java.lang.Integer>> component4();
method public java.util.Map<java.lang.String,java.util.Map<java.lang.String,java.lang.Integer>> component5();
method public com.autonomousapps.model.ProjectTypeUsage copy(String projectPath, com.autonomousapps.model.TypeUsageSummary summary, java.util.Map<java.lang.String,java.lang.Integer> internal, java.util.Map<java.lang.String,? extends java.util.Map<java.lang.String,java.lang.Integer>> projectDependencies, java.util.Map<java.lang.String,? extends java.util.Map<java.lang.String,java.lang.Integer>> libraryDependencies);
method public java.util.Map<java.lang.String,java.lang.Integer> getInternal();
method public java.util.Map<java.lang.String,java.util.Map<java.lang.String,java.lang.Integer>> getLibraryDependencies();
method public java.util.Map<java.lang.String,java.util.Map<java.lang.String,java.lang.Integer>> getProjectDependencies();
method public String getProjectPath();
method public com.autonomousapps.model.TypeUsageSummary getSummary();
method public boolean isEmpty();
property public final java.util.Map<java.lang.String,java.lang.Integer> internal;
property public final java.util.Map<java.lang.String,java.util.Map<java.lang.String,java.lang.Integer>> libraryDependencies;
property public final java.util.Map<java.lang.String,java.util.Map<java.lang.String,java.lang.Integer>> projectDependencies;
property public final String projectPath;
property public final com.autonomousapps.model.TypeUsageSummary summary;
}

@com.squareup.moshi.JsonClass(generateAdapter=false) public final class TypeUsageSummary {
ctor public TypeUsageSummary(int totalTypes, int totalFiles, int internalTypes, int projectDependencies, int libraryDependencies);
method public int component1();
method public int component2();
method public int component3();
method public int component4();
method public int component5();
method public com.autonomousapps.model.TypeUsageSummary copy(int totalTypes, int totalFiles, int internalTypes, int projectDependencies, int libraryDependencies);
method public int getInternalTypes();
method public int getLibraryDependencies();
method public int getProjectDependencies();
method public int getTotalFiles();
method public int getTotalTypes();
property public final int internalTypes;
property public final int libraryDependencies;
property public final int projectDependencies;
property public final int totalFiles;
property public final int totalTypes;
}

@com.squareup.moshi.JsonClass(generateAdapter=false) public final class Warning implements java.lang.Comparable<com.autonomousapps.model.Warning> {
ctor public Warning(java.util.Set<com.autonomousapps.model.DuplicateClass> duplicateClasses);
method public int compareTo(com.autonomousapps.model.Warning other);
Expand Down
100 changes: 100 additions & 0 deletions src/functionalTest/groovy/com/autonomousapps/jvm/TypeUsageSpec.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) 2026. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.jvm

import com.autonomousapps.jvm.projects.TypeUsageProject
import com.autonomousapps.jvm.projects.TypeUsageWithFiltersProject

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


def "generates type usage report (#gradleVersion)"() {
given:
def project = new TypeUsageProject()
gradleProject = project.gradleProject

when:
build(gradleVersion, gradleProject.rootDir, 'computeTypeUsageMain')

then: 'has correct summary'
def usage = project.actualTypeUsage()
assertThat(usage.projectPath).isEqualTo(':proj')
assertThat(usage.summary.totalTypes).isGreaterThan(0)
assertThat(usage.summary.totalFiles).isEqualTo(2)
assertThat(usage.summary.internalTypes).isEqualTo(1)

and: 'tracks internal usage'
assertThat(usage.internal).containsKey('com.example.Example')
// Note: Internal class is not tracked because it's defined but never used

and: 'tracks library dependencies'
assertThat(usage.libraryDependencies).isNotEmpty()

and: 'tracks commons-collections usage'
assertThat(usage.libraryDependencies)
.containsKey('org.apache.commons:commons-collections4')
def commonsUsage = usage.libraryDependencies.get('org.apache.commons:commons-collections4')
assertThat(commonsUsage).containsKey('org.apache.commons.collections4.bag.HashBag')

and: 'tracks kotlin stdlib usage'
assert usage.libraryDependencies.keySet().any { it.startsWith('org.jetbrains.kotlin:kotlin-stdlib') }

where:
gradleVersion << gradleVersions()
}

def "excludes filtered types (#gradleVersion)"() {
given:
def project = new TypeUsageWithFiltersProject()
gradleProject = project.gradleProject

when:
build(gradleVersion, gradleProject.rootDir, 'computeTypeUsageMain')

then: 'excluded packages are not present'
def usage = project.actualTypeUsage()
def allTypes = usage.libraryDependencies.values()
.collectMany { it.keySet() }

assertThat(allTypes).doesNotContain('org.apache.commons.collections4.bag.HashBag')

and: 'excluded types are not present'
assertThat(allTypes).doesNotContain('kotlin.Unit')

and: 'non-excluded types are still present'
assertThat(usage.internal).containsKey('com.example.Example')

where:
gradleVersion << gradleVersions()
}

def "categorizes dependencies correctly (#gradleVersion)"() {
given:
def project = new TypeUsageProject()
gradleProject = project.gradleProject

when:
build(gradleVersion, gradleProject.rootDir, 'computeTypeUsageMain')

then: 'internal types are in internal map'
def usage = project.actualTypeUsage()
assertThat(usage.internal).isNotEmpty()
assertThat(usage.internal).containsKey('com.example.Example')

and: 'library types are in libraryDependencies map'
assertThat(usage.libraryDependencies).isNotEmpty()

and: 'no project dependencies (single-project)'
assertThat(usage.projectDependencies).isEmpty()

and: 'summary counts match'
assertThat(usage.summary.internalTypes).isEqualTo(usage.internal.size())
assertThat(usage.summary.libraryDependencies).isEqualTo(usage.libraryDependencies.size())
assertThat(usage.summary.projectDependencies).isEqualTo(0)

where:
gradleVersion << gradleVersions()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) 2026. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.jvm.projects

import com.autonomousapps.AbstractProject
import com.autonomousapps.kit.GradleProject
import com.autonomousapps.kit.Source
import com.autonomousapps.kit.SourceType
import com.autonomousapps.model.ProjectTypeUsage

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)


final GradleProject gradleProject

TypeUsageProject() {
this.gradleProject = build()
}

private GradleProject build() {
return newGradleProjectBuilder()
.withSubproject('proj') { s ->
s.sources = sources
s.withBuildScript { bs ->
bs.plugins = kotlin
bs.dependencies = [
commonsCollections('implementation'),
kotlinStdLib('implementation')
]
}
}
.write()
}

private sources = [
new Source(
SourceType.KOTLIN, "Example", "com/example",
"""\
package com.example

import org.apache.commons.collections4.bag.HashBag

class Example {
private val bag = HashBag<String>()

fun doSomething() {
bag.add("test")
}
}
""".stripIndent()
),
new Source(
SourceType.KOTLIN, "Internal", "com/example",
"""\
package com.example

internal class Internal {
fun helper() = Example()
}
""".stripIndent()
)
]

ProjectTypeUsage actualTypeUsage() {
def typeUsage = gradleProject.singleArtifact(':proj',
com.autonomousapps.internal.OutputPathsKt.getTypeUsagePath('main'))
def adapter = com.autonomousapps.internal.utils.MoshiUtils.MOSHI
.adapter(com.autonomousapps.model.ProjectTypeUsage)
return adapter.fromJson(typeUsage.asPath.text)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) 2026. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.jvm.projects

import com.autonomousapps.AbstractProject
import com.autonomousapps.kit.GradleProject
import com.autonomousapps.kit.Source
import com.autonomousapps.kit.SourceType
import com.autonomousapps.model.ProjectTypeUsage

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)


final GradleProject gradleProject

TypeUsageWithFiltersProject() {
this.gradleProject = build()
}

private GradleProject build() {
return newGradleProjectBuilder()
.withSubproject('proj') { s ->
s.sources = sources
s.withBuildScript { bs ->
bs.plugins = kotlin
bs.dependencies = [
commonsCollections('implementation'),
kotlinStdLib('implementation')
]
bs.additions = """\
dependencyAnalysis {
typeUsage {
excludePackages('org.apache.commons')
excludeTypes('kotlin.Unit')
}
}
""".stripIndent()
}
}
.write()
}

private sources = [
new Source(
SourceType.KOTLIN, "Example", "com/example",
"""\
package com.example

import org.apache.commons.collections4.bag.HashBag

class Example {
private val bag = HashBag<String>()

fun doSomething(): Unit {
bag.add("test")
}
}
""".stripIndent()
),
new Source(
SourceType.KOTLIN, "Internal", "com/example",
"""\
package com.example

internal class Internal {
fun helper() = Example()
}
""".stripIndent()
)
]

ProjectTypeUsage actualTypeUsage() {
def typeUsage = gradleProject.singleArtifact(':proj',
com.autonomousapps.internal.OutputPathsKt.getTypeUsagePath('main'))
def adapter = com.autonomousapps.internal.utils.MoshiUtils.MOSHI
.adapter(com.autonomousapps.model.ProjectTypeUsage)
return adapter.fromJson(typeUsage.asPath.text)
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/com/autonomousapps/AbstractExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ public abstract class AbstractExtension @Inject constructor(
internal val dependenciesHandler: DependenciesHandler = dslService.get().dependenciesHandler
internal val reportingHandler: ReportingHandler = dslService.get().reportingHandler
internal val usageHandler: UsageHandler = dslService.get().usageHandler
internal val typeUsageHandler: TypeUsageHandler = objects.newInstance(TypeUsageHandler::class.java)

private val adviceOutput = objects.fileProperty()
private val typeUsageOutput = objects.fileProperty()
private var postProcessingTask: TaskProvider<out AbstractPostProcessingTask>? = null

internal var forceAppProject = false
Expand All @@ -47,6 +49,13 @@ public abstract class AbstractExtension @Inject constructor(
adviceOutput.set(output)
}

internal fun storeTypeUsageOutput(provider: Provider<RegularFile>) {
val output = objects.fileProperty().also {
it.set(provider)
}
typeUsageOutput.set(output)
}

/**
* Returns the output from the project-level advice.
*
Expand All @@ -55,6 +64,14 @@ public abstract class AbstractExtension @Inject constructor(
@Suppress("MemberVisibilityCanBePrivate") // explicit API
public fun adviceOutput(): RegularFileProperty = adviceOutput

/**
* Returns the output from the project-level type usage analysis.
*
* Never null, but may _contain_ a null value. Use with [RegularFileProperty.getOrNull].
*/
@Suppress("MemberVisibilityCanBePrivate") // explicit API
public fun typeUsageOutput(): RegularFileProperty = typeUsageOutput

/**
* Whether to force the project being treated as an app project even if only the `java` plugin is applied.
*/
Expand Down
Loading