Skip to main content

DSL markers on no-op type positions

DSL_MARKER_NOOP_TYPE_POSITION reports a @DslMarker annotation written directly on a type position where it has no effect on scope control.

DiagnosticDSL_MARKER_NOOP_TYPE_POSITION
Default severityError
Gradle propertydslMarkerNoopTypePosition
Exemptionnone

What it reports

A @DslMarker written on a function, a property, a plain parameter type, a return type, or a property or variable type marks a value that is only ever accessed by name, thus it restricts nothing:

@file:JvmName("Branches")
// Supporting DSL declarations
/** Marks branch DSL receivers. */
@DslMarker
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.TYPE,
AnnotationTarget.TYPEALIAS,
)
public annotation class BranchDsl
/** Branch accepted by the branch-building DSL. */
public class Branch
/** Appends [branch] to the current tree. */
public fun append(branch: @BranchDsl Branch) { }

Unlike the API-surface checks, this one also fires on non-public and even internal declarations: an inert marker misleads the library's own authors just as much as its users.

Rationale

A marker in a no-op position gives none of the protection @DslMarker exists for: inside a nested builder lambda, an outer builder's members stay implicitly callable, so code can silently call the wrong scope's functions. See the Kotlin guide on scope control for DSL markers.

Don't

/** Applies [block] while constructing a tree tag. */
@JvmSynthetic
public fun configure(block: Tag.() -> Unit): @TreeDsl Unit { }

Do

/** Node whose receiver participates in tree DSL scope control. */
@TreeDsl
public class Tag
/** Applies [block] while constructing a tree tag. */
@JvmSynthetic
public fun configure(block: Tag.() -> Unit) { }

Don't

@file:JvmName("Trees")
/** Adds [tag] to the current tree. */
public fun process(tag: @TreeDsl Tag) { }

Do

@file:JvmName("Trees")
// no scope control needed for a named value
/** Adds [tag] to the current tree. */
public fun process(tag: Tag) { }

Notes

  • A context parameter's type is an implicit value just like a receiver, so a marker there is effective and not reported.
  • Markers on supertypes, type parameter bounds, and type alias expansions are effective carriers and are not reported: class Div : @TreeDsl Tag(), typealias MarkedTag = @TreeDsl Tag.
  • A marker nested inside a type argument is not analyzed at all (List<@TreeDsl Tag> triggers nothing), which is a known limitation rather than an endorsement.

Exemption

There is no @Intentionally* annotation for this diagnostic: a marker on a no-op type position should normally be moved to an effective position (a receiver, a context parameter, or a supertype) or removed. A deliberate flow-through use is the exception: a value whose type carries the marker can later become an implicit receiver through type inference (with(value) { ... }). Suppress the diagnostic on that declaration with @Suppress("DSL_MARKER_NOOP_TYPE_POSITION") when that use is intended. To silence the check project-wide, lower its severity with the Gradle property below.

Configuration

apiWatchdog {
dslMarkerNoopTypePosition = WatchdogSeverity.WARNING
}

With direct compiler invocation:

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

See also