User Guide¶
This guide covers the user-facing model of kotlin-acyclic: how to wire it into a build, what each rule family checks, and how the source-level controls refine the module defaults.
Why This Model Exists¶
The project treats structural recursion and source-order policy as things teams should be able to make explicit.
What the plugin adds on top of plain Kotlin is:
- semantic file-cycle checking
- semantic same-file declaration-cycle checking
- optional declaration source-order enforcement
- narrow, explicit escape hatches for the cases that are genuinely intentional
Without that extra structure, the language still permits these shapes, but nothing makes the architectural dependency policy visible or enforceable.
Setup¶
Gradle¶
The normal integration path is the Gradle plugin:
// settings.gradle.kts
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
// build.gradle.kts
import one.wabbit.acyclic.gradle.AcyclicDeclarationOrderMode
import one.wabbit.acyclic.gradle.AcyclicEnforcementMode
plugins {
kotlin("jvm") version "2.3.10"
id("one.wabbit.acyclic") version "0.1.0"
}
repositories {
mavenCentral()
}
dependencies {
implementation("one.wabbit:kotlin-acyclic:0.1.0")
}
acyclic {
compilationUnits.set(AcyclicEnforcementMode.OPT_IN)
declarations.set(AcyclicEnforcementMode.ENABLED)
declarationOrder.set(AcyclicDeclarationOrderMode.TOP_DOWN)
}
The Gradle plugin selects the compiler-plugin artifact variant that matches the applied Kotlin Gradle plugin version.
Manual compiler wiring¶
If you are not using the Gradle plugin, you need:
- the annotations dependency
one.wabbit:kotlin-acyclic:0.1.0 - the compiler plugin artifact
one.wabbit:kotlin-acyclic-plugin:<baseVersion>-kotlin-<kotlinVersion> - compiler options in the standard plugin format
-Xplugin=/path/to/kotlin-acyclic-plugin.jar
-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
Module-Level Defaults¶
Three build-level controls exist:
compilationUnitsdeclarationsdeclarationOrder
Meanings:
compilationUnitscontrols file-cycle checkingdeclarationscontrols declaration-cycle checkingdeclarationOrdercontrols optional top-down/bottom-up source-order enforcement
The Gradle defaults are:
compilationUnits = OPT_INdeclarations = DISABLEDdeclarationOrder = NONE
Design Intent¶
The rule model is constrained on purpose:
- semantic rather than syntax-only
- explicit rather than inferred from naming or imports
- narrow escape hatches instead of broad suppression
- predictable enough that users can usually tell from source why a dependency is legal or illegal
Source-Level Controls¶
The public annotation surface is:
@Acyclic@AllowCompilationUnitCycles@AllowSelfRecursion@AllowMutualRecursionAcyclicOrder
Effective precedence¶
The final policy is resolved in this order:
- Gradle defaults or direct compiler-plugin options
- file annotations
- declaration annotations
- declaration-level order overrides
For declaration order:
- module default comes from
declarationOrder @file:Acyclic(order = ...)overrides that default for tracked declarations in the file@Acyclic(order = DEFAULT)resets one declaration back to the module default
Rule Semantics¶
Compilation-unit acyclicity¶
Compilation-unit analysis reports semantic cycles between Kotlin source files.
With compilationUnits = OPT_IN, a file opts in with:
@file:one.wabbit.acyclic.Acyclic
A file-level cycle is exempt only when every participating file uses @file:AllowCompilationUnitCycles.
Declaration acyclicity¶
Declaration analysis reports recursive dependency structure between tracked declarations.
Current tracked declaration nodes are:
- top-level classes
- top-level functions
- top-level properties
- top-level typealiases
- declarations nested inside classes using the same tracked kinds
Current boundary:
- declaration analysis is file-local today
- cross-file declaration edges are ignored by the declaration graph
- cross-file recursion is therefore enforced by compilation-unit analysis, not a module-wide declaration graph
Local declarations are not separate declaration nodes. Their resolved dependencies are attributed to the enclosing tracked declaration instead.
Declaration order¶
Declaration order adds an optional directional source-order rule.
TOP_DOWN: earlier declarations may depend on later declarationsBOTTOM_UP: later declarations may depend on earlier declarationsNONE: no source-order rule
If an edge is already part of a reported declaration cycle, the cycle diagnostic takes precedence and the redundant order diagnostic for that same edge is suppressed.
Legal And Illegal Shapes¶
Legal scoping¶
These are legal:
sealed interface Token {
class Word(val text: String) : Token
}
class Box {
fun self(): Box = this
}
The reason is simple: lexical containment and nominal self-typing are not treated as sibling recursion.
Illegal same-file recursion¶
fun a(): Int = b()
fun b(): Int = a()
With declaration checking enabled, that is rejected as a declaration cycle.
Illegal cross-file cycle¶
// A.kt
class A(val b: B)
// B.kt
class B(val a: A)
With compilation-unit checking enabled, that is rejected as a file cycle.
Illegal order violation¶
fun use(): Int = helper()
fun helper(): Int = 1
That file is valid under TOP_DOWN and rejected under BOTTOM_UP.
Escape Hatches¶
Escape hatches are narrow.
@AllowSelfRecursionpermits direct self-recursion@AllowMutualRecursionpermits a declaration cycle only when every declaration in the cycle opts out@AllowCompilationUnitCyclespermits a file cycle only when every participating file opts out
Example:
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.
Current Non-Goals¶
The current implementation does not try to:
- build a module-wide declaration graph
- treat every form of lexical nesting as a dependency edge
- infer hidden recursion from arbitrary runtime behavior
The project direction is to keep the rule set semantic and explicit, but still understandable enough that users can predict what the compiler will do.