Exemptions and internal API
library-api-watchdog gives library authors two ways to opt out of its checks without disabling them altogether:
- A per-declaration
@Intentionally*annotation acknowledges that one specific hard-to-evolve shape is a deliberate choice. - An annotation like
@InternalMyLibraryApiremoves declarations from consideration because they carry no compatibility guarantees at all, regardless of their visibility, making them internal by the library contract.
Use the first one when the shape itself is the deliberate part of the API. Use the second one when a declaration is public only for technical reasons and is not meant to be supported API.
Exempting a single declaration
Each check that has an exemption annotation names it in its "Exemption" section (Example for data classes). Applying the annotation silences that one check on that one declaration (or, for annotations placed on a type parameter or type usage, on that one type).
But an exemption is not a bare escape hatch. It has to be explained.
Every @Intentionally* annotation takes a reason: ExemptionReason (default OTHER) and a
description: String (empty string by default). Exemptions reasons are divided into two groups:
FOR_BACKWARDS_COMPATIBILITYandAPI_DESIGNexplain the exemption on their own, so the description is optional.INTEROP,EXTERNAL_CONTRACT,IGNORE_JAVA_INTEROP, andOTHERonly categorize the exemption - which interop constraint, which external contract, or why this declaration in particular gets to ignore Java callers is not obvious from the entry alone - so a non-emptydescriptionis required.
A bare @IntentionallyOpen (reason left at OTHER, description left empty) explains nothing and is rejected by the
Exemptions without explanation check. That check is always an
error and can't be configured or disabled. It fires on every exemption annotation usage, even on
non-public declarations, because leaving any exemption unexplained defeats the point of exemptions.
A well-explained exemption:
/*** Legacy RPC configuration.** @property host network host serving RPC requests.* @property port TCP port exposed by [host].*/@IntentionallyDataClass(reason = ExemptionReason.INTEROP,description = "Serialized as-is by the legacy RPC layer, " +"which reflects on componentN.",)public data class LegacyConfig(public val host: String,public val port: Int,)
When adopting the watchdog on a library whose API has already shipped, the exemptions for the
existing surface don't have to be written by hand: the Gradle plugin's
updateBackwardsCompatibilityExempts task
inserts them - with the self-explanatory FOR_BACKWARDS_COMPATIBILITY reason - for every
diagnostic the current sources trigger.
One Exception
There is one exception: @IntentionallyWrongDslMarkerTargetsForBackwardsCompatibility bakes its only accepted reason into
its name and takes just an optional description. This exemption targets DSL_MARKER_NOOP_TARGET
and DSL_MARKER_WITHOUT_EXPLICIT_TARGETS, and backwards compatibility is the
only valid reason for a exemption.
All exemption annotations
Internal API annotations
Some declarations are public only because the language requires it, not because they are supported
API - reflection helpers behind an opt-in marker, shared internals, and other. Rather than
exempting every one of them individually, annotate the library's own internal-API marker annotation
with @InternalAnnotationMarker:
/** Marks declarations that are public only for technical reasons. */@InternalAnnotationMarker@RequiresOptIn(level = RequiresOptIn.Level.ERROR)public annotation class InternalMyLibraryApi@InternalMyLibraryApi // Not watched, library's internal APIpublic class ReflectionHelper
Every declaration carrying the marked annotation is no longer watched by any check, and neither is anything nested inside it.
A supported public declaration must not expose one of those internal types in its signature. The
PUBLIC_TYPE_WITH_INTERNAL_API check reports
that mismatch. Either keep the type behind the implementation boundary or mark the exposing
declaration as internal API too.
The marker annotation class itself is ordinary public API and stays watched like any other declaration, so it still needs a KDoc comment and the rest.
Note that @PublishedApi declarations are not affected by this distinction between source
visibility and API surface in the opposite direction: they are internal in source, but a public
inline function can expose them to users, so they are watched exactly like public declarations.
The exceptions are Undocumented public API, because a
declaration that stays internal in sources is never referenced by name in user code, and the
stateful class checks, which are
likewise concerned with source-facing behavior.
Where the annotations come from
Every @Intentionally* annotation, @InternalAnnotationMarker, and ExemptionReason live in the
org.jetbrains.kotlin:kotlin-library-api-watchdog-plugin-annotations artifact.
Applying the Gradle plugin adds this library as a dependency automatically - no manual dependency declaration is needed.