Skip to main content

DSL markers with no-op targets

DSL_MARKER_NOOP_TARGET reports an explicit @Target entry on a @DslMarker annotation that names a target the marker has no effect on.

DiagnosticDSL_MARKER_NOOP_TARGET
Default severityError
Gradle propertydslMarkerNoopTarget
Exemption@IntentionallyWrongDslMarkerTargetsForBackwardsCompatibility

What it reports

The check only looks at @DslMarker-annotated annotation classes that declare an explicit @Target with noop targets.

/** Marks DSL receivers. */
@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
public annotation class MyDsl

Rationale

@DslMarker exists to make type-safe builder receivers unambiguous by hiding outer receivers from an inner scope. That mechanism only looks at the marker's placement on a class, a type, or a type alias. It misleads callers into thinking annotating a function or a property also scopes something, and it misleads the marker's author into thinking the surface is narrower than it is. See the DSL marker design note and the Kotlin docs on scope control with @DslMarker.

Don't

// This is the shape that broke Ktor's @KtorDsl (KTOR-8901).
/** Marks Ktor DSL receivers. */
@DslMarker
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.TYPEALIAS,
AnnotationTarget.TYPE,
AnnotationTarget.FUNCTION,
)
public annotation class KtorDsl

Do

/** Marks Ktor DSL receivers. */
@DslMarker
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.TYPE,
AnnotationTarget.TYPEALIAS,
)
public annotation class KtorDsl

Notes

  • ANNOTATION_CLASS is also effective because it is a classifier declaration, like CLASS.
  • Marker visibility is irrelevant: an internal or private marker is still applied across the library's - possibly public - DSL classes, so markers of any visibility are checked.
  • A plain annotation class without @DslMarker is outside the scope of this check, regardless of its @Target.
  • A marker without an explicit @Target is covered by the separate, related DSL_MARKER_WITHOUT_EXPLICIT_TARGETS, because the default target set has its own no-op entries plus forbids TYPE/TYPEALIAS.

Exemption

For a marker that already shipped with a no-op target, narrowing @Target rejects user code that applied the marker there - a breaking change. Acknowledge the legacy shape instead:

/** Marks Ktor DSL receivers with targets retained for compatibility. */
@IntentionallyWrongDslMarkerTargetsForBackwardsCompatibility
@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
public annotation class KtorDsl

Wrong marker targets are never good API design, so this annotation bakes its only accepted reason - backwards compatibility - into its name: it takes no reason parameter, just an optional description for extra context. New DSL markers should declare effective targets instead of reaching for this exemption.

Configuration

apiWatchdog {
dslMarkerNoopTarget = WatchdogSeverity.WARNING
}

With direct compiler invocation:

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

See also