com.autonomousapps.tasks.ComputeAdviceTask.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dependency-analysis-gradle-plugin Show documentation
Show all versions of dependency-analysis-gradle-plugin Show documentation
Analyzes dependency usage in Android and JVM projects
// Copyright (c) 2024. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.tasks
import com.autonomousapps.extension.DependenciesHandler
import com.autonomousapps.graph.Graphs.children
import com.autonomousapps.graph.Graphs.root
import com.autonomousapps.internal.Bundles
import com.autonomousapps.internal.utils.*
import com.autonomousapps.internal.utils.CoordinatesString.Companion.toStringCoordinates
import com.autonomousapps.model.*
import com.autonomousapps.model.declaration.Bucket
import com.autonomousapps.model.declaration.Configurations
import com.autonomousapps.model.declaration.Declaration
import com.autonomousapps.model.declaration.Variant
import com.autonomousapps.model.intermediates.*
import com.autonomousapps.transform.StandardTransform
import com.google.common.collect.SetMultimap
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
import javax.inject.Inject
/**
* Takes [usage][com.autonomousapps.model.intermediates.Usage] information from [ComputeUsagesTask] and emits the set of
* transforms a user should perform to have correct and simple dependency declarations. I.e., produces the advice.
*/
@CacheableTask
abstract class ComputeAdviceTask @Inject constructor(
private val workerExecutor: WorkerExecutor,
) : DefaultTask() {
init {
description = "Merges dependency usage reports from variant-specific computations"
}
@get:Input
abstract val projectPath: Property
@get:Input
abstract val buildPath: Property
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
abstract val dependencyUsageReports: ListProperty
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
abstract val dependencyGraphViews: ListProperty
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
abstract val androidScoreReports: ListProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile
abstract val declarations: RegularFileProperty
@get:Nested
abstract val bundles: Property
@get:Input
abstract val supportedSourceSets: SetProperty
@get:Input
abstract val ignoreKtx: Property
@get:Input
abstract val explicitSourceSets: SetProperty
@get:Input
abstract val kapt: Property
@get:Optional
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile
abstract val redundantJvmPluginReport: RegularFileProperty
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
abstract val duplicateClassesReports: ListProperty
/*
* Outputs.
*/
@get:OutputFile
abstract val output: RegularFileProperty
@get:OutputFile
abstract val dependencyUsages: RegularFileProperty
@get:OutputFile
abstract val annotationProcessorUsages: RegularFileProperty
@get:OutputFile
abstract val bundledTraces: RegularFileProperty
@TaskAction fun action() {
workerExecutor.noIsolation().submit(ComputeAdviceAction::class.java) {
projectPath.set([email protected])
buildPath.set([email protected])
dependencyUsageReports.set([email protected])
dependencyGraphViews.set([email protected])
androidScoreReports.set([email protected])
declarations.set([email protected])
bundles.set([email protected])
supportedSourceSets.set([email protected])
ignoreKtx.set([email protected])
explicitSourceSets.set([email protected])
kapt.set([email protected])
redundantPluginReport.set([email protected])
duplicateClassesReports.set([email protected])
output.set([email protected])
dependencyUsages.set([email protected])
annotationProcessorUsages.set([email protected])
bundledTraces.set([email protected])
}
}
interface ComputeAdviceParameters : WorkParameters {
val projectPath: Property
val buildPath: Property
val dependencyUsageReports: ListProperty
val dependencyGraphViews: ListProperty
val androidScoreReports: ListProperty
val declarations: RegularFileProperty
val bundles: Property
val supportedSourceSets: SetProperty
val ignoreKtx: Property
val explicitSourceSets: SetProperty
val kapt: Property
val redundantPluginReport: RegularFileProperty
val duplicateClassesReports: ListProperty
val output: RegularFileProperty
val dependencyUsages: RegularFileProperty
val annotationProcessorUsages: RegularFileProperty
val bundledTraces: RegularFileProperty
}
abstract class ComputeAdviceAction : WorkAction {
override fun execute() {
val output = parameters.output.getAndDelete()
val dependencyUsagesOut = parameters.dependencyUsages.getAndDelete()
val annotationProcessorUsagesOut = parameters.annotationProcessorUsages.getAndDelete()
val bundleTraces = parameters.bundledTraces.getAndDelete()
val projectPath = parameters.projectPath.get()
val buildPath = parameters.buildPath.get()
val declarations = parameters.declarations.fromJsonSet()
val dependencyGraph = parameters.dependencyGraphViews.get()
.map { it.fromJson() }
.associateBy { it.name }
val androidScore = parameters.androidScoreReports.get()
.map { it.fromJson() }
.run { AndroidScore.ofVariants(this) }
.toSetOrEmpty()
val bundleRules = parameters.bundles.get()
val traces = parameters.dependencyUsageReports.get().mapToSet { it.fromJson() }
val usageBuilder = UsageBuilder(
traces = traces,
// TODO: it would be clearer to get this from a SyntheticProject
variants = dependencyGraph.values.map { it.variant }
)
val dependencyUsages = usageBuilder.dependencyUsages
val annotationProcessorUsages = usageBuilder.annotationProcessingUsages
val supportedSourceSets = parameters.supportedSourceSets.get()
val explicitSourceSets = parameters.explicitSourceSets.get()
val isKaptApplied = parameters.kapt.get()
val directDependencies = computeDirectDependenciesMap(dependencyGraph)
val ignoreKtx = parameters.ignoreKtx.get()
val bundles = Bundles.of(
projectPath = projectPath,
dependencyGraph = dependencyGraph,
bundleRules = bundleRules,
dependencyUsages = dependencyUsages,
ignoreKtx = ignoreKtx,
)
val dependencyAdviceBuilder = DependencyAdviceBuilder(
projectPath = projectPath,
buildPath = buildPath,
bundles = bundles,
dependencyUsages = dependencyUsages,
annotationProcessorUsages = annotationProcessorUsages,
declarations = declarations,
directDependencies = directDependencies,
supportedSourceSets = supportedSourceSets,
explicitSourceSets = explicitSourceSets,
isKaptApplied = isKaptApplied,
)
val pluginAdviceBuilder = PluginAdviceBuilder(
isKaptApplied = isKaptApplied,
redundantPlugins = parameters.redundantPluginReport.fromNullableJsonSet(),
annotationProcessorUsages = annotationProcessorUsages,
)
val projectAdvice = ProjectAdvice(
projectPath = projectPath,
dependencyAdvice = dependencyAdviceBuilder.advice,
pluginAdvice = pluginAdviceBuilder.getPluginAdvice(),
moduleAdvice = androidScore,
warning = buildWarning(),
)
output.bufferWriteJson(projectAdvice)
// These must be transformed so that the Coordinates are Strings for serialization
dependencyUsagesOut.bufferWriteJsonMap(toStringCoordinates(dependencyUsages, buildPath))
annotationProcessorUsagesOut.bufferWriteJsonMap(toStringCoordinates(annotationProcessorUsages, buildPath))
bundleTraces.bufferWriteJsonSet(dependencyAdviceBuilder.bundledTraces)
}
private fun buildWarning(): Warning {
val duplicateClassesReports = parameters.duplicateClassesReports.get().asSequence()
.map { it.fromJsonSet() }
.flatten()
.toSortedSet()
return Warning(duplicateClassesReports)
}
/**
* Returns the set of direct (non-transitive) dependencies from [dependencyGraph], associated with the source sets
* ([Variant.variant][com.autonomousapps.model.declaration.Variant.variant]) they're used by.
*
* These are _direct_ dependencies that are not _declared_ because they're coming from associated classpaths. For
* example, the `test` source set extends from the `main` source set (and also the compile and runtime classpaths).
*/
private fun computeDirectDependenciesMap(
dependencyGraph: Map,
): SetMultimap {
return newSetMultimap().apply {
dependencyGraph.values.map { graphView ->
val root = graphView.graph.root()
graphView.graph.children(root).forEach { directDependency ->
val identifier = if (directDependency is IncludedBuildCoordinates) {
// An attempt to normalize the identifier
directDependency.resolvedProject.identifier
} else {
// TODO: just identifier and not gav()?
directDependency.identifier
}
put(identifier, graphView.variant)
}
}
}
}
}
}
internal class PluginAdviceBuilder(
isKaptApplied: Boolean,
redundantPlugins: Set,
annotationProcessorUsages: Map>,
) {
private val pluginAdvice = mutableSetOf()
fun getPluginAdvice(): Set = pluginAdvice
init {
pluginAdvice.addAll(redundantPlugins)
if (isKaptApplied) {
val usedProcs = annotationProcessorUsages.asSequence()
.filter { (_, usages) -> usages.any { it.bucket == Bucket.ANNOTATION_PROCESSOR } }
.map { it.key }
.toSet()
// kapt is unused
if (usedProcs.isEmpty()) {
pluginAdvice.add(PluginAdvice.redundantKapt())
}
}
}
}
internal class DependencyAdviceBuilder(
projectPath: String,
private val buildPath: String,
private val bundles: Bundles,
private val dependencyUsages: Map>,
private val annotationProcessorUsages: Map>,
private val declarations: Set,
private val directDependencies: SetMultimap,
private val supportedSourceSets: Set,
private val explicitSourceSets: Set,
private val isKaptApplied: Boolean,
) {
/** The unfiltered advice. */
val advice: Set
/** Dependencies that are removed from [advice] because they match a bundle rule. Used by **Reason**. */
val bundledTraces = mutableSetOf()
init {
advice = computeDependencyAdvice(projectPath)
.plus(computeAnnotationProcessorAdvice())
.toSortedSet()
}
private fun computeDependencyAdvice(projectPath: String): Sequence {
val declarations = declarations.filterToSet { Configurations.isForRegularDependency(it.configurationName) }
fun Advice.isRemoveTestDependencyOnSelf(): Boolean {
return coordinates.identifier == projectPath
// https://github.com/gradle/gradle/blob/d9303339298e6206182fd1f5c7e51f11e4bdff30/subprojects/plugins/src/main/java/org/gradle/api/plugins/JavaTestFixturesPlugin.java#L68
&& (fromConfiguration?.equals("testFixturesApi") == true
// https://github.com/gradle/gradle/blob/d9303339298e6206182fd1f5c7e51f11e4bdff30/subprojects/plugins/src/main/java/org/gradle/api/plugins/JavaTestFixturesPlugin.java#L70
|| fromConfiguration?.lowercase()?.endsWith("testimplementation") == true)
}
fun Advice.isAddTestDependencyOnSelf(): Boolean {
return coordinates.identifier == projectPath
&& (fromConfiguration == null && toConfiguration?.equals("testImplementation") == true)
}
return dependencyUsages.asSequence()
.flatMap { (coordinates, usages) ->
StandardTransform(
coordinates = coordinates,
declarations = declarations,
directDependencies = directDependencies,
supportedSourceSets = supportedSourceSets,
buildPath = buildPath,
explicitSourceSets = explicitSourceSets,
)
.reduce(usages)
.map { advice -> advice to coordinates }
}
// "null" removes the advice
.mapNotNull { (advice, originalCoordinates) ->
// Make sure to do all operations here based on the originalCoordinates used in the graph.
// The 'advice.coordinates' may be reduced - e.g. contain less capabilities in the GradleVariantIdentifier.
when {
// The user cannot change these
advice.isRemoveTestDependencyOnSelf() -> null
// The user should not have to add a test dependency on self
advice.isAddTestDependencyOnSelf() -> null
advice.isAdd() && bundles.hasParentInBundle(originalCoordinates) -> {
val parent = bundles.findParentInBundle(originalCoordinates)!!
bundledTraces += BundleTrace.DeclaredParent(parent = parent, child = originalCoordinates)
null
}
// Optionally map given advice to "primary" advice, if bundle has a primary
advice.isAdd() -> {
val p = bundles.maybePrimary(advice, originalCoordinates)
if (p != advice) {
bundledTraces += BundleTrace.PrimaryMap(primary = p.coordinates, subordinate = originalCoordinates)
}
p
}
advice.isRemove() && bundles.hasUsedChild(originalCoordinates) -> {
val child = bundles.findUsedChild(originalCoordinates)!!
bundledTraces += BundleTrace.UsedChild(parent = originalCoordinates, child = child)
null
}
// If the advice has a used child, don't change it
advice.isAnyChange() && bundles.hasUsedChild(originalCoordinates) -> {
val child = bundles.findUsedChild(originalCoordinates)!!
bundledTraces += BundleTrace.UsedChild(parent = originalCoordinates, child = child)
null
}
else -> advice
}
}
}
// nb: no bundle support for annotation processors
private fun computeAnnotationProcessorAdvice(): Sequence {
val declarations = declarations.filterToSet { Configurations.isForAnnotationProcessor(it.configurationName) }
return annotationProcessorUsages.asSequence()
.flatMap { (coordinates, usages) ->
StandardTransform(
coordinates = coordinates,
declarations = declarations,
directDependencies = emptySetMultimap(),
supportedSourceSets = supportedSourceSets,
buildPath = buildPath,
explicitSourceSets = explicitSourceSets,
isKaptApplied = isKaptApplied,
).reduce(usages)
}
}
}
/**
* Equivalent to
* ```
* someBoolean.also { b ->
* if (b) block()
* }
* ```
*/
internal inline fun Boolean.andIfTrue(block: () -> Unit): Boolean {
if (this) {
block()
}
return this
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy