All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.autonomousapps.tasks.DependencyMisuseTask.kt Maven / Gradle / Ivy

There is a newer version: 2.0.2
Show newest version
@file:Suppress("UnstableApiUsage")

package com.autonomousapps.tasks

import com.autonomousapps.TASK_GROUP_DEP
import com.autonomousapps.advice.ComponentWithTransitives
import com.autonomousapps.advice.Dependency
import com.autonomousapps.internal.*
import com.autonomousapps.internal.utils.*
import org.gradle.api.DefaultTask
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.result.ResolvedComponentResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.*

/**
 * Produces a report of unused direct dependencies and used transitive dependencies.
 */
@CacheableTask
abstract class DependencyMisuseTask : DefaultTask() {

  init {
    group = TASK_GROUP_DEP
    description = "Produces a report of unused direct dependencies and used transitive dependencies"
  }

  /**
   * This is the "official" input for wiring task dependencies correctly, but is otherwise unused.
   */
  @get:Classpath
  lateinit var artifactFiles: FileCollection

  /**
   * This is what the task actually uses as its input.
   */
  @get:Internal
  lateinit var runtimeConfiguration: Configuration

  @get:PathSensitive(PathSensitivity.NONE)
  @get:InputFile
  abstract val declaredDependencies: RegularFileProperty

  @get:PathSensitive(PathSensitivity.NONE)
  @get:InputFile
  abstract val usedClasses: RegularFileProperty

  @get:PathSensitive(PathSensitivity.NONE)
  @get:InputFile
  abstract val usedInlineDependencies: RegularFileProperty

  @get:PathSensitive(PathSensitivity.NONE)
  @get:InputFile
  abstract val usedConstantDependencies: RegularFileProperty

  @get:PathSensitive(PathSensitivity.NONE)
  @get:InputFile
  abstract val usedGenerally: RegularFileProperty

  @get:PathSensitive(PathSensitivity.NONE)
  @get:Optional
  @get:InputFile
  abstract val manifests: RegularFileProperty

  @get:PathSensitive(PathSensitivity.NONE)
  @get:Optional
  @get:InputFile
  abstract val usedAndroidResBySourceDependencies: RegularFileProperty

  @get:PathSensitive(PathSensitivity.NONE)
  @get:Optional
  @get:InputFile
  abstract val usedAndroidResByResDependencies: RegularFileProperty

  @get:PathSensitive(PathSensitivity.NONE)
  @get:Optional
  @get:InputFile
  abstract val nativeLibDependencies: RegularFileProperty

  @get:OutputFile
  abstract val outputAllComponents: RegularFileProperty

  @get:OutputFile
  abstract val outputUnusedComponents: RegularFileProperty

  @get:OutputFile
  abstract val outputUsedTransitives: RegularFileProperty

  @get:OutputFile
  abstract val outputUsedVariantDependencies: RegularFileProperty

  @TaskAction
  fun action() {
    // Outputs
    val outputAllComponentsFile = outputAllComponents.getAndDelete()
    val outputUnusedComponentsFile = outputUnusedComponents.getAndDelete()
    val outputUsedTransitivesFile = outputUsedTransitives.getAndDelete()
    val outputUsedVariantDependenciesFile = outputUsedVariantDependencies.getAndDelete()

    // Inputs
    val resolvedComponentResult: ResolvedComponentResult = runtimeConfiguration
      .incoming
      .resolutionResult
      .root

    val dependencyReport = MisusedDependencyDetector(
      declaredComponents = declaredDependencies.fromJsonSet(),
      usedClasses = usedClasses.fromJsonSet(),
      usedInlineDependencies = usedInlineDependencies.fromJsonSet(),
      usedConstantDependencies = usedConstantDependencies.fromJsonSet(),
      usedGenerally = usedGenerally.fromJsonSet(),
      manifests = manifests.fromNullableJsonSet(),
      usedAndroidResBySourceDependencies = usedAndroidResBySourceDependencies.fromNullableJsonSet(),
      usedAndroidResByResDependencies = usedAndroidResByResDependencies.fromNullableJsonSet(),
      nativeLibDependencies = nativeLibDependencies.fromNullableJsonSet(),
      root = resolvedComponentResult
    ).detect()

    // Reports
    outputAllComponentsFile.writeText(dependencyReport.allComponentsWithTransitives.toJson())
    outputUnusedComponentsFile.writeText(dependencyReport.unusedComponentsWithTransitives.toJson())
    outputUsedTransitivesFile.writeText(dependencyReport.usedTransitives.toJson())
    outputUsedVariantDependenciesFile.writeText(dependencyReport.usedDependencies.toJson())
  }
}

