Full Code of lihaoyi/acyclic for AI

main 7c886c778b7c cached
81 files
82.1 KB
25.7k tokens
1 symbols
1 requests
Download .txt
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
================================================
<plugin>
    <name>acyclic</name>
    <classname>acyclic.plugin.RuntimePlugin</classname>
</plugin>

================================================
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 <<EOF > "${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 <import>
[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

Download .txt
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
Download .txt
SYMBOL INDEX (1 symbols across 1 files)

FILE: acyclic/test/resources/success/java/SomeJava.java
  type SomeJava (line 2) | public interface SomeJava {
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (94K chars).
[
  {
    "path": ".git-blame-ignore-revs",
    "chars": 97,
    "preview": "# Reformatted code base with current scalafmt settings\nd910117c4c113c5358b0bb7f79be3f2090d55f77\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 119,
    "preview": "version: 2\nupdates:\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/actions.yml",
    "chars": 1158,
    "preview": "name: ci\n\non:\n  push:\n  pull_request:\n    branches:\n      - main\n  workflow_dispatch:\n    inputs:\n      scala_version:\n "
  },
  {
    "path": ".gitignore",
    "chars": 84,
    "preview": "out/*\ntarget/*\nproject/target/*\n.idea/*\n.idea_modules/*\n*.iml\n.bsp/\n.cursor/\n.idea/\n"
  },
  {
    "path": ".mill-jvm-version",
    "chars": 11,
    "preview": "temurin:17\n"
  },
  {
    "path": ".mill-version",
    "chars": 13,
    "preview": "1.0.5-native\n"
  },
  {
    "path": ".scalafmt.conf",
    "chars": 480,
    "preview": "version = \"3.8.1\"\n\nalign.preset = none\nalign.openParenCallSite = false\nalign.stripMargin = true\n\nassumeStandardLibrarySt"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2014 Li Haoyi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "acyclic/resources/plugin.properties",
    "chars": 41,
    "preview": "pluginClass=acyclic.plugin.RuntimePlugin\n"
  },
  {
    "path": "acyclic/resources/scalac-plugin.xml",
    "chars": 99,
    "preview": "<plugin>\n    <name>acyclic</name>\n    <classname>acyclic.plugin.RuntimePlugin</classname>\n</plugin>"
  },
  {
    "path": "acyclic/src/acyclic/package.scala",
    "chars": 724,
    "preview": "import scala.annotation.compileTimeOnly\npackage object acyclic {\n\n  /**\n   * Import this within a file to make Acyclic v"
  },
  {
    "path": "acyclic/src/acyclic/plugin/BasePluginPhase.scala",
    "chars": 4859,
    "preview": "package acyclic.plugin\n\nimport acyclic.plugin.Compat._\nimport scala.collection.{mutable, SortedSet}\n\ntrait BasePluginPha"
  },
  {
    "path": "acyclic/src/acyclic/plugin/GraphAnalysis.scala",
    "chars": 3081,
    "preview": "package acyclic.plugin\n\nimport acyclic.file\nimport collection.mutable\n\nsealed trait Value {\n  def pkg: List[String]\n  de"
  },
  {
    "path": "acyclic/src-2/acyclic/plugin/DependencyExtraction.scala",
    "chars": 4097,
    "preview": "//acyclic\npackage acyclic.plugin\nimport acyclic.file\nimport scala.tools.nsc.Global\nobject DependencyExtraction {\n  def a"
  },
  {
    "path": "acyclic/src-2/acyclic/plugin/Plugin.scala",
    "chars": 880,
    "preview": "package acyclic.plugin\nimport acyclic.file\nimport tools.nsc.Global\nimport scala.collection.SortedSet\n\nclass RuntimePlugi"
  },
  {
    "path": "acyclic/src-2/acyclic/plugin/PluginPhase.scala",
    "chars": 2792,
    "preview": "package acyclic.plugin\n\nimport acyclic.file\nimport acyclic.plugin.Compat._\nimport scala.collection.{SortedSet, mutable}\n"
  },
  {
    "path": "acyclic/src-2.11/acyclic/plugin/Compat.scala",
    "chars": 1043,
    "preview": "package acyclic.plugin\n\nimport acyclic.file\n\nimport scala.collection.{SortedSet, SortedSetLike}\nimport scala.collection."
  },
  {
    "path": "acyclic/src-2.12/acyclic/plugin/Compat.scala",
    "chars": 1043,
    "preview": "package acyclic.plugin\n\nimport acyclic.file\n\nimport scala.collection.{SortedSet, SortedSetLike}\nimport scala.collection."
  },
  {
    "path": "acyclic/src-2.13/acyclic/plugin/Compat.scala",
    "chars": 59,
    "preview": "package acyclic.plugin\n\nimport acyclic.file\n\nobject Compat\n"
  },
  {
    "path": "acyclic/src-3/acyclic/plugin/Compat.scala",
    "chars": 59,
    "preview": "package acyclic.plugin\n\nimport acyclic.file\n\nobject Compat\n"
  },
  {
    "path": "acyclic/src-3/acyclic/plugin/DependencyExtraction.scala",
    "chars": 4018,
    "preview": "package acyclic.plugin\n\nimport acyclic.file\nimport dotty.tools.dotc.ast.tpd\nimport dotty.tools.dotc.{CompilationUnit, re"
  },
  {
    "path": "acyclic/src-3/acyclic/plugin/Plugin.scala",
    "chars": 1035,
    "preview": "package acyclic.plugin\n\nimport acyclic.file\nimport dotty.tools.dotc.plugins.{PluginPhase, StandardPlugin}\nimport scala.c"
  },
  {
    "path": "acyclic/src-3/acyclic/plugin/PluginPhase.scala",
    "chars": 3561,
    "preview": "package acyclic.plugin\n\nimport acyclic.file\nimport scala.collection.SortedSet\nimport dotty.tools.dotc.ast.tpd\nimport dot"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicgraph/A.scala",
    "chars": 74,
    "preview": "package fail.cyclicgraph\nimport acyclic.file\n\nclass A {\n  val e = new E\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicgraph/B.scala",
    "chars": 76,
    "preview": "package fail.cyclicgraph\nimport acyclic.file\n\nclass B {\n  val a: A = null\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicgraph/C.scala",
    "chars": 87,
    "preview": "package fail.cyclicgraph\nimport acyclic.file\n\nobject C extends A {\n  val a: A = null\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicgraph/D.scala",
    "chars": 88,
    "preview": "package fail.cyclicgraph\nimport acyclic.file\n\nclass D {\n  val b: A = null\n  val c = C\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicgraph/E.scala",
    "chars": 92,
    "preview": "package fail.cyclicgraph\nimport acyclic.file\n\nclass E {\n  val a: A = null\n  val d = new D\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicpackage/a/A1.scala",
    "chars": 83,
    "preview": "package fail.cyclicpackage\npackage a\nimport acyclic.file\n\nclass A1 extends b.B1 {}\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicpackage/a/A2.scala",
    "chars": 41,
    "preview": "package fail.cyclicpackage.a\nclass A2 {}\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicpackage/a/package.scala",
    "chars": 70,
    "preview": "package fail.cyclicpackage\n\npackage object a {\n  import acyclic.pkg\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicpackage/b/B1.scala",
    "chars": 58,
    "preview": "package fail.cyclicpackage.b\nimport acyclic.file\nclass B1\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicpackage/b/B2.scala",
    "chars": 80,
    "preview": "package fail.cyclicpackage\npackage b\nimport acyclic.file\n\nclass B2 extends a.A2\n"
  },
  {
    "path": "acyclic/test/resources/fail/cyclicpackage/b/package.scala",
    "chars": 70,
    "preview": "package fail.cyclicpackage\n\npackage object b {\n  import acyclic.pkg\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/halfpackagecycle/A.scala",
    "chars": 62,
    "preview": "package fail.halfpackagecycle\n\nclass A {\n  val thing = c.C1\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/halfpackagecycle/B.scala",
    "chars": 49,
    "preview": "package fail.halfpackagecycle\n\nclass B extends A\n"
  },
  {
    "path": "acyclic/test/resources/fail/halfpackagecycle/c/C1.scala",
    "chars": 43,
    "preview": "package fail.halfpackagecycle.c\n\nobject C1\n"
  },
  {
    "path": "acyclic/test/resources/fail/halfpackagecycle/c/C2.scala",
    "chars": 75,
    "preview": "package fail.halfpackagecycle\npackage c\n\nclass C2 {\n  lazy val b = new B\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/halfpackagecycle/c/package.scala",
    "chars": 73,
    "preview": "package fail.halfpackagecycle\n\npackage object c {\n  import acyclic.pkg\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/indirect/A.scala",
    "chars": 82,
    "preview": "package fail.indirect\nimport acyclic.file\n\nobject A\nclass A {\n  val b: B = null\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/indirect/B.scala",
    "chars": 41,
    "preview": "package fail.indirect\n\nclass B extends C\n"
  },
  {
    "path": "acyclic/test/resources/fail/indirect/C.scala",
    "chars": 47,
    "preview": "package fail.indirect\n\nclass C {\n  val a = A\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/simple/A.scala",
    "chars": 71,
    "preview": "package fail.simple\nimport acyclic.file\n\nclass A {\n  val b: B = null\n}\n"
  },
  {
    "path": "acyclic/test/resources/fail/simple/B.scala",
    "chars": 73,
    "preview": "package fail.simple\n\nclass B {\n  val a1: A = new A\n  val a2: A = new A\n}\n"
  },
  {
    "path": "acyclic/test/resources/force/simple/A.scala",
    "chars": 52,
    "preview": "package force.simple\n\nclass A {\n  val b: B = null\n}\n"
  },
  {
    "path": "acyclic/test/resources/force/simple/B.scala",
    "chars": 74,
    "preview": "package force.simple\n\nclass B {\n  val a1: A = new A\n  val a2: A = new A\n}\n"
  },
  {
    "path": "acyclic/test/resources/force/skip/A.scala",
    "chars": 73,
    "preview": "package force.skip\nimport acyclic.skipped\n\nclass A {\n  val b: B = null\n}\n"
  },
  {
    "path": "acyclic/test/resources/force/skip/B.scala",
    "chars": 94,
    "preview": "package force.skip\nimport acyclic.skipped\nclass B {\n  val a1: A = new A\n  val a2: A = new A\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/cyclicunmarked/A.scala",
    "chars": 62,
    "preview": "package success.cyclicunmarked\n\nclass A {\n  val b: B = null\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/cyclicunmarked/B.scala",
    "chars": 84,
    "preview": "package success.cyclicunmarked\n\nclass B {\n  val a1: A = new A\n  val a2: A = new A\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/dag/A.scala",
    "chars": 32,
    "preview": "package success.dag\n\nclass A {}\n"
  },
  {
    "path": "acyclic/test/resources/success/dag/B.scala",
    "chars": 51,
    "preview": "package success.dag\n\nclass B {\n  val a: A = null\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/dag/C.scala",
    "chars": 40,
    "preview": "package success.dag\n\nobject C extends A\n"
  },
  {
    "path": "acyclic/test/resources/success/dag/D.scala",
    "chars": 63,
    "preview": "package success.dag\n\nclass D {\n  val b: A = null\n  val c = C\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/dag/E.scala",
    "chars": 67,
    "preview": "package success.dag\n\nclass E {\n  val a: A = null\n  val d = new D\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/java/SomeJava.java",
    "chars": 31,
    "preview": "\npublic interface SomeJava {\n\n}"
  },
  {
    "path": "acyclic/test/resources/success/pkg/halfacyclic/a/A1.scala",
    "chars": 71,
    "preview": "package success.halfacyclicpackage\npackage a\n\nclass A1 extends b.B1 {}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/halfacyclic/a/A2.scala",
    "chars": 50,
    "preview": "package success.halfacyclicpackage.a\n\nclass A2 {}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/halfacyclic/a/package.scala",
    "chars": 78,
    "preview": "package success.halfacyclicpackage\n\npackage object a {\n  import acyclic.pkg\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/halfacyclic/b/B1.scala",
    "chars": 50,
    "preview": "package success.halfacyclicpackage.b\n\nclass B1 {}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/halfacyclic/b/B2.scala",
    "chars": 71,
    "preview": "package success.halfacyclicpackage\npackage b\n\nclass B2 extends a.A2 {}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/innercycle/a/A1.scala",
    "chars": 78,
    "preview": "package success.pkg.innercycle.a\n\nclass A1 {\n  val x: A2 = null\n  def y = p\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/innercycle/a/A2.scala",
    "chars": 78,
    "preview": "package success.pkg.innercycle.a\n\nclass A2 {\n  val x: A1 = null\n  def z = p\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/innercycle/a/package.scala",
    "chars": 101,
    "preview": "package success.pkg.innercycle\n\npackage object a {\n  val p: A1 with A2 = null\n  import acyclic.pkg\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/mutualcyclic/a/A1.scala",
    "chars": 66,
    "preview": "package success.cyclicpackage\npackage a\n\nclass A1 extends b.B1 {}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/mutualcyclic/a/A2.scala",
    "chars": 45,
    "preview": "package success.cyclicpackage.a\n\nclass A2 {}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/mutualcyclic/b/B1.scala",
    "chars": 45,
    "preview": "package success.cyclicpackage.b\n\nclass B1 {}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/mutualcyclic/b/B2.scala",
    "chars": 66,
    "preview": "package success.cyclicpackage\npackage b\n\nclass B2 extends a.A2 {}\n"
  },
  {
    "path": "acyclic/test/resources/success/pkg/single/pkg/package.scala",
    "chars": 75,
    "preview": "package success.singlepackage\n\npackage object pkg {\n  import acyclic.pkg\n}\n"
  },
  {
    "path": "acyclic/test/resources/success/simple/A.scala",
    "chars": 35,
    "preview": "package success.simple\n\nclass A {}\n"
  },
  {
    "path": "acyclic/test/resources/success/simple/B.scala",
    "chars": 54,
    "preview": "package success.simple\n\nclass B {\n  val a: A = null\n}\n"
  },
  {
    "path": "acyclic/test/src/acyclic/BaseCycleTests.scala",
    "chars": 2395,
    "preview": "package acyclic\n\nimport utest._\nimport acyclic.plugin.Value.{Pkg, File}\nimport scala.collection.SortedSet\nimport acyclic"
  },
  {
    "path": "acyclic/test/src/acyclic/BaseTestUtils.scala",
    "chars": 2057,
    "preview": "package acyclic\n\nimport acyclic.plugin.Value\n\nimport java.util.jar.JarFile\nimport scala.collection.SortedSet\n\nabstract c"
  },
  {
    "path": "acyclic/test/src-2/acyclic/CycleTests.scala",
    "chars": 69,
    "preview": "package acyclic\n\nobject CycleTests extends BaseCycleTests(TestUtils)\n"
  },
  {
    "path": "acyclic/test/src-2/acyclic/TestUtils.scala",
    "chars": 3502,
    "preview": "package acyclic\n\nimport tools.nsc.{Global, Settings}\nimport tools.nsc.reporters.{ConsoleReporter, StoreReporter}\nimport "
  },
  {
    "path": "acyclic/test/src-3/acyclic/CycleTests.scala",
    "chars": 69,
    "preview": "package acyclic\n\nobject CycleTests extends BaseCycleTests(TestUtils)\n"
  },
  {
    "path": "acyclic/test/src-3/acyclic/TestUtils.scala",
    "chars": 4184,
    "preview": "package acyclic\n\nimport acyclic.plugin.Value\nimport java.io.OutputStream\nimport javax.print.attribute.standard.Severity\n"
  },
  {
    "path": "build.mill",
    "chars": 3161,
    "preview": "package build\n\nimport mill.*\nimport mill.api.BuildCtx\nimport mill.scalalib.*\nimport publish.*\nimport mill.util.{Version,"
  },
  {
    "path": "mill",
    "chars": 11344,
    "preview": "#!/usr/bin/env sh\n\n# This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub"
  },
  {
    "path": "mill.bat",
    "chars": 11550,
    "preview": "@echo off\n\nrem This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub relea"
  },
  {
    "path": "readme.adoc",
    "chars": 12237,
    "preview": "= Acyclic\n:version: 0.3.21\n:toc-placement: preamble\n:toc:\n:link-acyclic: https://github.com/com-lihaoyi/acyclic\n:link-ac"
  }
]

About this extraction

This page contains the full source code of the lihaoyi/acyclic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 81 files (82.1 KB), approximately 25.7k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!