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