Kotlin¶
Alpha
Kotlin support is early-stage. Expect higher false-positive rates, especially on non-standard codebases. Feedback welcome — file issues for patterns that don't look right.
Kotlin's concise syntax is a feature — until it hides the cost of what's happening underneath:
val activeIds = repository.getActiveOrderIds()
orders.filter { it.id in activeIds }
.forEach { process(it) }
The in keyword is syntactic sugar for contains(). On a List, that's O(n) — called once per order. Algorilla catches it:
warning · nested-lookup · Loop amplifiers · O(orders × activeIds) → O(orders + activeIds)
Linear contains on 'activeIds' inside filter()
→ Build a HashSet/Map from 'activeIds' before the loop
↗ https://tvinke.github.io/algorilla/rules/nested-lookup
3 │ orders.filter { it.id in activeIds }
⎿ filter() over orders O(orders)
⎿ contains on 'activeIds' O(activeIds) ← bottleneck
With a handful of active orders this doesn't matter. At scale it does — and now you know it's there.
Top complexity traps in Kotlin¶
Sequential coroutine awaits¶
async { ... }.await() inside a loop defeats the purpose of coroutines. Each iteration waits for the previous one to complete — you're writing sequential code with extra steps.
for (order in orders) {
val details = async { httpClient.get("/api/details/${order.id}").body<OrderDetails>() }
.await() // blocks until this one finishes before starting the next
results.add(details)
}
warning · sequential-async-join-in-loop · O(orders·wait) → O(max-wait)
await() called inside for-each loop — sequential instead of parallel
↗ https://tvinke.github.io/algorilla/rules/sequential-async-join-in-loop
Fix: launch all coroutines first, await them together with coroutineScope and awaitAll:
val results = coroutineScope {
orders.map { order ->
async { httpClient.get("/api/details/${order.id}").body<OrderDetails>() }
}.awaitAll()
}
See sequential-async-join-in-loop.
The scope function that hides a lookup¶
Kotlin's let, run, and also make code flow nicely but can obscure what's happening inside. A let block inside filter that does a linear search looks clean and reads well — the performance cost is invisible.
val allowedCodes = getActiveCodes() // List<String>
products.filter { product ->
product.categoryCode.let { code ->
allowedCodes.contains(code) // O(n) per product
}
}
warning · nested-lookup · O(products × allowedCodes) → O(products + allowedCodes)
→ Build a HashSet/Map from 'allowedCodes' before the loop
↗ https://tvinke.github.io/algorilla/rules/nested-lookup
Fix: val allowedSet = allowedCodes.toHashSet(). See nested-lookup.
Ktor client calls in a loop¶
Same pattern as Spring's RestTemplate, but with Ktor's suspend functions. Each httpClient.get() is a full HTTP round-trip.
for (user in users) {
val profile = httpClient.get("/api/profiles/${user.id}")
.body<UserProfile>()
profiles.add(profile)
}
warning · io-in-loop · O(users·IO) → O(1·IO + users)
get() called inside for-each loop — IO per iteration
→ Batch the IO operation outside the loop, or use a bulk API
↗ https://tvinke.github.io/algorilla/rules/io-in-loop
Fix: use async to parallelize, or call a batch endpoint. See io-in-loop.
Collection chain with hidden sort¶
Kotlin's collection stdlib makes chaining look effortless. But sortedBy followed by first() is O(n log n) when minBy does it in O(n).
warning · sort-for-last · Sort abuse · O(n log n) → O(n)
→ Use minByOrNull { it.price } — avoids sorting the entire collection
↗ https://tvinke.github.io/algorilla/rules/sort-for-last
One character difference in readability, orders of magnitude difference at scale. See sort-for-last.
What Kotlin does well (and why it's tricky to scan)¶
Kotlin's collection stdlib makes everything look like a single expression. Chaining filter, map, sortedBy, groupBy reads beautifully — but each operation has a cost, and the chain hides the multiplication.
Algorilla understands Kotlin's collection operations and treats them as iteration boundaries, the same way it treats for loops in Java.
Scope functions¶
The parser knows that let, run, apply, also, and with create execution contexts but don't introduce iteration. They won't trigger false positives on loop-based rules.
Framework knowledge¶
| Framework | What Algorilla knows | Most relevant rules |
|---|---|---|
| Kotlin Coroutines | launch, async, runBlocking, Flow, Channel, Dispatchers, Mutex |
sequential-async-join-in-loop, io-in-loop |
| Ktor | Routing, client (get, post), content negotiation, WebSockets |
io-in-loop, expensive-construction |
Kotlin also inherits all Java framework knowledge — Spring, JPA, Guava, Reactor, and Apache Commons all apply.
Known limitations¶
- Type inference for chained expressions is limited compared to Java — the
inoperator example above works becauseactiveIdsis declared as aList, but complex generic chains may fall back to unknown type - Use type hints in
.algorilla.ymlas a workaround when types can't be resolved
See also¶
- All rules — the full set of patterns Algorilla detects, all 28 apply to Kotlin
- Framework support — built-in knowledge of Kotlin Coroutines and Ktor
- Configuration — type hints, suppression, severity thresholds
- Understanding the output — severity levels, the complexity chain, confidence scores