= Acyclic :version: 0.3.15 :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:
package fail.simple
class A { val b: B = null }
package fail.simple
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:
package fail.simple import acyclic.file
And attempting to compile these files together will then result in a compilation error:
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]:
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:
// c/C1.scala package fail.halfpackagecycle.c
// c/C2.scala package fail.halfpackagecycle package c
// c/package.scala package fail.halfpackagecycle
// A.scala package fail.halfpackagecycle
// B.scala package fail.halfpackagecycle
These 5 files do not have any file-level cycles, and form a nice linear dependency chain:
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:
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
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:
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:
=== sbt
To use, add the following to your build.sbt
:
libraryDependencies += ("com.lihaoyi" %% "acyclic" % "{version}" cross (CrossVersion.full)) % "provided"
autoCompilerPlugins := true
=== Force
If you want to enforce acyclicity across all your files, you can pass in the command-line compiler flag:
Or via SBT:
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
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.
== 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:
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== 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
=== 0.3.15
=== 0.3.14
=== 0.3.13
=== 0.3.12
=== 0.3.11
=== 0.3.10
=== 0.3.9
=== 0.3.8
=== 0.3.7
=== 0.3.6
=== 0.3.5
=== 0.3.4
=== 0.3.3
=== 0.3.2
warn
to emit compiler warnings instead of errors=== 0.3.1
=== 0.3.0
=== 0.2.0
=== 0.1.7
import acyclic.skipped
, which was broken in 0.1.6=== 0.1.6
-P:acyclic:force
(scalaOptions += "-P:acyclic:force"
in SBT) to enforce acyclicity across
your entire codebase.=== 0.1.5
=== 0.1.4
acyclic.file
is now @compileTimeOnly
to provide better errors=== 0.1.3