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

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

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

import com.autonomousapps.TASK_GROUP_DEP
import com.autonomousapps.extension.DependenciesHandler.Companion.toLambda
import com.autonomousapps.internal.reason.DependencyAdviceExplainer
import com.autonomousapps.internal.reason.ModuleAdviceExplainer
import com.autonomousapps.internal.unsafeLazy
import com.autonomousapps.internal.utils.*
import com.autonomousapps.model.*
import com.autonomousapps.model.intermediates.BundleTrace
import com.autonomousapps.model.intermediates.Usage
import org.gradle.api.DefaultTask
import org.gradle.api.InvalidUserDataException
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.api.tasks.options.Option
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
import javax.inject.Inject

abstract class ReasonTask @Inject constructor(
  private val workerExecutor: WorkerExecutor
) : DefaultTask() {

  init {
    group = TASK_GROUP_DEP
    description = "Explain how a dependency is used"
  }

  @get:Input
  abstract val projectPath: Property

  /**
   * The dependency identifier or GAV coordinates being queried.
   *
   * See also [module].
   */
  @get:Optional
  @get:Input
  @set:Option(
    option = "id",
    description = "The dependency you'd like to reason about (com.foo:bar:1.0 or :other:module)"
  )
  var id: String? = null

  /**
   * The category of module-structure advice to query for. Only available option at this time is 'android'.
   *
   * See also [id].
   */
  @get:Optional
  @get:Input
  @set:Option(
    option = "module",
    description = "The module-structure-related advice you'd like more insight into ('android')"
  )
  var module: String? = null

  @get:Input
  abstract val dependencyMap: MapProperty

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

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

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

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

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

  @get:PathSensitive(PathSensitivity.RELATIVE)
  @get:InputFiles
  abstract val dependencyGraphViews: ListProperty

  // TODO InputDirectory of all dependencies for finding capabilities

  @TaskAction fun action() {
    val options = options()

    // Explain dependency advice
    options.id?.let { dependency ->
      workerExecutor.noIsolation().submit(ExplainDependencyAdviceAction::class.java) {
        id.set(dependency)
        projectPath.set([email protected])
        dependencyMap.set([email protected])
        dependencyUsageReport.set([email protected])
        annotationProcessorUsageReport.set([email protected])
        unfilteredAdviceReport.set([email protected])
        finalAdviceReport.set([email protected])
        bundleTracesReport.set([email protected])
        dependencyGraphViews.set([email protected])
      }
    }

    // Explain module structure advice
    options.module?.let { moduleStructure ->
      workerExecutor.noIsolation().submit(ExplainModuleAdviceAction::class.java) {
        module.set(moduleStructure)
        projectPath.set([email protected])
        unfilteredAdviceReport.set([email protected])
        finalAdviceReport.set([email protected])
      }
    }
  }

  private fun options(): Options {
    val id = id
    val module = module
    if (id == null && module == null) {
      throw InvalidUserDataException(help())
    }

    return Options(id = id, module = module)
  }

  private class Options(val id: String?, val module: String?)

  private fun help() = projectPath.get().let { path ->
    """
      You must call 'reason' with either the `--id` or `--module` option, or both.
      
      Usage for --id:
        ./gradlew ${path}:reason --id com.foo:bar:1.0
        ./gradlew ${path}:reason --id :other:module
        
      For external dependencies, the version is optional.
      
      Usage for --module:
        ./gradlew ${path}:reason --module android
    """.trimIndent()
  }

  interface ExplainDependencyAdviceParams : WorkParameters {
    val id: Property
    val projectPath: Property
    val dependencyMap: MapProperty
    val dependencyUsageReport: RegularFileProperty
    val annotationProcessorUsageReport: RegularFileProperty
    val unfilteredAdviceReport: RegularFileProperty
    val finalAdviceReport: RegularFileProperty
    val bundleTracesReport: RegularFileProperty
    val dependencyGraphViews: ListProperty
  }

  abstract class ExplainDependencyAdviceAction : WorkAction {

    private val logger = getLogger()

    private val projectPath = parameters.projectPath.get()
    private val dependencyGraph = parameters.dependencyGraphViews.get()
      .map { it.fromJson() }
      .associateBy { "${it.name},${it.configurationName}" }
    private val dependencyUsages = parameters.dependencyUsageReport.fromJsonMapSet()
    private val annotationProcessorUsages = parameters.annotationProcessorUsageReport.fromJsonMapSet()
    private val unfilteredProjectAdvice = parameters.unfilteredAdviceReport.fromJson()
    private val finalProjectAdvice = parameters.finalAdviceReport.fromJson()
    private val dependencyMap = parameters.dependencyMap.get().toLambda()

    // Derived from the above
    private val finalAdvice by unsafeLazy { findAdviceIn(finalProjectAdvice) }
    private val requestedCoord by unsafeLazy { getRequestedCoordinates(false) }
    private val coord by unsafeLazy { getRequestedCoordinates(true) }
    private val unfilteredAdvice by unsafeLazy { findAdviceIn(unfilteredProjectAdvice) }
    private val usages by unsafeLazy { getUsageFor(coord.gav()) }

    override fun execute() {
      val reason = DependencyAdviceExplainer(
        project = ProjectCoordinates(projectPath, GradleVariantIdentification(setOf("ROOT"), emptyMap()), ":"),
        requestedId = requestedCoord,
        target = coord,
        usages = usages,
        advice = finalAdvice,
        dependencyGraph = dependencyGraph,
        bundleTraces = bundleTraces(),
        wasFiltered = wasFiltered(),
        dependencyMap = dependencyMap,
      ).computeReason()

      logger.quiet(reason)
    }

    /**
     * Returns the requested ID as [Coordinates], even if user passed in a prefix.
     * normalized == true to return 'group:coordinate' notation even if the used requested via :project-path notation.
     * */
    private fun getRequestedCoordinates(normalize: Boolean): Coordinates {
      val requestedId = parameters.id.get()
      val requestedViaProjectPath = requestedId.startsWith(":")

      fun findInGraph(): String? = dependencyGraph.values.asSequence()
        .flatMap { it.nodes }
        .map { it.gav() }
        .find { gav ->
          gav == requestedId || gav.startsWith(requestedId) || dependencyMap(gav) == requestedId
        }

      // Guaranteed to find full GAV or throw
      val gavKey = dependencyUsages.entries.find(requestedId::equalsKey)?.key
        ?: dependencyUsages.entries.find(requestedId::startsWithKey)?.key
        ?: annotationProcessorUsages.entries.find(requestedId::equalsKey)?.key
        ?: annotationProcessorUsages.entries.find(requestedId::startsWithKey)?.key
        ?: findInGraph()
        ?: throw InvalidUserDataException("There is no dependency with coordinates '$requestedId' in this project.")

      val gav = if (requestedViaProjectPath && !normalize) {
        gavKey.secondCoordinatesKeySegment() ?: gavKey
      } else {
        gavKey.firstCoordinatesKeySegment()
      }

      return Coordinates.of(gav)
    }

    private fun getUsageFor(id: String): Set {
      return dependencyUsages.entries.find(id::equalsKey)?.value?.toSortedSet(Usage.BY_VARIANT)
        ?: annotationProcessorUsages.entries.find(id::equalsKey)?.value?.toSortedSet(Usage.BY_VARIANT)
        // Will be empty for runtimeOnly dependencies (no detected usages)
        ?: emptySet()
    }

    private fun findAdviceIn(projectAdvice: ProjectAdvice): Advice? {
      // Would be null if there is no advice for the given id.
      return projectAdvice.dependencyAdvice.find { it.coordinates.gav() == coord.gav() }
    }

    // TODO: I think for any target, there's only 0 or 1 trace?
    /** Find all bundle traces where the [BundleTrace.top] or [BundleTrace.bottom] is [coord]. */
    private fun bundleTraces(): Set =
      parameters.bundleTracesReport.fromJsonSet().filterToSet {
        it.top == coord || it.bottom == coord
      }

    private fun wasFiltered(): Boolean = finalAdvice == null && unfilteredAdvice != null
  }

  interface ExplainModuleAdviceParams : WorkParameters {
    val module: Property
    val projectPath: Property
    val unfilteredAdviceReport: RegularFileProperty
    val finalAdviceReport: RegularFileProperty
  }

  abstract class ExplainModuleAdviceAction : WorkAction {

    private val logger = getLogger()

    private val projectPath = parameters.projectPath.get()
    private val module = parameters.module.get()
    private val unfilteredAndroidScore = parameters.unfilteredAdviceReport
      .fromJson()
      .moduleAdvice
      .filterIsInstance()
      .singleOrNull()
    private val finalAndroidScore = parameters.finalAdviceReport
      .fromJson()
      .moduleAdvice
      .filterIsInstance()
      .singleOrNull()

    override fun execute() {
      validateModuleOption()
      val reason = ModuleAdviceExplainer(
        project = ProjectCoordinates(projectPath, GradleVariantIdentification(emptySet(), emptyMap())),
        unfilteredAndroidScore = unfilteredAndroidScore,
        finalAndroidScore = finalAndroidScore,
      ).computeReason()

      logger.quiet(reason)
    }

    private fun validateModuleOption() {
      if (module != "android") {
        throw InvalidUserDataException("'$module' unexpected. The only valid option for '--module' at this time is 'android'.")
      }
    }
  }

  internal interface Explainer {
    fun computeReason(): String
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy