com.autonomousapps.tasks.ComputeUsagesTask.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.internal.utils.*
import com.autonomousapps.model.*
import com.autonomousapps.model.declaration.Bucket
import com.autonomousapps.model.declaration.Declaration
import com.autonomousapps.model.intermediates.DependencyTraceReport
import com.autonomousapps.model.intermediates.DependencyTraceReport.Kind
import com.autonomousapps.model.intermediates.Reason
import com.autonomousapps.visitor.GraphViewReader
import com.autonomousapps.visitor.GraphViewVisitor
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
import javax.inject.Inject
@CacheableTask
abstract class ComputeUsagesTask @Inject constructor(
private val workerExecutor: WorkerExecutor,
) : DefaultTask() {
init {
description = "Computes actual dependency usage"
}
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile
abstract val graph: RegularFileProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile
abstract val declarations: RegularFileProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputDirectory
abstract val dependencies: DirectoryProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile
abstract val syntheticProject: RegularFileProperty
@get:Input
abstract val kapt: Property
@get:OutputFile
abstract val output: RegularFileProperty
@TaskAction fun action() {
workerExecutor.noIsolation().submit(ComputeUsagesAction::class.java) {
graph.set([email protected])
declarations.set([email protected])
dependencies.set([email protected])
syntheticProject.set([email protected])
kapt.set([email protected])
output.set([email protected])
}
}
interface ComputeUsagesParameters : WorkParameters {
val graph: RegularFileProperty
val declarations: RegularFileProperty
val dependencies: DirectoryProperty
val syntheticProject: RegularFileProperty
val kapt: Property
val output: RegularFileProperty
}
abstract class ComputeUsagesAction : WorkAction {
private val graph = parameters.graph.fromJson()
private val declarations = parameters.declarations.fromJsonSet()
private val project = parameters.syntheticProject.fromJson()
private val dependencies = project.dependencies(parameters.dependencies.get())
override fun execute() {
val output = parameters.output.getAndDelete()
val reader = GraphViewReader(
project = project,
dependencies = dependencies,
graph = graph,
declarations = declarations
)
val visitor = GraphVisitor(project, parameters.kapt.get())
reader.accept(visitor)
val report = visitor.report
output.bufferWriteJson(report)
}
}
}
private class GraphVisitor(
project: ProjectVariant,
private val kapt: Boolean,
) : GraphViewVisitor {
val report: DependencyTraceReport get() = reportBuilder.build()
private val reportBuilder = DependencyTraceReport.Builder(
buildType = project.buildType,
flavor = project.flavor,
variant = project.variant
)
override fun visit(dependency: Dependency, context: GraphViewVisitor.Context) {
val dependencyCoordinates = dependency.coordinates
var isAnnotationProcessor = false
var isAnnotationProcessorCandidate = false
var isApiCandidate = false
var isImplCandidate = false
var isImplByImportCandidate = false
var isUnusedCandidate = false
var isLintJar = false
var isCompileOnlyCandidate = false
var isRequiredAnnotationCandidate = false
var isCompileOnlyAnnotationCandidate = false
var isRuntimeAndroid = false
var usesTestInstrumentationRunner = false
var usesResBySource = false
var usesResByRes = false
var usesAssets = false
var usesConstant = false
var usesInlineMember = false
var hasServiceLoader = false
var hasSecurityProvider = false
var hasNativeLib = false
dependency.capabilities.values.forEach { capability ->
@Suppress("UNUSED_VARIABLE") // exhaustive when
val ignored: Any = when (capability) {
is AndroidLinterCapability -> {
isLintJar = capability.isLintJar
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Reason.LintJar.of(capability.lintRegistry)
}
is AndroidManifestCapability -> isRuntimeAndroid = isRuntimeAndroid(dependencyCoordinates, capability)
is AndroidAssetCapability -> usesAssets = usesAssets(dependencyCoordinates, capability, context)
is AndroidResCapability -> {
usesResBySource = usesResBySource(dependencyCoordinates, capability, context)
usesResByRes = usesResByRes(dependencyCoordinates, capability, context)
}
is AnnotationProcessorCapability -> {
isAnnotationProcessor = true
isAnnotationProcessorCandidate = usesAnnotationProcessor(dependencyCoordinates, capability, context)
}
is ClassCapability -> {
// We want to track this in addition to tracking one of the below, so it's not part of the same if/else-if
// chain.
if (containsAndroidTestInstrumentationRunner(dependencyCoordinates, capability, context)) {
usesTestInstrumentationRunner = true
}
if (isAbi(dependencyCoordinates, capability, context)) {
isApiCandidate = true
} else if (isImplementation(dependencyCoordinates, capability, context)) {
isImplCandidate = true
} else if (isImported(dependencyCoordinates, capability, context)) {
isImplByImportCandidate = true
} else if (usesAnnotation(dependencyCoordinates, capability, context)) {
isRequiredAnnotationCandidate = true
} else if (usesInvisibleAnnotation(dependencyCoordinates, capability, context)) {
isCompileOnlyAnnotationCandidate = true
} else {
isUnusedCandidate = true
}
}
is ConstantCapability -> usesConstant = usesConstant(dependencyCoordinates, capability, context)
is InferredCapability -> {
if (capability.isCompileOnlyAnnotations) {
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Reason.CompileTimeAnnotations()
}
isCompileOnlyCandidate = capability.isCompileOnlyAnnotations
}
is InlineMemberCapability -> usesInlineMember = usesInlineMember(dependencyCoordinates, capability, context)
is TypealiasCapability -> {
if (isImplementation(dependencyCoordinates, capability, context)) {
isImplCandidate = true
isUnusedCandidate = false
}
// for exhaustive when
Unit
}
is ServiceLoaderCapability -> {
val providers = capability.providerClasses
hasServiceLoader = providers.isNotEmpty()
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Reason.ServiceLoader(providers)
}
is NativeLibCapability -> {
val fileNames = capability.fileNames
hasNativeLib = fileNames.isNotEmpty()
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Reason.NativeLib(fileNames)
}
is SecurityProviderCapability -> {
val providers = capability.securityProviders
hasSecurityProvider = providers.isNotEmpty()
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Reason.SecurityProvider(providers)
}
}
}
// TODO KMP dependencies only contain metadata (pom/module files). This is good evidence of a facade and could be
// used for smarter detection of same.
// An example are KMP facades that resolve to -jvm artifacts
if (dependency.capabilities.isEmpty()) {
isUnusedCandidate = true
}
// this is not mutually exclusive with other buckets. E.g., Lombok is both an annotation processor and a "normal"
// dependency. See LombokSpec.
if (isAnnotationProcessorCandidate) {
reportBuilder[dependencyCoordinates, Kind.ANNOTATION_PROCESSOR] = Bucket.ANNOTATION_PROCESSOR
} else if (isAnnotationProcessor) {
// unused annotation processor
reportBuilder[dependencyCoordinates, Kind.ANNOTATION_PROCESSOR] = Bucket.NONE
}
/*
* The order below is critically important.
*/
if (isApiCandidate) {
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.API
} else if (isImplCandidate) {
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.IMPL
} else if (isCompileOnlyCandidate) {
// TODO compileOnlyApi? Only relevant for java-library projects
// compileOnly candidates are not also unused candidates. Some annotations are not detectable by bytecode
// analysis (SOURCE retention), and possibly not by source parsing either (they could be in the same package), so
// we don't suggest removing such dependencies.
isUnusedCandidate = false
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.COMPILE_ONLY
} else if (isImplByImportCandidate) {
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.IMPL
} else if (isRequiredAnnotationCandidate) {
// We detected an annotation, but it's a RUNTIME annotation, so we can't suggest it be moved to compileOnly.
// Don't suggest removing it!
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.IMPL
} else if (isCompileOnlyAnnotationCandidate) {
isUnusedCandidate = false
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.COMPILE_ONLY
} else if (noRealCapabilities(dependency)) {
isUnusedCandidate = true
}
if (isUnusedCandidate) {
// These weren't detected by direct presence in bytecode, but (in some cases) via source analysis. We can say less
// about them, so we dump them into `implementation` or `runtimeOnly`.
when {
usesResBySource -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.IMPL
usesResByRes -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.IMPL
usesConstant -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.IMPL
usesInlineMember -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.IMPL
isLintJar -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.RUNTIME_ONLY
isRuntimeAndroid -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.RUNTIME_ONLY
usesTestInstrumentationRunner -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.RUNTIME_ONLY
usesAssets -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.RUNTIME_ONLY
hasServiceLoader -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.RUNTIME_ONLY
hasSecurityProvider -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.RUNTIME_ONLY
hasNativeLib -> reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.RUNTIME_ONLY
else -> {
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Bucket.NONE
reportBuilder[dependencyCoordinates, Kind.DEPENDENCY] = Reason.Unused
}
}
}
}
private fun noRealCapabilities(dependency: Dependency): Boolean {
if (dependency.capabilities.isEmpty()) return true
val inferred = dependency.capabilities.values.singleOrNull { it is InferredCapability } as? InferredCapability
return inferred?.isCompileOnlyAnnotations == false
}
private fun isRuntimeAndroid(coordinates: Coordinates, capability: AndroidManifestCapability): Boolean {
val components = capability.componentMap
val activities = components[AndroidManifestCapability.Component.ACTIVITY]?.also {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.RuntimeAndroid.activities(it)
}
val providers = components[AndroidManifestCapability.Component.PROVIDER]?.also {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.RuntimeAndroid.providers(it)
}
val receivers = components[AndroidManifestCapability.Component.RECEIVER]?.also {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.RuntimeAndroid.receivers(it)
}
val services = components[AndroidManifestCapability.Component.SERVICE]?.also {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.RuntimeAndroid.services(it)
}
return activities != null || providers != null || receivers != null || services != null
}
private fun isAbi(
coordinates: Coordinates,
classCapability: ClassCapability,
context: GraphViewVisitor.Context,
): Boolean {
val exposedClasses = context.project.exposedClasses.asSequence().filter { exposedClass ->
classCapability.classes.contains(exposedClass)
}.toSortedSet()
return if (exposedClasses.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.Abi(exposedClasses)
true
} else {
false
}
}
private fun isImplementation(
coordinates: Coordinates,
classCapability: ClassCapability,
context: GraphViewVisitor.Context,
): Boolean {
val implClasses = context.project.implementationClasses.asSequence().filter { implClass ->
classCapability.classes.contains(implClass)
}.toSortedSet()
return if (implClasses.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.Impl(implClasses)
true
} else {
false
}
}
private fun usesAnnotation(
coordinates: Coordinates,
classCapability: ClassCapability,
context: GraphViewVisitor.Context,
): Boolean {
val annoClasses = context.project.usedAnnotationClassesBySrc.asSequence().filter { annoClass ->
classCapability.classes.contains(annoClass)
}.toSortedSet()
return if (annoClasses.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.Annotation(annoClasses)
true
} else {
false
}
}
private fun usesInvisibleAnnotation(
coordinates: Coordinates,
classCapability: ClassCapability,
context: GraphViewVisitor.Context,
): Boolean {
val annoClasses = context.project.usedInvisibleAnnotationClassesBySrc.asSequence().filter { annoClass ->
classCapability.classes.contains(annoClass)
}.toSortedSet()
return if (annoClasses.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.InvisibleAnnotation(annoClasses)
true
} else {
false
}
}
private fun isImplementation(
coordinates: Coordinates,
typealiasCapability: TypealiasCapability,
context: GraphViewVisitor.Context,
): Boolean {
val usedClasses = context.project.usedClassesBySrc.asSequence().filter { usedClass ->
typealiasCapability.typealiases.any { ta ->
ta.typealiases.map { "${ta.packageName}.${it.name}" }.contains(usedClass)
}
}.toSortedSet()
return if (usedClasses.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.Typealias(usedClasses)
true
} else {
false
}
}
private fun isImported(
coordinates: Coordinates,
classCapability: ClassCapability,
context: GraphViewVisitor.Context,
): Boolean {
val imports = context.project.imports.asSequence().filter { import ->
classCapability.classes.contains(import)
}.toSortedSet()
return if (imports.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.Imported(imports)
true
} else {
false
}
}
private fun containsAndroidTestInstrumentationRunner(
coordinates: Coordinates,
classCapability: ClassCapability,
context: GraphViewVisitor.Context,
): Boolean {
val testInstrumentationRunner = context.project.testInstrumentationRunner ?: return false
return if (classCapability.classes.contains(testInstrumentationRunner)) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.TestInstrumentationRunner(testInstrumentationRunner)
true
} else {
false
}
}
private fun usesConstant(
coordinates: Coordinates,
capability: ConstantCapability,
context: GraphViewVisitor.Context,
): Boolean {
fun optionalStarImport(fqcn: String): List {
return if (fqcn.contains(".")) {
listOf("${fqcn.substringBeforeLast('.')}.*")
} else {
// "fqcn" is not in a package, and so contains no dots
// a star import makes no sense in this context
emptyList()
}
}
val ktFiles = capability.ktFiles
val candidateImports = capability.constants.asSequence()
.flatMap { (fqcn, names) ->
val ktPrefix = ktFiles.find {
it.fqcn == fqcn
}?.name?.let { name ->
fqcn.removeSuffix(name)
}
val ktImports = names.mapNotNull { name -> ktPrefix?.let { "$it$name" } }
ktImports + listOf("$fqcn.*") + optionalStarImport(fqcn) + names.map { name -> "$fqcn.$name" }
}
// https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/issues/687
.map { it.replace('$', '.') }
.toSet()
val imports = context.project.imports.asSequence().filter { import ->
candidateImports.contains(import)
}.toSortedSet()
return if (imports.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.Constant(imports)
true
} else {
false
}
}
/**
* Returns `true` if `capability.assets` is not empty and if the project uses `android.content.res.AssetManager`.
*/
private fun usesAssets(
coordinates: Coordinates,
capability: AndroidAssetCapability,
context: GraphViewVisitor.Context,
): Boolean = (capability.assets.isNotEmpty()
&& context.project.usedNonAnnotationClassesBySrc.contains("android.content.res.AssetManager")
).andIfTrue {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.Asset(capability.assets)
}
private fun usesResBySource(
coordinates: Coordinates,
capability: AndroidResCapability,
context: GraphViewVisitor.Context,
): Boolean {
val projectImports = context.project.imports
val imports = listOf(capability.rImport, capability.rImport.removeSuffix("R") + "*").asSequence()
.filter { import -> projectImports.contains(import) }
.toSortedSet()
return if (imports.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.ResBySrc(imports)
true
} else {
false
}
}
// TODO(tsr): do we want a flag to report all usages, even though it's slower?
private fun usesResByRes(
coordinates: Coordinates,
capability: AndroidResCapability,
context: GraphViewVisitor.Context,
): Boolean {
val styleParentRefs = mutableSetOf()
val attrRefs = mutableSetOf()
// By exiting at the first discovered usage, we can conclude the dependency is used without being able to report ALL
// the usages via Reason. But that's ok, this should be faster.
outer@ for ((type, id) in capability.lines) {
for (candidate in context.project.androidResSource) {
val styleParentRef = candidate.styleParentRefs.find { styleParentRef ->
id == styleParentRef.styleParent
}
if (styleParentRef != null) {
styleParentRefs.add(styleParentRef)
break@outer
}
val attrRef = candidate.attrRefs.find { attrRef ->
type == attrRef.type && id == attrRef.id
}
if (attrRef != null) {
attrRefs.add(attrRef)
break@outer
}
// This is more expensive but finds _all_ usages.
// candidate.styleParentRefs.find { styleParentRef ->
// id == styleParentRef.styleParent
// }?.let { styleParentRefs.add(it) }
//
// candidate.attrRefs.find { attrRef ->
// type == attrRef.type && id == attrRef.id
// }?.let { attrRefs.add(it) }
}
}
var used = if (styleParentRefs.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.ResByRes.styleParentRefs(styleParentRefs)
true
} else {
false
}
used = used || if (attrRefs.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.ResByRes.attrRefs(attrRefs)
true
} else {
false
}
return used
}
private fun usesInlineMember(
coordinates: Coordinates,
capability: InlineMemberCapability,
context: GraphViewVisitor.Context,
): Boolean {
val candidateImports = capability.inlineMembers.asSequence()
.flatMap { (pn, names) ->
listOf("$pn.*") + names.map { name -> "$pn.$name" }
}
.toSet()
val imports = context.project.imports.asSequence().filter { import ->
candidateImports.contains(import)
}.toSortedSet()
return if (imports.isNotEmpty()) {
reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.Inline(imports)
true
} else {
false
}
}
private fun usesAnnotationProcessor(
coordinates: Coordinates,
capability: AnnotationProcessorCapability,
context: GraphViewVisitor.Context,
): Boolean = AnnotationProcessorDetector(
coordinates,
capability.supportedAnnotationTypes,
kapt,
reportBuilder
).usesAnnotationProcessor(context)
}
private class AnnotationProcessorDetector(
private val coordinates: Coordinates,
private val supportedTypes: Set,
private val isKaptApplied: Boolean,
private val reportBuilder: DependencyTraceReport.Builder,
) {
// convert ["lombok.*"] to [lombok.(package) regex]
private val stars = supportedTypes
.filter { it.endsWith("*") }
.map { it.replace(".", "\\.") }
.map { it.replace("*", JAVA_SUB_PACKAGE) }
.map { it.toRegex(setOf(RegexOption.IGNORE_CASE)) }
fun usesAnnotationProcessor(context: GraphViewVisitor.Context): Boolean {
return (context.project.usedByImport() || context.project.usedByClass()).also {
if (!it) reason(Reason.Unused)
}
}
private fun ProjectVariant.usedByImport(): Boolean {
val usedImports = mutableSetOf()
for (import in imports) {
if (supportedTypes.contains(import) || stars.any { it.matches(import) }) {
usedImports.add(import)
}
}
return if (usedImports.isNotEmpty()) {
reason(Reason.AnnotationProcessor.imports(usedImports, isKaptApplied))
true
} else {
false
}
}
private fun ProjectVariant.usedByClass(): Boolean {
val theUsedClasses = mutableSetOf()
for (clazz in (usedNonAnnotationClasses + usedAnnotationClassesBySrc)) {
if (supportedTypes.contains(clazz) || stars.any { it.matches(clazz) }) {
theUsedClasses.add(clazz)
}
}
return if (theUsedClasses.isNotEmpty()) {
reason(Reason.AnnotationProcessor.classes(theUsedClasses, isKaptApplied))
true
} else {
false
}
}
private fun reason(reason: Reason) {
reportBuilder[coordinates, Kind.ANNOTATION_PROCESSOR] = reason
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy