
com.autonomousapps.internal.reason.DependencyAdviceExplainer.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.internal.reason
import com.autonomousapps.graph.Graphs.shortestPath
import com.autonomousapps.internal.utils.Colors
import com.autonomousapps.internal.utils.Colors.colorize
import com.autonomousapps.internal.utils.appendReproducibleNewLine
import com.autonomousapps.internal.utils.lowercase
import com.autonomousapps.model.Advice
import com.autonomousapps.model.Coordinates
import com.autonomousapps.model.DependencyGraphView
import com.autonomousapps.model.ProjectCoordinates
import com.autonomousapps.model.declaration.SourceSetKind
import com.autonomousapps.model.declaration.Variant
import com.autonomousapps.model.intermediates.BundleTrace
import com.autonomousapps.model.intermediates.Reason
import com.autonomousapps.model.intermediates.Usage
import com.autonomousapps.tasks.ReasonTask
@Suppress("UnstableApiUsage") // guava
internal class DependencyAdviceExplainer(
private val project: ProjectCoordinates,
private val requestedId: Coordinates,
private val target: Coordinates,
private val usages: Set,
private val advice: Advice?,
private val dependencyGraph: Map,
private val bundleTraces: Set,
private val wasFiltered: Boolean,
private val dependencyMap: ((String) -> String?)? = null
) : ReasonTask.Explainer {
override fun computeReason() = buildString {
// Header
appendReproducibleNewLine()
append(Colors.BOLD)
appendReproducibleNewLine("-".repeat(40))
append("You asked about the dependency '${printableIdentifier(requestedId)}'.")
appendReproducibleNewLine(Colors.NORMAL)
appendReproducibleNewLine(adviceText())
append(Colors.BOLD)
append("-".repeat(40))
appendReproducibleNewLine(Colors.NORMAL)
// Shortest path
dependencyGraph.forEach { printGraph(it.value) }
// Usages
printUsages()
}
private val bundle = "bundle".colorize(Colors.BOLD)
private fun adviceText(): String = when {
advice == null -> {
if (bundleTraces.isNotEmpty()) {
when (val trace = findTrace() ?: error("There must be a match. Available traces: $bundleTraces")) {
is BundleTrace.DeclaredParent -> {
"There is no advice regarding this dependency. It was removed because it matched a $bundle rule for " +
"${printableIdentifier(trace.parent).colorize(Colors.BOLD)}, which is already declared."
}
is BundleTrace.UsedChild -> {
"There is no advice regarding this dependency. It was removed because it matched a $bundle rule for " +
"${printableIdentifier(trace.child).colorize(Colors.BOLD)}, which is declared and used."
}
else -> error("Trace was $trace, which makes no sense in this context")
}
} else if (wasFiltered) {
val exclude = "exclude".colorize(Colors.BOLD)
"There is no advice regarding this dependency. It was removed because it matched an $exclude rule."
} else {
"There is no advice regarding this dependency."
}
}
advice.isAdd() -> {
val trace = findTrace()
if (trace != null) {
check(trace is BundleTrace.PrimaryMap) { "Expected a ${BundleTrace.PrimaryMap::class.java.simpleName}" }
"You have been advised to add this dependency to '${advice.toConfiguration!!.colorize(Colors.GREEN)}'. " +
"It matched a $bundle rule: ${printableIdentifier(trace.primary).colorize(Colors.BOLD)} was substituted for " +
"${printableIdentifier(trace.subordinate).colorize(Colors.BOLD)}."
} else {
"You have been advised to add this dependency to '${advice.toConfiguration!!.colorize(Colors.GREEN)}'."
}
}
advice.isRemove() || advice.isProcessor() -> {
"You have been advised to remove this dependency from '${advice.fromConfiguration!!.colorize(Colors.RED)}'."
}
advice.isChange() || advice.isRuntimeOnly() || advice.isCompileOnly() -> {
"You have been advised to change this dependency to '${advice.toConfiguration!!.colorize(Colors.GREEN)}' " +
"from '${advice.fromConfiguration!!.colorize(Colors.YELLOW)}'."
}
else -> error("Unknown advice type: $advice")
}
// TODO: what are the valid scenarios? How many traces could there be for a single target?
private fun findTrace(): BundleTrace? = bundleTraces.find { it.top == target || it.bottom == target }
private fun StringBuilder.printGraph(graphView: DependencyGraphView) {
val name = graphView.configurationName
// Find the complete Coordinates (including variant identification) in the graph (if available)
val targetInGraph = graphView.graph.nodes().firstOrNull { it.identifier == target.identifier }
if (targetInGraph == null) {
appendReproducibleNewLine()
append(Colors.BOLD)
appendReproducibleNewLine(
"There is no path from ${project.printableName()} to ${printableIdentifier(target)} for $name"
)
appendReproducibleNewLine(Colors.NORMAL)
return
}
val nodes = graphView.graph.shortestPath(source = project, target = targetInGraph)
appendReproducibleNewLine()
append(Colors.BOLD)
append("Shortest path from ${project.printableName()} to ${printableIdentifier(target)} for $name:")
appendReproducibleNewLine(Colors.NORMAL)
appendReproducibleNewLine(project.gav())
nodes.drop(1).forEachIndexed { i, node ->
append(" ".repeat(i))
append("\\--- ")
appendReproducibleNewLine(node.gav())
}
}
private fun StringBuilder.printUsages() {
if (usages.isEmpty()) {
appendReproducibleNewLine()
appendReproducibleNewLine("No compile-time usages detected for this runtime-only dependency.")
return
}
usages.forEach { usage ->
val variant = usage.variant
appendReproducibleNewLine()
sourceText(variant).let { txt ->
append(Colors.BOLD)
appendReproducibleNewLine(txt)
append("-".repeat(txt.length))
appendReproducibleNewLine(Colors.NORMAL)
}
val reasons = usage.reasons.filter { it !is Reason.Unused && it !is Reason.Undeclared }
val isCompileOnly = reasons.any { it is Reason.CompileTimeAnnotations }
reasons.forEach { reason ->
append("""* """)
val prefix = when (variant.kind) {
SourceSetKind.MAIN -> ""
SourceSetKind.CUSTOM_JVM -> variant.variant
else -> "test"
}
appendReproducibleNewLine(reason.reason(prefix, isCompileOnly))
}
if (reasons.isEmpty()) {
appendReproducibleNewLine("(no usages)")
}
}
}
private fun printableIdentifier(coordinates: Coordinates): String {
val gav = coordinates.gav()
val mapped = dependencyMap?.invoke(gav) ?: dependencyMap?.invoke(coordinates.identifier)
return if (!mapped.isNullOrBlank()) "$gav ($mapped)" else gav
}
private fun ProjectCoordinates.printableName(): String {
val gav = gav()
return if (gav == ":") "root project" else gav
}
private fun sourceText(variant: Variant): String = when {
variant.variant in listOf(Variant.MAIN_NAME, Variant.TEST_NAME) || variant.kind == SourceSetKind.CUSTOM_JVM -> "Source: ${variant.variant}"
else -> "Source: ${variant.variant}, ${variant.kind.name.lowercase()}"
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy