IntelliJプラグインのAnActionと登録設定を実装・変更し、表示文言やアイコンの定義、非同期処理の起動方法を整えます。
- IDEに新しいアクションを追加したいとき
- アクションの登録やアイコンを変更したいとき
- 表示名と説明文の定義を整理したいとき
IntelliJ向けKotlinコルーチンのスコープ所有者やキャンセルの伝わり方を確認し、リークやキャンセル漏れを防ぐ修正方針を示します。
Write or review IntelliJ coroutine scope ownership and cancellation.
実行スコア 未検証
インストール方法を見るIntelliJ製品のKotlinコードで、コルーチンのスコープがどのオブジェクトに所有され、いつキャンセルされるかを整理します。親子間のキャンセルや失敗の伝わり方、長時間処理やFlow収集がキャンセルに協調するかを確認し、問題があれば影響度と小さな構造修正を提案します。GlobalScopeや切り離されたスコープ、キャンセル例外の握りつぶし、launchの失敗を外側のtryで捕まえようとするコードなども確認対象です。
コルーチンを新しく追加するときや、既存コードのリーク、停止しないループ、キャンセル後の不整合を調べるときに役立ちます。特に、サービスやUIのライフサイクルに処理を結び付けたい開発者に向いています。
対象はIntelliJベース製品のKotlinコルーチンコードです。UIやEDT(イベントディスパッチスレッド)の所有関係、コルーチン内部の詳細な解説は、関連する別スキルの対象として案内されています。
この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。
インストールする前に、エージェントに与えられる指示の中身を確認できます。
Keep cancellation, failures, and lifetimes predictable in Kotlin coroutine code across any IntelliJ-based product module. Three rules govern every decision:
coroutineScope, supervisorScope, a
lifecycle-bound scope) over hand-rolled scope wiring, the most common source of leaks.Reach for this skill when changing suspend functions, launch/async, CoroutineScope
construction, cancellation handling, or Flow collection — and when diagnosing leaked coroutines,
uncancellable loops, swallowed cancellation, GlobalScope, launch-in-init, detached scopes,
invokeOnCompletion, ProcessCanceledException, blocking/progress bridges, ModalityState
context capture, or promise/callback cancellation bridges.
Related skills: platform-deep-dives (coroutine internals, dispatchers, read/write actions); kotlin-ui-swing-component-architecture (Swing UI and EDT ownership).
CoroutineScope and the exact moment it is cancelled (Disposable disposal,
service teardown, explicit cancel()). If you cannot name that moment, that is the finding.Flow collectors) for a
cooperative cancellation checkpoint — do not flag short bodies that already suspend.catch on the path for a swallowed CancellationException / ProcessCanceledException.invokeOnCompletion, GlobalScope, manual CoroutineScope(...), or launch in init
against the rules below, then classify (severity table) and propose the smallest structural fix.Never use GlobalScope for feature work — it is never cancelled, so its coroutines outlive the
component that started them.
Prefer an injected or platform lifecycle scope (project/service scope, or the scope handed to
you). A standalone CoroutineScope(context) with no Job in the context gets a fresh root Job()
not linked to any parent — nothing cancels or awaits it, so it leaks:
// Wrong: detached root scope; cleanup depends on a manual cancel() that is easy to miss.
val scope = CoroutineScope(someContextElement.asContextElement())
// Right: child of an owning scope; cancelled structurally with its parent.
val scope = owner.coroutineScope.childScope("MyFeature", someContextElement.asContextElement())
If a manual scope is unavoidable, either make it a child of an owning scope, or register the
owning Disposable (cancel the scope in dispose()) before launching any work, and verify that
registration actually runs. Disposal-only cancellation with no parent link is fragile — a missed
dispose() leaks everything the scope launched.
launch in init {} — depends on what cancels the scope. If the object owns a scope tied to
its own Disposer registration, don't launch in init: the coroutine can run (touching a half-built
this) before registration completes and leak on failure — expose start() and call it after
Disposer.register. A @Service with an injected CoroutineScope is the exception: that scope
is cancelled with the service's container (app/project/plugin) by the platform — not via your
dispose(), and no Disposable is needed solely to cancel the injected scope — so a service
self-starting workers from init is acceptable. Just keep the constructor cheap and don't read
not-yet-initialized state.
// Owned scope, cancelled via your own Disposer registration:
init { scope.launch { observe() } } // Wrong — may run before Disposer.register completes
fun start() { scope.launch { observe() } } // Right — after Disposer.register(owner, this)
// @Service(private val scope: CoroutineScope) — self-start from init is fine; scope is platform-managed.
Prefer the shortest lifecycle that consumes the work; use an app/project service scope only when the result is genuinely owned for that whole lifetime.
Do not store per-submission UI context in a long-lived scope. Context elements such as current
ModalityState must be captured at the submit site, not at service/object construction. Avoid
CoroutineScope(ModalityState.defaultModalityState().asContextElement()); prefer
scope.launch(currentModality.asContextElement()) { ... } when modality belongs to that operation.
Modality dispatch semantics belong to kotlin-ui-swing-component-architecture /
platform-deep-dives.
CancellationException cancels its parent and siblings. SupervisorJob/supervisorScope opts
child failure out of cancelling the parent; parent cancellation still cancels supervised children.coroutineScope { } is all-or-nothing (one failure fails the scope and rethrows); supervisorScope { }
is only for genuinely independent children.cancel(). Add a
suspension point (delay, yield), ensureActive(), or checkCanceled() in progress-aware code.suspendCancellableCoroutine (not suspendCoroutine) and release
the resource in invokeOnCancellation.Dispatchers.IO) or, for progress-aware
blocking code, coroutineToIndicator { indicator -> ... }. Pair cancellation with an explicit
interrupt/close strategy; coroutine cancellation alone does not abort an in-progress blocking call.
Do not wrap blocking code in blockingContext: deprecated since 2024.2 because context is
installed implicitly (ReplaceWith("action()")).runCatching { launch { ... } } does not catch the child's failure. The child fails its parent
scope out-of-band while launch returns normally, so try/runCatching sees nothing. To isolate a
child, use supervisorScope and handle the failure inside the child.
The same trap applies to async: try/runCatching around async { } does not contain a
later failure — the exception surfaces at await(), and an unsupervised failure may already have
cancelled the parent before you await.
Never swallow cancellation. runCatching and bare catch (e: Throwable) catch
CancellationException too; swallowing it breaks structured concurrency and can hang cancellation.
ProcessCanceledException is a CancellationException subtype, so catching cancellation covers it
too. Rethrow cancellation first:
try {
body()
} catch (c: CancellationException) { // also covers ProcessCanceledException (a subtype)
throw c // never swallow cancellation
} catch (x: Throwable) {
handle(x)
}
When bridging to a non-coroutine promise/callback, settle it before rethrowing cancellation.
If a coroutine owns an AsyncPromise or a callback result, cancellation can skip the normal result
path and leave external awaiters pending. In catch (c: CancellationException), complete the
external primitive (setError(c) / cancel) before throw c. Plain Deferred does not need this —
cancellation completes it.
Do not report a caught CancellationException as a user-visible error, even if you rethrow it.
Do not call cancel() on the coroutine you are currently running in — it does not stop execution
immediately (only at the next suspension point) and poisons the surrounding scope. Return early or
throw CancellationException. A shared suspend fun must never cancel its caller's job.
Prefer to avoid it. Three failure modes make it a trap:
check-then-act on a shared field races; use
AtomicReference.compareAndSet.Preferred alternative — do completion work as the last step inside the coroutine:
scope.launch {
try { doWork() }
finally { withContext(NonCancellable + Dispatchers.EDT) { onFinished() } }
}
Use NonCancellable only for small, bounded cleanup that must run even after cancellation — never
wrap substantial work in it, and keep UI cleanup lifecycle/disposal-guarded (do not touch a disposed
component). Cancelling an already-completed Job is a no-op, so "clear my job handle on completion"
bookkeeping is often unnecessary — verify it changes behavior before adding it.
Adjust for actual impact.
| Pattern | Default | Smallest fix |
|---|---|---|
GlobalScope for feature work | Critical | child of a lifecycle scope |
Detached CoroutineScope never cancelled | Critical | childScope of an owning scope |
Swallowed CancellationException/ProcessCanceledException | Critical | rethrow cancellation before catch (Throwable) |
| Non-cooperative infinite/long loop | Critical | add ensureActive()/yield/suspension point |
runCatching/try around launch/async to contain failure | Critical | supervisorScope + handle inside child |
launch in init with a scope tied to own Disposer registration | Major | start() after registration |
Manual scope where childScope fits; scope not tied to lifecycle | Major | inject/childScope; register Disposable before launch |
invokeOnCompletion mutating shared state unsynchronized or that can throw | Major | compareAndSet, or clean up in coroutine finally |
Self-cancellation (cancel() on current job) | Major | return / throw CancellationException |
| Promise/callback bridge left pending on cancellation | Major | complete/cancel external primitive before rethrow |
Unnecessary invokeOnCompletion bookkeeping | Minor | remove it |
blockingContext wrapper (deprecated 2024.2; context now implicit) | Minor | delete the wrapper |
| Cancellation reported as a user-visible error | Minor | rethrow silently |
supervisorScope/SupervisorJob where a plain scope suffices | Minor | use coroutineScope |
For deep internals see platform-deep-dives (coroutine notebooks: cancellation model, context
propagation) and the ultimate coroutine docs docs/IntelliJ-Platform/4_man/Kotlin-Coroutines/,
especially 9_Gotchas-and-practices/ and 8_UI-EDT-Dispatchers.md.
まだレビューはありません。使ってみた感想をお寄せください。
ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。
まだコメントはありません。最初のコメントを書いてみませんか。
IntelliJプラグインのAnActionと登録設定を実装・変更し、表示文言やアイコンの定義、非同期処理の起動方法を整えます。
IntelliJのJPSテストモジュールをBazelへ移行し、Bazelだけで起きる失敗を依存関係や実行環境から切り分ける手順を案内します。
IntelliJのKotlin・Javaコードを新規作成やレビューする際に、言語の選び方、書式、型の扱い、例外処理などのプロジェクト規約に沿って整えるスキルです。
IntelliJ向けの変更を適切な単位に分け、チケットの有無に応じた形式で、理由や判断も伝わるコミットメッセージを作成・修正するスキルです。
Pythonコードの型推論や型エラーを複数の型チェッカーで比較し、各ツールの判定と診断内容をまとめたレポートを作成します。
Compose Desktopの画面で状態や副作用をどこに持たせるか整理し、表示と判断の境界、再利用部品やリストの設計を整えます。