COROUTINE_CONTEXT_WITH_JOB
Summary
- Rule ID:
COROUTINE_CONTEXT_WITH_JOB - Name: Coroutine context with Job
- Description: Detects kotlinx.coroutines builder and
withContextcalls that pass aCoroutineContextcontaining aJobelement created byJob()orSupervisorJob(), which breaks structured concurrency. - Annotation policy:
@Suppress-style suppression is unsupported. Annotation-driven semantics support JSpecify only; this rule has no annotation-driven semantics.
Motivation
Coroutine builders inherit their parent job from the receiver scope. When the caller passes a context that contains its own Job element, for example scope.launch(Job()) { ... } or withContext(SupervisorJob()) { ... }, the new coroutine is no longer a child of the scope. Cancelling the scope no longer cancels the coroutine, failures no longer propagate to the parent, and join/cancel semantics silently change.
The code compiles and often appears to work, so this defect survives code review easily. The rule mirrors the IntelliJ inspection CoroutineContextWithJob while operating on JVM bytecode from class files and JARs.
What it detects
The rule reports a builder or withContext call site in an analysis target method when all of the following hold:
- kotlinx.coroutines is visible to the scan. Otherwise the rule reports nothing.
- The method contains a call to one of the job factories, in plain or
$defaultcompiled form: Job()onkotlinx/coroutines/JobKtSupervisorJob()onkotlinx/coroutines/SupervisorKtThe factory is tracked whether or not a parent job argument is given to it.- Within the same method, the factory result reaches the
CoroutineContextargument of one of these calls, in plain or$defaultcompiled form: launchonkotlinx/coroutines/BuildersKtasynconkotlinx/coroutines/BuildersKtwithContextonkotlinx/coroutines/BuildersKtproduceonkotlinx/coroutines/channels/ProduceKtactoronkotlinx/coroutines/channels/ActorKt- The value may reach the context argument directly, through a local variable, or through
CoroutineContext.pluscombinations in either operand position, including chains such asDispatchers.IO + Job() + CoroutineName("x").
What it does NOT detect
- Job values that cross method boundaries. Jobs created in another method, received as a parameter, or read from a field are out of scope.
- Arbitrary
Job-typed values that were not produced byJob()orSupervisorJob()in the same method, including jobs obtained fromcoroutineContext[Job]or from an existing scope. - Contexts that flow into
CoroutineScope(...)construction. Creating a root scope with its own job is the intended use of the job factories and is never reported. - The
promisebuilder. It exists only on Kotlin/JS and never appears in JVM bytecode. futurefrom kotlinx-coroutines-jdk8, reactive builders from rxjava or reactor integrations, andrunBlocking.- The pattern hidden behind helper functions that wrap the builders.
- Classpath-only classes that contain the pattern but are not part of the analysis target.
- Any suppression behavior via
@Suppressor@SuppressWarnings.
Examples (TP/TN/Edge)
TP: Job passed directly to launch (reported)
Finding reported: the launched coroutine is not a child ofscope.
TP: SupervisorJob passed to async (reported)
Finding reported:SupervisorJob() detaches the coroutine from scope just like Job().
TP: Job passed to withContext (reported)
Finding reported: the block runs under a foreign job instead of the caller's job.TP: Job combined into a context with plus (reported)
Finding reported: the combined context still contains theJob element.
TP: Job stored in a local variable first (reported)
Finding reported: the flow through the local variable is tracked within the method.TN: context without a Job element (not reported)
No finding: the context contains no trackedJob element.
TN: builder call with default context (not reported)
No finding: the compiled$default form receives EmptyCoroutineContext.
TN: root scope construction (not reported)
No finding: giving a root scope its own job is the intended use of the factory.TN: job used only for lifecycle control (not reported)
No finding: the job never reaches a builder orwithContext context argument.
Edge: only the offending builder call is reported
Exactly one finding is reported, at the secondlaunch call.
Edge: channel builders (reported)
fun funOne(scope: CoroutineScope) {
scope.produce<Int>(Job()) { }
scope.actor<Int>(SupervisorJob()) { }
}
produce and the actor call are reported.
Edge: kotlinx-coroutines absent from the scan
If kotlinx.coroutines is not visible to the scan, the rule performs no analysis and reports nothing.
Output
- Report one finding per offending builder or
withContextcall site. - Message must be actionable:
<builder> call in <class>.<method><descriptor> passes a coroutine context containing a Job element created by <factory>; the coroutine will not be a child of the calling scope, so cancellation and failure propagation break. Remove the Job element from the context, or create an explicit CoroutineScope if an independent lifecycle is intended.where<builder>is one oflaunch,async,withContext,produce,actorand<factory>isJob()orSupervisorJob(). - Location is reported at the enclosing method logical location and, where available, the source line of the builder call.
Performance considerations
- When kotlinx.coroutines is not visible to the scan, the rule is a cheap no-op.
- Analysis is bounded by a single linear pass over the instructions of each analysis target method.
- No inter-procedural analysis and no exploration beyond the enclosing method.
- Traversal order must be deterministic: classes in analysis-target order, methods in declaration order, instructions in bytecode offset order. Findings are ordered by
(class, method, descriptor, builder call offset).
Acceptance criteria
- Reports
launch,async, andwithContextcalls whose context argument receives a value produced byJob()orSupervisorJob()in the same method, passed directly. - Reports the same flows when the value passes through a local variable or through
CoroutineContext.pluscombinations, including chained combinations. - Reports
produceandactorcalls under the same conditions. - Handles both plain and
$defaultcompiled forms of the factories and builders. - Does not report builder calls whose context contains no tracked job, including the default
EmptyCoroutineContextform. - Does not report
CoroutineScope(...)construction from a context containing a job. - Does not report jobs that are created but never reach a builder or
withContextcontext argument. - Reports nothing when kotlinx.coroutines is absent from the scan.
- Emits at most one finding per offending call site, and in a method with multiple builder calls reports only the offending ones.
- Covers TP, TN, and edge cases in tests.
- Produces deterministic finding order and count across repeated runs.
- Keeps
@Suppress-style suppression unsupported and does not add non-JSpecify annotation semantics.