Intermediate Sequence Methods Left Unused
ID |
kotlin.intermediate_sequence_methods_left_unused |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Kotlin |
Tags |
reliability |
Description
Reports a standalone statement whose outermost call is a lazy Sequence intermediate operation —
map, filter, onEach, take, drop, sortedBy, distinct, flatMap and friends — with
nothing chained after it.
A Sequence is lazy. Its intermediate operations only record what to do; not a single element
moves until a terminal operation (toList, count, first, forEach, sum, joinToString,
fold, associate, …) pulls elements through the pipeline. A pipeline whose result is
discarded therefore never runs at all, and any side effect the author expected from a lambda
passed to it never happens.
Rationale
fun publish(events: Sequence<Event>) {
events.onEach { audit.record(it) } // nothing is ever recorded
}
fun normalise(names: List<String>) {
names.asSequence().map { it.trim() } // the whole pipeline never runs
}
Both statements compile, run without error, and do nothing. The first is the more dangerous shape:
onEach reads as "for each", so the audit call looks like it happens, and the bug surfaces later
as missing records rather than as a failure at the call site.
The same shape on an eager collection is not this defect and is not reported. names.map { .. }
on a List really does run the lambda and merely throws the resulting list away — wasteful, but
not a no-op. That distinction is what the rule checks: it reports only where the pipeline is
proven lazy, either because the receiver is declared as a Sequence or because the chain contains
a Sequence factory (asSequence(), sequenceOf(..), generateSequence(..), lineSequence()).
A materialising call in the middle of a chain ends the lazy region, so
names.asSequence().toList().map { .. } is left alone: its map runs on a List.
Only operations known to return a Sequence are matched. A project’s own extension function on
Sequence is not reported, since there is no way to tell from its name whether it consumes the
sequence.
Remediation
Add the terminal operation the pipeline is missing — the one that says what the result is for.
fun publish(events: Sequence<Event>) {
events.forEach { audit.record(it) } // a terminal operation: it runs
}
fun normalise(names: List<String>): List<String> =
names.asSequence().map { it.trim() }.toList()
When the pipeline exists purely for its side effects, forEach is the terminal operation to
reach for, and onEach should be reserved for the middle of a chain that some later terminal
call consumes. When the result is genuinely not needed, delete the statement rather than leaving
a pipeline that looks like it does something.