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

sbt.internal.PluginsDebug.scala Maven / Gradle / Ivy

There is a newer version: 1.10.7
Show newest version
/*
 * sbt
 * Copyright 2023, Scala center
 * Copyright 2011 - 2022, Lightbend, Inc.
 * Copyright 2008 - 2010, Mark Harrah
 * Licensed under Apache License 2.0 (see LICENSE)
 */

package sbt
package internal

import sbt.internal.util.{ AttributeKey, Dag, Relation, Util }
import sbt.util.Logger

import Def.Setting
import sbt.SlashSyntax0._
import Plugins._
import PluginsDebug._
import java.net.URI

private[sbt] class PluginsDebug(
    val available: List[AutoPlugin],
    val nameToKey: Map[String, AttributeKey[_]],
    val provided: Relation[AutoPlugin, AttributeKey[_]]
) {

  /**
   * The set of [[AutoPlugin]]s that might define a key named `keyName`.
   * Because plugins can define keys in different scopes, this should only be used as a guideline.
   */
  def providers(keyName: String): Set[AutoPlugin] = nameToKey.get(keyName) match {
    case None      => Set.empty
    case Some(key) => provided.reverse(key)
  }

  /** Describes alternative approaches for defining key `keyName` in [[Context]]. */
  def toEnable(keyName: String, context: Context): List[PluginEnable] =
    providers(keyName).toList.map(plugin => pluginEnable(context, plugin))

  /** Provides text to suggest how `notFoundKey` can be defined in [[Context]]. */
  def debug(notFoundKey: String, context: Context): String = {
    val (activated, deactivated) = Util.separate(toEnable(notFoundKey, context)) {
      case pa: PluginActivated   => Left(pa)
      case pd: EnableDeactivated => Right(pd)
    }
    val activePrefix =
      if (activated.isEmpty) ""
      else
        s"Some already activated plugins define $notFoundKey: ${activated.mkString(", ")}\n"
    activePrefix + debugDeactivated(notFoundKey, deactivated)
  }

  private[this] def debugDeactivated(
      notFoundKey: String,
      deactivated: Seq[EnableDeactivated]
  ): String = {
    val (impossible, possible) = Util.separate(deactivated) {
      case pi: PluginImpossible   => Left(pi)
      case pr: PluginRequirements => Right(pr)
    }
    if (possible.nonEmpty) {
      val explained = possible.map(explainPluginEnable)
      val possibleString =
        if (explained.lengthCompare(1) > 0)
          explained.zipWithIndex
            .map { case (s, i) => s"$i. $s" }
            .mkString(s"Multiple plugins are available that can provide $notFoundKey:\n", "\n", "")
        else
          s"$notFoundKey is provided by an available (but not activated) plugin:\n${explained.mkString}"
      def impossiblePlugins = impossible.map(_.plugin.label).mkString(", ")
      val imPostfix =
        if (impossible.isEmpty) ""
        else
          s"\n\nThere are other available plugins that provide $notFoundKey, but they are " +
            s"impossible to add: $impossiblePlugins"
      possibleString + imPostfix
    } else if (impossible.isEmpty)
      s"No available plugin provides key $notFoundKey."
    else {
      val explanations = impossible.map(explainPluginEnable)
      val preamble = s"Plugins are available that could provide $notFoundKey"
      explanations.mkString(s"$preamble, but they are impossible to add:\n\t", "\n\t", "")
    }
  }

  /** Text that suggests how to activate [[AutoPlugin]] in [[Context]] if possible and if it is not already activated. */
  def help(plugin: AutoPlugin, context: Context): String =
    if (context.enabled.contains(plugin)) activatedHelp(plugin)
    else deactivatedHelp(plugin, context)

  private def activatedHelp(plugin: AutoPlugin): String = {
    val prefix = s"${plugin.label} is activated."
    val keys = provided.forward(plugin)
    val keysString =
      if (keys.isEmpty) "" else s"\nIt may affect these keys: ${multi(keys.toList.map(_.label))}"
    val configs = plugin.projectConfigurations
    val confsString =
      if (configs.isEmpty) ""
      else s"\nIt defines these configurations: ${multi(configs.map(_.name))}"
    prefix + keysString + confsString
  }

  private def deactivatedHelp(plugin: AutoPlugin, context: Context): String = {
    val prefix = s"${plugin.label} is NOT activated."
    val keys = provided.forward(plugin)
    val keysString =
      if (keys.isEmpty) ""
      else s"\nActivating it may affect these keys: ${multi(keys.toList.map(_.label))}"
    val configs = plugin.projectConfigurations
    val confsString =
      if (configs.isEmpty) ""
      else s"\nActivating it will define these configurations: ${multi(configs.map(_.name))}"
    val toActivate = explainPluginEnable(pluginEnable(context, plugin))
    s"$prefix$keysString$confsString\n$toActivate"
  }

  private[this] def multi(strs: Seq[String]): String =
    strs.mkString(if (strs.lengthCompare(4) > 0) "\n\t" else ", ")
}

