API docs, better terminology for negative cycle checking in logic system.

This commit is contained in:
Mark Harrah 2014-01-24 14:19:18 -05:00
parent 1afd1931c4
commit 9264099594
4 changed files with 61 additions and 29 deletions

View File

@ -76,7 +76,11 @@ abstract class AutoPlugin
// TODO?: def commands: Seq[Command]
}
final class AutoPluginException(val origin: LogicException, prefix: String) extends RuntimeException(prefix + Natures.translateMessage(origin)) {
/** An error that occurs when auto-plugins aren't configured properly.
* It translates the error from the underlying logic system to be targeted at end users. */
final class AutoPluginException(val origin: LogicException, prefix: String) extends RuntimeException(prefix + Natures.translateMessage(origin))
{
/** Prepends `p` to the error message derived from `origin`. */
def withPrefix(p: String) = new AutoPluginException(origin, p)
}

View File

@ -17,9 +17,12 @@ object PluginDiscovery
final val Builds = "sbt/sbt.builds"
final val AutoImports = "sbt/sbt.autoimports"
}
/** Names of top-level modules that subclass sbt plugin-related classes: [[Plugin]], [[AutoImport]], [[AutoPlugin]], and [[Build]]. */
final class DiscoveredNames(val plugins: Seq[String], val autoImports: Seq[String], val autoPlugins: Seq[String], val builds: Seq[String])
def emptyDiscoveredNames: DiscoveredNames = new DiscoveredNames(Nil, Nil, Nil, Nil)
/** Discovers and loads the sbt-plugin-related top-level modules from the classpath and source analysis in `data` and using the provided class `loader`. */
def discoverAll(data: PluginData, loader: ClassLoader): DetectedPlugins =
{
def discover[T](resource: String)(implicit mf: reflect.ClassManifest[T]) =
@ -27,6 +30,8 @@ object PluginDiscovery
import Paths._
new DetectedPlugins(discover[Plugin](Plugins), discover[AutoImport](AutoImports), discover[AutoPlugin](AutoPlugins), discover[Build](Builds))
}
/** Discovers the sbt-plugin-related top-level modules from the provided source `analysis`. */
def discoverSourceAll(analysis: inc.Analysis): DiscoveredNames =
{
def discover[T](implicit mf: reflect.ClassManifest[T]): Seq[String] =
@ -35,6 +40,7 @@ object PluginDiscovery
}
// TODO: for 0.14.0, consider consolidating into a single file, which would make the classpath search 4x faster
/** Writes discovered module `names` to zero or more files in `dir` as per [[writeDescriptor]] and returns the list of files written. */
def writeDescriptors(names: DiscoveredNames, dir: File): Seq[File] =
{
import Paths._
@ -47,6 +53,7 @@ object PluginDiscovery
files.flatMap(_.toList)
}
/** Stores the module `names` in `dir / path`, one per line, unless `names` is empty and then the file is deleted and `None` returned. */
def writeDescriptor(names: Seq[String], dir: File, path: String): Option[File] =
{
val descriptor: File = new File(dir, path)
@ -62,13 +69,15 @@ object PluginDiscovery
}
}
/** Discovers the names of top-level modules listed in resources named `resourceName` as per [[binaryModuleNames]] or
* available as analyzed source and extending from any of `subclasses` as per [[sourceModuleNames]]. */
def binarySourceModuleNames(classpath: Seq[Attributed[File]], loader: ClassLoader, resourceName: String, subclasses: String*): Seq[String] =
(
binaryModuleNames(data(classpath), loader, resourceName) ++
(analyzed(classpath) flatMap ( a => sourceModuleNames(a, subclasses : _*) ))
).distinct
/** Discovers top-level modules in `analysis` that inherit from any of `subclasses`. */
def sourceModuleNames(analysis: inc.Analysis, subclasses: String*): Seq[String] =
{
val subclassSet = subclasses.toSet
@ -80,6 +89,9 @@ object PluginDiscovery
}
}
/** Obtains the list of modules identified in all resource files `resourceName` from `loader` that are on `classpath`.
* `classpath` and `loader` are both required to ensure that `loader`
* doesn't bring in any resources outside of the intended `classpath`, such as from parent loaders. */
def binaryModuleNames(classpath: Seq[File], loader: ClassLoader, resourceName: String): Seq[String] =
{
import collection.JavaConversions._
@ -87,6 +99,8 @@ object PluginDiscovery
IO.readLinesURL(u).map( _.trim).filter(!_.isEmpty)
}
}
/** Returns `true` if `url` is an entry in `classpath`.*/
def onClasspath(classpath: Seq[File])(url: URL): Boolean =
IO.urlAsFile(url) exists (classpath.contains _)

View File

@ -72,39 +72,52 @@ object Dag
new Cyclic(value, a :: all, false)
}
private[sbt] trait System[A] {
type B
def dependencies(t: A): List[B]
def isNegated(b: B): Boolean
def toA(b: B): A
/** A directed graph with edges labeled positive or negative. */
private[sbt] trait DirectedSignedGraph[Node]
{
/** Directed edge type that tracks the sign and target (head) vertex.
* The sign can be obtained via [[isNegative]] and the target vertex via [[head]]. */
type Arrow
/** List of initial nodes. */
def nodes: List[Arrow]
/** Outgoing edges for `n`. */
def dependencies(n: Node): List[Arrow]
/** `true` if the edge `a` is "negative", false if it is "positive". */
def isNegative(a: Arrow): Boolean
/** The target of the directed edge `a`. */
def head(a: Arrow): Node
}
private[sbt] def findNegativeCycle[T](system: System[T])(nodes: List[system.B]): List[system.B] =
/** Traverses a directed graph defined by `graph` looking for a cycle that includes a "negative" edge.
* The directed edges are weighted by the caller as "positive" or "negative".
* If a cycle containing a "negative" edge is detected, its member edges are returned in order.
* Otherwise, the empty list is returned. */
private[sbt] def findNegativeCycle[Node](graph: DirectedSignedGraph[Node]): List[graph.Arrow] =
{
import scala.annotation.tailrec
import system._
val finished = new mutable.HashSet[T]
val visited = new mutable.HashSet[T]
import graph._
val finished = new mutable.HashSet[Node]
val visited = new mutable.HashSet[Node]
def visit(nodes: List[B], stack: List[B]): List[B] = nodes match {
def visit(edges: List[Arrow], stack: List[Arrow]): List[Arrow] = edges match {
case Nil => Nil
case node :: tail =>
def indent = "\t" * stack.size
val atom = toA(node)
if(!visited(atom))
case edge :: tail =>
val node = head(edge)
if(!visited(node))
{
visited += atom
visit(dependencies(atom), node :: stack) match {
visited += node
visit(dependencies(node), edge :: stack) match {
case Nil =>
finished += atom
finished += node
visit(tail, stack)
case cycle => cycle
}
}
else if(!finished(atom))
else if(!finished(node))
{
// cycle. If negation is involved, it is an error.
val between = node :: stack.takeWhile(f => toA(f) != atom)
if(between exists isNegated)
// cycle. If a negative edge is involved, it is an error.
val between = edge :: stack.takeWhile(f => head(f) != node)
if(between exists isNegative)
between
else
visit(tail, stack)
@ -113,7 +126,7 @@ object Dag
visit(tail, stack)
}
visit(nodes, Nil)
visit(graph.nodes, Nil)
}
}

View File

@ -122,17 +122,18 @@ object Logic
private[this] def checkAcyclic(clauses: Clauses): Option[CyclicNegation] = {
val deps = dependencyMap(clauses)
val cycle = Dag.findNegativeCycle(system(deps))(deps.keys.toList)
val cycle = Dag.findNegativeCycle(graph(deps))
if(cycle.nonEmpty) Some(new CyclicNegation(cycle)) else None
}
private[this] def system(deps: Map[Atom, Set[Literal]]) = new Dag.System[Atom] {
type B = Literal
private[this] def graph(deps: Map[Atom, Set[Literal]]) = new Dag.DirectedSignedGraph[Atom] {
type Arrow = Literal
def nodes = deps.keys.toList
def dependencies(a: Atom) = deps.getOrElse(a, Set.empty).toList
def isNegated(b: Literal) = b match {
def isNegative(b: Literal) = b match {
case Negated(_) => true
case Atom(_) => false
}
def toA(b: Literal) = b.atom
def head(b: Literal) = b.atom
}
private[this] def dependencyMap(clauses: Clauses): Map[Atom, Set[Literal]] =