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

com.autonomousapps.internal.advice.Advisor.kt Maven / Gradle / Ivy

There is a newer version: 2.0.2
Show newest version
package com.autonomousapps.internal.advice

import com.autonomousapps.advice.ComponentWithTransitives
import com.autonomousapps.advice.Dependency
import com.autonomousapps.advice.HasDependency
import com.autonomousapps.advice.TransitiveDependency
import com.autonomousapps.internal.*
import com.autonomousapps.internal.ServiceLoader
import com.autonomousapps.internal.advice.filter.FilterSpecBuilder
import com.autonomousapps.internal.advice.filter.KtxFilter
import com.autonomousapps.internal.advice.filter.DependencyBundleFilter
import com.autonomousapps.internal.utils.filterNoneMatchingSorted
import com.autonomousapps.internal.utils.filterToOrderedSet
import com.autonomousapps.internal.utils.mapToOrderedSet
import com.autonomousapps.internal.utils.mapToSet
import java.util.*

/**
 * Classes of advice:
 * 1. Declared dependencies which are not used: remove
 * 2. Undeclared (transitive) dependencies are are used: add
 * 3. `implementation` dependencies incorrectly declared `api`
 * 4. `api` dependencies incorrectly declared `implementation`.
 * 5. `compileOnly` dependencies incorrectly declared as `api` or `implementation`.
 *
 * [ignoreKtx] is applied to the remove-advice to filter (or not) ktx-related advice; supplied by
 * user. If true, we don't suggest removing an "unused" `-ktx` dependency iff any of its
 * dependencies are used (directly or transitively).
 */
