Repository: lihaoyi/acyclic Branch: main Commit: 7c886c778b7c Files: 81 Total size: 82.1 KB Directory structure: gitextract_xy6ngxcf/ ├── .git-blame-ignore-revs ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── actions.yml ├── .gitignore ├── .mill-jvm-version ├── .mill-version ├── .scalafmt.conf ├── LICENSE ├── acyclic/ │ ├── resources/ │ │ ├── plugin.properties │ │ └── scalac-plugin.xml │ ├── src/ │ │ └── acyclic/ │ │ ├── package.scala │ │ └── plugin/ │ │ ├── BasePluginPhase.scala │ │ └── GraphAnalysis.scala │ ├── src-2/ │ │ └── acyclic/ │ │ └── plugin/ │ │ ├── DependencyExtraction.scala │ │ ├── Plugin.scala │ │ └── PluginPhase.scala │ ├── src-2.11/ │ │ └── acyclic/ │ │ └── plugin/ │ │ └── Compat.scala │ ├── src-2.12/ │ │ └── acyclic/ │ │ └── plugin/ │ │ └── Compat.scala │ ├── src-2.13/ │ │ └── acyclic/ │ │ └── plugin/ │ │ └── Compat.scala │ ├── src-3/ │ │ └── acyclic/ │ │ └── plugin/ │ │ ├── Compat.scala │ │ ├── DependencyExtraction.scala │ │ ├── Plugin.scala │ │ └── PluginPhase.scala │ └── test/ │ ├── resources/ │ │ ├── fail/ │ │ │ ├── cyclicgraph/ │ │ │ │ ├── A.scala │ │ │ │ ├── B.scala │ │ │ │ ├── C.scala │ │ │ │ ├── D.scala │ │ │ │ └── E.scala │ │ │ ├── cyclicpackage/ │ │ │ │ ├── a/ │ │ │ │ │ ├── A1.scala │ │ │ │ │ ├── A2.scala │ │ │ │ │ └── package.scala │ │ │ │ └── b/ │ │ │ │ ├── B1.scala │ │ │ │ ├── B2.scala │ │ │ │ └── package.scala │ │ │ ├── halfpackagecycle/ │ │ │ │ ├── A.scala │ │ │ │ ├── B.scala │ │ │ │ └── c/ │ │ │ │ ├── C1.scala │ │ │ │ ├── C2.scala │ │ │ │ └── package.scala │ │ │ ├── indirect/ │ │ │ │ ├── A.scala │ │ │ │ ├── B.scala │ │ │ │ └── C.scala │ │ │ └── simple/ │ │ │ ├── A.scala │ │ │ └── B.scala │ │ ├── force/ │ │ │ ├── simple/ │ │ │ │ ├── A.scala │ │ │ │ └── B.scala │ │ │ └── skip/ │ │ │ ├── A.scala │ │ │ └── B.scala │ │ └── success/ │ │ ├── cyclicunmarked/ │ │ │ ├── A.scala │ │ │ └── B.scala │ │ ├── dag/ │ │ │ ├── A.scala │ │ │ ├── B.scala │ │ │ ├── C.scala │ │ │ ├── D.scala │ │ │ └── E.scala │ │ ├── java/ │ │ │ └── SomeJava.java │ │ ├── pkg/ │ │ │ ├── halfacyclic/ │ │ │ │ ├── a/ │ │ │ │ │ ├── A1.scala │ │ │ │ │ ├── A2.scala │ │ │ │ │ └── package.scala │ │ │ │ └── b/ │ │ │ │ ├── B1.scala │ │ │ │ └── B2.scala │ │ │ ├── innercycle/ │ │ │ │ └── a/ │ │ │ │ ├── A1.scala │ │ │ │ ├── A2.scala │ │ │ │ └── package.scala │ │ │ ├── mutualcyclic/ │ │ │ │ ├── a/ │ │ │ │ │ ├── A1.scala │ │ │ │ │ └── A2.scala │ │ │ │ └── b/ │ │ │ │ ├── B1.scala │ │ │ │ └── B2.scala │ │ │ └── single/ │ │ │ └── pkg/ │ │ │ └── package.scala │ │ └── simple/ │ │ ├── A.scala │ │ └── B.scala │ ├── src/ │ │ └── acyclic/ │ │ ├── BaseCycleTests.scala │ │ └── BaseTestUtils.scala │ ├── src-2/ │ │ └── acyclic/ │ │ ├── CycleTests.scala │ │ └── TestUtils.scala │ └── src-3/ │ └── acyclic/ │ ├── CycleTests.scala │ └── TestUtils.scala ├── build.mill ├── mill ├── mill.bat └── readme.adoc ================================================ FILE CONTENTS ================================================ ================================================ FILE: .git-blame-ignore-revs ================================================ # Reformatted code base with current scalafmt settings d910117c4c113c5358b0bb7f79be3f2090d55f77 ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/actions.yml ================================================ name: ci on: push: pull_request: branches: - main workflow_dispatch: inputs: scala_version: description: 'Single Scala version to publish' required: true type: string jobs: test: runs-on: ubuntu-latest env: SCALA_VERSION: ${{ inputs.scala_version }} steps: - uses: actions/checkout@v4 - name: Run tests run: ./mill __.publishArtifacts + __.test publish-sonatype: if: github.repository == 'com-lihaoyi/acyclic' && contains(github.ref, 'refs/tags/') needs: test runs-on: ubuntu-latest env: SCALA_VERSION: ${{ inputs.scala_version }} MILL_SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} MILL_SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} MILL_PGP_SECRET_BASE64: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY }} MILL_PGP_PASSPHRASE: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY_PASSWORD }} LANG: "en_US.UTF-8" LC_MESSAGES: "en_US.UTF-8" LC_ALL: "en_US.UTF-8" steps: - uses: actions/checkout@v4 - name: Publish to Maven Central run: ./mill -i mill.scalalib.SonatypeCentralPublishModule/ ================================================ FILE: .gitignore ================================================ out/* target/* project/target/* .idea/* .idea_modules/* *.iml .bsp/ .cursor/ .idea/ ================================================ FILE: .mill-jvm-version ================================================ temurin:17 ================================================ FILE: .mill-version ================================================ 1.0.5-native ================================================ FILE: .scalafmt.conf ================================================ version = "3.8.1" align.preset = none align.openParenCallSite = false align.stripMargin = true assumeStandardLibraryStripMargin = true continuationIndent.callSite = 2 continuationIndent.defnSite = 4 docstrings.style = Asterisk docstrings.oneline = keep docstrings.wrap = no maxColumn = 120 newlines.source = keep runner.dialect = scala213 fileOverride { "glob:**/*.mill" { runner.dialect = scala3 } "glob:**/src-3/**/*.scala" { runner.dialect = scala3 } } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2014 Li Haoyi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: acyclic/resources/plugin.properties ================================================ pluginClass=acyclic.plugin.RuntimePlugin ================================================ FILE: acyclic/resources/scalac-plugin.xml ================================================ acyclic acyclic.plugin.RuntimePlugin ================================================ FILE: acyclic/src/acyclic/package.scala ================================================ import scala.annotation.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 = () } ================================================ FILE: acyclic/src/acyclic/plugin/BasePluginPhase.scala ================================================ package acyclic.plugin import acyclic.plugin.Compat._ import scala.collection.{mutable, SortedSet} trait BasePluginPhase[CompilationUnit, Tree, Symbol] { self: GraphAnalysis[Tree] => protected val cycleReporter: Seq[(Value, SortedSet[Int])] => Unit protected def force: Boolean protected def fatal: Boolean def treeLine(tree: Tree): Int def treeSymbolString(tree: Tree): String def reportError(msg: String): Unit def reportWarning(msg: String): Unit def reportInform(msg: String): Unit def reportEcho(msg: String, tree: Tree): Unit def units: Seq[CompilationUnit] def unitTree(unit: CompilationUnit): Tree def unitPath(unit: CompilationUnit): String def unitPkgName(unit: CompilationUnit): List[String] def findPkgObjects(tree: Tree): List[Tree] def pkgObjectName(pkgObject: Tree): String def hasAcyclicImport(tree: Tree, selector: String): Boolean def extractDependencies(unit: CompilationUnit): Seq[(Symbol, Tree)] def symbolPath(sym: Symbol): String def isValidSymbol(sym: Symbol): Boolean final def findAcyclics(): (Seq[Value.File], Seq[Value.File], Seq[Value.Pkg]) = { val acyclicNodePaths = for { unit <- units if hasAcyclicImport(unitTree(unit), "file") } yield { Value.File(unitPath(unit), unitPkgName(unit)) } val skipNodePaths = for { unit <- units if hasAcyclicImport(unitTree(unit), "skipped") } yield { Value.File(unitPath(unit), unitPkgName(unit)) } val acyclicPkgNames = for { unit <- units pkgObject <- findPkgObjects(unitTree(unit)) if hasAcyclicImport(pkgObject, "pkg") } yield Value.Pkg(pkgObjectName(pkgObject).split('.').toList) (skipNodePaths, acyclicNodePaths, acyclicPkgNames) } final def runAllUnits(): Unit = { val unitMap = units.map(u => unitPath(u) -> u).toMap val nodes = for (unit <- units) yield { val deps = extractDependencies(unit) val connections = for { (sym, tree) <- deps if isValidSymbol(sym) if symbolPath(sym) != unitPath(unit) if unitMap.contains(symbolPath(sym)) } yield (symbolPath(sym), tree) Node[Value.File, Tree]( Value.File(unitPath(unit), unitPkgName(unit)), connections.groupBy(c => Value.File(c._1, unitPkgName(unitMap(c._1))): Value) .mapValues(_.map(_._2)) .toMap ) } 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)) .toMap ) } val linkedNodes: Seq[DepNode] = (nodes ++ pkgNodes).map { d => val extraLinks = d.dependencies.flatMap { case (value: Value.File, pos) => for { acyclicPkg <- acyclicPkgs if nodeMap(value).value.pkg.startsWith(acyclicPkg.pkg) if !d.value.pkg.startsWith(acyclicPkg.pkg) } yield (acyclicPkg, pos) case (_: Value.Pkg, _) => Nil } 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(treeLine).to(SortedSet) } ) val msg = "Unwanted cyclic dependency" if (fatal) { reportError(msg) } else { reportWarning(msg) } for (Seq((value, locs), (nextValue, _)) <- (cycleInfo :+ cycleInfo.head).sliding(2)) { reportInform("") value match { case Value.Pkg(pkg) => reportInform(s"package ${pkg.mkString(".")}") case Value.File(_, _) => } reportEcho("", locs.head) val otherLines = locs.tail .map(treeLine) .filter(_ != treeLine(locs.head)) reportInform("symbol: " + treeSymbolString(locs.head)) if (!otherLines.isEmpty) { reportInform("More dependencies at lines " + otherLines.mkString(" ")) } } reportInform("") usedNodes ++= cycle } } } ================================================ FILE: acyclic/src/acyclic/plugin/GraphAnalysis.scala ================================================ package acyclic.plugin import acyclic.file 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) } } case class Node[+T <: Value, Tree](value: T, dependencies: Map[Value, Seq[Tree]]) { override def toString = s"DepNode(\n $value, \n ${dependencies.keys}\n)" } trait GraphAnalysis[Tree] { type DepNode = Node[Value, Tree] type FileNode = Node[Value.File, Tree] type PkgNode = Node[Value.Pkg, Tree] 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 ++= children } 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) } } } ================================================ FILE: acyclic/src-2/acyclic/plugin/DependencyExtraction.scala ================================================ //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 != null && tpe.typeSymbol != null && !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 } } ================================================ FILE: acyclic/src-2/acyclic/plugin/Plugin.scala ================================================ 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 var fatal = true // 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 } if (options.contains("warn")) { fatal = false } } val description = "Allows the developer to prohibit inter-file dependencies" val components = List[tools.nsc.plugins.PluginComponent]( new PluginPhase(this.global, cycleReporter, force, fatal) ) } ================================================ FILE: acyclic/src-2/acyclic/plugin/PluginPhase.scala ================================================ package acyclic.plugin import acyclic.file import acyclic.plugin.Compat._ 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, fatal: => Boolean ) extends PluginComponent { t => import global._ val runsAfter = List("typer") override val runsBefore = List("patmat") val phaseName = "acyclic" private object base extends BasePluginPhase[CompilationUnit, Tree, Symbol] with GraphAnalysis[Tree] { protected val cycleReporter = t.cycleReporter protected lazy val force = t.force protected lazy val fatal = t.fatal def treeLine(tree: Tree): Int = tree.pos.line def treeSymbolString(tree: Tree): String = tree.symbol.toString def reportError(msg: String): Unit = global.error(msg) def reportWarning(msg: String): Unit = global.warning(msg) def reportInform(msg: String): Unit = global.inform(msg) def reportEcho(msg: String, tree: Tree): Unit = global.reporter.echo(tree.pos, msg) def units: Seq[CompilationUnit] = global.currentRun.units.toSeq.sortBy(_.source.content.mkString.hashCode()) def unitTree(unit: CompilationUnit): Tree = unit.body def unitPath(unit: CompilationUnit): String = unit.source.path def unitPkgName(unit: CompilationUnit): List[String] = unit.body.collect { case x: PackageDef => x.pid.toString }.flatMap(_.split('.')) def findPkgObjects(tree: Tree): List[Tree] = tree.collect { case x: ModuleDef if x.name.toString == "package" => x } def pkgObjectName(pkgObject: Tree): String = pkgObject.symbol.enclosingPackageClass.fullName def hasAcyclicImport(tree: Tree, selector: String): Boolean = tree.collect { case Import(expr, List(sel)) => expr.symbol.toString == "package acyclic" && sel.name.toString == selector }.exists(identity) def extractDependencies(unit: CompilationUnit): Seq[(Symbol, Tree)] = DependencyExtraction(global)(unit) def symbolPath(sym: Symbol): String = sym.sourceFile.path def isValidSymbol(sym: Symbol): Boolean = sym != NoSymbol && sym.sourceFile != null } override def newPhase(prev: Phase): Phase = new Phase(prev) { override def run(): Unit = base.runAllUnits() def name: String = "acyclic" } } ================================================ FILE: acyclic/src-2.11/acyclic/plugin/Compat.scala ================================================ package acyclic.plugin import acyclic.file import scala.collection.{SortedSet, SortedSetLike} import scala.collection.mutable.Builder import scala.collection.generic.{CanBuildFrom, SortedSetFactory} import scala.language.implicitConversions object Compat { // from https://github.com/scala/scala-collection-compat/blob/746a7de28223812b19d0d9f68d2253e0c5f655ca/compat/src/main/scala-2.11_2.12/scala/collection/compat/CompatImpl.scala#L8-L11 private def simpleCBF[A, C](f: => Builder[A, C]): CanBuildFrom[Any, A, C] = new CanBuildFrom[Any, A, C] { def apply(from: Any): Builder[A, C] = apply() def apply(): Builder[A, C] = f } // from https://github.com/scala/scala-collection-compat/blob/746a7de28223812b19d0d9f68d2253e0c5f655ca/compat/src/main/scala-2.11_2.12/scala/collection/compat/PackageShared.scala#L46-L49 implicit def sortedSetCompanionToCBF[A: Ordering, CC[X] <: SortedSet[X] with SortedSetLike[X, CC[X]]]( fact: SortedSetFactory[CC] ): CanBuildFrom[Any, A, CC[A]] = simpleCBF(fact.newBuilder[A]) } ================================================ FILE: acyclic/src-2.12/acyclic/plugin/Compat.scala ================================================ package acyclic.plugin import acyclic.file import scala.collection.{SortedSet, SortedSetLike} import scala.collection.mutable.Builder import scala.collection.generic.{CanBuildFrom, SortedSetFactory} import scala.language.implicitConversions object Compat { // from https://github.com/scala/scala-collection-compat/blob/746a7de28223812b19d0d9f68d2253e0c5f655ca/compat/src/main/scala-2.11_2.12/scala/collection/compat/CompatImpl.scala#L8-L11 private def simpleCBF[A, C](f: => Builder[A, C]): CanBuildFrom[Any, A, C] = new CanBuildFrom[Any, A, C] { def apply(from: Any): Builder[A, C] = apply() def apply(): Builder[A, C] = f } // from https://github.com/scala/scala-collection-compat/blob/746a7de28223812b19d0d9f68d2253e0c5f655ca/compat/src/main/scala-2.11_2.12/scala/collection/compat/PackageShared.scala#L46-L49 implicit def sortedSetCompanionToCBF[A: Ordering, CC[X] <: SortedSet[X] with SortedSetLike[X, CC[X]]]( fact: SortedSetFactory[CC] ): CanBuildFrom[Any, A, CC[A]] = simpleCBF(fact.newBuilder[A]) } ================================================ FILE: acyclic/src-2.13/acyclic/plugin/Compat.scala ================================================ package acyclic.plugin import acyclic.file object Compat ================================================ FILE: acyclic/src-3/acyclic/plugin/Compat.scala ================================================ package acyclic.plugin import acyclic.file object Compat ================================================ FILE: acyclic/src-3/acyclic/plugin/DependencyExtraction.scala ================================================ package acyclic.plugin import acyclic.file import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.{CompilationUnit, report} import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.Symbols.Symbol import dotty.tools.dotc.core.Types.Type object DependencyExtraction { def apply(unit: CompilationUnit)(using Context): Seq[(Symbol, tpd.Tree)] = { class CollectTypeTraverser[T](pf: PartialFunction[Type, T]) extends tpd.TreeAccumulator[List[T]] { def apply(acc: List[T], tree: tpd.Tree)(using Context) = foldOver( if (pf.isDefinedAt(tree.tpe)) pf(tree.tpe) :: acc else acc, tree ) } abstract class ExtractDependenciesTraverser extends tpd.TreeTraverser { protected val depBuf = collection.mutable.ArrayBuffer.empty[(Symbol, tpd.Tree)] protected def addDependency(sym: Symbol, tree: tpd.Tree): Unit = depBuf += ((sym, tree)) def dependencies: collection.immutable.Set[(Symbol, tpd.Tree)] = { // convert to immutable set and remove NoSymbol if we have one depBuf.toSet } } class ExtractDependenciesByMemberRefTraverser extends ExtractDependenciesTraverser { override def traverse(tree: tpd.Tree)(using Context): Unit = { tree match { case i @ tpd.Import(expr, selectors) => selectors.foreach { s => def lookupImported(name: Name) = expr.symbol.info.member(name).symbol if (s.isWildcard) { addDependency(lookupImported(s.name.toTermName), tree) addDependency(lookupImported(s.name.toTypeName), tree) } } case select: tpd.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: tpd.Ident => addDependency(ident.symbol, tree) case typeTree: tpd.TypeTree => val typeSymbolCollector = new CollectTypeTraverser({ case tpe if tpe != null && tpe.typeSymbol != null && !tpe.typeSymbol.is(Flags.Package) => tpe.typeSymbol }) val deps = typeSymbolCollector(Nil, typeTree).toSet deps.foreach(addDependency(_, tree)) case t: tpd.Template => traverse(t.body) case other => () } foldOver((), tree) } } def byMembers(): collection.immutable.Set[(Symbol, tpd.Tree)] = { val traverser = new ExtractDependenciesByMemberRefTraverser if (!unit.isJava) traverser.traverse(unit.tpdTree) traverser.dependencies } class ExtractDependenciesByInheritanceTraverser extends ExtractDependenciesTraverser { override def traverse(tree: tpd.Tree)(using Context): Unit = tree match { case t: tpd.Template => // we are using typeSymbol and not typeSymbolDirect because we want // type aliases to be expanded val parentTypeSymbols = t.parents.map(parent => parent.tpe.typeSymbol).toSet report.debuglog("Parent type symbols for " + tree.sourcePos.show + ": " + parentTypeSymbols.map(_.fullName)) parentTypeSymbols.foreach(addDependency(_, tree)) traverse(t.body) case tree => foldOver((), tree) } } def byInheritence(): collection.immutable.Set[(Symbol, tpd.Tree)] = { val traverser = new ExtractDependenciesByInheritanceTraverser if (!unit.isJava) traverser.traverse(unit.tpdTree) traverser.dependencies } (byMembers() | byInheritence()).toSeq } } ================================================ FILE: acyclic/src-3/acyclic/plugin/Plugin.scala ================================================ package acyclic.plugin import acyclic.file import dotty.tools.dotc.plugins.{PluginPhase, StandardPlugin} import scala.collection.SortedSet import dotty.tools.dotc.core.Contexts.Context class RuntimePlugin extends TestPlugin() class TestPlugin(cycleReporter: Seq[(Value, SortedSet[Int])] => Unit = _ => ()) extends StandardPlugin { val name = "acyclic" val description = "Allows the developer to prohibit inter-file dependencies" var force = false var fatal = true var alreadyRun = false private class Phase() extends PluginPhase { val phaseName = "acyclic" override val runsBefore = Set("patternMatcher") override def run(using Context): Unit = { if (!alreadyRun) { alreadyRun = true new acyclic.plugin.PluginPhase(cycleReporter, force, fatal).run() } } } override def init(options: List[String]): List[PluginPhase] = { if (options.contains("force")) { force = true } if (options.contains("warn")) { fatal = false } List(Phase()) } } ================================================ FILE: acyclic/src-3/acyclic/plugin/PluginPhase.scala ================================================ package acyclic.plugin import acyclic.file import scala.collection.SortedSet import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.{CompilationUnit, report} import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol} import dotty.tools.dotc.util.NoSource /** * - 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( protected val cycleReporter: Seq[(Value, SortedSet[Int])] => Unit, protected val force: Boolean, protected val fatal: Boolean )(using ctx: Context) extends BasePluginPhase[CompilationUnit, tpd.Tree, Symbol], GraphAnalysis[tpd.Tree] { def treeLine(tree: tpd.Tree): Int = tree.sourcePos.line + 1 def treeSymbolString(tree: tpd.Tree): String = tree.symbol.toString def reportError(msg: String): Unit = report.error(msg) def reportWarning(msg: String): Unit = report.warning(msg) def reportInform(msg: String): Unit = report.echo(msg) def reportEcho(msg: String, tree: tpd.Tree): Unit = report.echo(msg, tree.srcPos) private val pkgNameAccumulator = new tpd.TreeAccumulator[List[String]] { @annotation.tailrec private def definitivePackageDef(pkg: tpd.PackageDef): tpd.PackageDef = pkg.stats.collectFirst { case p: tpd.PackageDef => p } match { case Some(p) => definitivePackageDef(p) case None => pkg } def apply(acc: List[String], tree: tpd.Tree)(using Context) = tree match { case p: tpd.PackageDef => definitivePackageDef(p).pid.show :: acc case _ => foldOver(acc, tree) } } private val pkgObjectAccumulator = new tpd.TreeAccumulator[List[tpd.Tree]] { def apply(acc: List[tpd.Tree], tree: tpd.Tree)(using Context): List[tpd.Tree] = foldOver( if (tree.symbol.isPackageObject) tree :: acc else acc, tree ) } private def hasAcyclicImportAccumulator(selector: String) = new tpd.TreeAccumulator[Boolean] { def apply(acc: Boolean, tree: tpd.Tree)(using Context): Boolean = tree match { case tpd.Import(expr, List(sel)) => acc || (expr.symbol.toString == "object acyclic" && sel.name.show == selector) case _ => foldOver(acc, tree) } } lazy val units = Option(ctx.run) match { case Some(run) => run.units.toSeq.sortBy(_.source.content.mkString.hashCode()) case None => Seq() } def unitTree(unit: CompilationUnit): tpd.Tree = unit.tpdTree def unitPath(unit: CompilationUnit): String = unit.source.path def unitPkgName(unit: CompilationUnit): List[String] = pkgNameAccumulator(Nil, unit.tpdTree).reverse.flatMap(_.split('.')) def findPkgObjects(tree: tpd.Tree): List[tpd.Tree] = pkgObjectAccumulator(Nil, tree).reverse def pkgObjectName(pkgObject: tpd.Tree): String = pkgObject.symbol.enclosingPackageClass.fullName.toString def hasAcyclicImport(tree: tpd.Tree, selector: String): Boolean = hasAcyclicImportAccumulator(selector)(false, tree) def extractDependencies(unit: CompilationUnit): Seq[(Symbol, tpd.Tree)] = DependencyExtraction(unit) def symbolPath(sym: Symbol): String = sym.source.path def isValidSymbol(sym: Symbol): Boolean = sym != NoSymbol && sym.source != null && sym.source != NoSource def run(): Unit = runAllUnits() } ================================================ FILE: acyclic/test/resources/fail/cyclicgraph/A.scala ================================================ package fail.cyclicgraph import acyclic.file class A { val e = new E } ================================================ FILE: acyclic/test/resources/fail/cyclicgraph/B.scala ================================================ package fail.cyclicgraph import acyclic.file class B { val a: A = null } ================================================ FILE: acyclic/test/resources/fail/cyclicgraph/C.scala ================================================ package fail.cyclicgraph import acyclic.file object C extends A { val a: A = null } ================================================ FILE: acyclic/test/resources/fail/cyclicgraph/D.scala ================================================ package fail.cyclicgraph import acyclic.file class D { val b: A = null val c = C } ================================================ FILE: acyclic/test/resources/fail/cyclicgraph/E.scala ================================================ package fail.cyclicgraph import acyclic.file class E { val a: A = null val d = new D } ================================================ FILE: acyclic/test/resources/fail/cyclicpackage/a/A1.scala ================================================ package fail.cyclicpackage package a import acyclic.file class A1 extends b.B1 {} ================================================ FILE: acyclic/test/resources/fail/cyclicpackage/a/A2.scala ================================================ package fail.cyclicpackage.a class A2 {} ================================================ FILE: acyclic/test/resources/fail/cyclicpackage/a/package.scala ================================================ package fail.cyclicpackage package object a { import acyclic.pkg } ================================================ FILE: acyclic/test/resources/fail/cyclicpackage/b/B1.scala ================================================ package fail.cyclicpackage.b import acyclic.file class B1 ================================================ FILE: acyclic/test/resources/fail/cyclicpackage/b/B2.scala ================================================ package fail.cyclicpackage package b import acyclic.file class B2 extends a.A2 ================================================ FILE: acyclic/test/resources/fail/cyclicpackage/b/package.scala ================================================ package fail.cyclicpackage package object b { import acyclic.pkg } ================================================ FILE: acyclic/test/resources/fail/halfpackagecycle/A.scala ================================================ package fail.halfpackagecycle class A { val thing = c.C1 } ================================================ FILE: acyclic/test/resources/fail/halfpackagecycle/B.scala ================================================ package fail.halfpackagecycle class B extends A ================================================ FILE: acyclic/test/resources/fail/halfpackagecycle/c/C1.scala ================================================ package fail.halfpackagecycle.c object C1 ================================================ FILE: acyclic/test/resources/fail/halfpackagecycle/c/C2.scala ================================================ package fail.halfpackagecycle package c class C2 { lazy val b = new B } ================================================ FILE: acyclic/test/resources/fail/halfpackagecycle/c/package.scala ================================================ package fail.halfpackagecycle package object c { import acyclic.pkg } ================================================ FILE: acyclic/test/resources/fail/indirect/A.scala ================================================ package fail.indirect import acyclic.file object A class A { val b: B = null } ================================================ FILE: acyclic/test/resources/fail/indirect/B.scala ================================================ package fail.indirect class B extends C ================================================ FILE: acyclic/test/resources/fail/indirect/C.scala ================================================ package fail.indirect class C { val a = A } ================================================ FILE: acyclic/test/resources/fail/simple/A.scala ================================================ package fail.simple import acyclic.file class A { val b: B = null } ================================================ FILE: acyclic/test/resources/fail/simple/B.scala ================================================ package fail.simple class B { val a1: A = new A val a2: A = new A } ================================================ FILE: acyclic/test/resources/force/simple/A.scala ================================================ package force.simple class A { val b: B = null } ================================================ FILE: acyclic/test/resources/force/simple/B.scala ================================================ package force.simple class B { val a1: A = new A val a2: A = new A } ================================================ FILE: acyclic/test/resources/force/skip/A.scala ================================================ package force.skip import acyclic.skipped class A { val b: B = null } ================================================ FILE: acyclic/test/resources/force/skip/B.scala ================================================ package force.skip import acyclic.skipped class B { val a1: A = new A val a2: A = new A } ================================================ FILE: acyclic/test/resources/success/cyclicunmarked/A.scala ================================================ package success.cyclicunmarked class A { val b: B = null } ================================================ FILE: acyclic/test/resources/success/cyclicunmarked/B.scala ================================================ package success.cyclicunmarked class B { val a1: A = new A val a2: A = new A } ================================================ FILE: acyclic/test/resources/success/dag/A.scala ================================================ package success.dag class A {} ================================================ FILE: acyclic/test/resources/success/dag/B.scala ================================================ package success.dag class B { val a: A = null } ================================================ FILE: acyclic/test/resources/success/dag/C.scala ================================================ package success.dag object C extends A ================================================ FILE: acyclic/test/resources/success/dag/D.scala ================================================ package success.dag class D { val b: A = null val c = C } ================================================ FILE: acyclic/test/resources/success/dag/E.scala ================================================ package success.dag class E { val a: A = null val d = new D } ================================================ FILE: acyclic/test/resources/success/java/SomeJava.java ================================================ public interface SomeJava { } ================================================ FILE: acyclic/test/resources/success/pkg/halfacyclic/a/A1.scala ================================================ package success.halfacyclicpackage package a class A1 extends b.B1 {} ================================================ FILE: acyclic/test/resources/success/pkg/halfacyclic/a/A2.scala ================================================ package success.halfacyclicpackage.a class A2 {} ================================================ FILE: acyclic/test/resources/success/pkg/halfacyclic/a/package.scala ================================================ package success.halfacyclicpackage package object a { import acyclic.pkg } ================================================ FILE: acyclic/test/resources/success/pkg/halfacyclic/b/B1.scala ================================================ package success.halfacyclicpackage.b class B1 {} ================================================ FILE: acyclic/test/resources/success/pkg/halfacyclic/b/B2.scala ================================================ package success.halfacyclicpackage package b class B2 extends a.A2 {} ================================================ FILE: acyclic/test/resources/success/pkg/innercycle/a/A1.scala ================================================ package success.pkg.innercycle.a class A1 { val x: A2 = null def y = p } ================================================ FILE: acyclic/test/resources/success/pkg/innercycle/a/A2.scala ================================================ package success.pkg.innercycle.a class A2 { val x: A1 = null def z = p } ================================================ FILE: acyclic/test/resources/success/pkg/innercycle/a/package.scala ================================================ package success.pkg.innercycle package object a { val p: A1 with A2 = null import acyclic.pkg } ================================================ FILE: acyclic/test/resources/success/pkg/mutualcyclic/a/A1.scala ================================================ package success.cyclicpackage package a class A1 extends b.B1 {} ================================================ FILE: acyclic/test/resources/success/pkg/mutualcyclic/a/A2.scala ================================================ package success.cyclicpackage.a class A2 {} ================================================ FILE: acyclic/test/resources/success/pkg/mutualcyclic/b/B1.scala ================================================ package success.cyclicpackage.b class B1 {} ================================================ FILE: acyclic/test/resources/success/pkg/mutualcyclic/b/B2.scala ================================================ package success.cyclicpackage package b class B2 extends a.A2 {} ================================================ FILE: acyclic/test/resources/success/pkg/single/pkg/package.scala ================================================ package success.singlepackage package object pkg { import acyclic.pkg } ================================================ FILE: acyclic/test/resources/success/simple/A.scala ================================================ package success.simple class A {} ================================================ FILE: acyclic/test/resources/success/simple/B.scala ================================================ package success.simple class B { val a: A = null } ================================================ FILE: acyclic/test/src/acyclic/BaseCycleTests.scala ================================================ package acyclic import utest._ import acyclic.plugin.Value.{Pkg, File} import scala.collection.SortedSet import acyclic.file class BaseCycleTests(utils: BaseTestUtils) extends TestSuite { import utils.{make, makeFail, srcDirName} def tests = Tests { test("fail") - { test("simple") - makeFail("fail/simple")(Seq( File("B.scala") -> SortedSet(4, 5), File("A.scala") -> SortedSet(5) )) test("indirect") - makeFail("fail/indirect")(Seq( File("A.scala") -> SortedSet(6), File("B.scala") -> SortedSet(3), File("C.scala") -> SortedSet(4) )) test("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) ) ) test("cyclicpackage") - makeFail("fail/cyclicpackage")( Seq( Pkg("fail.cyclicpackage.b") -> SortedSet(5), Pkg("fail.cyclicpackage.a") -> SortedSet(5) ) ) test("halfpackagecycle") - makeFail("fail/halfpackagecycle")(Seq( File("B.scala") -> SortedSet(3), File("A.scala") -> SortedSet(4), Pkg("fail.halfpackagecycle.c") -> SortedSet(5) )) } test("success") - { test("simple") - make("success/simple") test("ignorejava") - make("success/java") test("cyclicunmarked") - make("success/cyclicunmarked") test("dag") - make("success/dag") test("pkg") { test("single") - make("success/pkg/single") test("mutualcyclic") - make("success/pkg/mutualcyclic") test("halfacyclic") - make("success/pkg/halfacyclic") test("innercycle") - make("success/pkg/innercycle") } } test("self") - make(s"../../$srcDirName", extraIncludes = Nil) test("force") - { test("warn") - { test("fail") - { assert(make("force/simple", force = true, warn = true).exists { case ("Unwanted cyclic dependency", "WARNING") => true case _ => false }) } } test("fail") - makeFail("force/simple", force = true)(Seq( File("B.scala") -> SortedSet(4, 5), File("A.scala") -> SortedSet(4) )) test("pass") - make("force/simple") test("skip") - make("force/skip", force = true) } } } ================================================ FILE: acyclic/test/src/acyclic/BaseTestUtils.scala ================================================ package acyclic import acyclic.plugin.Value import java.util.jar.JarFile import scala.collection.SortedSet abstract class BaseTestUtils { val srcDirName: String val workspaceRoot = sys.env("MILL_WORKSPACE_ROOT") val testResources = sys.env("TEST_ACYCLIC_TEST_RESOURCES") /** * 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("acyclic/src/acyclic/package.scala"), force: Boolean = false, warn: Boolean = false, collectInfo: Boolean = true ): Seq[(String, String)] def makeFail(path: String, force: Boolean = false)(expected: Seq[(Value, SortedSet[Int])]*): Unit case class CompilationException(cycles: Seq[Seq[(Value, SortedSet[Int])]]) extends Exception final 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) } def getJavaClasspathEntries(): Seq[String] = { System.getProperty("java.class.path") .split(java.io.File.pathSeparator) .toIndexedSeq .flatMap { f => // If an (empty) classpath pathing jar is used, we extract the `Class-Path` manifest entry // and those entries to the classpath val extra = if (!f.toLowerCase().endsWith(".jar")) Seq() else for { manifest <- Option(new JarFile(f).getManifest()).toSeq mainAttr <- Option(manifest.getMainAttributes()).toSeq cp <- Option(mainAttr.getValue("Class-Path")).toSeq entry <- cp.split(" ") if entry.nonEmpty } yield entry match { case url if url.startsWith("file:///") => url.substring("file://".length) case url if url.startsWith("file:/") => url.substring("file:".length) case s => s } Seq(f) ++ extra } } } ================================================ FILE: acyclic/test/src-2/acyclic/CycleTests.scala ================================================ package acyclic object CycleTests extends BaseCycleTests(TestUtils) ================================================ FILE: acyclic/test/src-2/acyclic/TestUtils.scala ================================================ package acyclic import tools.nsc.{Global, Settings} import tools.nsc.reporters.{ConsoleReporter, StoreReporter} import tools.nsc.plugins.Plugin import java.net.URLClassLoader import scala.tools.nsc.util.ClassPath import utest._ import asserts._ import scala.reflect.io.VirtualDirectory import acyclic.plugin.Value import java.io.OutputStream import javax.print.attribute.standard.Severity import scala.collection.SortedSet object TestUtils extends BaseTestUtils { val srcDirName: String = "src-2" /** * 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(workspaceRoot + "/acyclic/src/acyclic/package.scala"), force: Boolean = false, warn: Boolean = false, collectInfo: Boolean = true ): Seq[(String, String)] = { val src = testResources + "/" + path val sources = getFilePaths(src) ++ extraIncludes val vd = new VirtualDirectory("(memory)", None) lazy val settings = new Settings val entries = getJavaClasspathEntries() 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: _*) val opts = List( if (force) Seq("force") else Seq(), if (warn) Seq("warn") else Seq() ).flatten if (opts.nonEmpty) { val options = opts.map("acyclic:" + _) println("options: " + options) settings.pluginOptions.value = options } var cycles: Option[Seq[Seq[(acyclic.plugin.Value, SortedSet[Int])]]] = None val storeReporter = if (collectInfo) Some(new StoreReporter()) else None lazy val compiler = new Global(settings, storeReporter.getOrElse(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) storeReporter.map(_.infos.toSeq.map(i => (i.msg, i.severity.toString))).getOrElse(Seq.empty) } 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, collectInfo = false) } 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(testResources + "/" + 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)) } } ================================================ FILE: acyclic/test/src-3/acyclic/CycleTests.scala ================================================ package acyclic object CycleTests extends BaseCycleTests(TestUtils) ================================================ FILE: acyclic/test/src-3/acyclic/TestUtils.scala ================================================ package acyclic import acyclic.plugin.Value import java.io.OutputStream import javax.print.attribute.standard.Severity import scala.collection.SortedSet import dotty.tools.io.{ClassPath, Path, PlainFile, VirtualDirectory} import dotty.tools.dotc.Compiler import dotty.tools.dotc.config.ScalaSettings import dotty.tools.dotc.core.Contexts.{Context, ContextBase, FreshContext, NoContext} import dotty.tools.dotc.interfaces.Diagnostic.{ERROR, INFO, WARNING} import dotty.tools.dotc.plugins.Plugin import dotty.tools.dotc.reporting.{ConsoleReporter, StoreReporter} import java.net.URLClassLoader import java.nio.file.Paths import utest._ import utest.asserts._ object TestUtils extends BaseTestUtils { val srcDirName: String = "src-3" /** * 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(workspaceRoot + "/acyclic/src/acyclic/package.scala"), force: Boolean = false, warn: Boolean = false, collectInfo: Boolean = true ): Seq[(String, String)] = { val src = testResources + "/" + path val sources = (getFilePaths(src) ++ extraIncludes).map(f => PlainFile(Path(Paths.get(f)))) val vd = new VirtualDirectory("(memory)", None) val entries = getJavaClasspathEntries() val scalaSettings = new ScalaSettings {} val settingsState1 = scalaSettings.outputDir.updateIn(scalaSettings.defaultState, vd) val settingsState2 = scalaSettings.classpath.updateIn(settingsState1, ClassPath.join(entries*)) val opts = List( if (force) Seq("force") else Seq(), if (warn) Seq("warn") else Seq() ).flatten val settingsState3 = if (opts.nonEmpty) { val options = opts.map("acyclic:" + _) println("options: " + options) scalaSettings.pluginOptions.updateIn(settingsState2, options) } else { settingsState2 } var cycles: Option[Seq[Seq[(Value, SortedSet[Int])]]] = None val storeReporter = if (collectInfo) Some(new StoreReporter()) else None val ctxBase = new ContextBase { override val initialCtx: Context = FreshContext.initial(NoContext.base, settings) override protected def loadRoughPluginsList(using Context): List[Plugin] = List(new plugin.TestPlugin(foundCycles => cycles = cycles match { case None => Some(Seq(foundCycles)) case Some(oldCycles) => Some(oldCycles :+ foundCycles) } )) } given ctx: Context = FreshContext.initial( ctxBase, new ScalaSettings { override val defaultState = settingsState3 } ) .asInstanceOf[FreshContext] .setReporter(storeReporter.getOrElse(ConsoleReporter())) ctx.initialize() val compiler = new Compiler() val run = compiler.newRun run.compile(sources) if (vd.toList.isEmpty) throw CompilationException(cycles.get) storeReporter.map(_.pendingMessages.toSeq.map(i => ( i.msg.message, i.level match { case ERROR => "ERROR" case INFO => "INFO" case WARNING => "WARNING" } ) )).getOrElse(Seq.empty) } 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, collectInfo = false) } 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(testResources + "/" + 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)) } } ================================================ FILE: build.mill ================================================ package build import mill.* import mill.api.BuildCtx import mill.scalalib.* import publish.* import mill.util.{Version, VcsVersion} object Deps { val scala211: Seq[String] = Seq("2.11.12") val scala212: Seq[String] = 9.to(21).map("2.12." + _) val scala213: Seq[String] = 3.to(18).map("2.13." + _) val scala33: Seq[String] = 0.to(7).map("3.3." + _) val scala34: Seq[String] = 0.to(3).map("3.4." + _) val scala35: Seq[String] = 0.to(2).map("3.5." + _) val scala36: Seq[String] = 0.to(4).map("3.6." + _) val scala37: Seq[String] = 0.to(4).map("3.7." + _) val scala38: Seq[String] = 0.to(3).map("3.8." + _) val unreleased: Seq[String] = scala33 ++ scala34 ++ scala35 ++ scala36 ++ scala37 ++ scala38 def scalaCompiler(scalaVersion: String): Dep = if scalaVersion.startsWith("3.") then mvn"org.scala-lang::scala3-compiler:$scalaVersion" else mvn"org.scala-lang:scala-compiler:$scalaVersion" val utest = mvn"com.lihaoyi::utest:0.8.2" } val crosses: Seq[String] = sys.env.get("SCALA_VERSION") .map(_.trim) .filter(_.nonEmpty) .map(Seq(_)) .getOrElse( Deps.scala211 ++ Deps.scala212 ++ Deps.scala213 ++ Deps.scala33 ++ Deps.scala34 ++ Deps.scala35 ++ Deps.scala36 ++ Deps.scala37 ++ Deps.scala38 ) object acyclic extends Cross[AcyclicModule](crosses) trait AcyclicModule extends CrossScalaModule with SonatypeCentralPublishModule { override def crossFullScalaVersion = true override def artifactName = "acyclic" override def publishVersion: T[String] = VcsVersion.vcsState().format() override def pomSettings: T[PomSettings] = PomSettings( description = artifactName(), organization = "com.lihaoyi", url = "https://github.com/com-lihaoyi/acyclic", licenses = Seq(License.MIT), versionControl = VersionControl.github(owner = "com-lihaoyi", repo = "acyclic"), developers = Seq( Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi") ) ) override def compileMvnDeps: T[Seq[Dep]] = Seq(Deps.scalaCompiler(crossScalaVersion)) override def javacOptions: T[Seq[String]] = Seq( "-source", "8", "-target", "8", "-encoding", "UTF-8" ) override def scalacOptions: T[Seq[String]] = { val sv = Version.parse(crossScalaVersion) implicit val ord = Version.IgnoreQualifierOrdering if (sv.major == 2) Seq("-target:jvm-1.8") else if(!sv.isAtLeast(Version.parse("2.8"))) Seq("-java-output-version", "8") else Seq("-java-output-version", "17") } object test extends ScalaTests with TestModule.Utest { private def customSources: T[Seq[PathRef]] = Task.Sources("resources") override def sources: T[Seq[PathRef]] = Task { super.sources() ++ customSources() } override def mvnDeps: T[Seq[Dep]] = Seq( Deps.utest, Deps.scalaCompiler(crossScalaVersion) ) override def scalacPluginMvnDeps: T[Seq[Dep]] = Seq.empty[Dep] override def forkEnv: T[Map[String, String]] = super.forkEnv() ++ Map( "MILL_WORKSPACE_ROOT" -> BuildCtx.workspaceRoot.toString, "TEST_ACYCLIC_TEST_RESOURCES" -> (moduleDir / "resources").toString ) } } ================================================ FILE: mill ================================================ #!/usr/bin/env sh # This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub release pages. # # This script determines the Mill version to use by trying these sources # - env-variable `MILL_VERSION` # - local file `.mill-version` # - local file `.config/mill-version` # - `mill-version` from YAML frontmatter of current buildfile # - if accessible, find the latest stable version available on Maven Central (https://repo1.maven.org/maven2) # - env-variable `DEFAULT_MILL_VERSION` # # If a version has the suffix '-native' a native binary will be used. # If a version has the suffix '-jvm' an executable jar file will be used, requiring an already installed Java runtime. # If no such suffix is found, the script will pick a default based on version and platform. # # Once a version was determined, it tries to use either # - a system-installed mill, if found and it's version matches # - an already downloaded version under ~/.cache/mill/download # # If no working mill version was found on the system, # this script downloads a binary file from Maven Central or Github Pages (this is version dependent) # into a cache location (~/.cache/mill/download). # # Mill Project URL: https://github.com/com-lihaoyi/mill # Script Version: 1.0.5 # # If you want to improve this script, please also contribute your changes back! # This script was generated from: dist/scripts/src/mill.sh # # Licensed under the Apache License, Version 2.0 set -e if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="0.12.16" fi if [ -z "${GITHUB_RELEASE_CDN}" ] ; then GITHUB_RELEASE_CDN="" fi MILL_REPO_URL="https://github.com/com-lihaoyi/mill" if [ -z "${CURL_CMD}" ] ; then CURL_CMD=curl fi # Explicit commandline argument takes precedence over all other methods if [ "$1" = "--mill-version" ] ; then echo "The --mill-version option is no longer supported." 1>&2 fi MILL_BUILD_SCRIPT="" if [ -f "build.mill" ] ; then MILL_BUILD_SCRIPT="build.mill" elif [ -f "build.mill.scala" ] ; then MILL_BUILD_SCRIPT="build.mill.scala" elif [ -f "build.sc" ] ; then MILL_BUILD_SCRIPT="build.sc" fi # Please note, that if a MILL_VERSION is already set in the environment, # We reuse it's value and skip searching for a value. # If not already set, read .mill-version file if [ -z "${MILL_VERSION}" ] ; then if [ -f ".mill-version" ] ; then MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" elif [ -f ".config/mill-version" ] ; then MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then # `s/.*://`: # This is a greedy match that removes everything from the beginning of the line up to (and including) the last # colon (:). This effectively isolates the value part of the declaration. # # `s/#.*//`: # This removes any comments at the end of the line. # # `s/['\"]//g`: # This removes all single and double quotes from the string, wherever they appear (g is for "global"). # # `s/^[[:space:]]*//; s/[[:space:]]*$//`: # These two expressions trim any leading or trailing whitespace ([[:space:]] matches spaces and tabs). MILL_VERSION="$(grep -E "//\|.*mill-version" "${MILL_BUILD_SCRIPT}" | sed -E "s/.*://; s/#.*//; s/['\"]//g; s/^[[:space:]]*//; s/[[:space:]]*$//")" fi fi MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" if [ -z "${MILL_DOWNLOAD_PATH}" ] ; then MILL_DOWNLOAD_PATH="${MILL_USER_CACHE_DIR}/download" fi # If not already set, try to fetch newest from Github if [ -z "${MILL_VERSION}" ] ; then # TODO: try to load latest version from release page echo "No mill version specified." 1>&2 echo "You should provide a version via a '//| mill-version: ' comment or a '.mill-version' file." 1>&2 mkdir -p "${MILL_DOWNLOAD_PATH}" LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || ( # we might be on OSX or BSD which don't have -d option for touch # but probably a -A [-][[hh]mm]SS touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest" ) || ( # in case we still failed, we retry the first touch command with the intention # to show the (previously suppressed) error message LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" ) # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993 # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then # we know a current latest version MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) fi if [ -z "${MILL_VERSION}" ] ; then # we don't know a current latest version echo "Retrieving latest mill version ..." 1>&2 LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest" MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) fi if [ -z "${MILL_VERSION}" ] ; then # Last resort MILL_VERSION="${DEFAULT_MILL_VERSION}" echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2 else echo "Using mill version ${MILL_VERSION}" 1>&2 fi fi MILL_NATIVE_SUFFIX="-native" MILL_JVM_SUFFIX="-jvm" FULL_MILL_VERSION=$MILL_VERSION ARTIFACT_SUFFIX="" set_artifact_suffix(){ if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" = "Linux" ]; then if [ "$(uname -m)" = "aarch64" ]; then ARTIFACT_SUFFIX="-native-linux-aarch64" else ARTIFACT_SUFFIX="-native-linux-amd64" fi elif [ "$(uname)" = "Darwin" ]; then if [ "$(uname -m)" = "arm64" ]; then ARTIFACT_SUFFIX="-native-mac-aarch64" else ARTIFACT_SUFFIX="-native-mac-amd64" fi else echo "This native mill launcher supports only Linux and macOS." 1>&2 exit 1 fi } case "$MILL_VERSION" in *"$MILL_NATIVE_SUFFIX") MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} set_artifact_suffix ;; *"$MILL_JVM_SUFFIX") MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} ;; *) case "$MILL_VERSION" in 0.1.*) ;; 0.2.*) ;; 0.3.*) ;; 0.4.*) ;; 0.5.*) ;; 0.6.*) ;; 0.7.*) ;; 0.8.*) ;; 0.9.*) ;; 0.10.*) ;; 0.11.*) ;; 0.12.*) ;; *) set_artifact_suffix esac ;; esac MILL="${MILL_DOWNLOAD_PATH}/$MILL_VERSION$ARTIFACT_SUFFIX" try_to_use_system_mill() { if [ "$(uname)" != "Linux" ]; then return 0 fi MILL_IN_PATH="$(command -v mill || true)" if [ -z "${MILL_IN_PATH}" ]; then return 0 fi SYSTEM_MILL_FIRST_TWO_BYTES=$(head --bytes=2 "${MILL_IN_PATH}") if [ "${SYSTEM_MILL_FIRST_TWO_BYTES}" = "#!" ]; then # MILL_IN_PATH is (very likely) a shell script and not the mill # executable, ignore it. return 0 fi SYSTEM_MILL_PATH=$(readlink -e "${MILL_IN_PATH}") SYSTEM_MILL_SIZE=$(stat --format=%s "${SYSTEM_MILL_PATH}") SYSTEM_MILL_MTIME=$(stat --format=%y "${SYSTEM_MILL_PATH}") if [ ! -d "${MILL_USER_CACHE_DIR}" ]; then mkdir -p "${MILL_USER_CACHE_DIR}" fi SYSTEM_MILL_INFO_FILE="${MILL_USER_CACHE_DIR}/system-mill-info" if [ -f "${SYSTEM_MILL_INFO_FILE}" ]; then parseSystemMillInfo() { LINE_NUMBER="${1}" # Select the line number of the SYSTEM_MILL_INFO_FILE, cut the # variable definition in that line in two halves and return # the value, and finally remove the quotes. sed -n "${LINE_NUMBER}p" "${SYSTEM_MILL_INFO_FILE}" |\ cut -d= -f2 |\ sed 's/"\(.*\)"/\1/' } CACHED_SYSTEM_MILL_PATH=$(parseSystemMillInfo 1) CACHED_SYSTEM_MILL_VERSION=$(parseSystemMillInfo 2) CACHED_SYSTEM_MILL_SIZE=$(parseSystemMillInfo 3) CACHED_SYSTEM_MILL_MTIME=$(parseSystemMillInfo 4) if [ "${SYSTEM_MILL_PATH}" = "${CACHED_SYSTEM_MILL_PATH}" ] \ && [ "${SYSTEM_MILL_SIZE}" = "${CACHED_SYSTEM_MILL_SIZE}" ] \ && [ "${SYSTEM_MILL_MTIME}" = "${CACHED_SYSTEM_MILL_MTIME}" ]; then if [ "${CACHED_SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then MILL="${SYSTEM_MILL_PATH}" return 0 else return 0 fi fi fi SYSTEM_MILL_VERSION=$(${SYSTEM_MILL_PATH} --version | head -n1 | sed -n 's/^Mill.*version \(.*\)/\1/p') cat < "${SYSTEM_MILL_INFO_FILE}" CACHED_SYSTEM_MILL_PATH="${SYSTEM_MILL_PATH}" CACHED_SYSTEM_MILL_VERSION="${SYSTEM_MILL_VERSION}" CACHED_SYSTEM_MILL_SIZE="${SYSTEM_MILL_SIZE}" CACHED_SYSTEM_MILL_MTIME="${SYSTEM_MILL_MTIME}" EOF if [ "${SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then MILL="${SYSTEM_MILL_PATH}" fi } try_to_use_system_mill # If not already downloaded, download it if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then case $MILL_VERSION in 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* ) DOWNLOAD_SUFFIX="" DOWNLOAD_FROM_MAVEN=0 ;; 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* ) DOWNLOAD_SUFFIX="-assembly" DOWNLOAD_FROM_MAVEN=0 ;; *) DOWNLOAD_SUFFIX="-assembly" DOWNLOAD_FROM_MAVEN=1 ;; esac case $MILL_VERSION in 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11 ) DOWNLOAD_EXT="jar" ;; 0.12.* ) DOWNLOAD_EXT="exe" ;; 0.* ) DOWNLOAD_EXT="jar" ;; *) DOWNLOAD_EXT="exe" ;; esac DOWNLOAD_FILE=$(mktemp mill.XXXXXX) if [ "$DOWNLOAD_FROM_MAVEN" = "1" ] ; then DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${DOWNLOAD_EXT}" else MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}" unset MILL_VERSION_TAG fi if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then echo $DOWNLOAD_URL echo $MILL exit 0 fi # TODO: handle command not found echo "Downloading mill ${MILL_VERSION} from ${DOWNLOAD_URL} ..." 1>&2 ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" "${DOWNLOAD_URL}" chmod +x "${DOWNLOAD_FILE}" mkdir -p "${MILL_DOWNLOAD_PATH}" mv "${DOWNLOAD_FILE}" "${MILL}" unset DOWNLOAD_FILE unset DOWNLOAD_SUFFIX fi if [ -z "$MILL_MAIN_CLI" ] ; then MILL_MAIN_CLI="${0}" fi MILL_FIRST_ARG="" if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then # Need to preserve the first position of those listed options MILL_FIRST_ARG=$1 shift fi unset MILL_DOWNLOAD_PATH unset MILL_OLD_DOWNLOAD_PATH unset OLD_MILL unset MILL_VERSION unset MILL_REPO_URL # -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 # We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes # shellcheck disable=SC2086 exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" ================================================ FILE: mill.bat ================================================ @echo off rem This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub release pages. rem rem This script determines the Mill version to use by trying these sources rem - env-variable `MILL_VERSION` rem - local file `.mill-version` rem - local file `.config/mill-version` rem - `mill-version` from YAML frontmatter of current buildfile rem - if accessible, find the latest stable version available on Maven Central (https://repo1.maven.org/maven2) rem - env-variable `DEFAULT_MILL_VERSION` rem rem If a version has the suffix '-native' a native binary will be used. rem If a version has the suffix '-jvm' an executable jar file will be used, requiring an already installed Java runtime. rem If no such suffix is found, the script will pick a default based on version and platform. rem rem Once a version was determined, it tries to use either rem - a system-installed mill, if found and it's version matches rem - an already downloaded version under %USERPROFILE%\.mill\download rem rem If no working mill version was found on the system, rem this script downloads a binary file from Maven Central or Github Pages (this is version dependent) rem into a cache location (%USERPROFILE%\.mill\download). rem rem Mill Project URL: https://github.com/com-lihaoyi/mill rem Script Version: 1.0.5 rem rem If you want to improve this script, please also contribute your changes back! rem This script was generated from: dist/scripts/src/mill.bat rem rem Licensed under the Apache License, Version 2.0 rem setlocal seems to be unavailable on Windows 95/98/ME rem but I don't think we need to support them in 2019 setlocal enabledelayedexpansion if [!DEFAULT_MILL_VERSION!]==[] ( set "DEFAULT_MILL_VERSION=1.0.5" ) if [!MILL_GITHUB_RELEASE_CDN!]==[] ( set "MILL_GITHUB_RELEASE_CDN=" ) if [!MILL_MAIN_CLI!]==[] ( set "MILL_MAIN_CLI=%~f0" ) set "MILL_REPO_URL=https://github.com/com-lihaoyi/mill" SET MILL_BUILD_SCRIPT= if exist "build.mill" ( set MILL_BUILD_SCRIPT=build.mill ) else ( if exist "build.mill.scala" ( set MILL_BUILD_SCRIPT=build.mill.scala ) else ( if exist "build.sc" ( set MILL_BUILD_SCRIPT=build.sc ) else ( rem no-op ) ) ) if [!MILL_VERSION!]==[] ( if exist .mill-version ( set /p MILL_VERSION=<.mill-version ) else ( if exist .config\mill-version ( set /p MILL_VERSION=<.config\mill-version ) else ( if not "%MILL_BUILD_SCRIPT%"=="" ( rem Find the line and process it for /f "tokens=*" %%a in ('findstr /R /C:"//\|.*mill-version" "%MILL_BUILD_SCRIPT%"') do ( set "line=%%a" rem --- 1. Replicate sed 's/.*://' --- rem This removes everything up to and including the first colon set "line=!line:*:=!" rem --- 2. Replicate sed 's/#.*//' --- rem Split on '#' and keep the first part for /f "tokens=1 delims=#" %%b in ("!line!") do ( set "line=%%b" ) rem --- 3. Replicate sed 's/['"]//g' --- rem Remove all quotes set "line=!line:'=!" set "line=!line:"=!" rem --- 4. NEW: Replicate sed's trim/space removal --- rem Remove all space characters from the result. This is more robust. set "MILL_VERSION=!line: =!" rem We found the version, so we can exit the loop goto :version_found ) :version_found rem no-op ) else ( rem no-op ) ) ) ) if [!MILL_VERSION!]==[] ( echo No mill version specified. >&2 echo You should provide a version via a '//^| mill-version: ' comment or a '.mill-version' file. >&2 set MILL_VERSION=%DEFAULT_MILL_VERSION% ) if [!MILL_DOWNLOAD_PATH!]==[] set MILL_DOWNLOAD_PATH=%USERPROFILE%\.mill\download rem without bat file extension, cmd doesn't seem to be able to run it set "MILL_NATIVE_SUFFIX=-native" set "MILL_JVM_SUFFIX=-jvm" set "FULL_MILL_VERSION=%MILL_VERSION%" set "MILL_EXT=.bat" set "ARTIFACT_SUFFIX=" REM Check if MILL_VERSION contains MILL_NATIVE_SUFFIX echo !MILL_VERSION! | findstr /C:"%MILL_NATIVE_SUFFIX%" >nul if !errorlevel! equ 0 ( set "MILL_VERSION=%MILL_VERSION:-native=%" REM -native images compiled with graal do not support windows-arm REM https://github.com/oracle/graal/issues/9215 IF /I NOT "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( set "ARTIFACT_SUFFIX=-native-windows-amd64" set "MILL_EXT=.exe" ) else ( rem no-op ) ) else ( echo !MILL_VERSION! | findstr /C:"%MILL_JVM_SUFFIX%" >nul if !errorlevel! equ 0 ( set "MILL_VERSION=%MILL_VERSION:-jvm=%" ) else ( set "SKIP_VERSION=false" set "MILL_PREFIX=%MILL_VERSION:~0,4%" if "!MILL_PREFIX!"=="0.1." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.2." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.3." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.4." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.5." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.6." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.7." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.8." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.9." set "SKIP_VERSION=true" set "MILL_PREFIX=%MILL_VERSION:~0,5%" if "!MILL_PREFIX!"=="0.10." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.11." set "SKIP_VERSION=true" if "!MILL_PREFIX!"=="0.12." set "SKIP_VERSION=true" if "!SKIP_VERSION!"=="false" ( IF /I NOT "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( set "ARTIFACT_SUFFIX=-native-windows-amd64" set "MILL_EXT=.exe" ) ) else ( rem no-op ) ) ) set MILL=%MILL_DOWNLOAD_PATH%\!FULL_MILL_VERSION!!MILL_EXT! set MILL_RESOLVE_DOWNLOAD= if not exist "%MILL%" ( set MILL_RESOLVE_DOWNLOAD=true ) else ( if defined MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT ( set MILL_RESOLVE_DOWNLOAD=true ) else ( rem no-op ) ) if [!MILL_RESOLVE_DOWNLOAD!]==[true] ( set MILL_VERSION_PREFIX=%MILL_VERSION:~0,4% set MILL_SHORT_VERSION_PREFIX=%MILL_VERSION:~0,2% rem Since 0.5.0 set MILL_DOWNLOAD_SUFFIX=-assembly rem Since 0.11.0 set MILL_DOWNLOAD_FROM_MAVEN=1 if [!MILL_VERSION_PREFIX!]==[0.0.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.1.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.2.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.3.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.4.] ( set MILL_DOWNLOAD_SUFFIX= set MILL_DOWNLOAD_FROM_MAVEN=0 ) if [!MILL_VERSION_PREFIX!]==[0.5.] set MILL_DOWNLOAD_FROM_MAVEN=0 if [!MILL_VERSION_PREFIX!]==[0.6.] set MILL_DOWNLOAD_FROM_MAVEN=0 if [!MILL_VERSION_PREFIX!]==[0.7.] set MILL_DOWNLOAD_FROM_MAVEN=0 if [!MILL_VERSION_PREFIX!]==[0.8.] set MILL_DOWNLOAD_FROM_MAVEN=0 if [!MILL_VERSION_PREFIX!]==[0.9.] set MILL_DOWNLOAD_FROM_MAVEN=0 set MILL_VERSION_PREFIX=%MILL_VERSION:~0,5% if [!MILL_VERSION_PREFIX!]==[0.10.] set MILL_DOWNLOAD_FROM_MAVEN=0 set MILL_VERSION_PREFIX=%MILL_VERSION:~0,8% if [!MILL_VERSION_PREFIX!]==[0.11.0-M] set MILL_DOWNLOAD_FROM_MAVEN=0 set MILL_VERSION_PREFIX=%MILL_VERSION:~0,5% set DOWNLOAD_EXT=exe if [!MILL_SHORT_VERSION_PREFIX!]==[0.] set DOWNLOAD_EXT=jar if [!MILL_VERSION_PREFIX!]==[0.12.] set DOWNLOAD_EXT=exe if [!MILL_VERSION!]==[0.12.0] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.1] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.2] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.3] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.4] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.5] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.6] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.7] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.8] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.9] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.10] set DOWNLOAD_EXT=jar if [!MILL_VERSION!]==[0.12.11] set DOWNLOAD_EXT=jar set MILL_VERSION_PREFIX= set MILL_SHORT_VERSION_PREFIX= for /F "delims=- tokens=1" %%A in ("!MILL_VERSION!") do set MILL_VERSION_BASE=%%A set MILL_VERSION_MILESTONE= for /F "delims=- tokens=2" %%A in ("!MILL_VERSION!") do set MILL_VERSION_MILESTONE=%%A set MILL_VERSION_MILESTONE_START=!MILL_VERSION_MILESTONE:~0,1! if [!MILL_VERSION_MILESTONE_START!]==[M] ( set MILL_VERSION_TAG=!MILL_VERSION_BASE!-!MILL_VERSION_MILESTONE! ) else ( set MILL_VERSION_TAG=!MILL_VERSION_BASE! ) if [!MILL_DOWNLOAD_FROM_MAVEN!]==[1] ( set MILL_DOWNLOAD_URL=https://repo1.maven.org/maven2/com/lihaoyi/mill-dist!ARTIFACT_SUFFIX!/!MILL_VERSION!/mill-dist!ARTIFACT_SUFFIX!-!MILL_VERSION!.!DOWNLOAD_EXT! ) else ( set MILL_DOWNLOAD_URL=!MILL_GITHUB_RELEASE_CDN!%MILL_REPO_URL%/releases/download/!MILL_VERSION_TAG!/!MILL_VERSION!!MILL_DOWNLOAD_SUFFIX! ) if defined MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT ( echo !MILL_DOWNLOAD_URL! echo !MILL! exit /b 0 ) rem there seems to be no way to generate a unique temporary file path (on native Windows) set MILL_DOWNLOAD_FILE=%MILL%.tmp echo Downloading mill !MILL_VERSION! from !MILL_DOWNLOAD_URL! ... 1>&2 if not exist "%MILL_DOWNLOAD_PATH%" mkdir "%MILL_DOWNLOAD_PATH%" rem curl is bundled with recent Windows 10 rem but I don't think we can expect all the users to have it in 2019 where /Q curl if !ERRORLEVEL! EQU 0 ( curl -f -L "!MILL_DOWNLOAD_URL!" -o "!MILL_DOWNLOAD_FILE!" ) else ( rem bitsadmin seems to be available on Windows 7 rem without /dynamic, github returns 403 rem bitsadmin is sometimes needlessly slow but it looks better with /priority foreground bitsadmin /transfer millDownloadJob /dynamic /priority foreground "!MILL_DOWNLOAD_URL!" "!MILL_DOWNLOAD_FILE!" ) if not exist "!MILL_DOWNLOAD_FILE!" ( echo Could not download mill !MILL_VERSION! 1>&2 exit /b 1 ) move /y "!MILL_DOWNLOAD_FILE!" "%MILL%" set MILL_DOWNLOAD_FILE= set MILL_DOWNLOAD_SUFFIX= ) set MILL_DOWNLOAD_PATH= set MILL_VERSION= set MILL_REPO_URL= rem Need to preserve the first position of those listed options set MILL_FIRST_ARG= if [%~1%]==[--bsp] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[-i] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[--interactive] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[--no-server] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[--no-daemon] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[--repl] ( set MILL_FIRST_ARG=%1% ) else ( if [%~1%]==[--help] ( set MILL_FIRST_ARG=%1% ) ) ) ) ) ) ) set "MILL_PARAMS=%*%" if not [!MILL_FIRST_ARG!]==[] ( for /f "tokens=1*" %%a in ("%*") do ( set "MILL_PARAMS=%%b" ) ) rem -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 "%MILL%" %MILL_FIRST_ARG% -D "mill.main.cli=%MILL_MAIN_CLI%" %MILL_PARAMS% ================================================ FILE: readme.adoc ================================================ = Acyclic :version: 0.3.21 :toc-placement: preamble :toc: :link-acyclic: https://github.com/com-lihaoyi/acyclic :link-acyclic-gitter: https://gitter.im/lihaoyi/acyclic :link-utest: https://github.com/com-lihaoyi/utest :link-scalatags: https://github.com/com-lihaoyi/scalatags :link-scalarx: https://github.com/lihaoyi/scala.rx image:https://badges.gitter.im/Join%20Chat.svg["Join the chat", link="{link-acyclic-gitter}?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] *Acyclic* is a Scala compiler plugin that allows you to mark files within a build as `acyclic`, turning circular dependencies between files into compilation errors. == Introduction *Acyclic* is a Scala compiler plugin that allows you to mark files within a build as `acyclic`, turning circular dependencies between files into compilation errors. For example, the following two files have a circular dependency between them: [source,scala] ---- package fail.simple class A { val b: B = null } ---- [source,scala] ---- package fail.simple class B { val a: A = null } ---- In this case it is very obvious that there is a circular dependency, but in larger projects the fact that a circular dependency exists can be difficult to spot. With *Acyclic*, you can annotate either source file with an `acyclic` import: [source,scala] ---- package fail.simple import acyclic.file class A { val b: B = null } ---- And attempting to compile these files together will then result in a compilation error: [source,scala] ---- error: Unwanted cyclic dependency src/test/resources/fail/simple/B.scala:4: val a1: A = new A ^ symbol: class A More dependencies at lines 5 src/test/resources/fail/simple/A.scala:6: val b: B = null ^ symbol: class B ---- This applies to term-dependencies, type-dependencies, as well as cycles that span more than two files. Circular dependencies between files is something that people often don't want, but are difficult to avoid as introducing cycles is hard to detect while working or during code review. *Acyclic* is designed to help you guard against unwanted cycles at compile-time, and tells you exactly where the cycles are when they appear so you can deal with them. A more realistic example of a cycle that *Acyclic* may find is this one taken from a cycle in {link-utest}[uTest]: [source,scala] ---- [error] Circular dependency between acyclic files: [info] /Users/haoyi/Dropbox (Personal)/Workspace/utest/shared/main/scala/utest/Formatter.scala:58: acyclic [info] val traceStr = r.value match{ [info] ^ [info] Other dependencies at lines: 20, 54, 44, 66, 40, 15, 67, 2, 45, 42 [info] /Users/haoyi/Dropbox (Personal)/Workspace/utest/shared/main/scala/utest/framework/Model.scala:76: [info] v.runAsync(onComplete, path :+ i, strPath :+ v.value.name, thisError) [info] ^ [info] Other dependencies at lines: 120 [info] /Users/haoyi/Dropbox (Personal)/Workspace/utest/shared/main/scala/utest/package.scala:72: [info] val TestSuite = framework.TestSuite [info] ^ [info] Other dependencies at lines: 73 [info] /Users/haoyi/Dropbox (Personal)/Workspace/utest/shared/main/scala/utest/framework/TestSuite.scala:37: [info] log(formatter.formatSingle(path, s)) [info] ^ [info] Other dependencies at lines: 41, 33 ---- As you can see, there is a dependency cycle between `Formatter.scala`, `Model.scala`, `package.scala` and `TestSuite.scala`. `package.scala` has been explicitly marked `acyclic`, and so compilation fails with an error. Apart from the line shown, *Acyclic* also gives other lines in the same file which contain dependencies contributing to this cycle. Spotting this dependency cycle spanning 4 different files, and knowing exactly which pieces of code are causing it, is something that is virtually impossible to do manually via inspection or code-review. Using *Acyclic*, there is no chance of accidentally introducing a dependency cycle you don't want, and even when you do, it shows you exactly what's causing the cycle that you need to fix to make it go away. == Package Cycles *Acyclic* also allows you to annotate entire packages as `acyclic` by placing a `import acyclic.pkg` inside the package object. Consider the following set of files: [source,scala] ---- // c/C1.scala package fail.halfpackagecycle.c object C1 ---- [source,scala] ---- // c/C2.scala package fail.halfpackagecycle package c class C2 { lazy val b = new B } ---- [source,scala] ---- // c/package.scala package fail.halfpackagecycle package object c { import acyclic.pkg } ---- [source,scala] ---- // A.scala package fail.halfpackagecycle class A { val thing = c.C1 } ---- [source,scala] ---- // B.scala package fail.halfpackagecycle class B extends A ---- These 5 files do not have any file-level cycles, and form a nice linear dependency chain: ---- c/C2.scala -> B.scala -> A.scala -> c/C1.scala ---- However, we may want to preserve the invariant that the package `c` does not have any cyclic dependencies with other packages or files.. By annotating the package with `import acyclic.pkg` in its package objects as shown above, we can make this circular package dependency error out: [source,scala] ---- error: Unwanted cyclic dependency src/test/resources/fail/halfpackagecycle/B.scala:3: class B extends A ^ symbol: constructor A src/test/resources/fail/halfpackagecycle/A.scala:4: val thing = c.C1 ^ symbol: object C1 package fail.halfpackagecycle.c src/test/resources/fail/halfpackagecycle/c/C2.scala:5: lazy val b = new B ^ symbol: class B ---- Since, `c` as a whole must be acyclic, the dependency cycle between `c`, `B.scala` and `A.scala` is prohibited, and *Acyclic* errors out. As you can see, it tells you exactly where the dependencies are in the source files, giving you an opportunity to find and remove them. Here's a realistic example from Scala.Rx: [source,scala] ---- [error] Unwanted cyclic dependency [info] [info] /Users/haoyi/Dropbox (Personal)/Workspace/scala.rx/shared/main/scala/rx/core/Dynamic.scala:10: [info] import rx.ops.Spinlock [info] ^ [info] symbol: value [info] More dependencies at lines 29 60 33 41 27 23 [info] [info] package rx.ops [info] /Users/haoyi/Dropbox (Personal)/Workspace/scala.rx/shared/main/scala/rx/ops/Async.scala:78: [info] super.ping(incoming) [info] ^ [info] symbol: method ping [info] More dependencies at lines 69 101 97 95 67 ---- As you can see, `Dynamic.scala` in `rx.core` was accidentally depending on `Spinlock` in `rx.ops`. That cross-module dependency from `rx.core` to `rx.ops` should not exist, and the proper solution was to move `Spinlock` over to `rx.core`. Without *Acyclic*, this circular dependency would likely have gone un-noticed. == How to Use === Mill For Mill, use the following: [source,scala,subs="attributes,verbatim"] ---- def compileIvyDeps = Agg(ivy"com.lihaoyi:::acyclic:{version}") def scalacPluginIvyDeps = Agg(ivy"com.lihaoyi:::acyclic:{version}") ---- === sbt To use, add the following to your `build.sbt`: [source,scala,subs="attributes,verbatim"] ---- libraryDependencies += ("com.lihaoyi" %% "acyclic" % "{version}" cross (CrossVersion.full)) % "provided" autoCompilerPlugins := true addCompilerPlugin("com.lihaoyi" %% "acyclic" % "{version}" cross (CrossVersion.full)) ---- === Force If you want to enforce acyclicity across _all_ your files, you can pass in the command-line compiler flag: ---- -P:acyclic:force ---- Or via SBT: [source,scala] ---- scalacOptions += "-P:acyclic:force" ---- This will make the acyclic plugin complain if _any_ file in your project is involved in an import cycle, without needing to annotate everything with `import acyclic.file`. If you want to white-list a small number of files whose cycles you've decided are OK, you can use [source,scala] ---- import acyclic.skipped ---- to tell the acyclic plugin to ignore them. === Warnings instead of errors If you want the plugin to only emit warnings instead of errors, add `warn` to the plugin's flags. [source,scala] ---- scalacOptions += "-P:acyclic:warn" ---- == Who uses it? *Acyclic* is currently being used in {link-utest}[uTest], {link-scalatags}[Scalatags] and {link-scalarx}[Scala.Rx], and helped remove many cycle between files which had no good reason for being cyclic. It is also being used to verify the acyclicity of {link-acyclic}/blob/main/acyclic/src/acyclic/plugin/PluginPhase.scala[its own code]. It works with Scala 2.11, 2.12 and 2.13. If you're using incremental compilation, you may need to do a clean compile for *Acyclic* to find all unwanted cycles in the compilation run. == Limitations Acyclic has problems in a number of cases: * If you use curly-braced `package XXX {}` acyclic inside your source files, it does the wrong thing. Acyclic assumes all packages are listed in a sequence of statements at the top of each file * Under incremental compilation, Acyclic does not always find all possible cycles, since one cycles within the files currently getting compiled will get caught. A solution is to do a clean build every once in a while. == License *_Acyclic* is published under the MIT License:_ The MIT License (MIT) Copyright (c) 2014 Li Haoyi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. == ChangeLog === `main` branch * Add support for Scala 3.8.3 === 0.3.21 * Add support for Scala 2.12.21, 3.3.7, 3.8.0, 3.8.1, 3.8.2 * (2026-03-31) Back-published support for Scala 3.8.3 === 0.3.20 * Added support for Scala 2.13.18 and 3.7.4 * Back-published for Scala 2.12.21 === 0.3.19 * Added support for Scala 2.13.17, 3.3.4, 3.3.5, 3.3.6, 3.7.0, 3.7.1, 3.7.2, 3.7.3 === 0.3.18 * Added support for Scala 3.6.4 === 0.3.17 * Added support for Scala 3.6.x === 0.3.16 * Added support for Scala 2.13.16 === 0.3.15 * Scala 3.x support https://github.com/com-lihaoyi/acyclic/pull/136[#136] === 0.3.14 * Added support for Scala 2.13.15 === 0.3.13 * Added support for Scala 2.12.20 === 0.3.12 * Added support for Scala 2.13.14 === 0.3.11 * Added support for Scala 2.13.13 === 0.3.10 * Added suport for Scala 2.12.19 === 0.3.9 * Added support for Scala 2.13.12 === 0.3.8 * Added support for Scala 2.13.11 === 0.3.7 * Added support for Scala 2.12.18 === 0.3.6 * Added support for Scala 2.13.10 === 0.3.5 * Added support for Scala 2.13.9 === 0.3.4 * Added support for Scala 2.12.17 === 0.3.3 * Added support for Scala 2.12.16 === 0.3.2 * Added plugin option `warn` to emit compiler warnings instead of errors === 0.3.1 * Support for Scala 2.13.8 === 0.3.0 * Cross-build across all scala point versions === 0.2.0 * Support for Scala 2.13.0 final === 0.1.7 * Fix `import acyclic.skipped`, which was broken in 0.1.6 === 0.1.6 * You can now use the scalac option `-P:acyclic:force` (`scalaOptions += "-P:acyclic:force"` in SBT) to enforce acyclicity across your entire codebase. === 0.1.5 * Scala 2.12.x support === 0.1.4 * Loosen restrictions on compiler plugin placement, to allow better interactions with other plugins. Also, `acyclic.file` is now `@compileTimeOnly` to provide better errors === 0.1.3 * Ignore, but don't crash, on Java sources