structured-coroutines
1.1.0indexedEnforces structured concurrency for coroutines via compiler checks, static analyzers, IDE inspections, lint rules and annotations — compile-time errors, quick fixes, tool window, zero runtime overhead.
Enforces structured concurrency for coroutines via compiler checks, static analyzers, IDE inspections, lint rules and annotations — compile-time errors, quick fixes, tool window, zero runtime overhead.
A comprehensive toolkit for enforcing structured concurrency in Kotlin Coroutines, inspired by Swift Concurrency. It provides multiple layers of protection through compile-time checks and static analysis.
Kotlin Coroutines are powerful but can be misused, leading to:
GlobalScopeThis toolkit enforces structured concurrency best practices through:
51 documented patterns (v1.0.0) — single source of truth: docs/rule-codes.yml.
Full descriptions, examples, and suppression IDs: docs/BEST_PRACTICES_COROUTINES.md, docs/SUPPRESSING_RULES.md.
v1.0 highlights: COMPOSE_002/003, TEST_005/006, FLOW_009/011, INTEROP_003/004, DEBUG_001 (opt-in).
Platform guides: Compose · KMP · Ktor · Spring.
Per-module rule lists and configuration examples: detekt-rules · lint-rules · intellij-plugin.
// settings.gradle.kts
pluginManagement {
repositories {
mavenLocal()
mavenCentral()
gradlePluginPortal()
}
}
// build.gradle.kts
plugins {
kotlin("jvm") version "2.3.0"
id("io.github.santimattius.structured-coroutines") version "0.1.0"
}
dependencies {
implementation("io.github.santimattius:structured-coroutines-annotations:0.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}
// build.gradle.kts
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.7"
}
dependencies {
detektPlugins("io.github.santimattius:structured-coroutines-detekt-rules:0.1.0")
}
// build.gradle.kts (Android project)
dependencies {
lintChecks("io.github.santimattius:structured-coroutines-lint-rules:0.1.0")
}
Note: Android Lint Rules are only available for Android projects. For multiplatform projects, use the Compiler Plugin or Detekt Rules.
Install from JetBrains Marketplace or build from source:
# Build the plugin
./gradlew :intellij-plugin:build
# Run IDE sandbox for testing
./gradlew :intellij-plugin:runIde
Or install manually:
intellij-plugin/build/distributions/intellij-plugin-*.zipplugins {
kotlin("multiplatform") version "2.3.0"
id("io.github.santimattius.structured-coroutines") version "0.1.0"
}
kotlin {
jvm()
iosArm64()
iosSimulatorArm64()
js(IR) { browser(); nodejs() }
sourceSets {
commonMain {
dependencies {
implementation("io.github.santimattius:structured-coroutines-annotations:0.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}
}
}
}
The plugin automatically recognizes lifecycle-aware framework scopes:
// ✅ Android ViewModel - No annotation needed
class MyViewModel : () {
{
viewModelScope.launch { fetchData() }
}
}
: () {
{
lifecycleScope.launch { fetchData() }
}
}
{
scope = rememberCoroutineScope()
Button(onClick = { scope.launch { doWork() } }) {
Text()
}
}
Recognized Framework Scopes:
// ❌ ERROR
GlobalScope.launch { work() }
// ✅ Use framework scopes or @StructuredScope
viewModelScope.launch { work() }
// ❌ ERROR
CoroutineScope(Dispatchers.IO).launch { work() }
// ✅ Use a managed scope
class MyClass(@StructuredScope private val scope: CoroutineScope) {
fun doWork() = scope.launch { work() }
}
// ❌ ERROR
suspend fun bad() {
runBlocking { delay(1000) }
}
// ✅ Just suspend
suspend fun good() {
delay(1000)
}
// ❌ ERROR
scope.launch(Job()) { work() }
scope.launch(SupervisorJob()) { work() }
// ✅ Use supervisorScope
suspend fun process() = supervisorScope {
launch { task1() }
launch { task2() }
}
// ❌ ERROR
class MyError : CancellationException()
// ✅ Use regular Exception
class MyError : Exception()
// ⚠️ WARNING - May swallow cancellation
suspend fun bad() {
try {
work()
} catch (e: Exception) {
log(e)
}
}
// ✅ Handle separately
suspend fun good() {
try {
work()
} catch (e: CancellationException) {
e
} (e: Exception) {
log(e)
}
}
// ⚠️ WARNING - May not execute
try {
work()
} finally {
saveToDb() // Suspend call
}
// ✅ Protected
try {
work()
} finally {
withContext(NonCancellable) {
saveToDb()
}
}
// ⚠️ Detekt WARNING
scope.launch {
Thread.sleep(1000) // Blocking!
inputStream.read() // Blocking I/O!
jdbcStatement.execute() // Blocking JDBC!
}
// ✅ Use non-blocking alternatives
scope.launch {
delay(1000)
withContext(Dispatchers.IO) {
inputStream.read()
}
}
// ⚠️ Detekt WARNING - Slow test
@Test
fun test() = runBlocking {
delay(1000) // Real delay!
}
// ✅ Fast test with virtual time
@Test
fun test() = runTest {
delay(1000) // Instant!
}
// ⚠️ Detekt WARNING - Can't cancel
suspend fun process(items: List<Item>) {
for (item in items) {
heavyWork(item) // No cooperation point
}
}
{
(item items) {
ensureActive()
heavyWork(item)
}
}
📖 Full Documentation: Detekt Rules Documentation
# Publish locally
./gradlew publishToMavenLocal
# Run compiler plugin tests
./gradlew :compiler:test
# Run detekt rules tests
./gradlew :detekt-rules:test
# Run all tests
./gradlew test
Notes:
docs/rule-codes.ymlCopyright 2026 Santiago Mattiauda
Licensed under the Apache License, Version 2.0
Contributions welcome! See CONTRIBUTING.md for setup, module layout, and how to add rules.
This project follows the Code of Conduct. Security issues should be reported per SECURITY.md, not via public issues.
git clone https://github.com/santimattius/structured-coroutines.git
cd structured-coroutines
./gradlew publishToMavenLocal
./gradlew test
Each module contains its own detailed documentation:
All user-facing text is externalized for localization:
To add a new language, add a _<locale> properties file (e.g. CompilerBundle_de.properties) with
the same keys.
All user-facing text is externalized for localization:
To add a new language, add a _<locale> properties file (e.g. CompilerBundle_de.properties) with the same keys.
| Module | Status | Documentation |
|---|
| Compiler Plugin | ✅ Complete (14 FIR checkers) | gradle-plugin/README.md |
| Gradle Plugin | ✅ Complete (profiles incl. Spring/Ktor) | gradle-plugin/README.md |
| Detekt Rules | ✅ Complete (40 rules) | detekt-rules/README.md |
| Android Lint | ✅ Complete (35 detectors) | lint-rules/README.md |
| IntelliJ Plugin | ✅ Complete (35 inspections, tool window, Flow Analyzer) | intellij-plugin/README.md |
| Annotations | ✅ Complete | annotations/README.md |
| Sample | ✅ Compilation examples per rule | compilation/README |
| Sample (Detekt) | ✅ Detekt rule validation (30+ examples) | sample-detekt/README.md |
| Kotlin Coroutines Agent Skill | ✅ AI/agent guidance | docs/KOTLIN_COROUTINES_SKILL.md |
runBlocking in suspend functionsCancellationException| Module | Purpose | When |
|---|
compiler | K2/FIR Compiler Plugin | Compile-time errors |
detekt-rules | Detekt custom rules | Static analysis |
lint-rules | Android Lint rules | Android projects |
intellij-plugin | IntelliJ/Android Studio Plugin | Real-time IDE analysis |
annotations | @StructuredScope annotation | Runtime/Compile |
gradle-plugin | Gradle integration | Build configuration |
@StructuredScope annotationviewModelScope, lifecycleScope, rememberCoroutineScope()sample-detekt module with one example per Detekt rule (
./gradlew :sample-detekt:detekt)| Layer | Count | Coverage |
|---|
| Compiler (FIR) | 14 checkers | 7 errors + 7 warnings; INTEROP_001/002 in K2 |
| Detekt | 40 rules | KMP source-set aware; backend + Compose profiles |
| Android Lint | 35 detectors | Android/Compose + shared compiler rules |
| IntelliJ | 35 inspections | Real-time analysis, quick fixes, Flow Chain Analyzer |
import io.github.santimattius.structured.annotations.StructuredScope
// Function parameter
fun loadData( scope: CoroutineScope) {
scope.launch { fetchData() }
}
// Constructor injection
class UserService(
private val scope: CoroutineScope
) {
fun fetchUser(id: String) {
scope.launch { /* ... */ }
}
}
// Class property
class Repository {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun fetchData() {
scope.launch { /* ... */ }
}
}
| Scope | Framework | Package |
|---|
viewModelScope | Android ViewModel | androidx.lifecycle |
lifecycleScope | Android Lifecycle | androidx.lifecycle |
rememberCoroutineScope() | Jetpack Compose | androidx.compose.runtime |
structured-coroutines:
# Compiler Plugin Rules
GlobalScopeUsage:
active: true
severity: error
InlineCoroutineScope:
active: true
severity: error
RunBlockingInSuspend:
active: true
severity: warning
DispatchersUnconfined:
active: true
severity: warning
CancellationExceptionSubclass:
active: true
severity: error
# Detekt-Only Rules
BlockingCallInCoroutine:
active: true
excludes: [ 'commonMain', 'iosMain' ] # JVM-only
RunBlockingWithDelayInTest:
active: true
ExternalScopeLaunch:
active: true
LoopWithoutYield:
active: true
structured-coroutines/
├── annotations/ # @StructuredScope (Multiplatform)
├── compiler/ # K2/FIR Compiler Plugin
│ ├── UnstructuredLaunchChecker
│ ├── RunBlockingInSuspendChecker
│ ├── JobInBuilderContextChecker
│ ├── DispatchersUnconfinedChecker
│ ├── CancellationExceptionSubclassChecker
│ ├── SuspendInFinallyChecker
│ ├── CancellationExceptionSwallowedChecker
│ ├── UnusedDeferredChecker
│ ├── RedundantLaunchInCoroutineScopeChecker
│ └── LoopWithoutYieldChecker
├── detekt-rules/ # Detekt Custom Rules
│ ├── GlobalScopeUsageRule
│ ├── InlineCoroutineScopeRule
│ ├── RunBlockingInSuspendRule
│ ├── DispatchersUnconfinedRule
│ ├── CancellationExceptionSubclassRule
│ ├── BlockingCallInCoroutineRule
│ ├── RunBlockingWithDelayInTestRule
│ ├── ExternalScopeLaunchRule
│ ├── LoopWithoutYieldRule
│ ├── ScopeReuseAfterCancelRule
│ ├── ChannelNotClosedRule
│ ├── ConsumeEachMultipleConsumersRule
│ └── FlowBlockingCallRule
├── lint-rules/ # Android Lint Rules
│ ├── GlobalScopeUsageDetector
│ ├── MainDispatcherMisuseDetector
│ ├── ViewModelScopeLeakDetector
│ └── ... (21 rules total)
├── intellij-plugin/ # IntelliJ/Android Studio Plugin
│ ├── inspections/ # 13 real-time inspections (incl. LoopWithoutYield, LifecycleAwareFlowCollection)
│ ├── quickfixes/ # 12 automatic quick fixes
│ ├── intentions/ # 6 refactoring intentions (incl. Convert to runTest)
│ ├── guttericons/ # Scope & dispatcher visualization
│ └── view/ # Tool window (findings list, runner, tree visitor)
├── gradle-plugin/ # Gradle Integration
├── sample/ # Examples
│ └── compilation/ # One example per compiler rule (errors & warnings)
└── kotlin-coroutines-skill/ # AI/agent skill for coroutine best practices
| Platform | Compiler Plugin | Detekt Rules | Android Lint | IDE Plugin |
|---|
| JVM | ✅ | ✅ | ❌ | ✅ |
| Android | ✅ | ✅ | ✅ | ✅ |
| iOS | ✅ | ✅ | ❌ | ✅ |
| macOS | ✅ | ✅ | ❌ | ✅ |
| watchOS | ✅ | ✅ | ❌ | ✅ |
| tvOS | ✅ | ✅ | ❌ | ✅ |
| Linux | ✅ | ✅ | ❌ | ✅ |
| Windows | ✅ | ✅ | ❌ | ✅ |
| JS | ✅ | ✅ | ❌ | ✅ |
| WASM | ✅ | ✅ | ❌ | ✅ |
| Approach | When | Rules (approx.) | CI | Real-time | Platform |
|---|
| Compiler Plugin | Compile | 14 checkers | ✅ | ❌ | All (KMP) |
| Detekt Rules | Analysis | 40 rules | ✅ | ❌ | All (KMP) |
| Android Lint Rules | Analysis | 35 detectors | ✅ | ❌ | Android only |
| IDE Plugin | Editing | 35 inspections | ❌ | ✅ | All |
| Combined (All) | All | 51 patterns | ✅ | ✅ | - |
| Code Review | Manual | — | ❌ | ❌ | - |
| Runtime | Late | — | ❌ | ❌ | - |
| Module | Documentation | Description |
|---|
| Gradle Plugin | gradle-plugin/README.md | Installation, configuration, severity settings |
| Detekt Rules | detekt-rules/README.md | 40 rules with examples and configuration |
| Android Lint | lint-rules/README.md | 35 detectors, Android/Compose-specific detection |
| IntelliJ Plugin | intellij-plugin/README.md | 35 inspections, quick fixes, Flow Analyzer, K2 support |
| Annotations | annotations/README.md | @StructuredScope usage and multiplatform support |
| Compiler | compiler/README.md | K2/FIR checker implementation details |
| Sample (compilation) | compilation/README.md | One example per compiler rule (errors and warnings) |
| Sample (Detekt) | sample-detekt/README.md | One example per Detekt rule; run :sample-detekt:detekt to validate |
| Kotlin Coroutines Agent Skill | docs/KOTLIN_COROUTINES_SKILL.md | AI/agent skill and decision resource: 32 practices, strict rules, triage playbook (v2.0.0) |
| Best Practices | docs/BEST_PRACTICES_COROUTINES.md | Canonical guide to coroutine good/bad practices |
| Decision Guide | docs/DECISION_GUIDE.md | Quick-reference: launch vs async, scope, dispatcher, timeout, Flow |
| Suppressing Rules | docs/SUPPRESSING_RULES.md | Unified suppression IDs (Compiler, Detekt, Lint, IntelliJ) by rule code |
compiler/src/main/resources/messages/CompilerBundle*.properties. Default language is English
so builds and CI are predictable. To use Spanish (or the JVM default locale), set the system
property:
-Dstructured.coroutines.compiler.locale=es (e.g. in gradle.properties:
org.gradle.jvmargs=... -Dstructured.coroutines.compiler.locale=es)-Dstructured.coroutines.compiler.locale=defaultStructuredCoroutinesBundle.properties; the IDE uses the platform
language. Spanish: StructuredCoroutinesBundle_es.properties.compiler/src/main/resources/messages/CompilerBundle*.properties. Default language is English so builds and CI are predictable. To use Spanish (or the JVM default locale), set the system property:
-Dstructured.coroutines.compiler.locale=es (e.g. in gradle.properties: org.gradle.jvmargs=... -Dstructured.coroutines.compiler.locale=es)-Dstructured.coroutines.compiler.locale=defaultStructuredCoroutinesBundle.properties; the IDE uses the platform language. Spanish: StructuredCoroutinesBundle_es.properties.Surfaced from shared tags and platforms — no rankings paid for.