Get started
library-api-watchdog helps library authors to detect public API declarations
that are hard to evolve. It runs as a set of Kotlin K2 compiler frontend checks in the module it is applied to.
The tool is intentionally restrictive by default: all checks are enabled and report a compilation error until demoted to a warning, disabled, or exempted in place.
What it looks like
A public data class that hands out a mutable collection, without documentation, triggers three diagnostics at once:
Diagnostics in code examples are interactive. Hover over or focus an underlined range to read the diagnostic and follow its link to the full documentation.
public data class Config(public val tags: MutableList<String>)
- The generated
copyandcomponentNfunctions and the constructor bake the exact property list into the compiled API. tagsshares a mutable collection across the API boundary: it is unclear whether user-side and library-side mutations affect each other.- Neither the class nor the property has KDoc.
The fixed version documents the declaration, exposes a read-only collection, and drops the data class
shape, while having its equals, hashCode, and toString generated by
Poko:
/*** Immutable request configuration.** @property tags Labels attached to this configuration.*/@Pokopublic class Config(public val tags: List<String>)
Quick setup
Add Kotlin and Watchdog plugins and enable explicit API mode:
plugins {kotlin("multiplatform") version "2.4.0"kotlin("library.api-watchdog") version "0.1.0-SNAPSHOT"}kotlin {explicitApi() // Required to enable checks}
The plugin also works for Kotlin/JVM only projects.
See Configuration for full configuration options and build suggestions overview.
All checks
API surface checks
Checks about how callers use the source API ignore declarations that are visible only through
@PublishedApi: those declarations remain internal and cannot be named by library users.
Checks that protect binary linkage still include them where noted.
- Data classes in public API: data classes, whose generated
copy,componentN, and constructor bake the exact property list into the compiled API. - Open API without subclass opt-in: open or abstract classes and interfaces that any outside code can subclass without restriction.
- Subclass opt-in without markers:
@SubclassOptInRequiredannotations that list no marker classes don't actually restrict subclassing. - Exhaustive public API: enums and sealed hierarchies, which users can match exhaustively, thus adding an entry or a subtype later breaks user code.
- Undocumented public API: public declarations of every kind that have no KDoc.
- Function type aliases in public API: type aliases that abbreviate function types erase from the compiled API, so the type can't evolve into a richer abstraction later.
- Stateful classes without equals, hashCode, and toString: classes with a
backing-field property that neither declare nor inherit
equals,hashCode, andtoString, so instances render as an opaque default in logs and debuggers, and comparison is reference based. - Mutable collections in public API: mutable collection types in public signatures leave it unclear whether user-side and library-side mutations affect each other.
- Pair and Triple in public API: the tuple types
PairandTriple, whose components carry no domain meaning and whose fixed shape can't evolve. - Boolean parameters in public API: Boolean value parameters,
whose positional
true/falseargument reveals nothing about its meaning at the call site. - Nullable Booleans in public API: nullable
Booleans in public signatures model three states while naming only two. - Required parameters after optional ones: required parameters declared after an optional one can't be passed positionally without restating the earlier defaults.
- Inconsistent parameter order in overloads: overloads whose same-named parameters appear in a different relative order, inviting silently swapped arguments.
- Inline functions with logic: public inline functions, whose body does more than delegate, since the compiler copies that logic, and its bugs, into every user binary.
- Public types from non-transitive dependencies:
dependency types exposed in public signatures but hidden from consumers by an
implementationdeclaration. - Public types marked as internal API: supported public signatures exposing types that explicitly carry no supported API contract. This diagnostic is always an error while its Boolean whole-check switch is enabled.
Java interop checks
These checks only run in JVM compilations, and only pay off for libraries that support Java
consumers. The whole group is disabled with javaInterop { enabled = false }. See
Java interop checks for the group overview.
- Mangled JVM names in public API: public API that Java sources can't call because a value class in the signature gets its JVM name mangled.
- Kotlin-only API without JvmSynthetic: functions whose shape only Kotlin callers can use idiomatically, yet still land in the API surface Java sources see.
- Companion API without JvmStatic: public companion object functions without
@JvmStatic, which Java callers can only reach through theCompanioninstance. - Companion constants without JvmField: constant-shaped companion object properties that Java can only read through the companion instance getter.
- Top-level API without JvmName: files whose public top-level
API compiles into a file facade class without a pinned
@file:JvmName, so renaming the file breaks Java sources and binaries built against it. - Default parameters without JvmOverloads: default parameter values without
@JvmOverloads, forcing Java callers to pass every argument.
DSL marker checks
- DSL markers with no-op targets:
@DslMarkerannotation targets on which the marker has no effect, giving a false sense of scope control. - DSL markers without explicit targets:
@DslMarkerannotations without an explicit@Target, whose default target set allows mostly no-op targets. - DSL markers on no-op type positions: DSL markers written on type positions where scope control doesn't react to them.
Meaningful exemptions
- Exemptions without explanation: an
@Intentionally*exemption annotation left with the defaultOTHERreason and a blank description explains nothing. This diagnostic is always an error and can't be configured like the others.