Skip to main content

Mutable collections in public API

MUTABLE_COLLECTION_PUBLIC_API reports mutable collection and array types in public signatures.

DiagnosticMUTABLE_COLLECTION_PUBLIC_API
Default severityError
Gradle propertymutableCollectionPublicApi
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.
*/
@Poko
public class Holder(public val items: MutableList<Int>)

Do

/**
* Captures the values scheduled at construction time.
*
* @property items immutable snapshot of the scheduled values.
*/
@Poko
public 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 internal declarations are not reported because their types do not cross the supported source API boundary.
  • vararg parameters 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.
*/
@Poko
public 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