In a nested type-safe builder, why can inner blocks accidentally call outer-receiver methods, and how does `@DslMarker` fix it?
answer
- Outer receivers stay implicit -> scope leak
- @DslMarker is a meta-annotation
- Annotate all receiver types of one DSL
- Only nearest same-marker receiver is implicit
- Explicit this@outer still works; compile-time only
basics
~20 sBecause outer receivers stay in scope inside nested blocks, you can mistakenly call an outer builder's method from a deeply nested one. @DslMarker tells the compiler to hide outer receivers of the same DSL unless you name them explicitly.
solid answer
~40 sIn nested builders, every enclosing receiver remains an implicit receiver, so inside `row { cell { ... } }` you can still call `Table`'s members — sometimes nonsensically (e.g. starting a new `row` from inside a `cell`). This is the classic DSL scope-leak. `@DslMarker` solves it: you define an annotation annotated with `@DslMarker` and apply it to your builder receiver types (`@HtmlDsl class Table`, `@HtmlDsl class Row`). The compiler then enforces that, within a block, **only the nearest implicit receiver of each marked DSL is accessible implicitly**; outer same-marker receivers are shadowed and require an explicit qualified `this@table`. This catches structural mistakes at compile time and keeps autocomplete focused on the valid level. It's a compile-time restriction only — no runtime behavior changes.
code
kotlin · 12 lines@DslMarker
annotation class HtmlDsl
@HtmlDsl class Table { fun row(init: Row.() -> Unit) {} }
@HtmlDsl class Row { fun cell(init: Cell.() -> Unit) {} }
table {
row {
// row { } // error: outer Table receiver shadowed
this@table.row { } // ok: explicit access still allowed
}
}go deeper
May not know the leak exists; can recognize that nesting brings outer methods into scope.
Explains the scope-leak and that an annotation fixes it, with rough mechanics.
Defines a @DslMarker annotation correctly, states the nearest-receiver rule, and the explicit-this@ escape hatch.
Treats scope-safety as a design requirement for multi-level DSLs, cites kotlinx.html/Gradle, and weighs marker granularity across composed DSLs.
## The problem: receiver leakage Nested builders stack receivers. Inside the innermost block, **all** enclosing receivers are still implicit `this` candidates: ```kotlin class Table { fun row(init: Row.() -> Unit) {} } class Row { fun cell(init: Cell.() -> Unit) {} } class Cell { fun text(s: String) {} } table { row { cell { row { } // OOPS: Table.row is still in scope here, so this compiles } } } ``` Starting a new `row` from inside a `cell` is meaningless, but because `Table` is an outer implicit receiver, the call resolves and compiles. This **scope-leak** undermines the safety the builder was supposed to give. ## The fix: `@DslMarker` `@DslMarker` is a meta-annotation in the standard library. You create your own marker annotation and apply it to all receiver types of one DSL: ```kotlin @DslMarker annotation class HtmlDsl @HtmlDsl class Table { fun row(init: Row.() -> Unit) {} } @HtmlDsl class Row { fun cell(init: Cell.() -> Unit) {} } @HtmlDsl class Cell { fun text(s: String) {} } ``` Now the compiler enforces the rule: **within any block, for receivers sharing the same `@DslMarker`, only the closest one is available as an implicit receiver.** Outer same-marker receivers are *shadowed*. ```kotlin table { row { cell { row { } // COMPILE ERROR: outer Table receiver is shadowed [email protected] { } // OK: explicit qualified access still allowed } } } ``` ## What `@DslMarker` does and doesn't do - **Compile-time only**: it restricts implicit-receiver resolution. There is no runtime cost or behavior change. - **Per marker**: receivers with *different* markers don't shadow each other; the restriction applies within one DSL's marker. - **Escape hatch preserved**: you can always reach an outer receiver via labeled `this@outer`, so legitimate cross-level access is still possible — just explicit. - **Improves tooling**: IDE autocomplete now shows only the relevant level's members. ## Why seniors care Without `@DslMarker`, a builder is *type*-safe but not *scope*-safe: it prevents wrong types, not wrong nesting. Adding the marker is the standard step that makes a multi-level Kotlin DSL robust, and it's exactly how `kotlinx.html` and Gradle Kotlin DSL constrain their receivers.
- Does `@DslMarker` change runtime behavior or add cost?No. It only restricts which implicit receivers the compiler accepts; generated code and runtime behavior are unchanged.
- After applying `@DslMarker`, can you ever call an outer builder's method from an inner block?Yes, but only explicitly via a labeled receiver like `[email protected] { }`. Implicit (unqualified) access is blocked.
Like nested rooms where, by default, you can reach through every doorway at once; @DslMarker shuts the outer doors so you only touch the room you're in — unless you deliberately reach back.
saying these in an interview costs you the question
- Thinking type-safe builders are automatically scope-safe without `@DslMarker`
- Believing `@DslMarker` blocks outer access entirely (it allows explicit `this@`)
- Applying the marker to only some receiver types of the DSL
- Claiming it has a runtime cost