kotlin-acyclic-plugin¶
kotlin-acyclic-plugin is the K2/FIR compiler plugin that enforces the one.wabbit.acyclic rule set.
Most projects should apply one.wabbit.acyclic through the companion Gradle plugin, but this module is the actual compiler-side implementation and is the right entry point for direct compiler integration, build-tool adapters, and Dokka API documentation.
Use this module when you need direct compiler wiring or want to inspect the compiler-side behavior. For normal build setup, start with the Gradle plugin README, the user guide, and the API reference.
Status¶
This module is pre-1.0 and publishes Kotlin-line-specific variants for the repository's supported Kotlin matrix.
Artifact¶
The compiler plugin is published as a Kotlin-line-specific artifact:
one.wabbit:kotlin-acyclic-plugin:0.1.0-kotlin-2.3.10
The -kotlin-<kotlinVersion> suffix matters. FIR compiler-plugin binaries are coupled to the Kotlin compiler APIs they were built against.
The current release train publishes Kotlin-specific compiler-plugin variants for:
2.3.102.4.0-Beta1
If you use Gradle, the companion plugin resolves the matching variant automatically.
What The Compiler Plugin Enforces¶
The compiler plugin evaluates three rule families:
- compilation-unit acyclicity
- declaration acyclicity
- declaration order
Compilation-unit acyclicity reports cycles between Kotlin source files.
Declaration acyclicity reports recursive dependency structure between tracked declarations in a file. The declaration graph is file-local today.
Declaration order adds an optional directional rule on top of declaration acyclicity and checks whether declaration dependencies respect top-down or bottom-up source order.
When an edge is already part of a reported declaration cycle, the cycle diagnostic takes precedence and the redundant declaration-order diagnostic for that same edge is suppressed.
Compiler Options¶
The plugin accepts three module-level options:
compilationUnits=disabled|opt-in|enableddeclarations=disabled|opt-in|enableddeclarationOrder=none|top-down|bottom-up
Raw CLI form:
-P plugin:one.wabbit.acyclic:compilationUnits=disabled|opt-in|enabled
-P plugin:one.wabbit.acyclic:declarations=disabled|opt-in|enabled
-P plugin:one.wabbit.acyclic:declarationOrder=none|top-down|bottom-up
These options define the build-level defaults for the compilation. Source annotations from one.wabbit:kotlin-acyclic can then opt specific files or declarations in, override declaration-order policy, or declare narrowly scoped recursion exemptions.
Control Precedence¶
The effective policy is resolved in this order:
- compiler-plugin options establish the build-level defaults for the compilation
- file annotations can opt whole files in, allow whole-file compilation-unit cycles, and set a file-local declaration-order default
- declaration annotations can opt individual tracked declarations in and grant narrow recursion exceptions
- declaration-level
@Acyclic(order = DEFAULT|NONE|TOP_DOWN|BOTTOM_UP)can replace the file-level order rule or reset back to the build-level default
For declaration order specifically:
- module default comes from
declarationOrder @file:Acyclic(order = ...)overrides that default for tracked declarations in the file@Acyclic(order = DEFAULT)on a declaration resets that declaration back to the module default
Installation And Direct Usage¶
If you are wiring the plugin into the Kotlin compiler directly:
-Xplugin=/path/to/kotlin-acyclic-plugin.jar
-P plugin:one.wabbit.acyclic:compilationUnits=enabled
-P plugin:one.wabbit.acyclic:declarations=enabled
-P plugin:one.wabbit.acyclic:declarationOrder=top-down
If source code uses one.wabbit.acyclic.*, the annotations library still needs to be present on the compilation classpath.
To verify the plugin is active, compile a small source set with declarations=enabled and a same-file mutual recursion pair such as fun a() = b(); fun b() = a(). The compilation should fail with a declaration-cycle diagnostic.
Analysis Model¶
The compiler-plugin pipeline is:
AcyclicCommandLineProcessorparses raw compiler-plugin options.AcyclicCompilerPluginRegistrarregisters the FIR checker extension.AcyclicFileAnalysiswalks resolved FIR and records dependency evidence.AcyclicDependencyGraphevaluates file-level strongly connected components.AcyclicDeclarationGraphevaluates declaration cycles and order violations.AcyclicDiagnosticsreports compiler errors.
The critical design choice is semantic analysis. Dependencies come from resolved FIR symbols and resolved types rather than from imports or syntax-only heuristics.
Scope¶
Declaration analysis distinguishes lexical containment from dependency.
Examples that remain legal:
sealed interface Foo { class Boo : Foo }class Foo { fun self(): Foo = this }
Declaration analysis currently covers top-level declarations and declarations nested inside classes, and it only evaluates declaration dependencies within the current file. Local declarations inside function bodies, accessors, and other local scopes are not tracked as separate declaration nodes.
Local declarations still matter semantically: their resolved dependencies are attributed to the enclosing tracked declaration instead of becoming separate graph nodes.
Worked Examples¶
Legal scoping¶
These shapes stay legal because they express containment or self-typing, not sibling recursion:
package sample
sealed interface Foo {
class Boo : Foo
}
class Box {
fun self(): Box = this
}
Illegal declaration recursion¶
With declaration analysis enabled, same-file mutual recursion is rejected:
package sample
fun parseA(): Node = parseB()
fun parseB(): Node = parseA()
Illegal file cycles¶
With compilation-unit analysis enabled, cross-file semantic cycles are rejected:
// sample/A.kt
package sample
class A(val b: B)
// sample/B.kt
package sample
class B(val a: A)
Order violations¶
With -P plugin:one.wabbit.acyclic:declarationOrder=bottom-up, the following file is rejected because use() appears earlier but depends on helper():
package sample
fun use(): Int = helper()
fun helper(): Int = 1
Under top-down, the same file is valid.
Explicit opt-outs¶
Escape hatches are all-or-nothing at the cycle level:
package sample
import one.wabbit.acyclic.AllowMutualRecursion
@AllowMutualRecursion
fun even(n: Int): Boolean =
if (n == 0) true else odd(n - 1)
@AllowMutualRecursion
fun odd(n: Int): Boolean =
if (n == 0) false else even(n - 1)
If only one participant opts out, the cycle is still reported.
When To Use This Module Directly¶
Use this artifact directly when:
- integrating with a non-Gradle build pipeline
- debugging compiler-plugin behavior
- testing Kotlin-version-specific compiler-plugin variants
- reading the Dokka API surface for compiler-side internals
If you are using Gradle, prefer ../gradle-plugin/README.md.
Release notes live in ../CHANGELOG.md. For diagnostics and setup issues, start with ../docs/troubleshooting.md and the contribution/support guidance in the root README.