Skip to main content

Subclass opt-in without markers

SUBCLASS_OPT_IN_WITHOUT_MARKERS reports @SubclassOptInRequired annotations that list no marker classes.

DiagnosticSUBCLASS_OPT_IN_WITHOUT_MARKERS
Default severityError
Gradle propertysubclassOptInWithoutMarkers
Exemptionnone, replace with @IntentionallyOpen

What it reports

markerClass is a vararg parameter, so @SubclassOptInRequired compiles fine with zero arguments. The annotation restricts nothing in this case: the class or interface stays open to external subclassing exactly as if it were unannotated.

/** Base type for application extensions. */
@SubclassOptInRequired
public abstract class ExtensionPoint

Rationale

@SubclassOptInRequired exists so a library can add abstract members or otherwise change a contract later, because every external subclasser had to explicitly opt in to that instability first. An annotation with no marker doesn't protect against this. Any external class can extend the type without acknowledging anything, so the library keeps the evolution risk it meant to opt out of. See the opt-in requirements guide for the intended pattern.

Don't

/** Establishes communication with a remote service. */
@SubclassOptInRequired
public abstract class Connector

Do

/** Marks unstable API. */
@RequiresOptIn
public annotation class UnstableApi
/** A connector implemented under an opt-in contract. */
@SubclassOptInRequired(UnstableApi::class)
public abstract class Connector
/** A plugin implemented under an opt-in contract. */
@SubclassOptInRequired(UnstableApi::class)
public interface Plugin

Notes

  • A class or interface that is not open to external subclassing in the first place (a final class, a class whose constructors are all internal or private, or a sealed interface) is outside the scope of this check regardless of what @SubclassOptInRequired lists.
  • Multiple marker classes are allowed, and each further constrains who may subclass.

Exemption

There is no @Intentionally* annotation for this diagnostic: an unmarkered @SubclassOptInRequired never restricts anything, so keeping it as-is is never a valid design choice. Fix it by listing at least one marker class in @SubclassOptInRequired.

To exempt this check for binary compatibility reasons, replace the @SubclassOptInRequired with @IntentionallyOpen:

/** A connector kept unrestricted for compatibility. */
@IntentionallyOpen(
reason = ExemptionReason.FOR_BACKWARDS_COMPATIBILITY,
)
public abstract class Connector

updateBackwardsCompatibilityExempts performs exactly this replacement: it drops the markerless @SubclassOptInRequired and puts @IntentionallyOpen in its place.

Configuration

apiWatchdog {
subclassOptInWithoutMarkers = WatchdogSeverity.WARNING
}

With direct compiler invocation:

-P plugin:org.jetbrains.kotlin.library.api.watchdog:diagnosticSeverity=SUBCLASS_OPT_IN_WITHOUT_MARKERS:warning

See also