From 28dbe29a2ab566249642e81405a953f33507828a Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 16 Nov 2017 03:40:58 -0800 Subject: Vendor `com.lihaoyi:acyclic` codebase as a cross-building example, first non-working experiments in cross building working... --- scalaplugin/src/test/resource/acyclic/build.sbt | 49 ++++++ scalaplugin/src/test/resource/acyclic/build.sc | 116 +++++++++++++ .../test/resource/acyclic/project/build.properties | 1 + .../src/test/resource/acyclic/project/build.sbt | 2 + .../acyclic/src/main/resources/scalac-plugin.xml | 4 + .../acyclic/src/main/scala/acyclic/package.scala | 23 +++ .../acyclic/plugin/DependencyExtraction.scala | 100 ++++++++++++ .../main/scala/acyclic/plugin/GraphAnalysis.scala | 103 ++++++++++++ .../src/main/scala/acyclic/plugin/Plugin.scala | 26 +++ .../main/scala/acyclic/plugin/PluginPhase.scala | 180 +++++++++++++++++++++ .../src/test/resources/fail/cyclicgraph/A.scala | 6 + .../src/test/resources/fail/cyclicgraph/B.scala | 6 + .../src/test/resources/fail/cyclicgraph/C.scala | 6 + .../src/test/resources/fail/cyclicgraph/D.scala | 7 + .../src/test/resources/fail/cyclicgraph/E.scala | 7 + .../test/resources/fail/cyclicpackage/a/A1.scala | 7 + .../test/resources/fail/cyclicpackage/a/A2.scala | 4 + .../resources/fail/cyclicpackage/a/package.scala | 5 + .../test/resources/fail/cyclicpackage/b/B1.scala | 3 + .../test/resources/fail/cyclicpackage/b/B2.scala | 5 + .../resources/fail/cyclicpackage/b/package.scala | 5 + .../test/resources/fail/halfpackagecycle/A.scala | 5 + .../test/resources/fail/halfpackagecycle/B.scala | 3 + .../resources/fail/halfpackagecycle/c/C1.scala | 3 + .../resources/fail/halfpackagecycle/c/C2.scala | 6 + .../fail/halfpackagecycle/c/package.scala | 5 + .../src/test/resources/fail/indirect/A.scala | 7 + .../src/test/resources/fail/indirect/B.scala | 3 + .../src/test/resources/fail/indirect/C.scala | 5 + .../acyclic/src/test/resources/fail/simple/A.scala | 7 + .../acyclic/src/test/resources/fail/simple/B.scala | 6 + .../src/test/resources/force/simple/A.scala | 7 + .../src/test/resources/force/simple/B.scala | 6 + .../acyclic/src/test/resources/force/skip/A.scala | 7 + .../acyclic/src/test/resources/force/skip/B.scala | 6 + .../test/resources/success/cyclicunmarked/A.scala | 5 + .../test/resources/success/cyclicunmarked/B.scala | 6 + .../acyclic/src/test/resources/success/dag/A.scala | 4 + .../acyclic/src/test/resources/success/dag/B.scala | 5 + .../acyclic/src/test/resources/success/dag/C.scala | 3 + .../acyclic/src/test/resources/success/dag/D.scala | 6 + .../acyclic/src/test/resources/success/dag/E.scala | 6 + .../src/test/resources/success/java/SomeJava.java | 4 + .../resources/success/pkg/halfacyclic/a/A1.scala | 6 + .../resources/success/pkg/halfacyclic/a/A2.scala | 5 + .../success/pkg/halfacyclic/a/package.scala | 5 + .../resources/success/pkg/halfacyclic/b/B1.scala | 5 + .../resources/success/pkg/halfacyclic/b/B2.scala | 6 + .../resources/success/pkg/innercycle/a/A1.scala | 6 + .../resources/success/pkg/innercycle/a/A2.scala | 6 + .../success/pkg/innercycle/a/package.scala | 6 + .../resources/success/pkg/mutualcyclic/a/A1.scala | 7 + .../resources/success/pkg/mutualcyclic/a/A2.scala | 6 + .../resources/success/pkg/mutualcyclic/b/B1.scala | 6 + .../resources/success/pkg/mutualcyclic/b/B2.scala | 7 + .../resources/success/pkg/single/pkg/package.scala | 5 + .../src/test/resources/success/simple/A.scala | 4 + .../src/test/resources/success/simple/B.scala | 5 + .../src/test/scala/acyclic/CycleTests.scala | 68 ++++++++ .../acyclic/src/test/scala/acyclic/TestUtils.scala | 92 +++++++++++ 60 files changed, 1025 insertions(+) create mode 100644 scalaplugin/src/test/resource/acyclic/build.sbt create mode 100644 scalaplugin/src/test/resource/acyclic/build.sc create mode 100644 scalaplugin/src/test/resource/acyclic/project/build.properties create mode 100644 scalaplugin/src/test/resource/acyclic/project/build.sbt create mode 100644 scalaplugin/src/test/resource/acyclic/src/main/resources/scalac-plugin.xml create mode 100644 scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/package.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/DependencyExtraction.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/GraphAnalysis.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/Plugin.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/PluginPhase.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/A.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/B.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/C.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/D.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/E.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/A1.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/A2.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/package.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/B1.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/B2.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/package.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/A.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/B.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/C1.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/C2.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/package.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/A.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/B.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/C.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/simple/A.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/fail/simple/B.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/force/simple/A.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/force/simple/B.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/force/skip/A.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/force/skip/B.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/cyclicunmarked/A.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/cyclicunmarked/B.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/A.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/B.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/C.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/D.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/E.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/java/SomeJava.java create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/A1.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/A2.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/package.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/b/B1.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/b/B2.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/A1.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/A2.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/package.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/a/A1.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/a/A2.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/b/B1.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/b/B2.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/single/pkg/package.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/simple/A.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/resources/success/simple/B.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/scala/acyclic/CycleTests.scala create mode 100644 scalaplugin/src/test/resource/acyclic/src/test/scala/acyclic/TestUtils.scala (limited to 'scalaplugin/src/test/resource/acyclic') diff --git a/scalaplugin/src/test/resource/acyclic/build.sbt b/scalaplugin/src/test/resource/acyclic/build.sbt new file mode 100644 index 00000000..3fd0f8e4 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/build.sbt @@ -0,0 +1,49 @@ + +organization := "com.lihaoyi" + +name := "acyclic" + +version := "0.1.7" + +scalaVersion := "2.11.8" + +crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0") + +resolvers += Resolver.sonatypeRepo("releases") + +libraryDependencies ++= Seq( + "com.lihaoyi" %% "utest" % "0.4.4" % "test", + "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided" +) + +testFrameworks += new TestFramework("utest.runner.Framework") + +unmanagedSourceDirectories in Test <+= baseDirectory(_ / "src" / "test" / "resources") + +// Sonatype +publishArtifact in Test := false + +publishTo <<= version { (v: String) => + Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2") +} + +pomExtra := ( + https://github.com/lihaoyi/acyclic + + + MIT license + http://www.opensource.org/licenses/mit-license.php + + + + git://github.com/lihaoyi/utest.git + scm:git://github.com/lihaoyi/acyclic.git + + + + lihaoyi + Li Haoyi + https://github.com/lihaoyi + + + ) diff --git a/scalaplugin/src/test/resource/acyclic/build.sc b/scalaplugin/src/test/resource/acyclic/build.sc new file mode 100644 index 00000000..39bbc77a --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/build.sc @@ -0,0 +1,116 @@ + + +val acyclic = + for(crossVersion <- Cross("2.10.6", "2.11.8", "2.12.0")) + yield new ScalaModule{ main => + def organization = "com.lihaoyi" + def name = "acyclic" + def scalaVersion = crossVersion + def version = "0.1.7" + + override def compileIvyDeps = Seq( + Dep.Java("org.scala-lang", "scala-compiler", scalaVersion()) + ) + + object Tests extends Module{ + override def projectDeps = Seq(main) + + override def ivyDeps = Seq( + Dep("com.lihaoyi", "utest", "0.6.0") + ) + def test() = T.command{ + TestRunner.apply( + "mill.UTestFramework", + runDepClasspath().map(_.path) :+ compile().path, + Seq(compile().path) + ) + } + } + } + + + +// +//object acyclic extends crossModule(Seq("2.10.6", "2.11.8", "2.12.0"))(crossVersion => +// new ScalaModule{ +// def organization = "com.lihaoyi" +// def name = "acyclic" +// def scalaVersion = crossVersion +// def version = "0.1.7" +// +// override def compileIvyDeps = Seq( +// Dep.Java("org.scala-lang", "scala-compiler", scalaVersion()) +// ) +// +// object Tests extends Module{ +// override def projectDeps = Seq(Acyclic(scalaVersion)) +// +// override def ivyDeps = Seq( +// Dep("com.lihaoyi", "utest", "0.6.0") +// ) +// def test() = T.command{ +// TestRunner.apply( +// "mill.UTestFramework", +// runDepClasspath().map(_.path) :+ compile().path, +// Seq(compile().path) +// ) +// } +// } +// } +// +//) +// + +// Seq("2.10.6", "2.11.8", "2.12.0") +//case class Acyclic(scalaVersion: String = "2.11.8") extends CrossModule{ +// def organization = "com.lihaoyi" +// def name = "acyclic" +// def version = "0.1.7" +// +// override def compileIvyDeps = Seq( +// Dep.Java("org.scala-lang", "scala-compiler", scalaVersion()) +// ) +//} +// +//case class AcyclicTests(scalaVersion: String = "2.11.8") extends CrossModule{ +// override def projectDeps = Seq(Acyclic(scalaVersion)) +// +// override def ivyDeps = Seq( +// Dep("com.lihaoyi", "utest", "0.6.0") +// ) +// def test() = T.command{ +// TestRunner.apply( +// "mill.UTestFramework", +// runDepClasspath().map(_.path) :+ compile().path, +// Seq(compile().path) +// ) +// } +//} + +//unmanagedSourceDirectories in Test <+= baseDirectory(_ / "src" / "test" / "resources") +// +//// Sonatype +//publishTo <<= version { (v: String) => +// Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2") +//} +// +//pomExtra := ( +// https://github.com/lihaoyi/acyclic +// +// +// MIT license +// http://www.opensource.org/licenses/mit-license.php +// +// +// +// git://github.com/lihaoyi/utest.git +// scm:git://github.com/lihaoyi/acyclic.git +// +// +// +// lihaoyi +// Li Haoyi +// https://github.com/lihaoyi +// +// +// ) diff --git a/scalaplugin/src/test/resource/acyclic/project/build.properties b/scalaplugin/src/test/resource/acyclic/project/build.properties new file mode 100644 index 00000000..817bc38d --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.9 diff --git a/scalaplugin/src/test/resource/acyclic/project/build.sbt b/scalaplugin/src/test/resource/acyclic/project/build.sbt new file mode 100644 index 00000000..7a1f37db --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/project/build.sbt @@ -0,0 +1,2 @@ + +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") \ No newline at end of file diff --git a/scalaplugin/src/test/resource/acyclic/src/main/resources/scalac-plugin.xml b/scalaplugin/src/test/resource/acyclic/src/main/resources/scalac-plugin.xml new file mode 100644 index 00000000..7fd6e95b --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/main/resources/scalac-plugin.xml @@ -0,0 +1,4 @@ + + acyclic + acyclic.plugin.RuntimePlugin + \ No newline at end of file diff --git a/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/package.scala b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/package.scala new file mode 100644 index 00000000..0d656160 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/package.scala @@ -0,0 +1,23 @@ +import scala.reflect.internal.annotations.compileTimeOnly +package object acyclic { + /** + * Import this within a file to make Acyclic verify that the file does not + * have any circular dependencies with other files. + */ + @compileTimeOnly("acyclic.file is just a marker and not a real value") + def file = () + + /** + * + */ + @compileTimeOnly("acyclic.file is just a marker and not a real value") + def skipped = () + + /** + * Import this within a package object to make Acyclic verify that the entire + * package does not have any circular dependencies with other files or + * packages. Circular dependencies *within* the package are Ok. + */ + @compileTimeOnly("acyclic.pkg is just a marker and not a real value") + def pkg = () +} diff --git a/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/DependencyExtraction.scala b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/DependencyExtraction.scala new file mode 100644 index 00000000..46aacc2b --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/DependencyExtraction.scala @@ -0,0 +1,100 @@ +//acyclic +package acyclic.plugin +import acyclic.file +import scala.tools.nsc.Global +object DependencyExtraction{ + def apply(global: Global)(unit: global.CompilationUnit): Seq[(global.Symbol, global.Tree)] = { + import global._ + + class CollectTypeTraverser[T](pf: PartialFunction[Type, T]) extends TypeTraverser { + var collected: List[T] = Nil + def traverse(tpe: Type): Unit = { + if (pf.isDefinedAt(tpe)) + collected = pf(tpe) :: collected + mapOver(tpe) + } + } + + class ExtractDependenciesTraverser extends Traverser { + protected val depBuf = collection.mutable.ArrayBuffer.empty[(Symbol, Tree)] + protected def addDependency(sym: Symbol, tree: Tree): Unit = depBuf += ((sym, tree)) + def dependencies: collection.immutable.Set[(Symbol, Tree)] = { + // convert to immutable set and remove NoSymbol if we have one + depBuf.toSet + } + + } + + class ExtractDependenciesByMemberRefTraverser extends ExtractDependenciesTraverser { + override def traverse(tree: Tree): Unit = { + tree match { + case i @ Import(expr, selectors) => + + selectors.foreach { + case ImportSelector(nme.WILDCARD, _, null, _) => + // in case of wildcard import we do not rely on any particular name being defined + // on `expr`; all symbols that are being used will get caught through selections + case ImportSelector(name: Name, _, _, _) => + def lookupImported(name: Name) = expr.symbol.info.member(name) + // importing a name means importing both a term and a type (if they exist) + addDependency(lookupImported(name.toTermName), tree) + addDependency(lookupImported(name.toTypeName), tree) + } + case select: Select => + addDependency(select.symbol, tree) + /* + * Idents are used in number of situations: + * - to refer to local variable + * - to refer to a top-level package (other packages are nested selections) + * - to refer to a term defined in the same package as an enclosing class; + * this looks fishy, see this thread: + * https://groups.google.com/d/topic/scala-internals/Ms9WUAtokLo/discussion + */ + case ident: Ident => + addDependency(ident.symbol, tree) + case typeTree: TypeTree => + val typeSymbolCollector = new CollectTypeTraverser({ + case tpe if !tpe.typeSymbol.isPackage => tpe.typeSymbol + }) + typeSymbolCollector.traverse(typeTree.tpe) + val deps = typeSymbolCollector.collected.toSet + deps.foreach(addDependency(_, tree)) + case Template(parents, self, body) => + traverseTrees(body) + case other => () + } + super.traverse(tree) + } + } + + def byMembers(): collection.immutable.Set[(Symbol, Tree)] = { + val traverser = new ExtractDependenciesByMemberRefTraverser + if (!unit.isJava) + traverser.traverse(unit.body) + traverser.dependencies + } + + + class ExtractDependenciesByInheritanceTraverser extends ExtractDependenciesTraverser { + override def traverse(tree: Tree): Unit = tree match { + case Template(parents, self, body) => + // we are using typeSymbol and not typeSymbolDirect because we want + // type aliases to be expanded + val parentTypeSymbols = parents.map(parent => parent.tpe.typeSymbol).toSet + debuglog("Parent type symbols for " + tree.pos + ": " + parentTypeSymbols.map(_.fullName)) + parentTypeSymbols.foreach(addDependency(_, tree)) + traverseTrees(body) + case tree => super.traverse(tree) + } + } + + def byInheritence(): collection.immutable.Set[(Symbol, Tree)] = { + val traverser = new ExtractDependenciesByInheritanceTraverser + if (!unit.isJava) + traverser.traverse(unit.body) + traverser.dependencies + } + + (byMembers() | byInheritence()).toSeq + } +} diff --git a/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/GraphAnalysis.scala b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/GraphAnalysis.scala new file mode 100644 index 00000000..bf72ce39 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/GraphAnalysis.scala @@ -0,0 +1,103 @@ +package acyclic.plugin +import acyclic.file +import scala.tools.nsc.Global +import collection.mutable + +sealed trait Value{ + def pkg: List[String] + def prettyPrint: String +} +object Value{ + case class File(path: String, pkg: List[String] = Nil) extends Value{ + def prettyPrint = s"file $path" + } + case class Pkg(pkg: List[String]) extends Value{ + def prettyPrint = s"package ${pkg.mkString(".")}" + } + object Pkg{ + def apply(s: String): Pkg = apply(s.split('.').toList) + } +} + +trait GraphAnalysis{ + val global: Global + import global._ + + case class Node[+T <: Value](value: T, dependencies: Map[Value, Seq[Tree]]){ + override def toString = s"DepNode(\n $value, \n ${dependencies.keys}\n)" + } + + type DepNode = Node[Value] + type FileNode = Node[Value.File] + type PkgNode = Node[Value.Pkg] + + object DepNode{ + /** + * Does a double Breadth-First-Search to find the shortest cycle starting + * from `from` within the DepNodes in `among`. + */ + def smallestCycle(from: DepNode, among: Seq[DepNode]): Seq[DepNode] = { + val nodeMap = among.map(n => n.value -> n).toMap + val distances = mutable.Map(from -> 0) + val queue = mutable.Queue(from) + while(queue.nonEmpty){ + val next = queue.dequeue() + val children = next.dependencies + .keys + .collect(nodeMap) + .filter(!distances.contains(_)) + + children.foreach(distances(_) = distances(next) + 1) + queue.enqueue(children.toSeq:_*) + } + var route = List(from) + while(route.length == 1 || route.head != from){ + route ::= among.filter(x => x.dependencies.keySet.contains(route.head.value)) + .minBy(distances) + } + route.tail + } + + /** + * Finds the strongly-connected components of the directed DepNode graph + * by finding cycles in a Depth-First manner and collapsing any components + * whose nodes are involved in the cycle. + */ + def stronglyConnectedComponents(nodes: Seq[DepNode]): Seq[Seq[DepNode]] = { + + val nodeMap = nodes.map(n => n.value -> n).toMap + + val components = mutable.Map.empty[DepNode, Int] ++ nodes.zipWithIndex.toMap + val visited = mutable.Set.empty[DepNode] + + nodes.foreach(n => rec(n, Nil)) + + def rec(node: DepNode, path: List[DepNode]): Unit = { + if (path.exists(components(_) == components(node))) { + val cycle = path.reverse + .dropWhile(components(_) != components(node)) + + val involved = cycle.map(components) + val firstIndex = involved.head + for ((n, i) <- components.toSeq){ + if (involved.contains(i)){ + components(n) = firstIndex + } + } + } else if (!visited(node)) { + visited.add(node) + // sketchy sorting to make sure we're doing this deterministically... + for((key, lines) <- node.dependencies.toSeq.sortBy(_._1.toString)){ + rec(nodeMap(key), node :: path) + } + } + } + + components.groupBy{case (node, i) => i} + .toSeq + .sortBy(_._1) + .map(_._2.keys.toSeq) + } + } + +} \ No newline at end of file diff --git a/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/Plugin.scala b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/Plugin.scala new file mode 100644 index 00000000..257894c9 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/Plugin.scala @@ -0,0 +1,26 @@ +package acyclic.plugin +import acyclic.file +import tools.nsc.Global +import scala.collection.SortedSet + +class RuntimePlugin(global: Global) extends TestPlugin(global) +class TestPlugin(val global: Global, + cycleReporter: Seq[(Value, SortedSet[Int])] => Unit = _ => ()) + extends tools.nsc.plugins.Plugin { + + val name = "acyclic" + + var force = false + // Yeah processOptions is deprecated but keep using it anyway for 2.10.x compatibility + override def processOptions(options: List[String], error: String => Unit): Unit = { + if (options.contains("force")) { + force = true + } + } + val description = "Allows the developer to prohibit inter-file dependencies" + + + val components = List[tools.nsc.plugins.PluginComponent]( + new PluginPhase(this.global, cycleReporter, force) + ) +} diff --git a/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/PluginPhase.scala b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/PluginPhase.scala new file mode 100644 index 00000000..eaee91a7 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/main/scala/acyclic/plugin/PluginPhase.scala @@ -0,0 +1,180 @@ + +package acyclic.plugin +import acyclic.file +import scala.collection.{SortedSet, mutable} +import scala.tools.nsc.{Global, Phase} +import tools.nsc.plugins.PluginComponent + +/** + * - Break dependency graph into strongly connected components + * - Turn acyclic packages into virtual "files" in the dependency graph, as + * aggregates of all the files within them + * - Any strongly connected component which includes an acyclic.file or + * acyclic.pkg is a failure + * - Pick an arbitrary cycle and report it + * - Don't report more than one cycle per file/pkg, to avoid excessive spam + */ +class PluginPhase(val global: Global, + cycleReporter: Seq[(Value, SortedSet[Int])] => Unit, + force: => Boolean) + extends PluginComponent + with GraphAnalysis { t => + + import global._ + + val runsAfter = List("typer") + + override val runsBefore = List("patmat") + + val phaseName = "acyclic" + def pkgName(unit: CompilationUnit) = { + unit.body + .collect{case x: PackageDef => x.pid.toString} + .flatMap(_.split('.')) + } + + def units = global.currentRun + .units + .toSeq + .sortBy(_.source.content.mkString.hashCode()) + + def findAcyclics() = { + val acyclicNodePaths = for { + unit <- units + if unit.body.children.collect{ + case Import(expr, List(sel)) => + expr.symbol.toString == "package acyclic" && sel.name.toString == "file" + }.exists(x => x) + } yield { + Value.File(unit.source.path, pkgName(unit)) + } + val skipNodePaths = for { + unit <- units + if unit.body.children.collect{ + case Import(expr, List(sel)) => + expr.symbol.toString == "package acyclic" && sel.name.toString == "skipped" + }.exists(x => x) + } yield { + Value.File(unit.source.path, pkgName(unit)) + } + + val acyclicPkgNames = for { + unit <- units + pkgObject <- unit.body.collect{case x: ModuleDef if x.name.toString == "package" => x } + if pkgObject.impl.children.collect{case Import(expr, List(sel)) => + expr.symbol.toString == "package acyclic" && sel.name.toString == "pkg" + }.exists(x => x) + } yield { + Value.Pkg( + pkgObject.symbol + .enclosingPackageClass + .fullName + .split('.') + .toList + ) + } + (skipNodePaths, acyclicNodePaths, acyclicPkgNames) + } + + override def newPhase(prev: Phase): Phase = new Phase(prev) { + override def run() { + val unitMap = units.map(u => u.source.path -> u).toMap + val nodes = for (unit <- units) yield { + + val deps = DependencyExtraction(t.global)(unit) + + val connections = for{ + (sym, tree) <- deps + if sym != NoSymbol + if sym.sourceFile != null + if sym.sourceFile.path != unit.source.path + } yield (sym.sourceFile.path, tree) + + Node[Value.File]( + Value.File(unit.source.path, pkgName(unit)), + connections.groupBy(c => Value.File(c._1, pkgName(unitMap(c._1))): Value) + .mapValues(_.map(_._2)) + ) + } + + val nodeMap = nodes.map(n => n.value -> n).toMap + + val (skipNodePaths, acyclicFiles, acyclicPkgs) = findAcyclics() + + val allAcyclics = acyclicFiles ++ acyclicPkgs + + // synthetic nodes for packages, which aggregate the dependencies of + // their contents + val pkgNodes = acyclicPkgs.map{ value => + Node( + value, + nodes.filter(_.value.pkg.startsWith(value.pkg)) + .flatMap(_.dependencies.toSeq) + .groupBy(_._1) + .mapValues(_.flatMap(_._2)) + ) + } + + val linkedNodes: Seq[DepNode] = (nodes ++ pkgNodes).map{ d => + val extraLinks = for{ + (value: Value.File, pos) <- d.dependencies + acyclicPkg <- acyclicPkgs + if nodeMap(value).value.pkg.startsWith(acyclicPkg.pkg) + if !d.value.pkg.startsWith(acyclicPkg.pkg) + } yield (acyclicPkg, pos) + d.copy(dependencies = d.dependencies ++ extraLinks) + } + + // only care about cycles with size > 1 here + val components = DepNode.stronglyConnectedComponents(linkedNodes) + .filter(_.size > 1) + + val usedNodes = mutable.Set.empty[DepNode] + for{ + c <- components + n <- c + if !usedNodes.contains(n) + if (!force && allAcyclics.contains(n.value)) || (force && !skipNodePaths.contains(n.value)) + }{ + val cycle = DepNode.smallestCycle(n, c) + val cycleInfo = + (cycle :+ cycle.head).sliding(2) + .map{ case Seq(a, b) => (a.value, a.dependencies(b.value))} + .toSeq + cycleReporter( + cycleInfo.map{ case (a, b) => a -> b.map(_.pos.line).to[SortedSet]} + ) + + global.error("Unwanted cyclic dependency") + for (Seq((value, locs), (nextValue, _)) <- (cycleInfo :+ cycleInfo.head).sliding(2)){ + global.inform("") + value match{ + case Value.Pkg(pkg) => global.inform(s"package ${pkg.mkString(".")}") + case Value.File(_, _) => + } + + units.find(_.source.path == locs.head.pos.source.path) + .get + .echo(locs.head.pos, "") + + val otherLines = locs.tail + .map(_.pos.line) + .filter(_ != locs.head.pos.line) + + global.inform("symbol: " + locs.head.symbol.toString) + + if (!otherLines.isEmpty){ + global.inform("More dependencies at lines " + otherLines.mkString(" ")) + } + + } + global.inform("") + usedNodes ++= cycle + } + } + + def name: String = "acyclic" + } + + +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/A.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/A.scala new file mode 100644 index 00000000..a0ff0100 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/A.scala @@ -0,0 +1,6 @@ +package fail.cyclicgraph +import acyclic.file + +class A{ + val e = new E +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/B.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/B.scala new file mode 100644 index 00000000..d1004f5a --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/B.scala @@ -0,0 +1,6 @@ +package fail.cyclicgraph +import acyclic.file + +class B { + val a: A = null +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/C.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/C.scala new file mode 100644 index 00000000..9aebe3a0 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/C.scala @@ -0,0 +1,6 @@ +package fail.cyclicgraph +import acyclic.file + +object C extends A{ + val a: A = null +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/D.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/D.scala new file mode 100644 index 00000000..9c148b0a --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/D.scala @@ -0,0 +1,7 @@ +package fail.cyclicgraph +import acyclic.file + +class D { + val b: A = null + val c = C +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/E.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/E.scala new file mode 100644 index 00000000..00551a06 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicgraph/E.scala @@ -0,0 +1,7 @@ +package fail.cyclicgraph +import acyclic.file + +class E { + val a: A = null + val d = new D +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/A1.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/A1.scala new file mode 100644 index 00000000..530e7820 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/A1.scala @@ -0,0 +1,7 @@ +package fail.cyclicpackage +package a +import acyclic.file + +class A1 extends b.B1{ + +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/A2.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/A2.scala new file mode 100644 index 00000000..95606566 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/A2.scala @@ -0,0 +1,4 @@ +package fail.cyclicpackage.a +class A2 { + +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/package.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/package.scala new file mode 100644 index 00000000..9ee69111 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/a/package.scala @@ -0,0 +1,5 @@ +package fail.cyclicpackage + +package object a { + import acyclic.pkg +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/B1.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/B1.scala new file mode 100644 index 00000000..9b9de725 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/B1.scala @@ -0,0 +1,3 @@ +package fail.cyclicpackage.b +import acyclic.file +class B1 \ No newline at end of file diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/B2.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/B2.scala new file mode 100644 index 00000000..87cabd93 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/B2.scala @@ -0,0 +1,5 @@ +package fail.cyclicpackage +package b +import acyclic.file + +class B2 extends a.A2 \ No newline at end of file diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/package.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/package.scala new file mode 100644 index 00000000..5f6d9041 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/cyclicpackage/b/package.scala @@ -0,0 +1,5 @@ +package fail.cyclicpackage + +package object b { + import acyclic.pkg +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/A.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/A.scala new file mode 100644 index 00000000..d8d118b6 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/A.scala @@ -0,0 +1,5 @@ +package fail.halfpackagecycle + +class A { + val thing = c.C1 +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/B.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/B.scala new file mode 100644 index 00000000..114d6197 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/B.scala @@ -0,0 +1,3 @@ +package fail.halfpackagecycle + +class B extends A diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/C1.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/C1.scala new file mode 100644 index 00000000..be4eecf8 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/C1.scala @@ -0,0 +1,3 @@ +package fail.halfpackagecycle.c + +object C1 diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/C2.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/C2.scala new file mode 100644 index 00000000..be3e0c63 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/C2.scala @@ -0,0 +1,6 @@ +package fail.halfpackagecycle +package c + +class C2 { + lazy val b = new B +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/package.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/package.scala new file mode 100644 index 00000000..295a9e7a --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/halfpackagecycle/c/package.scala @@ -0,0 +1,5 @@ +package fail.halfpackagecycle + +package object c { + import acyclic.pkg +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/A.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/A.scala new file mode 100644 index 00000000..ec4fa106 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/A.scala @@ -0,0 +1,7 @@ +package fail.indirect +import acyclic.file + +object A +class A { + val b: B = null +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/B.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/B.scala new file mode 100644 index 00000000..f9f8450a --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/B.scala @@ -0,0 +1,3 @@ +package fail.indirect + +class B extends C diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/C.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/C.scala new file mode 100644 index 00000000..986baaf3 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/indirect/C.scala @@ -0,0 +1,5 @@ +package fail.indirect + +class C { + val a = A +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/simple/A.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/simple/A.scala new file mode 100644 index 00000000..e1f95ae9 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/simple/A.scala @@ -0,0 +1,7 @@ +package fail.simple +import acyclic.file + + +class A { + val b: B = null +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/simple/B.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/simple/B.scala new file mode 100644 index 00000000..fa9ee63f --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/fail/simple/B.scala @@ -0,0 +1,6 @@ +package fail.simple + +class B { + val a1: A = new A + val a2: A = new A +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/force/simple/A.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/force/simple/A.scala new file mode 100644 index 00000000..24a2a633 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/force/simple/A.scala @@ -0,0 +1,7 @@ +package force.simple + + + +class A { + val b: B = null +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/force/simple/B.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/force/simple/B.scala new file mode 100644 index 00000000..50c5d305 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/force/simple/B.scala @@ -0,0 +1,6 @@ +package force.simple + +class B { + val a1: A = new A + val a2: A = new A +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/force/skip/A.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/force/skip/A.scala new file mode 100644 index 00000000..3f2464cd --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/force/skip/A.scala @@ -0,0 +1,7 @@ +package force.skip +import acyclic.skipped + + +class A { + val b: B = null +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/force/skip/B.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/force/skip/B.scala new file mode 100644 index 00000000..b00c6db2 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/force/skip/B.scala @@ -0,0 +1,6 @@ +package force.skip +import acyclic.skipped +class B { + val a1: A = new A + val a2: A = new A +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/cyclicunmarked/A.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/cyclicunmarked/A.scala new file mode 100644 index 00000000..902ee5fe --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/cyclicunmarked/A.scala @@ -0,0 +1,5 @@ +package success.cyclicunmarked + +class A { + val b: B = null +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/cyclicunmarked/B.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/cyclicunmarked/B.scala new file mode 100644 index 00000000..203707ed --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/cyclicunmarked/B.scala @@ -0,0 +1,6 @@ +package success.cyclicunmarked + +class B { + val a1: A = new A + val a2: A = new A +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/A.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/A.scala new file mode 100644 index 00000000..c9a27490 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/A.scala @@ -0,0 +1,4 @@ +package success.dag + +class A { +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/B.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/B.scala new file mode 100644 index 00000000..3858e677 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/B.scala @@ -0,0 +1,5 @@ +package success.dag + +class B { + val a: A = null +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/C.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/C.scala new file mode 100644 index 00000000..c4635adf --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/C.scala @@ -0,0 +1,3 @@ +package success.dag + +object C extends A diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/D.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/D.scala new file mode 100644 index 00000000..3ab67e39 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/D.scala @@ -0,0 +1,6 @@ +package success.dag + +class D { + val b: A = null + val c = C +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/E.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/E.scala new file mode 100644 index 00000000..4148d75a --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/dag/E.scala @@ -0,0 +1,6 @@ +package success.dag + +class E { + val a: A = null + val d = new D +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/java/SomeJava.java b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/java/SomeJava.java new file mode 100644 index 00000000..cad93696 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/java/SomeJava.java @@ -0,0 +1,4 @@ + +public interface SomeJava { + +} \ No newline at end of file diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/A1.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/A1.scala new file mode 100644 index 00000000..3d5bc5b3 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/A1.scala @@ -0,0 +1,6 @@ +package success.halfacyclicpackage +package a + +class A1 extends b.B1{ + +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/A2.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/A2.scala new file mode 100644 index 00000000..88ee4a03 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/A2.scala @@ -0,0 +1,5 @@ +package success.halfacyclicpackage.a + +class A2 { + + } diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/package.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/package.scala new file mode 100644 index 00000000..54f98aff --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/a/package.scala @@ -0,0 +1,5 @@ +package success.halfacyclicpackage + +package object a { + import acyclic.pkg +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/b/B1.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/b/B1.scala new file mode 100644 index 00000000..074f808a --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/b/B1.scala @@ -0,0 +1,5 @@ +package success.halfacyclicpackage.b + +class B1 { + + } diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/b/B2.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/b/B2.scala new file mode 100644 index 00000000..6e4dfdd5 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/halfacyclic/b/B2.scala @@ -0,0 +1,6 @@ +package success.halfacyclicpackage +package b + +class B2 extends a.A2{ + +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/A1.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/A1.scala new file mode 100644 index 00000000..583e6c68 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/A1.scala @@ -0,0 +1,6 @@ +package success.pkg.innercycle.a + +class A1 { + val x: A2 = null + def y = p +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/A2.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/A2.scala new file mode 100644 index 00000000..65f656a4 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/A2.scala @@ -0,0 +1,6 @@ +package success.pkg.innercycle.a + +class A2 { + val x: A1 = null + def z = p +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/package.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/package.scala new file mode 100644 index 00000000..165fda66 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/innercycle/a/package.scala @@ -0,0 +1,6 @@ +package success.pkg.innercycle + +package object a { + val p: A1 with A2 = null + import acyclic.pkg +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/a/A1.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/a/A1.scala new file mode 100644 index 00000000..3158f120 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/a/A1.scala @@ -0,0 +1,7 @@ +package success.cyclicpackage +package a + + +class A1 extends b.B1{ + +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/a/A2.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/a/A2.scala new file mode 100644 index 00000000..1c36fe2a --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/a/A2.scala @@ -0,0 +1,6 @@ +package success.cyclicpackage.a + + +class A2 { + + } diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/b/B1.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/b/B1.scala new file mode 100644 index 00000000..33e10fc1 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/b/B1.scala @@ -0,0 +1,6 @@ +package success.cyclicpackage.b + + +class B1 { + + } diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/b/B2.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/b/B2.scala new file mode 100644 index 00000000..57e324ce --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/mutualcyclic/b/B2.scala @@ -0,0 +1,7 @@ +package success.cyclicpackage +package b + + +class B2 extends a.A2{ + +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/single/pkg/package.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/single/pkg/package.scala new file mode 100644 index 00000000..c39b5e62 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/pkg/single/pkg/package.scala @@ -0,0 +1,5 @@ +package success.singlepackage + +package object pkg { + import acyclic.pkg +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/simple/A.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/simple/A.scala new file mode 100644 index 00000000..24b9d0d3 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/simple/A.scala @@ -0,0 +1,4 @@ +package success.simple + +class A { +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/resources/success/simple/B.scala b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/simple/B.scala new file mode 100644 index 00000000..b7ca5335 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/resources/success/simple/B.scala @@ -0,0 +1,5 @@ +package success.simple + +class B { + val a: A = null +} diff --git a/scalaplugin/src/test/resource/acyclic/src/test/scala/acyclic/CycleTests.scala b/scalaplugin/src/test/resource/acyclic/src/test/scala/acyclic/CycleTests.scala new file mode 100644 index 00000000..ff831aad --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/scala/acyclic/CycleTests.scala @@ -0,0 +1,68 @@ +package acyclic + +import utest._ +import TestUtils.{make, makeFail} +import scala.tools.nsc.util.ScalaClassLoader.URLClassLoader +import acyclic.plugin.Value.{Pkg, File} +import scala.collection.SortedSet +import acyclic.file + +object CycleTests extends TestSuite{ + + def tests = TestSuite{ + 'fail{ + 'simple-makeFail("fail/simple")(Seq( + File("B.scala") -> SortedSet(4, 5), + File("A.scala") -> SortedSet(6) + )) + + 'indirect-makeFail("fail/indirect")(Seq( + File("A.scala") -> SortedSet(6), + File("B.scala") -> SortedSet(3), + File("C.scala") -> SortedSet(4) + )) + 'cyclicgraph-makeFail("fail/cyclicgraph")( + Seq( + File("A.scala") -> SortedSet(5), + File("E.scala") -> SortedSet(6), + File("D.scala") -> SortedSet(6), + File("C.scala") -> SortedSet(4, 5) + ) + ) + 'cyclicpackage-makeFail("fail/cyclicpackage")( + Seq( + Pkg("fail.cyclicpackage.b") -> SortedSet(5), + Pkg("fail.cyclicpackage.a") -> SortedSet(5) + ) + ) + 'halfpackagecycle-makeFail("fail/halfpackagecycle")(Seq( + File("B.scala") -> SortedSet(3), + File("A.scala") -> SortedSet(4), + Pkg("fail.halfpackagecycle.c") -> SortedSet(5) + )) + } + 'success{ + 'simple-make("success/simple") + 'ignorejava-make("success/java") + 'cyclicunmarked-make("success/cyclicunmarked") + 'dag-make("success/dag") + 'pkg{ + "single" - make("success/pkg/single") + "mutualcyclic" - make("success/pkg/mutualcyclic") + "halfacyclic" - make("success/pkg/halfacyclic") + "innercycle" - make("success/pkg/innercycle") + } + } + 'self-make("../../main/scala", extraIncludes = Nil) + 'force{ + 'fail-makeFail("force/simple", force = true)(Seq( + File("B.scala") -> SortedSet(4, 5), + File("A.scala") -> SortedSet(6) + )) + 'pass-make("force/simple") + 'skip-make("force/skip", force = true) + } + } +} + + diff --git a/scalaplugin/src/test/resource/acyclic/src/test/scala/acyclic/TestUtils.scala b/scalaplugin/src/test/resource/acyclic/src/test/scala/acyclic/TestUtils.scala new file mode 100644 index 00000000..7bff8248 --- /dev/null +++ b/scalaplugin/src/test/resource/acyclic/src/test/scala/acyclic/TestUtils.scala @@ -0,0 +1,92 @@ +package acyclic + +import tools.nsc.{Global, Settings} +import tools.nsc.reporters.ConsoleReporter +import tools.nsc.plugins.Plugin + +import java.net.URLClassLoader +import scala.tools.nsc.util.ClassPath +import utest._, asserts._ +import scala.reflect.io.VirtualDirectory +import acyclic.plugin.Value +import scala.collection.SortedSet + +object TestUtils { + def getFilePaths(src: String): List[String] = { + val f = new java.io.File(src) + if (f.isDirectory) f.list.toList.flatMap(x => getFilePaths(src + "/" + x)) + else List(src) + } + + /** + * Attempts to compile a resource folder as a compilation run, in order + * to test whether it succeeds or fails correctly. + */ + def make(path: String, + extraIncludes: Seq[String] = Seq("src/main/scala/acyclic/package.scala"), + force: Boolean = false) = { + val src = "src/test/resources/" + path + val sources = getFilePaths(src) ++ extraIncludes + + val vd = new VirtualDirectory("(memory)", None) + lazy val settings = new Settings + val loader = getClass.getClassLoader.asInstanceOf[URLClassLoader] + val entries = loader.getURLs map(_.getPath) + settings.outputDirs.setSingleOutput(vd) + + // annoyingly, the Scala library is not in our classpath, so we have to add it manually + val sclpath = entries.map( + _.replaceAll("scala-compiler.jar", "scala-library.jar") + ) + + settings.classpath.value = ClassPath.join(entries ++ sclpath : _*) + + if (force) settings.pluginOptions.value = List("acyclic:force") + + var cycles: Option[Seq[Seq[(acyclic.plugin.Value, SortedSet[Int])]]] = None + lazy val compiler = new Global(settings, new ConsoleReporter(settings)){ + override protected def loadRoughPluginsList(): List[Plugin] = { + List(new plugin.TestPlugin(this, foundCycles => cycles = cycles match{ + case None => Some(Seq(foundCycles)) + case Some(oldCycles) => Some(oldCycles :+ foundCycles) + })) + } + } + val run = new compiler.Run() + run.compile(sources) + + if (vd.toList.isEmpty) throw CompilationException(cycles.get) + } + + def makeFail(path: String, force: Boolean = false)(expected: Seq[(Value, SortedSet[Int])]*) = { + def canonicalize(cycle: Seq[(Value, SortedSet[Int])]): Seq[(Value, SortedSet[Int])] = { + val startIndex = cycle.indexOf(cycle.minBy(_._1.toString)) + cycle.toList.drop(startIndex) ++ cycle.toList.take(startIndex) + } + + val ex = intercept[CompilationException]{ make(path, force = force) } + val cycles = ex.cycles + .map(canonicalize) + .map( + _.map{ + case (Value.File(p, pkg), v) => (Value.File(p, Nil), v) + case x => x + } + ) + .toSet + + def expand(v: Value) = v match{ + case Value.File(filePath, pkg) => Value.File("src/test/resources/" + path + "/" + filePath, Nil) + case v => v + } + + val fullExpected = expected.map(_.map(x => x.copy(_1 = expand(x._1)))) + .map(canonicalize) + .toSet + + assert(fullExpected.forall(cycles.contains)) + } + + case class CompilationException(cycles: Seq[Seq[(Value, SortedSet[Int])]]) extends Exception + +} -- cgit v1.2.3