internal class MisusedDependencyDetector(
  private val declaredComponents: Set,
  private val usedClasses: Set,
  private val usedInlineDependencies: Set,
  private val usedConstantDependencies: Set,
  private val usedGenerally: Set,
  private val manifests: Set?,
  private val usedAndroidResBySourceDependencies: Set?,
  private val usedAndroidResByResDependencies: Set?,
  private val nativeLibDependencies: Set?,
  private val root: ResolvedComponentResult
) {
  fun detect(): DependencyReport {
    val unusedDeps = mutableListOf()
    val usedTransitiveComponents = mutableSetOf()
    val usedDirectClasses = mutableSetOf()
    val usedDependencies = mutableMapOf>()

    declaredComponents
      // Exclude dependencies with zero class files (such as androidx.legacy:legacy-support-v4)
      .filterNot { it.classes.isEmpty() }
      .forEach { component ->
        var count = 0
        val variantClasses = sortedSetOf()

        component.classes.forEach { declClass ->
          // Find the "variant-aware" class
          val variantClass = usedClasses.find { it.theClass == declClass }

          // Looking for unused direct dependencies
          if (!component.isTransitive) {
            if (variantClass == null) {
              // Unused class
              count++
            } else {
              // Used class
              usedDirectClasses.add(declClass)
              usedDependencies.merge(component.dependency, variantClass.variants.toMutableSet()) { oldSet, newSet ->
                oldSet.apply { addAll(newSet) }
              }
            }
          }

          // Looking for used transitive dependencies
          if (component.isTransitive
            // Assume all these come from android.jar
            && !declClass.startsWith("android.")
            && variantClass != null
            // Not in the set of used direct dependencies
            && !usedDirectClasses.contains(declClass)
          ) {
            variantClasses.add(variantClass)
          }
        }

        if (count == component.classes.size
          // Exclude modules that have inline usages
          && component.hasNoInlineUsages()
          // Exclude modules that have Android res (by source) usages
          && component.hasNoAndroidResBySourceUsages()
          // Exclude modules that have Android res (by res) usages
          && component.hasNoAndroidResByResUsages()
          // Exclude modules that have bundled native libs (.so files)
          && component.hasNoNativeLibUsages()
          // Exclude modules that have constant usages
          && component.hasNoConstantUsages()
          // Exclude modules that have types used in a general context
          && component.hasNoGeneralUsages()
          // Exclude modules that appear in the manifest (e.g., they supply Android components like
          // ContentProviders)
          && component.hasNoManifestMatches()
        ) {
          unusedDeps.add(component.dependency)
        }

        if (variantClasses.isNotEmpty()) {
          val classes = variantClasses.mapToOrderedSet { it.theClass }
          val variants = variantClasses.flatMapToOrderedSet { it.variants }
          usedTransitiveComponents.add(TransitiveComponent(
            dependency = component.dependency,
            usedTransitiveClasses = classes,
            variants = variants
          ))
        }
      }

    // Connect used-transitives to direct dependencies
    val declaredComponentsWithTransitives: Set =
      declaredComponents.mapToSet { it.dependency }.mapNotNullToSet { dep ->
        dep.asResolvedDependencyResult()?.let { rdr ->
          relate(
            unusedDependency = rdr,
            unusedDirectComponent = ComponentWithTransitives(dep, mutableSetOf()),
            usedTransitiveComponents = usedTransitiveComponents
          )
        }
      }

    // Filter above to only get those that are unused
    val unusedDepsWithTransitives: Set = declaredComponentsWithTransitives
      .filterToSet { comp ->
        unusedDeps.any { it == comp.dependency }
      }

    // Performance diagnostics
    //println("Counts:\n" + counter.entries.joinToString(separator = "\n") { "${it.key}: ${it.value}" })

    return DependencyReport(
      allComponentsWithTransitives = declaredComponentsWithTransitives,
      unusedComponentsWithTransitives = unusedDepsWithTransitives,
      usedTransitives = usedTransitiveComponents,
      usedDependencies = usedDependencies.toVariantDependencies()
    )
  }

  private fun Map>.toVariantDependencies(): Set {
    val set = mutableSetOf()
    forEach { (dep, variants) ->
      set.add(VariantDependency(dep, variants))
    }
    return set
  }

  private fun Component.hasNoInlineUsages(): Boolean {
    return usedInlineDependencies.none { it == dependency }
  }

  private fun Component.hasNoAndroidResBySourceUsages(): Boolean {
    return usedAndroidResBySourceDependencies?.none { it == dependency } ?: true
  }

  private fun Component.hasNoAndroidResByResUsages(): Boolean {
    return usedAndroidResByResDependencies?.none { it.dependency == dependency } ?: true
  }

  private fun Component.hasNoNativeLibUsages(): Boolean {
    return nativeLibDependencies?.none { it.dependency == dependency } ?: true
  }

  private fun Component.hasNoConstantUsages(): Boolean {
    return usedConstantDependencies.none { it == dependency }
  }

  private fun Component.hasNoGeneralUsages(): Boolean {
    return usedGenerally.none { it == dependency }
  }

  /**
   * If the component's dependency matches any of our manifest dependencies, and that manifest provides an Android
   * component, then it is used.
   */
  private fun Component.hasNoManifestMatches(): Boolean {
    val manifest = manifests?.find { it.dependency == dependency } ?: return true
    return !manifest.hasComponents
  }

  private fun Dependency.asResolvedDependencyResult(): ResolvedDependencyResult? =
    root.dependencies.filterIsInstance().find { rdr ->
      identifier == rdr.selected.id.asString()
    }

  /**
   * This algorithm "relates" direct components (those declared in the build script) with the
   * used transitive components it may contribute (to n degrees) to the graph. Multiple components
   * may contribute the same transitive dependency. The algorithm ensures this many-to-many
   * relationship is discovered.
   *
   * This algorithm must traverse the full graph because the following is possible:
   *
   * ```
   *     a -> b -> ... -> n
   * ```
   * where `a` is directly declared but is unused, `a` declares `b`, and `b` declares ..., which
   * eventually declares `n`. `n` is used by the project (and is not declared). The algorithm will
   * relate `a` to `n` so we know the many provenances of `n`. This knowledge is used in at least
   * one place, [KtxFilter.computeKtxTransitives][com.autonomousapps.internal.advice.filter.KtxFilter.computeKtxTransitives]
   *
   * - [unusedDependency] contains information about the dependency graph rooted on the unused
   *   dependency, and then on each node below it as the algorithm traverses the graph recursively.
   * - [unusedDirectComponent] is the "UnusedDirectComponent", which we are currently building with
   *   the set of transitive dependencies it brings along and that are used. It represents the same
   *   "thing" as `unusedDependency`, but with additional information included in its model.
   * - [usedTransitiveComponents] is the set of transitively-declared components that are used
   *   directly by the project.
   * - [visitedNodes] is the set of nodes in the graph that have already been visited _for the
   *   `unusedDependency`_ node. This is a massive performance optimization, eliding 99% of the
   *   potential work in one test.
   */
  private fun relate(
    unusedDependency: ResolvedDependencyResult,
    unusedDirectComponent: ComponentWithTransitives,
    usedTransitiveComponents: Set,
    visitedNodes: MutableSet = mutableSetOf()
  ): ComponentWithTransitives {
    unusedDependency
      // the dependency actually selected by dependency resolution
      .selected
      // the dependencies of the selected dependency
      .dependencies
      // only those that have been fully resolved
      .filterIsInstance()
      .forEach { transitiveNode ->
        val transitiveIdentifier = transitiveNode.selected.id.asString()
        val transitiveResolvedVersion = transitiveNode.selected.id.resolvedVersion()

        if (!visitedNodes.contains(transitiveIdentifier)) {
          // Performance diagnostics
          //counter.merge(transitiveIdentifier, 1) { oldValue, increment -> oldValue + increment }

          if (usedTransitiveComponents.contains(transitiveIdentifier)) {
            unusedDirectComponent.usedTransitiveDependencies.add(Dependency(
              identifier = transitiveIdentifier,
              resolvedVersion = transitiveResolvedVersion
            ))
          }
          relate(
            unusedDependency = transitiveNode,
            unusedDirectComponent = unusedDirectComponent,
            usedTransitiveComponents = usedTransitiveComponents,
            visitedNodes = visitedNodes.apply { add(transitiveIdentifier) }
          )
        }
      }
    return unusedDirectComponent
  }

  // Performance diagnostics
  //private val counter = mutableMapOf()

  private fun Set.contains(identifier: String): Boolean {
    return map { trans -> trans.dependency.identifier }.contains(identifier)
  }

  internal class DependencyReport(
    val allComponentsWithTransitives: Set,
    val unusedComponentsWithTransitives: Set,
    val usedTransitives: Set,
    val usedDependencies: Set
  )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy