IntelliJ `AnAction` 클래스를 구현하거나 수정하고 `plugin.xml`에 등록하도록 안내합니다. 표시 이름과 설명은 메시지 번들에 두고, 필요하면 아이콘을 설정하며, 이벤트 코루틴 범위에서 중단 함수를 실행해 동기식 처리로 IDE가 멈추는 일을 방지합니다.
- IDE 메뉴에 새 동작 추가
- 동작 등록이나 아이콘 변경
- 동작 이름과 설명 정리
IntelliJ 제품의 Kotlin 코루틴 코드에서 스코프 소유자와 취소·실패의 전파 방식을 확인하고, 장시간 작업과 `Flow` 수집이 취소에 협조하는지 검토합니다. 누수나 취소 누락에 대한 구조 수정 방향을 제시합니다.
Write or review IntelliJ coroutine scope ownership and cancellation.
실행 점수 미검증
설치 방법 보기이 스킬은 IntelliJ 제품의 Kotlin 코루틴 코드에서 각 스코프를 어떤 객체가 소유하고 언제 취소하는지 정리합니다. 부모와 자식 사이에서 취소와 실패가 어떻게 전파되는지, 장시간 작업이나 Flow 수집이 취소에 협조하는지도 확인합니다. 문제가 있으면 영향 범위와 작은 구조 변경안을 제안합니다. GlobalScope, 분리된 스코프, 취소 예외를 삼키는 처리, 바깥쪽 try로 launch 실패를 잡으려는 코드도 검토 대상입니다.
코루틴을 추가하거나 작업 누수, 멈추지 않는 반복문, 취소 후 상태 불일치를 조사할 때 유용합니다. 작업의 수명을 서비스나 UI의 수명에 연결하려는 경우에도 적합합니다.
Flow 수집 코드에 취소 누락이 있는지 조사해 주세요.”대상은 IntelliJ 기반 제품의 Kotlin 코루틴 코드입니다. UI나 EDT(이벤트 디스패치 스레드)의 소유 관계와 코루틴 내부 동작에 대한 자세한 설명은 관련 스킬에서 다룹니다.
이 소개문은 공개된 SKILL.md를 바탕으로 AI(OpenAI Codex)가 작성했습니다. 정확한 사양은 아래 원문을 확인하세요.
AI가 SKILL.md의 내용에서 주의가 필요한 작업과 도입 전제를 확인했습니다. 점수는 실제로 실행한 결과(실행 검증)만으로 매깁니다.
점수에는 넣지 않았습니다. 2026년 10월 28일 기준으로 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 skill: kotlin-ui-swing-component-architecture (Swing UI and EDT ownership).
Deeper reading on the platform internals:
docs/IntelliJ-Platform/4_man/Kotlin-Coroutines/ (services, EDT
dispatchers, read actions), docs/IntelliJ-Platform/4_man/Performance/ (freezes, background write actions,
profiling) and docs/IntelliJ-Platform/4_man/Indexes/ (the indexing system, dumb-aware code).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` 클래스를 구현하거나 수정하고 `plugin.xml`에 등록하도록 안내합니다. 표시 이름과 설명은 메시지 번들에 두고, 필요하면 아이콘을 설정하며, 이벤트 코루틴 범위에서 중단 함수를 실행해 동기식 처리로 IDE가 멈추는 일을 방지합니다.
IntelliJ JPS 테스트 모듈을 Bazel로 옮기고 Bazel에서만 발생하는 실패를 조사하는 안내서입니다. 의존성, 플러그인, 테스트 데이터와 runfiles 문제를 확인하고 생성된 대상과 이전된 테스트를 검증합니다.
IntelliJ 프로젝트 규칙에 맞춰 Kotlin과 Java 코드를 작성하거나 검토합니다. 언어 선택, 서식, 봉인 타입, 스레드 주석, 제어 흐름 예외 처리를 확인합니다.
이 가이드는 IntelliJ 프로젝트의 변경 사항을 목적별로 나누고 프로젝트 형식에 맞는 커밋 메시지를 작성하도록 돕습니다. 티켓이 있으면 번호와 변경 설명을, 없으면 레이블과 하위 시스템명을 사용하며 필요에 따라 이유와 판단을 본문에 담습니다.
여러 Python 타입 검사기로 코드 조각이나 파일의 타입 추론과 오류 판정을 비교합니다. 도구별 결과, 실행 시간, 진단 내용을 Markdown 보고서로 모아 저장할 수 있습니다.
Compose Desktop 화면을 구성할 때 컴포저블은 렌더링을 맡고 프레젠터나 코디네이터는 앱 결정을 맡도록 구조를 정리합니다. 상태 소유권, Effect, 재사용 컴포넌트 API, 지연 목록 키, 애니메이션과 성능을 개별 UI 변경에 맞춰 살펴봅니다.