Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ final class AdviceHelper {
return STRATEGY.actualComprehensiveAdviceForProject(gradleProject, projectName)
}

static FlatCoordinates flatCoordinates(String identifier) {
return new FlatCoordinates(identifier)
}

static ModuleCoordinates moduleCoordinates(com.autonomousapps.kit.gradle.Dependency dep) {
return moduleCoordinates(dep.identifier, dep.version)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2026. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.jvm

import com.autonomousapps.jvm.projects.BuildLogicVersionCatalogProject
import com.autonomousapps.utils.Colors

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

// TODO(tsr): I can imagine improving this further and permitting analysis of "Gradle Jars". See ArtifactsReportTask and
// `filterNonGradle()`.
final class BuildLogicVersionCatalogSpec extends AbstractJvmSpec {

// The bad advice looks like this:
// > Task :buildHealth
// Advice for root project
// Unused dependencies which should be removed:
// implementation ''
//
// Or in model form:
// Advice(coordinates=FlatCoordinates(identifier=), fromConfiguration=implementation, toConfiguration=null)
def "can handle an opaque dependency on the gradle version catalog (#gradleVersion)"() {
given:
def project = new BuildLogicVersionCatalogProject(true)
gradleProject = project.gradleProject

when:
def result = build(gradleVersion, gradleProject.rootDir, '-p', 'build-logic', 'buildHealth', ':reason', '--id', 'gradle-version-catalog')

then:
assertThat(project.actualProjectAdvice()).containsExactlyElementsIn(project.expectedProjectAdvice())

and:
assertThat(Colors.decolorize(result.output)).contains(project.expectedReason())

where:
gradleVersion << gradleVersions()
}

def "advice contains meaningful representation of flat coordinates (#gradleVersion)"() {
given:
def project = new BuildLogicVersionCatalogProject(false)
gradleProject = project.gradleProject

when:
def result = build(gradleVersion, gradleProject.rootDir, '-p', 'build-logic', 'buildHealth', ':reason', '--id', 'gradle-version-catalog')

then:
assertThat(project.actualProjectAdvice()).containsExactlyElementsIn(project.expectedProjectAdvice())

and:
assertThat(Colors.decolorize(result.output)).contains(project.expectedReason())

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

import com.autonomousapps.AbstractProject
import com.autonomousapps.internal.utils.OpaqueNames
import com.autonomousapps.kit.GradleProject
import com.autonomousapps.kit.Source
import com.autonomousapps.kit.gradle.Plugin
import com.autonomousapps.kit.gradle.dependencies.Plugins
import com.autonomousapps.model.Advice
import com.autonomousapps.model.ProjectAdvice

import static com.autonomousapps.AdviceHelper.*
import static com.autonomousapps.kit.gradle.Dependency.implementation

final class BuildLogicVersionCatalogProject extends AbstractProject {

private static final String BUILD_LOGIC = 'build-logic'
private final boolean used
final GradleProject gradleProject

BuildLogicVersionCatalogProject(boolean used) {
this.used = used
this.gradleProject = build()
}

private GradleProject build() {
return newGradleProjectBuilder()
.withRootProject { r ->
r.withVersionCatalog(
"""\
[versions]
dagp = "3.7.0"

[libraries]
dagp = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dagp" }

[plugins]
dependencyAnalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dagp" }""".stripIndent()
)
r.settingsScript.additions = "includeBuild '$BUILD_LOGIC'"
}
.withIncludedBuild(BUILD_LOGIC) { buildLogic ->
buildLogic.withRootProject { r ->
r.gradleProperties += ADDITIONAL_PROPERTIES
r.withSettingsScript { s ->
s.additions = """\
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}""".stripIndent()
}
r.withBuildScript { bs ->
bs.plugins(Plugins.dependencyAnalysis, Plugins.kotlinJvm, Plugin.javaGradle)
bs.dependencies(
// Kotlin DSL: files(libs::class.java.superclass.protectionDomain.codeSource.location)
implementation('files(libs.class.superclass.protectionDomain.codeSource.location)').raw()
)
}
r.sources = pluginSources()
}
}
.write()
}

private List<Source> pluginSources() {
if (used) {
[
Source.kotlin(
'''\
package mutual.aid

import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.Plugin
import org.gradle.api.Project

abstract class MyPlugin : Plugin<Project> {
override fun apply(target: Project) {
val libs: LibrariesForLibs = target.extensions.getByName("libs") as LibrariesForLibs
val dagp = libs.plugins.dependencyAnalysis
}
}'''.stripIndent()
).build()
]
} else {
[
Source.kotlin(
'''\
package mutual.aid

import org.gradle.api.Plugin
import org.gradle.api.Project

abstract class MyPlugin : Plugin<Project> {
override fun apply(target: Project) {}
}'''.stripIndent()
).build()
]
}
}

Set<ProjectAdvice> actualProjectAdvice() {
return [
actualProjectAdviceForProject(gradleProject.includedBuilds.first(), ':'),
]
}

private Set<Advice> unusedAdvice() {
[Advice.ofRemove(flatCoordinates(OpaqueNames.GRADLE_VERSION_CATALOG), 'implementation')]
}

Set<ProjectAdvice> expectedProjectAdvice() {
if (used) {
[emptyProjectAdviceFor(':')]
} else {
[projectAdviceForDependencies(':', unusedAdvice())]
}
}

String expectedReason() {
if (used) {
'* Uses 2 classes: org.gradle.accessors.dm.LibrariesForLibs, org.gradle.accessors.dm.LibrariesForLibs$PluginAccessors (implies implementation).'
} else {
'''\
You asked about the dependency 'gradle-version-catalog'.
You have been advised to remove this dependency from 'implementation'.'''.stripIndent()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import com.autonomousapps.AbstractProject
import com.autonomousapps.kit.GradleProject
import com.autonomousapps.kit.Source
import com.autonomousapps.kit.SourceType
import com.autonomousapps.kit.gradle.Dependency
import com.autonomousapps.kit.gradle.Plugin
import com.autonomousapps.kit.gradle.dependencies.Plugins
import com.autonomousapps.model.Advice
import com.autonomousapps.model.ProjectAdvice

import static com.autonomousapps.AdviceHelper.*
import static com.autonomousapps.kit.gradle.Dependency.implementation
import static com.autonomousapps.kit.gradle.Dependency.testImplementation

final class IncludedBuildProject extends AbstractProject {
Expand All @@ -29,7 +29,7 @@ final class IncludedBuildProject extends AbstractProject {
.withRootProject { root ->
root.withBuildScript { bs ->
bs.plugins.add(Plugin.javaLibrary)
bs.dependencies = [new Dependency('implementation', 'second:second-build:1.0')]
bs.dependencies(implementation('second:second-build:1.0'))
bs.group = 'first'
bs.version = '1.0'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package com.autonomousapps.internal.analyzer

import com.autonomousapps.internal.OutputPaths
import com.autonomousapps.internal.artifactsFor
import com.autonomousapps.internal.opaqueComponentArtifacts
import com.autonomousapps.internal.utils.project.buildPath
import com.autonomousapps.model.DuplicateClass
import com.autonomousapps.model.source.SourceKind
Expand All @@ -16,6 +17,7 @@ import org.gradle.api.UnknownDomainObjectException
import org.gradle.api.artifacts.Configuration
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import org.gradle.internal.component.local.model.OpaqueComponentArtifactIdentifier

/** Abstraction for differentiating between android-app, android-lib, and java-lib projects. */
internal interface DependencyAnalyzer {
Expand Down Expand Up @@ -215,6 +217,9 @@ internal abstract class AbstractDependencyAnalyzer(
t.setConfiguration(project.configurations.named(compileConfigurationName)) { c ->
c.artifactsFor(attributeValueJar)
}
t.setOpaqueConfiguration(project.configurations.named(compileConfigurationName)) { c ->
c.opaqueComponentArtifacts()
}
t.buildPath.set(project.buildPath(compileConfigurationName))

t.output.set(outputPaths.compileArtifactsPath)
Expand All @@ -227,6 +232,9 @@ internal abstract class AbstractDependencyAnalyzer(
t.setConfiguration(project.configurations.named(runtimeConfigurationName)) { c ->
c.artifactsFor(attributeValueJar)
}
t.setOpaqueConfiguration(project.configurations.named(runtimeConfigurationName)) { c ->
c.opaqueComponentArtifacts()
}
t.buildPath.set(project.buildPath(runtimeConfigurationName))

t.output.set(outputPaths.runtimeArtifactsPath)
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/com/autonomousapps/internal/artifactViews.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.Category
import org.gradle.internal.component.local.model.OpaqueComponentArtifactIdentifier

/**
* This is different than [org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE], which has type
Expand All @@ -20,6 +21,13 @@ private val attributeKey = Attribute.of("artifactType", String::class.java)

internal fun Configuration.artifactsFor(attrValue: String): ArtifactCollection = artifactViewFor(attrValue).artifacts

/** Captures things like the Gradle version catalog and Gradle API jar. */
internal fun Configuration.opaqueComponentArtifacts(): ArtifactCollection = incoming.artifactView { view ->
view
.componentFilter { id -> id is OpaqueComponentArtifactIdentifier }
.lenient(true)
}.artifacts

private fun Configuration.artifactViewFor(attrValue: String): ArtifactView = incoming.artifactView {
it.attributes.attribute(attributeKey, attrValue)
it.lenient(true)
Expand Down
49 changes: 47 additions & 2 deletions src/main/kotlin/com/autonomousapps/internal/utils/gradleStrings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.internal.utils

import com.autonomousapps.internal.utils.OpaqueNames.GRADLE_VERSION_CATALOG
import com.autonomousapps.model.*
import org.gradle.api.GradleException
import org.gradle.api.artifacts.*
Expand All @@ -27,6 +28,20 @@ import org.gradle.internal.component.local.model.OpaqueComponentIdentifier
import java.io.File
import java.io.Serializable

public object OpaqueNames {
public const val GRADLE_VERSION_CATALOG: String = "gradle-version-catalog"

/**
* An example file/URL path is:
* ```
* "file:/Users/tony/.gradle/caches/9.2.1/dependencies-accessors/d174284f561fa71e6bcefa60fe17d9f7c7a5acf7/classes/"
* ```
*/
internal fun isGradleVersionCatalog(fileLike: String): Boolean {
return fileLike.contains("dependencies-accessors")
}
}

/** Converts this [ResolvedDependencyResult] to group-artifact-version (GAV) coordinates in a tuple of (GA, V?). */
internal fun ResolvedDependencyResult.toCoordinates(): Coordinates =
compositeRequest() ?: selected.id.wrapInIncludedBuildCoordinates(resolvedVariant)
Expand Down Expand Up @@ -130,7 +145,13 @@ private fun ComponentIdentifier.toIdentifier(): String = when (this) {
// e.g. "Gradle API"
is OpaqueComponentIdentifier -> displayName
// for a file dependency
is OpaqueComponentArtifactIdentifier -> displayName
is OpaqueComponentArtifactIdentifier -> {
if (OpaqueNames.isGradleVersionCatalog(file.toString())) {
GRADLE_VERSION_CATALOG
} else {
displayName
}
}
else -> throw GradleException("Cannot identify ComponentIdentifier subtype. Was ${javaClass.simpleName}, named $this")
}.intern()

Expand Down Expand Up @@ -221,10 +242,34 @@ internal fun Dependency.toIdentifier(): Pair<ModuleInfo, GradleVariantIdentifica
// Handle weirdness that seems to come from KGP? Unclear. See comments from
// https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/997#issuecomment-1826627186
when (firstFile) {
null -> null
is Function0<*> -> null // "() -> Any?"
is Provider<*> -> null // "property 'destinationDirectory'"
is File -> firstFile.invariantSeparatorsPath.substringAfterLast('/')
else -> firstFile?.toString()?.let { it.substring(it.lastIndexOfAny(charArrayOf('/', '\\')) + 1) }

// TODO: can be a URL too
// TODO: cleanup

else -> {
// This can result in an empty string when this is the value (a gradle version catalog):
// "file:/Users/tony/.gradle/caches/9.2.1/dependencies-accessors/d174284f561fa71e6bcefa60fe17d9f7c7a5acf7/classes/"
val s = firstFile.toString()

if (OpaqueNames.isGradleVersionCatalog(s)) {
// "...dependencies-accessors..." => GRADLE_VERSION_CATALOG
GRADLE_VERSION_CATALOG
} else if (s.endsWith('/') || s.endsWith('\\')) {
// ".../classes/" => "classes"
// "...\classes\" => "classes"
s
.substringBeforeLast('/').substringBeforeLast('\\')
.let { it.substring(it.lastIndexOfAny(charArrayOf('/', '\\')) + 1) }
} else {
// ".../foo" => "foo"
// "...\foo" => "foo"
s.let { it.substring(it.lastIndexOfAny(charArrayOf('/', '\\')) + 1) }
}
}
}
}?.let {
Pair(ModuleInfo(it.intern()), GradleVariantIdentification.EMPTY)
Expand Down
Loading
Loading