Mutable collections in public API
MUTABLE_COLLECTION_PUBLIC_API reports mutable collection and array types in public signatures.
| Diagnostic | MUTABLE_COLLECTION_PUBLIC_API |
| Default severity | Error |
| Gradle property | mutableCollectionPublicApi |
| Exemption | @IntentionallyMutableCollection |
What it reports
Flags return types, property types, value parameter types, and type parameter bounds that mention
a mutable collection type: any of the kotlin.collections mutable interfaces (MutableList,
MutableSet, MutableMap, MutableCollection, MutableIterable, MutableIterator,
MutableListIterator, MutableMap.MutableEntry), a classifier implementing one of them
(ArrayList, a hand-written MutableList subtype, ...), or an array. Type arguments are checked
too, so a mutable type nested in an otherwise read-only container still counts:
@file:JvmName("Collections")/** Returns a caller-owned list of pending job names. */public fun produce(): MutableList<String> = mutableListOf()/** Returns batches that callers may edit in place. */public fun nested(): List<MutableList<Int>> = emptyList()
Rationale
A mutable return type or property lets users mutate a collection they don't own. A mutable parameter lets the library mutate an argument the user still holds. Either way, once a mutable collection crosses the API boundary it is unclear whose mutations are safe, and the library can no longer swap its internal representation for a different collection type without risking a behavioral change for users that relied on mutating the exposed instance. See the Kotlin guide on avoiding exposing mutable state.
Don't
/*** Exposes the values scheduled for processing.** @property items live collection of scheduled values.*/@Pokopublic class Holder(public val items: MutableList<Int>)
Do
/*** Captures the values scheduled at construction time.** @property items immutable snapshot of the scheduled values.*/@Pokopublic class Holder(items: List<Int>) {public val items: List<Int> = items.toList()}
Don't
@file:JvmName("Collections")/** Removes all scheduled item IDs from [items]. */public fun consume(items: MutableSet<Int>) {items.clear()}
Do
@file:JvmName("Collections")/** Reads scheduled item IDs from [items]. */public fun consume(items: Set<Int>) {// copy internally before mutating, if needed}
Notes
@PublishedApi internaldeclarations are not reported because their types do not cross the supported source API boundary.varargparameters are not reported themselves - the compiler already passes a defensive copy of the array - but a mutable element type still is (vararg groups: MutableList<Int>).- Extension receivers are not reported: an extension on a mutable collection serves values the user already holds, unlike a builder lambda receiver, which is reported.
- Overrides are not reported: their signature is fixed by the overridden declaration, which is reported instead.
- Java platform types are not reported: their mutability is not declared in Kotlin sources, so only the read-only upper bound is inspected.
- A type alias resolves to its expansion, and a mutable bound on a type parameter
(
<T : MutableList<Int>>) is reported the same as a direct mention of the bound.
Exemption
Apply @IntentionallyMutableCollection when sharing the mutable collection is a deliberate part of
the API contract.
@file:JvmName("Collections")/*** Exposes the values scheduled for processing as a deliberately shared collection.** @property items live collection of scheduled values.*/@Pokopublic class Holder(public val items: @IntentionallyMutableCollection(reason = ExemptionReason.API_DESIGN,) MutableList<Int>,)/** Removes all scheduled item IDs from [items]. */public fun consume(@IntentionallyMutableCollection(reason = ExemptionReason.API_DESIGN,)items: MutableSet<Int>,) {items.clear()}
Configuration
apiWatchdog {mutableCollectionPublicApi = WatchdogSeverity.WARNING}
With direct compiler invocation:
-P plugin:org.jetbrains.kotlin.library.api.watchdog:diagnosticSeverity=MUTABLE_COLLECTION_PUBLIC_API:warning
See also
- Avoid exposing mutable state
- Pair and Triple in public API, a sibling check for tuple types found by the same signature sweep.
- Exemptions and internal API