private[sbt] object PluginsDebug {
  def helpAll(s: State): String =
    if (Project.isProjectLoaded(s)) {
      val extracted = Project.extract(s)
      import extracted._

      def helpBuild(build: LoadedBuildUnit): String =
        build.projects.toList
          .sortBy(_.id)
          .map { p =>
            import p.autoPlugins
            val `s?` = if (autoPlugins.length > 1) "s" else ""

            new StringBuilder()
              .append(s"  Enabled plugin${`s?`} in ${Highlight.bold(p.id)}:\n")
              .append {
                autoPlugins.map(_.label).sorted.mkString("    ", s"\n    ", "")
              }
              .toString
          }
          .mkString("\n")
          .concat {
            val autoPlugins = availableAutoPlugins(build)
            val notEnabledPlugins =
              autoPlugins.filter(p => build.projects.forall(!_.autoPlugins.contains(p)))
            if (notEnabledPlugins.nonEmpty) {
              val message = notEnabledPlugins.map(_.label).sorted.mkString("  ", "\n  ", "")
              "\nPlugins that are loaded to the build but not enabled in any subprojects:\n" + message
            } else ""
          }

      val buildStrings = for {
        (uri, build) <- structure.units
      } yield s"In build ${uri.getPath}:\n${helpBuild(build)}"
      buildStrings.mkString("\n")
    } else "No project is currently loaded."

  def autoPluginMap(s: State): Map[String, AutoPlugin] = {
    val extracted = Project.extract(s)
    import extracted._
    structure.units.values.toList
      .flatMap(availableAutoPlugins)
      .map(plugin => (plugin.label, plugin))
      .toMap
  }

  private[this] def availableAutoPlugins(build: LoadedBuildUnit): Seq[AutoPlugin] =
    build.unit.plugins.detected.autoPlugins map { _.value }

  def help(plugin: AutoPlugin, s: State): String = {
    val extracted = Project.extract(s)
    import extracted._
    def definesPlugin(p: ResolvedProject): Boolean = p.autoPlugins.contains(plugin)
    def projectForRef(ref: ProjectRef): ResolvedProject = get(ref / Keys.thisProject)
    val perBuild: Map[URI, Set[AutoPlugin]] =
      structure.units.mapValues(unit => availableAutoPlugins(unit).toSet).toMap
    val pluginsThisBuild = perBuild.getOrElse(currentRef.build, Set.empty).toList
    lazy val context = Context(
      currentProject.plugins,
      currentProject.autoPlugins,
      Plugins.deducer(pluginsThisBuild),
      pluginsThisBuild,
      s.log
    )
    lazy val debug = PluginsDebug(context.available)
    if (!pluginsThisBuild.contains(plugin)) {
      val availableInBuilds: List[URI] = perBuild.toList.withFilter(_._2(plugin)).map(_._1)
      val s1 = s"Plugin ${plugin.label} is only available in builds:"
      val s2 = availableInBuilds.mkString("\n\t")
      val s3 =
        s"Switch to a project in one of those builds using `project` and rerun this command for more information."
      s"$s1\n\t$s2\n$s3"
    } else if (definesPlugin(currentProject))
      debug.activatedHelp(plugin)
    else {
      val thisAggregated =
        BuildUtil.dependencies(structure.units).aggregateTransitive.getOrElse(currentRef, Nil)
      val definedInAggregated = thisAggregated.filter(ref => definesPlugin(projectForRef(ref)))
      if (definedInAggregated.nonEmpty) {
        val projectNames = definedInAggregated.map(_.project) // TODO: usually in this build, but could technically require the build to be qualified
        val s2 = projectNames.mkString("\n\t")
        s"Plugin ${plugin.label} is not activated on this project, but this project aggregates projects where it is activated:\n\t$s2"
      } else {
        val base = debug.deactivatedHelp(plugin, context)
        val aggNote =
          if (thisAggregated.nonEmpty) "Note: This project aggregates other projects and this"
          else "Note: This"
        val common = " information is for this project only."
        val helpOther =
          "To see how to activate this plugin for another project, change to the project using `project ` and rerun this command."
        s"$base\n$aggNote$common\n$helpOther"
      }
    }
  }

  /** Pre-computes information for debugging plugins. */
  def apply(available: List[AutoPlugin]): PluginsDebug = {
    val keyR = definedKeys(available)
    val nameToKey: Map[String, AttributeKey[_]] =
      keyR._2s.toList.map(key => (key.label, key)).toMap
    new PluginsDebug(available, nameToKey, keyR)
  }

  /**
   * The context for debugging a plugin (de)activation.
   * @param initial The initially defined [[AutoPlugin]]s.
   * @param enabled The resulting model.
   * @param deducePlugin The function used to compute the model.
   * @param available All [[AutoPlugin]]s available for consideration.
   */
  final case class Context(
      initial: Plugins,
      enabled: Seq[AutoPlugin],
      deducePlugin: (Plugins, Logger) => Seq[AutoPlugin],
      available: List[AutoPlugin],
      log: Logger
  )

  /** Describes the steps to activate a plugin in some context. */
  sealed abstract class PluginEnable

  /** Describes a [[plugin]] that is already activated in the [[context]].*/
  final case class PluginActivated(plugin: AutoPlugin, context: Context) extends PluginEnable

  sealed abstract class EnableDeactivated extends PluginEnable

  /** Describes a [[plugin]] that cannot be activated in a [[context]] due to [[contradictions]] in requirements. */
  final case class PluginImpossible(
      plugin: AutoPlugin,
      context: Context,
      contradictions: Set[AutoPlugin]
  ) extends EnableDeactivated

  /**
   * Describes the requirements for activating [[plugin]] in [[context]].
   * @param context The base plugins, exclusions, and ultimately activated plugins
   * @param blockingExcludes Existing exclusions that prevent [[plugin]] from being activated and must be dropped
   * @param enablingPlugins [[AutoPlugin]]s that are not currently enabled,
   *                        but need to be enabled for [[plugin]] to activate
   * @param extraEnabledPlugins Plugins that will be enabled as a result of [[plugin]] activating,
   *                            but are not required for [[plugin]] to activate
   * @param willRemove Plugins that will be deactivated as a result of [[plugin]] activating
   * @param deactivate Describes plugins that must be deactivated for [[plugin]] to activate.
   *                   These require an explicit exclusion or dropping a transitive [[AutoPlugin]].
   */
  final case class PluginRequirements(
      plugin: AutoPlugin,
      context: Context,
      blockingExcludes: Set[AutoPlugin],
      enablingPlugins: Set[AutoPlugin],
      extraEnabledPlugins: Set[AutoPlugin],
      willRemove: Set[AutoPlugin],
      deactivate: List[DeactivatePlugin]
  ) extends EnableDeactivated

  /**
   * Describes a [[plugin]] that must be removed in order to activate another plugin in some context.
   * The [[plugin]] can always be directly, explicitly excluded.
   * @param removeOneOf If non-empty, removing one of these [[AutoPlugin]]s will deactivate [[plugin]] without
   *                    affecting the other plugin.  If empty, a direct exclusion is required.
   * @param newlySelected If false, this plugin was selected in the original context.
   */
  final case class DeactivatePlugin(
      plugin: AutoPlugin,
      removeOneOf: Set[AutoPlugin],
      newlySelected: Boolean
  )

  /** Determines how to enable [[AutoPlugin]] in [[Context]]. */
  def pluginEnable(context: Context, plugin: AutoPlugin): PluginEnable =
    if (context.enabled.contains(plugin))
      PluginActivated(plugin, context)
    else
      enableDeactivated(context, plugin)

  private[this] def enableDeactivated(context: Context, plugin: AutoPlugin): PluginEnable = {
    // deconstruct the context
    val initialModel = context.enabled.toSet
    val initial = flatten(context.initial)
    val initialPlugins = plugins(initial)
    val initialExcludes = excludes(initial)

    val minModel = minimalModel(plugin)

    /* example 1
    A :- B, not C
    C :- D, E
    initial: B, D, E
    propose: drop D or E

    initial: B, not A
    propose: drop 'not A'

    example 2
    A :- B, not C
    C :- B
    initial: 
    propose: B, exclude C
     */

    // `plugin` will only be activated when all of these plugins are activated
    // Deactivating any one of these would deactivate `plugin`.
    val minRequiredPlugins = plugins(minModel)

    // The presence of any one of these plugins would deactivate `plugin`
    val minAbsentPlugins = excludes(minModel)

    // Plugins that must be both activated and deactivated for `plugin` to activate.
    //  A non-empty list here cannot be satisfied and is an error.
    val contradictions = minAbsentPlugins & minRequiredPlugins

    if (contradictions.nonEmpty) PluginImpossible(plugin, context, contradictions)
    else {
      // Plugins that the user has to add to the currently selected plugins in order to enable `plugin`.
      val addToExistingPlugins = minRequiredPlugins -- initialPlugins

      // Plugins that are currently excluded that need to be allowed.
      val blockingExcludes = initialExcludes & minRequiredPlugins

      // The model that results when the minimal plugins are enabled and the minimal plugins are excluded.
      //  This can include more plugins than just `minRequiredPlugins` because the plugins required for `plugin`
      //  might activate other plugins as well.
      val incrementalInputs = and(
        includeAll(minRequiredPlugins ++ initialPlugins),
        excludeAll(minAbsentPlugins ++ initialExcludes -- minRequiredPlugins)
      )
      val incrementalModel = context.deducePlugin(incrementalInputs, context.log).toSet

      // Plugins that are newly enabled as a result of selecting the plugins needed for `plugin`, but aren't strictly required for `plugin`.
      //   These could be excluded and `plugin` and the user's current plugins would still be activated.
      val extraPlugins = incrementalModel -- minRequiredPlugins -- initialModel

      // Plugins that will no longer be enabled as a result of enabling `plugin`.
      val willRemove = initialModel -- incrementalModel

      // Determine the plugins that must be independently deactivated.
      // If both A and B must be deactivated, but A transitively depends on B, deactivating B will deactivate A.
      // If A must be deactivated, but one if its (transitively) required plugins isn't present, it won't be activated.
      //   So, in either of these cases, A doesn't need to be considered further and won't be included in this set.
      val minDeactivate =
        minAbsentPlugins.filter(p => Plugins.satisfied(p.requires, incrementalModel))

      val deactivate = for (d <- minDeactivate.toList) yield {
        // removing any one of these plugins will deactivate `d`.  TODO: This is not an especially efficient implementation.
        val removeToDeactivate = plugins(minimalModel(d)) -- minRequiredPlugins
        val newlySelected = !initialModel(d)
        // a. suggest removing a plugin in removeOneToDeactivate to deactivate d
        // b. suggest excluding `d` to directly deactivate it in any case
        // c. note whether d was already activated (in context.enabled) or is newly selected
        DeactivatePlugin(d, removeToDeactivate, newlySelected)
      }

      PluginRequirements(
        plugin,
        context,
        blockingExcludes,
        addToExistingPlugins,
        extraPlugins,
        willRemove,
        deactivate
      )
    }
  }

  private[this] def includeAll[T <: Basic](basic: Set[T]): Plugins = And(basic.toList)
  private[this] def excludeAll(plugins: Set[AutoPlugin]): Plugins =
    And(plugins map (p => Exclude(p)) toList)

  private[this] def excludes(bs: Seq[Basic]): Set[AutoPlugin] =
    bs.collect { case Exclude(b) => b }.toSet
  private[this] def plugins(bs: Seq[Basic]): Set[AutoPlugin] =
    bs.collect { case n: AutoPlugin => n }.toSet

  // If there is a model that includes `plugin`, it includes at least what is returned by this method.
  // This is the list of plugins that must be included as well as list of plugins that must not be present.
  // It might not be valid, such as if there are contradictions or if there are cycles that are unsatisfiable.
  // The actual model might be larger, since other plugins might be enabled by the selected plugins.
  private[this] def minimalModel(plugin: AutoPlugin): Seq[Basic] =
    Dag.topologicalSortUnchecked(plugin: Basic) {
      case _: Exclude     => Nil
      case ap: AutoPlugin => Plugins.flatten(ap.requires) :+ plugin
    }

  /** String representation of [[PluginEnable]], intended for end users. */
  def explainPluginEnable(ps: PluginEnable): String =
    ps match {
      case PluginRequirements(
          plugin,
          _,
          blockingExcludes,
          enablingPlugins,
          extraEnabledPlugins,
          toBeRemoved,
          deactivate
          ) =>
        def indent(str: String) = if (str.isEmpty) "" else s"\t$str"
        def note(str: String) = if (str.isEmpty) "" else s"Note: $str"
        val parts =
          indent(excludedError(false /* TODO */, blockingExcludes.toList)) ::
            indent(required(enablingPlugins.toList)) ::
            indent(needToDeactivate(deactivate)) ::
            note(willAdd(plugin, extraEnabledPlugins.toList)) ::
            note(willRemove(plugin, toBeRemoved.toList)) ::
            Nil
        parts.filterNot(_.isEmpty).mkString("\n")
      case PluginImpossible(plugin, _, contradictions) => pluginImpossible(plugin, contradictions)
      case PluginActivated(plugin, _)                  => s"Plugin ${plugin.label} already activated."
    }

  /**
   * Provides a [[Relation]] between plugins and the keys they potentially define.
   * Because plugins can define keys in different scopes and keys can be overridden, this is not definitive.
   */
  def definedKeys(available: List[AutoPlugin]): Relation[AutoPlugin, AttributeKey[_]] = {
    def extractDefinedKeys(ss: Seq[Setting[_]]): Seq[AttributeKey[_]] =
      ss.map(_.key.key)
    def allSettings(p: AutoPlugin): Seq[Setting[_]] =
      p.projectSettings ++ p.buildSettings ++ p.globalSettings
    val empty = Relation.empty[AutoPlugin, AttributeKey[_]]
    available.foldLeft(empty)((r, p) => r + (p, extractDefinedKeys(allSettings(p))))
  }

  private[this] def excludedError(transitive: Boolean, dependencies: List[AutoPlugin]): String =
    str(dependencies)(excludedPluginError(transitive), excludedPluginsError(transitive))

  private[this] def excludedPluginError(transitive: Boolean)(dependency: AutoPlugin) =
    s"Required ${transitiveString(transitive)}dependency ${dependency.label} was excluded."

  private[this] def excludedPluginsError(transitive: Boolean)(dependencies: List[AutoPlugin]) =
    s"Required ${transitiveString(transitive)}dependencies were excluded:\n\t${labels(dependencies)
      .mkString("\n\t")}"

  private[this] def transitiveString(transitive: Boolean) =
    if (transitive) "(transitive) " else ""

  private[this] def required(plugins: List[AutoPlugin]): String =
    str(plugins)(requiredPlugin, requiredPlugins)

  private[this] def requiredPlugin(plugin: AutoPlugin) =
    s"Required plugin ${plugin.label} not present."

  private[this] def requiredPlugins(plugins: List[AutoPlugin]) =
    s"Required plugins not present:\n\t${plugins.map(_.label).mkString("\n\t")}"

  private[this] def str[A](list: List[A])(f: A => String, fs: List[A] => String): String =
    list match {
      case Nil           => ""
      case single :: Nil => f(single)
      case _             => fs(list)
    }

  private[this] def willAdd(base: AutoPlugin, plugins: List[AutoPlugin]): String =
    str(plugins)(willAddPlugin(base), willAddPlugins(base))

  private[this] def willAddPlugin(base: AutoPlugin)(plugin: AutoPlugin) =
    s"Enabling ${base.label} will also enable ${plugin.label}"

  private[this] def willAddPlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) =
    s"Enabling ${base.label} will also enable:\n\t${labels(plugins).mkString("\n\t")}"

  private[this] def willRemove(base: AutoPlugin, plugins: List[AutoPlugin]): String =
    str(plugins)(willRemovePlugin(base), willRemovePlugins(base))

  private[this] def willRemovePlugin(base: AutoPlugin)(plugin: AutoPlugin) =
    s"Enabling ${base.label} will disable ${plugin.label}"

  private[this] def willRemovePlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) =
    s"Enabling ${base.label} will disable:\n\t${labels(plugins).mkString("\n\t")}"

  private[this] def labels(plugins: List[AutoPlugin]): List[String] =
    plugins.map(_.label)

  private[this] def needToDeactivate(deactivate: List[DeactivatePlugin]): String =
    str(deactivate)(deactivate1, deactivateN)

  private[this] def deactivateN(plugins: List[DeactivatePlugin]): String =
    plugins.map(deactivateString).mkString("These plugins need to be deactivated:\n\t", "\n\t", "")

  private[this] def deactivate1(deactivate: DeactivatePlugin): String =
    s"Need to deactivate ${deactivateString(deactivate)}"

  private[this] def deactivateString(d: DeactivatePlugin): String = {
    val removePluginsString: String =
      d.removeOneOf.toList match {
        case Nil      => ""
        case x :: Nil => s" or no longer include $x"
        case xs       => s" or remove one of ${xs.mkString(", ")}"
      }
    s"${d.plugin.label}: directly exclude it${removePluginsString}"
  }

  private[this] def pluginImpossible(plugin: AutoPlugin, contradictions: Set[AutoPlugin]): String =
    str(contradictions.toList)(pluginImpossible1(plugin), pluginImpossibleN(plugin))

  private[this] def pluginImpossible1(plugin: AutoPlugin)(contradiction: AutoPlugin): String = {
    val s1 = s"There is no way to enable plugin ${plugin.label}."
    val s2 =
      s"It (or its dependencies) requires plugin ${contradiction.label} to both be present and absent."
    val s3 = s"Please report the problem to the plugin's author."
    s"$s1  $s2  $s3"
  }

  private[this] def pluginImpossibleN(
      plugin: AutoPlugin
  )(contradictions: List[AutoPlugin]): String = {
    val s1 = s"There is no way to enable plugin ${plugin.label}."
    val s2 = s"It (or its dependencies) requires these plugins to be both present and absent:"
    val s3 = s"Please report the problem to the plugin's author."
    s"$s1  $s2:\n\t${labels(contradictions).mkString("\n\t")}\n$s3"
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy