diff --git a/compile/interface/Analyzer.scala b/compile/interface/Analyzer.scala
index bd45d7721..a1a7a5f58 100644
--- a/compile/interface/Analyzer.scala
+++ b/compile/interface/Analyzer.scala
@@ -15,13 +15,13 @@ import xsbti.{AnalysisCallback, AnalysisCallbackContainer}
class Analyzer(val global: Global) extends Plugin
{
val callback = global.asInstanceOf[AnalysisCallbackContainer].analysisCallback
-
+
import global._
-
+
val name = "xsbt-analyze"
val description = "A plugin to find all concrete instances of a given class and extract dependency information."
val components = List[PluginComponent](Component)
-
+
/* ================================================== */
// These two templates abuse scope for source compatibility between Scala 2.7.x and 2.8.x so that a single
// sbt codebase compiles with both series of versions.
@@ -39,7 +39,7 @@ class Analyzer(val global: Global) extends Plugin
override val runsAfter = afterPhase :: runsBefore
}
/* ================================================== */
-
+
private object Component extends CompatiblePluginComponent("jvm")
{
val global = Analyzer.this.global
@@ -53,16 +53,8 @@ class Analyzer(val global: Global) extends Plugin
def run
{
val outputDirectory = new File(global.settings.outdir.value)
- val superclassNames = callback.superclassNames.map(newTermName)
- val superclassesAll =
- for(name <- superclassNames) yield
- {
- try { Some(global.definitions.getClass(name)) }
- catch { case fe: scala.tools.nsc.FatalError => callback.superclassNotFound(name.toString); None }
- }
- val superclasses = superclassesAll.filter(_.isDefined).map(_.get)
- //println("Superclass names: " + superclassNames.mkString(", ") + "\n\tall: " + superclasses.mkString(", "))
-
+ val superclasses = callback.superclassNames flatMap(classForName)
+
for(unit <- currentRun.units)
{
// build dependencies structure
@@ -90,7 +82,7 @@ class Analyzer(val global: Global) extends Plugin
else
callback.sourceDependency(onSource.file, sourceFile)
}
-
+
// find subclasses and modules with main methods
for(clazz @ ClassDef(mods, n, _, _) <- unit.body)
{
@@ -105,7 +97,7 @@ class Analyzer(val global: Global) extends Plugin
callback.foundApplication(sourceFile, sym.fullNameString)
}
}
-
+
// build list of generated classes
for(iclass <- unit.icode)
{
@@ -129,11 +121,25 @@ class Analyzer(val global: Global) extends Plugin
}
}
}
-
+
+ private def classForName(name: String) =
+ {
+ try
+ {
+ if(name.indexOf('.') < 0)
+ {
+ val sym = definitions.EmptyPackageClass.info.member(newTypeName(name))
+ if(sym != NoSymbol) Some( sym ) else { callback.superclassNotFound(name); None }
+ }
+ else
+ Some( global.definitions.getClass(newTermName(name)) )
+ }
+ catch { case fe: scala.tools.nsc.FatalError => callback.superclassNotFound(name); None }
+ }
private def classFile(sym: Symbol): Option[AbstractFile] =
{
import scala.tools.nsc.symtab.Flags
- val name = sym.fullNameString(java.io.File.separatorChar) + (if (sym.hasFlag(Flags.MODULE)) "$" else "")
+ val name = sym.fullNameString(File.separatorChar) + (if (sym.hasFlag(Flags.MODULE)) "$" else "")
val entry = classPath.root.find(name, false)
if (entry ne null)
Some(entry.classFile)
@@ -148,7 +154,7 @@ class Analyzer(val global: Global) extends Plugin
else
None
}
-
+
private def isTopLevelModule(sym: Symbol): Boolean =
atPhase (currentRun.picklerPhase.next) {
sym.isModuleClass && !sym.isImplClass && !sym.isNestedClass
@@ -169,29 +175,32 @@ class Analyzer(val global: Global) extends Plugin
else
new File(packageFile(outputDirectory, s.owner.enclClass), s.simpleName.toString)
}
-
+
private def hasMainMethod(sym: Symbol): Boolean =
{
val main = sym.info.nonPrivateMember(newTermName("main"))//nme.main)
- main.tpe match
- {
- case OverloadedType(pre, alternatives) => alternatives.exists(alt => isVisible(alt) && isMainType(pre.memberType(alt)))
- case tpe => isVisible(main) && isMainType(main.owner.thisType.memberType(main))
+ atPhase(currentRun.typerPhase.next) {
+ main.tpe match
+ {
+ case OverloadedType(pre, alternatives) => alternatives.exists(alt => isVisible(alt) && isMainType(pre.memberType(alt)))
+ case tpe => isVisible(main) && isMainType(main.owner.thisType.memberType(main))
+ }
}
}
private def isVisible(sym: Symbol) = sym != NoSymbol && sym.isPublic && !sym.isDeferred
- private def isMainType(tpe: Type) =
+ private def isMainType(tpe: Type): Boolean =
{
tpe match
{
// singleArgument is of type Symbol in 2.8.0 and type Type in 2.7.x
case MethodType(List(singleArgument), result) => isUnitType(result) && isStringArray(singleArgument)
- case _ => false
+ case PolyType(typeParams, result) => isMainType(result)
+ case _ => false
}
}
private lazy val StringArrayType = appliedType(definitions.ArrayClass.typeConstructor, definitions.StringClass.tpe :: Nil)
// isStringArray is overloaded to handle the incompatibility between 2.7.x and 2.8.0
- private def isStringArray(tpe: Type): Boolean = tpe.typeSymbol == StringArrayType.typeSymbol
+ private def isStringArray(tpe: Type): Boolean = tpe =:= StringArrayType
private def isStringArray(sym: Symbol): Boolean = isStringArray(sym.tpe)
private def isUnitType(tpe: Type) = tpe.typeSymbol == definitions.UnitClass
}
\ No newline at end of file
diff --git a/compile/interface/src/test/scala/ApplicationsTest.scala b/compile/interface/src/test/scala/ApplicationsTest.scala
new file mode 100644
index 000000000..8be9fc955
--- /dev/null
+++ b/compile/interface/src/test/scala/ApplicationsTest.scala
@@ -0,0 +1,117 @@
+package xsbt
+
+import java.io.File
+import java.net.URLClassLoader
+import org.specs.Specification
+
+/** Verifies that the analyzer plugin properly detects main methods. The main method must be
+* public with the right signature and be defined on a public, top-level module.*/
+object ApplicationsTest extends Specification
+{
+ val sourceContent =
+ """
+ object Main { def main(args: Array[String]) {} }
+ """ :: """
+ class Main2 { def main(args: Array[String]) {} }
+ """ :: """
+ object Main3 { private def main(args: Array[String]) {} }
+ private object Main3b extends Main2
+ object Main3c { private def main(args: Array[String]) {} }
+ protected object Main3d { def main(args: Array[String]) {} }
+ object Main3e {
+ protected def main(args: Array[String]) {}
+ }
+ package a {
+ object Main3f { private[a] def main(args: Array[String]) {} }
+ object Main3g { protected[a] def main(args: Array[String]) {} }
+ }
+ """ ::"""
+ object Main4 extends Main2
+ """ :: """
+ trait Main5 { def main(args: Array[String]) {} }; trait Main5b extends Main5; trait Main5c extends Main2; abstract class Main5d { def main(args: Array[String]) {} }
+ """ :: """
+ object Main6a { var main = () }
+ object Main6b { var main = (args: Array[String]) => () }
+ """ :: """
+ object Main7 { object Main7b extends Main2 }
+ """ :: """
+ object Main8 extends Main2 { object Main7b extends Main2 }
+ """ :: """
+ object Main9 {
+ def main() {}
+ def main(i: Int) {}
+ def main(args: Array[String]) {}
+ }
+ """ :: """
+ object MainA {
+ def main() {}
+ def main(i: Int) {}
+ def main(args: Array[String], other: String) {}
+ def main(i: Array[Int]) {}
+ }
+ object MainA2 {
+ def main[T](args: Array[T]) {}
+ }
+ """ :: """
+ object MainB extends Main2 {
+ def main() {}
+ def main(i: Int) {}
+ }
+ """ :: """
+ object MainC1 {
+ def main(args: Array[String]) = 3
+ }
+ object MainC2 {
+ def main1(args: Array[String]) {}
+ }
+ """ :: """
+ object MainD1 {
+ val main = ()
+ }
+ object MainD2 {
+ val main = (args: Array[String]) => ()
+ }
+ """ :: """
+ object MainE1 {
+ type T = String
+ def main(args: Array[T]) {}
+ }
+ object MainE2 {
+ type AT = Array[String]
+ def main(args: AT) {}
+ }
+ object MainE3 {
+ type U = Unit
+ type T = String
+ def main(args: Array[T]): U = ()
+ }
+ object MainE4 {
+ def main[T](args: Array[String]) {}
+ }
+ object MainE5 {
+ type A[T] = Array[String]
+ def main[T](args: A[T]) {}
+ }
+ """ ::
+ Nil
+ val sources = for((source, index) <- sourceContent.zipWithIndex) yield new File("Main" + (index+1) + ".scala") -> source
+
+ "Analysis plugin should detect applications" in {
+ WithFiles(sources : _*) { case files @ Seq(main, main2, main3, main4, main5, main6, main7, main8, main9, mainA, mainB, mainC, mainD, mainE) =>
+ CallbackTest(files, Nil) { (callback, file, log) =>
+ val expected = Seq( main -> "Main", main4 -> "Main4", main8 -> "Main8", main9 -> "Main9", mainB -> "MainB",
+ mainE -> "MainE1", mainE -> "MainE2", mainE -> "MainE3", mainE -> "MainE4", mainE -> "MainE5" )
+ (callback.applications) must haveTheSameElementsAs(expected)
+ val loader = new URLClassLoader(Array(file.toURI.toURL), getClass.getClassLoader)
+ for( (_, className) <- expected) testRun(loader, className)
+ }
+ }
+ }
+ private def testRun(loader: ClassLoader, className: String)
+ {
+ val obj = Class.forName(className+"$", true, loader)
+ val singletonField = obj.getField("MODULE$")
+ val singleton = singletonField.get(null)
+ singleton.asInstanceOf[{def main(args: Array[String]): Unit}].main(Array[String]())
+ }
+}
\ No newline at end of file
diff --git a/compile/interface/src/test/scala/CheckBasic.scala b/compile/interface/src/test/scala/CheckBasic.scala
index 7af655d13..e3a41b381 100644
--- a/compile/interface/src/test/scala/CheckBasic.scala
+++ b/compile/interface/src/test/scala/CheckBasic.scala
@@ -7,60 +7,18 @@ object CheckBasic extends Specification
{
val basicName = new File("Basic.scala")
val basicSource = "package org.example { object Basic }"
-
- val mainName = new File("Main.scala")
- val mainSource = "object Main { def main(args: Array[String]) {} }"
-
- val super1Name = new File("a/Super.scala")
- val super2Name = new File("a/Super2.scala")
- val midName = new File("b/Middle.scala")
- val sub1Name = new File("b/SubA.scala")
- val sub2Name = new File("b/SubB.scala")
- val sub3Name = new File("SubC.scala")
- val super1Source = "package a; trait Super"
- val super2Source = "class Super2"
- val midSource = "package y.w; trait Mid extends a.Super"
- val subSource1 = "package a; trait Sub1 extends y.w.Mid"
- val subSource2 = "trait Sub2 extends a.Super"
- val subSource3 = "private class F extends a.Super; package c { object Sub3 extends Super2 }"
-
+
"Compiling basic file should succeed" in {
WithFiles(basicName -> basicSource){ files =>
TestCompile(files){ loader => Class.forName("org.example.Basic", false, loader) }
+ true must be(true) // don't know how to just check that previous line completes without exception
}
}
-
- "Analysis plugin" should {
- "send source begin and end" in {
- WithFiles(basicName -> basicSource) { files =>
- CallbackTest(files) { callback =>
- (callback.beganSources) must haveTheSameElementsAs(files)
- (callback.endedSources) must haveTheSameElementsAs(files)
- }
- }
- }
-
- "detect applications" in {
- WithFiles(mainName -> mainSource ) { files =>
- CallbackTest(files) { callback =>
- (callback.applications) must haveTheSameElementsAs(files.map(file => (file, "Main")))
- }
- }
- }
-
- "detect subclasses" in {
- WithFiles(super1Name -> super1Source, midName -> midSource, sub1Name -> subSource1, sub2Name -> subSource2,
- super2Name -> super2Source, sub3Name -> subSource3)
- {
- case files @ Seq(supFile, midFile, sub1File, sub2File, sup2File, sub3File) =>
- CallbackTest(files,Seq( "a.Super", "Super2", "x.Super3")) { (callback, ignore, ignore2) =>
- val expected = (sub1File, "a.Super", "a.Sub1", false) :: (sub2File, "a.Super", "a.Sub2", false) ::
- (sub3File, "Super2", "Sub3", true) :: Nil
- //println(callback.foundSubclasses)
- //println(callback.invalidSuperclasses)
- (callback.foundSubclasses) must haveTheSameElementsAs(expected)
- (callback.invalidSuperclasses) must haveTheSameElementsAs(Seq("x.Super3"))
- }
+ "Analyzer plugin should send source begin and end" in {
+ WithFiles(basicName -> basicSource) { files =>
+ CallbackTest(files) { callback =>
+ (callback.beganSources) must haveTheSameElementsAs(files)
+ (callback.endedSources) must haveTheSameElementsAs(files)
}
}
}
diff --git a/compile/interface/src/test/scala/DetectSubclasses.scala b/compile/interface/src/test/scala/DetectSubclasses.scala
new file mode 100644
index 000000000..a502bbe50
--- /dev/null
+++ b/compile/interface/src/test/scala/DetectSubclasses.scala
@@ -0,0 +1,33 @@
+package xsbt
+
+import java.io.File
+import org.specs.Specification
+
+object DetectSubclasses extends Specification
+{
+ val sources =
+ ("a/Super.scala" -> "package a; trait Super") ::
+ ("a/Super2.scala" -> "class Super2") ::
+ ("b/Middle.scala" -> "package y.w; trait Mid extends a.Super") ::
+ ("b/Sub1.scala" -> "package a; class Sub1 extends y.w.Mid") ::
+ ("b/Sub2.scala" -> "final class Sub2 extends a.Super") ::
+ ("Sub3.scala" -> "private class F extends a.Super; package c { object Sub3 extends Super2 }") ::
+ Nil
+
+ "Analysis plugin should detect subclasses" in {
+ WithFiles(sources.map{case (file, content) => (new File(file), content)} : _*)
+ {
+ case files @ Seq(supFile, sup2File, midFile, sub1File, sub2File, sub3File) =>
+ CallbackTest(files, Seq( "a.Super", "Super2", "x.Super3", "Super4") ) { (callback, x, xx) =>
+ val expected =
+ (sub1File, "a.Sub1", "a.Super", false) ::
+ (sub2File, "Sub2", "a.Super", false) ::
+ (sup2File, "Super2", "Super2", false) ::
+ (sub3File, "c.Sub3", "Super2", true) ::
+ Nil
+ (callback.foundSubclasses) must haveTheSameElementsAs(expected)
+ (callback.invalidSuperclasses) must haveTheSameElementsAs(Seq("x.Super3", "Super4"))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compile/interface/src/test/scala/TestCompile.scala b/compile/interface/src/test/scala/TestCompile.scala
index 29135832b..013dd4188 100644
--- a/compile/interface/src/test/scala/TestCompile.scala
+++ b/compile/interface/src/test/scala/TestCompile.scala
@@ -7,6 +7,8 @@ import FileUtilities.{classLocationFile, withTemporaryDirectory, write}
object TestCompile
{
+ /** Tests running the compiler interface with the analyzer plugin with a test callback. The test callback saves all information
+ * that the plugin sends it for post-compile analysis by the provided function.*/
def apply[T](arguments: Seq[String], superclassNames: Seq[String])(f: (TestCallback, Logger) => T): T =
{
val pluginLocation = classLocationFile[Analyzer]
@@ -21,6 +23,8 @@ object TestCompile
f(testCallback, log)
}
}
+ /** Tests running the compiler interface with the analyzer plugin. The provided function is given a ClassLoader that can
+ * load the compiled classes..*/
def apply[T](sources: Seq[File])(f: ClassLoader => T): T =
CallbackTest.apply(sources, Nil){ case (callback, outputDir, log) => f(new URLClassLoader(Array(outputDir.toURI.toURL))) }
}
@@ -38,6 +42,9 @@ object CallbackTest
}
object WithFiles
{
+ /** Takes the relative path -> content pairs and writes the content to a file in a temporary directory. The written file
+ * path is the relative path resolved against the temporary directory path. The provided function is called with the resolved file paths
+ * in the same order as the inputs. */
def apply[T](sources: (File, String)*)(f: Seq[File] => T): T =
{
withTemporaryDirectory { dir =>
diff --git a/interface/src/main/java/xsbti/AnalysisCallback.java b/interface/src/main/java/xsbti/AnalysisCallback.java
index 2ceedd4a6..70870965c 100644
--- a/interface/src/main/java/xsbti/AnalysisCallback.java
+++ b/interface/src/main/java/xsbti/AnalysisCallback.java
@@ -17,7 +17,9 @@ public interface AnalysisCallback
* discovered.*/
public void foundSubclass(File source, String subclassName, String superclassName, boolean isModule);
/** Called to indicate that the source file source depends on the source file
- * dependsOn.*/
+ * dependsOn. Note that only source files included in the current compilation will
+ * passed to this method. Dependencies on classes generated by sources not in the current compilation will
+ * be passed as class dependencies to the classDependency method.*/
public void sourceDependency(File dependsOn, File source);
/** Called to indicate that the source file source depends on the jar
* jar.*/
diff --git a/project/build/XSbt.scala b/project/build/XSbt.scala
index c6daa57de..96a11a673 100644
--- a/project/build/XSbt.scala
+++ b/project/build/XSbt.scala
@@ -21,7 +21,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
def utilPath = path("util")
def compilePath = path("compile")
-
+
class CommonDependencies(info: ProjectInfo) extends DefaultProject(info)
{
val sc = "org.scala-tools.testing" % "scalacheck" % "1.5" % "test->default"
@@ -35,7 +35,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
{
//override def compileOptions = super.compileOptions ++ List(Unchecked,ExplainTypes, CompileOption("-Xlog-implicits"))
}
- class Base(info: ProjectInfo) extends DefaultProject(info) with AssemblyProject
+ class Base(info: ProjectInfo) extends DefaultProject(info)
{
override def scratch = true
}
@@ -53,7 +53,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
// these set up the test so that the classes and resources are both in the output resource directory
// the main output path is removed so that the plugin (xsbt.Analyzer) is found in the output resource directory so that
// the tests can configure that directory as -Xpluginsdir (which requires the scalac-plugin.xml and the classes to be together)
- override def testCompileAction = super.testCompileAction dependsOn(packageForTest)
+ override def testCompileAction = super.testCompileAction dependsOn(packageForTest, ioSub.testCompile)
override def mainResources = super.mainResources +++ "scalac-plugin.xml"
override def testClasspath = (super.testClasspath --- super.mainCompilePath) +++ ioSub.testClasspath +++ testPackagePath
def testPackagePath = outputPath / "test.jar"
diff --git a/tasks/TaskScheduler.scala b/tasks/TaskScheduler.scala
index a04406cf8..9c2f47a9f 100644
--- a/tasks/TaskScheduler.scala
+++ b/tasks/TaskScheduler.scala
@@ -58,12 +58,12 @@ private final class TaskScheduler[O](root: Task[O], strategy: ScheduleStrategy[W
private val strategyRun = strategy.run
private val failed = new mutable.HashSet[Task[_]]
private val failureReports = new mutable.ArrayBuffer[WorkFailure[Task[_]]]
-
+
{
val initialized = addGraph(root, root) // TODO: replace second root with something better? (it is ignored here anyway)
assert(initialized)
}
-
+
private def addReady[O](m: Task[O])
{
def add[I](m: ITask[I,O])
@@ -72,7 +72,7 @@ private final class TaskScheduler[O](root: Task[O], strategy: ScheduleStrategy[W
strategyRun.workReady(new Work(m, input))
listener.runnable(m)
}
-
+
assert(!forwardDeps.contains(m), m)
assert(reverseDeps.contains(m), m)
assert(!completed.contains(m), m)
@@ -172,20 +172,25 @@ private final class TaskScheduler[O](root: Task[O], strategy: ScheduleStrategy[W
retire(dependsOnM, None)
}
}
-
+
private def success[O](task: Task[O], value: Result[O]): Unit =
value match
{
case NewTask(t) =>
- if(t == task)
+ if(t eq task)
{
failureReports += WorkFailure(t, CircularDependency(t, task))
retire(task, None)
}
else if(addGraph(t, task))
{
- calls(t) = task
- listener.calling(task, t)
+ if(completed.contains(t))
+ retire(task, Some(completed(t)))
+ else
+ {
+ calls(t) = task
+ listener.calling(task, t)
+ }
}
else
retire(task, None)
diff --git a/tasks/src/test/scala/TaskRunnerCircular.scala b/tasks/src/test/scala/TaskRunnerCircular.scala
index 8034b47e4..795875fb2 100644
--- a/tasks/src/test/scala/TaskRunnerCircular.scala
+++ b/tasks/src/test/scala/TaskRunnerCircular.scala
@@ -8,12 +8,9 @@ object TaskRunnerCircularTest extends Properties("TaskRunner Circular")
specify("Catches circular references", (intermediate: Int, workers: Int) =>
(workers > 0 && intermediate >= 0) ==> checkCircularReferences(intermediate, workers)
)
- /*specify("Check root complete", (intermediate: Int, workers: Int) =>
- (workers > 0 && intermediate >= 0) ==> checkRootComplete(intermediate, workers)
- )*/
- specify("Allows noncircular references", (intermediate: Int, workers: Int) =>
+ specify("Allows references to completed tasks", forAllNoShrink(Arbitrary.arbitrary[(Int,Int)])(x=>x) { case (intermediate: Int, workers: Int) =>
(workers > 0 && intermediate >= 0) ==> allowedReference(intermediate, workers)
- )
+ })
final def allowedReference(intermediate: Int, workers: Int) =
{
val top = Task(intermediate) named("top")
@@ -24,7 +21,7 @@ object TaskRunnerCircularTest extends Properties("TaskRunner Circular")
else
iterate(Task(t-1) named (t-1).toString)
}
- try { checkResult(TaskRunner(iterate(top), workers), 0) }
+ try { checkResult(TaskRunner(iterate(top), workers), intermediate) }
catch { case e: CircularDependency => ("Unexpected exception: " + e) |: false }
}
final def checkCircularReferences(intermediate: Int, workers: Int) =
@@ -44,21 +41,4 @@ object TaskRunnerCircularTest extends Properties("TaskRunner Circular")
try { TaskRunner(top, workers); false }
catch { case TasksFailed(failures) => failures.exists(_.exception.isInstanceOf[CircularDependency]) }
}
- final def checkRootComplete(intermediate: Int, workers: Int) =
- {
- val top = Task(intermediate)
- def iterate(task: Task[Int]): Task[Int] =
- {
- lazy val it: Task[Int] =
- task bind { t =>
- if(t <= 0)
- it
- else
- iterate(Task(t-1) named (t-1).toString)
- } named("it")
- it
- }
- try { TaskRunner(iterate(top), workers); false }
- catch { case e: CircularDependency => true }
- }
}
\ No newline at end of file
diff --git a/tasks/src/test/scala/TaskRunnerFork.scala b/tasks/src/test/scala/TaskRunnerFork.scala
index b1b49e00f..cffaffa1b 100644
--- a/tasks/src/test/scala/TaskRunnerFork.scala
+++ b/tasks/src/test/scala/TaskRunnerFork.scala
@@ -14,6 +14,12 @@ object TaskRunnerForkTest extends Properties("TaskRunner Fork")
true
}
)
+ specify("Fork and reduce 2", (m: Int, workers: Int) =>
+ (workers > 0 && m > 1) ==> {
+ val task = (0 to m) fork {_ * 10} reduce{_ + _}
+ checkResult(TaskRunner(task, workers), 5*(m+1)*m)
+ }
+ )
specify("Double join", (a: Int, b: Int, workers: Int) =>
(workers > 0) ==> { runDoubleJoin(abs(a),abs(b),workers); true }
)