
Enforces explicit opt-in for global mutable state via RequiresGlobalState annotation; detects top-level and singleton mutables, offers configurable blacklist and Gradle integration.
kotlin-no-globals is a Kotlin K2 compiler-plugin stack for making global mutable state explicit.
For a JVM project, the smallest useful setup is:
// settings.gradle.kts
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
}
}
rootProject.name = "no-globals-quickstart"// build.gradle.kts
plugins {
kotlin("jvm") version "2.3.10"
application
id("one.wabbit.no-globals") version "0.1.1"
}
dependencies {
implementation("one.wabbit:kotlin-no-globals:0.1.1")
}
kotlin {
jvmToolchain(21)
}
application {
mainClass = "sample.MainKt"
}
noGlobals {
enabled.set(true)
}Then add src/main/kotlin/sample/Main.kt:
package sample
import one.wabbit.noglobals.RequiresGlobalState
@RequiresGlobalState
var counter: Int = 0
@RequiresGlobalState
@OptIn(RequiresGlobalState::class)
fun nextCounter(): Int {
counter += 1
return counter
}
@OptIn(RequiresGlobalState::class)
fun main() {
println(nextCounter())
}Run it:
./gradlew runExpected output:
1
Remove @RequiresGlobalState from counter to see the compiler reject the global var. Remove the @OptIn markers to see Kotlin's normal opt-in checks reject use of the global-state API.
Instead of silently allowing top-level mutation and singleton-backed mutable state, it requires
those declarations to be marked with @RequiresGlobalState. Because that marker is a real Kotlin
@RequiresOptIn annotation, every caller must then acknowledge the dependency explicitly with
@OptIn(RequiresGlobalState::class).
The stack combines:
@RequiresGlobalState
Global mutable state is sometimes necessary, but it is rarely harmless.
It tends to blur ownership, complicate testing, make initialization order matter, and create
surprising transitive dependencies between otherwise ordinary APIs. Kotlin makes these patterns easy
to write; kotlin-no-globals makes them visible and explicit.
The goal is not to prove “all hidden mutability everywhere.” The goal is to put a hard compiler boundary around the most important and predictable forms of global mutable state, and to make the remaining exceptions obvious in source.
This repository is experimental and pre-1.0.
supportedKotlinVersions in gradle.properties, currently 2.3.10 and 2.4.0-Beta1.| Module | Gradle project | Purpose |
|---|---|---|
library/ |
:kotlin-no-globals |
Published annotation artifact containing @RequiresGlobalState
|
compiler-plugin/ |
:kotlin-no-globals-plugin |
K2 FIR compiler plugin that reports diagnostics |
ij-plugin/ |
:kotlin-no-globals-ij-plugin |
IntelliJ IDEA helper plugin for external compiler-plugin loading |
gradle-plugin/ |
:kotlin-no-globals-gradle-plugin |
Gradle integration for id("one.wabbit.no-globals")
|
The current rule set is narrow and predictable.
Rejected by default:
var
lateinit varvar declared inside object singletons, including companion objects and data object
vals whose declared type is on the mutable-type blacklistvals holding anonymous objects with mutable membersExplicitly allowed:
vals with an explicit getter and no initializer or delegateFor the exact semantics and edge cases, see docs/rules.md.
Gradle DSL:
noGlobals {
enabled.set(true)
blacklistedTypes.add("sample.MutableBox")
}Available options:
enabled: turn checking on or off for the current moduleblacklistedTypes: extend the default mutable-type blacklist with additional fully qualified type namesInvalid blacklist entries fail fast during compiler option processing.
The built-in blacklist covers common stored mutable carriers:
MutableCollection, MutableList, MutableSet, MutableMap
kotlinx.coroutines mutable flows, Mutex, coroutine Semaphore, and Channel
kotlinx.atomicfu atomicsStringBuilder and StringBuffer
The checker also matches subtypes of those carriers.
For the exact current list, see
NoGlobalsConfiguration.kt.
Most consumers only need the annotation library and the Gradle plugin.
| Module | Coordinates | Role |
|---|---|---|
| Annotation library | one.wabbit:kotlin-no-globals |
@RequiresGlobalState for source code |
| Gradle plugin | one.wabbit:kotlin-no-globals-gradle-plugin |
Gradle wiring for the compiler plugin |
| Compiler plugin | one.wabbit:kotlin-no-globals-plugin:<baseVersion>-kotlin-<kotlinVersion> |
Kotlin-line-specific K2 compiler plugin implementation |
The Gradle plugin selects the matching compiler-plugin artifact automatically.
The compiler plugin is versioned per Kotlin compiler line:
one.wabbit:kotlin-no-globals-plugin:<baseVersion>-kotlin-<kotlinVersion>That suffix matters. Compiler plugins are Kotlin-version-sensitive.
The library and Gradle plugin use the base project version, while the compiler plugin appends the Kotlin line it was built for.
If you are not using Gradle, wire the compiler plugin directly:
-Xplugin=/path/to/kotlin-no-globals-plugin.jar
-P plugin:one.wabbit.no-globals:enabled=true|false
-P plugin:one.wabbit.no-globals:blacklistedType=com.example.MutableBox
If source code uses @RequiresGlobalState, the annotation library still needs to be on the
compilation classpath.
For compiler-plugin-specific details, see
compiler-plugin/README.md.
Before publication, or when testing locally across repositories, consumers need both forms of composite-build wiring:
pluginManagement {
includeBuild("../kotlin-no-globals")
}
includeBuild("../kotlin-no-globals")The first resolves the Gradle plugin ID. The second allows Gradle to substitute the compiler plugin and annotation artifacts.
https://wabbit-corp.github.io/kotlin-no-globals/
Common commands from the repo root:
./gradlew build
./gradlew :kotlin-no-globals-plugin:test
./gradlew :kotlin-no-globals-gradle-plugin:test
./gradlew :kotlin-no-globals:compileKotlinJvmThe Gradle plugin suite includes real TestKit builds, including native functional coverage, so it is slower than the pure compiler-plugin test suite.
kotlin-no-globals is a Kotlin K2 compiler-plugin stack for making global mutable state explicit.
For a JVM project, the smallest useful setup is:
// settings.gradle.kts
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
}
}
rootProject.name = "no-globals-quickstart"// build.gradle.kts
plugins {
kotlin("jvm") version "2.3.10"
application
id("one.wabbit.no-globals") version "0.1.1"
}
dependencies {
implementation("one.wabbit:kotlin-no-globals:0.1.1")
}
kotlin {
jvmToolchain(21)
}
application {
mainClass = "sample.MainKt"
}
noGlobals {
enabled.set(true)
}Then add src/main/kotlin/sample/Main.kt:
package sample
import one.wabbit.noglobals.RequiresGlobalState
@RequiresGlobalState
var counter: Int = 0
@RequiresGlobalState
@OptIn(RequiresGlobalState::class)
fun nextCounter(): Int {
counter += 1
return counter
}
@OptIn(RequiresGlobalState::class)
fun main() {
println(nextCounter())
}Run it:
./gradlew runExpected output:
1
Remove @RequiresGlobalState from counter to see the compiler reject the global var. Remove the @OptIn markers to see Kotlin's normal opt-in checks reject use of the global-state API.
Instead of silently allowing top-level mutation and singleton-backed mutable state, it requires
those declarations to be marked with @RequiresGlobalState. Because that marker is a real Kotlin
@RequiresOptIn annotation, every caller must then acknowledge the dependency explicitly with
@OptIn(RequiresGlobalState::class).
The stack combines:
@RequiresGlobalState
Global mutable state is sometimes necessary, but it is rarely harmless.
It tends to blur ownership, complicate testing, make initialization order matter, and create
surprising transitive dependencies between otherwise ordinary APIs. Kotlin makes these patterns easy
to write; kotlin-no-globals makes them visible and explicit.
The goal is not to prove “all hidden mutability everywhere.” The goal is to put a hard compiler boundary around the most important and predictable forms of global mutable state, and to make the remaining exceptions obvious in source.
This repository is experimental and pre-1.0.
supportedKotlinVersions in gradle.properties, currently 2.3.10 and 2.4.0-Beta1.| Module | Gradle project | Purpose |
|---|---|---|
library/ |
:kotlin-no-globals |
Published annotation artifact containing @RequiresGlobalState
|
compiler-plugin/ |
:kotlin-no-globals-plugin |
K2 FIR compiler plugin that reports diagnostics |
ij-plugin/ |
:kotlin-no-globals-ij-plugin |
IntelliJ IDEA helper plugin for external compiler-plugin loading |
gradle-plugin/ |
:kotlin-no-globals-gradle-plugin |
Gradle integration for id("one.wabbit.no-globals")
|
The current rule set is narrow and predictable.
Rejected by default:
var
lateinit varvar declared inside object singletons, including companion objects and data object
vals whose declared type is on the mutable-type blacklistvals holding anonymous objects with mutable membersExplicitly allowed:
vals with an explicit getter and no initializer or delegateFor the exact semantics and edge cases, see docs/rules.md.
Gradle DSL:
noGlobals {
enabled.set(true)
blacklistedTypes.add("sample.MutableBox")
}Available options:
enabled: turn checking on or off for the current moduleblacklistedTypes: extend the default mutable-type blacklist with additional fully qualified type namesInvalid blacklist entries fail fast during compiler option processing.
The built-in blacklist covers common stored mutable carriers:
MutableCollection, MutableList, MutableSet, MutableMap
kotlinx.coroutines mutable flows, Mutex, coroutine Semaphore, and Channel
kotlinx.atomicfu atomicsStringBuilder and StringBuffer
The checker also matches subtypes of those carriers.
For the exact current list, see
NoGlobalsConfiguration.kt.
Most consumers only need the annotation library and the Gradle plugin.
| Module | Coordinates | Role |
|---|---|---|
| Annotation library | one.wabbit:kotlin-no-globals |
@RequiresGlobalState for source code |
| Gradle plugin | one.wabbit:kotlin-no-globals-gradle-plugin |
Gradle wiring for the compiler plugin |
| Compiler plugin | one.wabbit:kotlin-no-globals-plugin:<baseVersion>-kotlin-<kotlinVersion> |
Kotlin-line-specific K2 compiler plugin implementation |
The Gradle plugin selects the matching compiler-plugin artifact automatically.
The compiler plugin is versioned per Kotlin compiler line:
one.wabbit:kotlin-no-globals-plugin:<baseVersion>-kotlin-<kotlinVersion>That suffix matters. Compiler plugins are Kotlin-version-sensitive.
The library and Gradle plugin use the base project version, while the compiler plugin appends the Kotlin line it was built for.
If you are not using Gradle, wire the compiler plugin directly:
-Xplugin=/path/to/kotlin-no-globals-plugin.jar
-P plugin:one.wabbit.no-globals:enabled=true|false
-P plugin:one.wabbit.no-globals:blacklistedType=com.example.MutableBox
If source code uses @RequiresGlobalState, the annotation library still needs to be on the
compilation classpath.
For compiler-plugin-specific details, see
compiler-plugin/README.md.
Before publication, or when testing locally across repositories, consumers need both forms of composite-build wiring:
pluginManagement {
includeBuild("../kotlin-no-globals")
}
includeBuild("../kotlin-no-globals")The first resolves the Gradle plugin ID. The second allows Gradle to substitute the compiler plugin and annotation artifacts.
https://wabbit-corp.github.io/kotlin-no-globals/
Common commands from the repo root:
./gradlew build
./gradlew :kotlin-no-globals-plugin:test
./gradlew :kotlin-no-globals-gradle-plugin:test
./gradlew :kotlin-no-globals:compileKotlinJvmThe Gradle plugin suite includes real TestKit builds, including native functional coverage, so it is slower than the pure compiler-plugin test suite.