internal class Advisor(
  private val usedVariantDependencies: Set,
  private val allComponents: Set,
  private val allComponentsWithTransitives: Set,
  private val unusedComponentsWithTransitives: Set,
  private val usedTransitiveComponents: Set,
  private val abiDeps: Set,
  private val allDeclaredDeps: Set,
  private val unusedProcs: Set,
  private val serviceLoaders: Set,
  private val dependencyBundles: Map>,
  private val ignoreKtx: Boolean = false
) {

  private val compileOnlyCandidates: Set = TreeSet()
  private val securityProviders: Set = TreeSet()

  init {
    computeHelpers()
  }

  /**
   * Computes all the advice in one pass.
   */
  fun compute(filterSpecBuilder: FilterSpecBuilder = FilterSpecBuilder()): ComputedAdvice {
    val unusedComponents = computeUnusedDependencies()
    val unusedDependencies = unusedComponents.mapToSet { it.dependency }

    val undeclaredApiDependencies = computeUndeclaredApiDependencies()
    val undeclaredImplDependencies = computeUndeclaredImplDependencies(undeclaredApiDependencies)

    val changeToApi = computeApiDepsWronglyDeclared()
    val changeToImpl = computeImplDepsWronglyDeclared(unusedDependencies)

    if (dependencyBundles.isNotEmpty()) {
      filterSpecBuilder.dependencyBundleFilter = DependencyBundleFilter(dependencyBundles)
    }

    // update filterSpecBuilder with ktxFilter
    if (ignoreKtx) {
      val ktxFilter = KtxFilter(
        allComponents = allComponents,
        unusedDirectComponents = unusedComponentsWithTransitives,
        usedTransitiveComponents = usedTransitiveComponents,
        unusedDependencies = unusedDependencies
      )
      filterSpecBuilder.addToUniversalFilter(ktxFilter)
    }

    return ComputedAdvice(
      compileOnlyCandidates = compileOnlyCandidates,
      unusedComponents = unusedComponents,
      undeclaredApiDependencies = undeclaredApiDependencies,
      undeclaredImplDependencies = undeclaredImplDependencies,
      changeToApi = changeToApi,
      changeToImpl = changeToImpl,
      unusedProcs = unusedProcs,
      filterSpecBuilder = filterSpecBuilder
    )
  }

  /**
   * Computes both sets of internal helpers in one pass, to avoid having to iterate over the full
   * set of [Component]s twice.
   *
   * A [Component] is a compileOnly candidate iff:
   * 1. It has already been determined to be based on analysis done in [AnalyzedJar]; OR
   * 2. It is currently on a variant of the `compileOnly` configuration (here we assume users know
   *    what they're doing).
   *
   * A [Component] is a security provider iff:
   * 1. It contains a class that extends the [java.security.Provider] class.
   */
  private fun computeHelpers() {
    allComponents.forEach {
      if (it.isCompileOnlyAnnotations || it.dependency.configurationName?.endsWith("compileOnly", ignoreCase = true) == true) {
        (compileOnlyCandidates as MutableSet).add(it)
      } else if (it.isSecurityProvider) {
        (securityProviders as MutableSet).add(it)
      }
    }
  }

  /**
   * A [Dependency] is unused (and should be removed) iff:
   * 1. It is in the set of [unusedComponentsWithTransitives] AND
   * 2. It is not also in the set [compileOnlyCandidates] (we do not suggest removing such
   *    candidates, even if they appear unused) AND
   * 3. It is not also in the set [serviceLoaders] (we cannot safely suggest removing service
   *    loaders, since they are used at runtime) AND
   * 4. It is not also in the set of [securityProviders] (we cannot safely suggest removing security
   *    providers, since they are  used at runtime).
   */
  private fun computeUnusedDependencies(): Set {
    return unusedComponentsWithTransitives
      .stripCompileOnly()
      .stripServiceLoaders()
      .stripSecurityProviders()
  }

  /**
   * A [Dependency] is an undeclared `api` dependency (and should be declared as such) iff:
   * 1. It is part of the project's ABI AND
   * 2. It was not declared (it's [configurationName][Dependency.configurationName] is `null`) AND
   * 3. It was not declared to be `compileOnly` (here we assume users know what they're doing)
   */
  private fun computeUndeclaredApiDependencies(): Set {
    return abiDeps
      .filterToOrderedSet { it.configurationName == null }
      .stripCompileOnly()
      .mapToSet { it.withParents() }
      .mapToSet { it.withVariants() }
  }

  /**
   * A [Dependency] is an undeclared `implementation` dependency (and should be declared as such) iff:
   * 1. It is in the set of [usedTransitiveComponents] AND
   * 2. It is not an undeclared `api` dependency (see [computeUndeclaredApiDependencies]) AND
   * 3. It is not a `compileOnly` candidate (see [computeHelpers])
   */
  private fun computeUndeclaredImplDependencies(
    undeclaredApiDeps: Set
  ): Set {
    return usedTransitiveComponents
      .mapToOrderedSet { it.dependency }
      .stripCompileOnly()
      .mapToSet { it.withParents() }
      // Exclude any transitives which will be api dependencies
      .filterNoneMatchingSorted(undeclaredApiDeps)
      .mapToSet { it.withVariants() }
  }

  /**
   * A [Dependency] is a "wrongly declared" api dep (and should be changed) iff:
   * 1. It is not transitive ([configuration][Dependency.configurationName] must be non-null).
   * 2. It _should_ be on `api`, but is on something else AND
   * 3. It is not a `compileOnly` candidate (see [computeHelpers]).
   */
  private fun computeApiDepsWronglyDeclared(): Set {
    return abiDeps
      // Filter out those with a null configuration, as they are handled elsewhere
      .filterToOrderedSet { it.configurationName != null }
      .stripCompileOnly()
      .mapToOrderedSet { it.withVariants() }
  }

  /**
   * A [Dependency] is a "wrongly declared" impl dep (and should be changed) iff:
   * 1. It is not transitive ([configuration][Dependency.configurationName] must be non-null); AND
   * 2. It is used; AND
   * 2. It is not part of the project's ABI; AND
   * 3. It is not a `compileOnly` candidate (see [computeHelpers]).
   */
  private fun computeImplDepsWronglyDeclared(
    unusedDependencies: Set
  ): Set {
    return allDeclaredDeps
      // Filter out those with a null configuration, as they are handled elsewhere
      .filterToOrderedSet { it.configurationName != null }
      // Filter out those that are unused
      .filterNoneMatchingSorted(unusedDependencies)
      // Filter out those that actually should be api
      .filterNoneMatchingSorted(abiDeps)
      .stripCompileOnly()
      .mapToOrderedSet { it.withVariants() }
  }

  private fun Dependency.withParents(): TransitiveDependency {
    val parents = mutableSetOf()
    allComponentsWithTransitives.forEach { component ->
      if (component.usedTransitiveDependencies.any { it == this }) {
        parents.add(component.dependency)
      }
    }
    return TransitiveDependency(this, parents)
  }

  private fun TransitiveDependency.withVariants(): TransitiveDependency {
    return usedTransitiveComponents.find { it.dependency == dependency }?.let {
      copy(variants = it.variants)
    } ?: this
  }

  private fun Dependency.withVariants(): VariantDependency {
    val variants = usedVariantDependencies.find { it.dependency == this }?.variants ?: emptySet()
    return VariantDependency(this, variants)
  }

  private fun  Iterable.stripCompileOnly(): Set {
    return filterToOrderedSet { container ->
      compileOnlyCandidates.none { compileOnly ->
        container.dependency == compileOnly.dependency
      }
    }
  }

  private fun  Iterable.stripServiceLoaders(): Set {
    return filterToOrderedSet { container ->
      serviceLoaders.none { serviceLoader ->
        container.dependency == serviceLoader.dependency
      }
    }
  }

  private fun  Iterable.stripSecurityProviders(): Set {
    return filterToOrderedSet { container ->
      securityProviders.none { securityProvider ->
        container.dependency == securityProvider.dependency
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy