kotlinannotationsdslkotlin-dsl

Valid targets for @MyDslMarker annotations for Kotlin DSLs and the purpose of annotating functions and properties


Preconditions

The @DslMarker annotation can only be applied to annotation class by design. However, once our own annotation is defined (e.g., annotation class MyDslMarker), it can be applied to any target without restrictions.

In traditional examples—such as those found in the Kotlin documentation, their HTML library, other sources, Stack Overflow, and practically everywhere—the primary target for @MyDslMarker annotations is a class (or possibly an object).

I could only find one or two mentions of applying it directly to a function, and none for properties or other targets.

A good example with fun, also explaining how color styles are derived

Common Usage

@DslMarker was specifically designed to restrict implicit access to outer context receivers from within inner lambdas. It is typically applied indirectly to a class that represents a context.

If the context class belongs to a third-party library—where you are unable to apply an annotation directly to it—you would handle it as follows (simplified):

@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) // Added a target "TYPE"
@DslMarker
annotation class MyDslMarker 

// Can't apply the annotation here because it's outside our codebase
class ThirdPartyClassContext {
    fun doPrint() = println("Third-party")
}

@MyDslMarker
class OuterContext {

    // Applying to the receiver type
    fun third(init: (@MyDslMarker ThirdPartyClassContext).() -> Unit) {
        ThirdPartyClassContext().init()
        //...
    }
}

@MyDslMarker
class OuterThing

fun outerThing(init: OuterContext.() -> Unit): OuterThing {
    val context = OuterContext()
    context.init()
    return OuterThing()
    // in the real code, of course, it would be more like:
    // return OuterThing(context), 
    // but that is beside the point
}

fun main() {
    outerThing {
        third {
            doPrint()
            // As desired: 'fun third(init: (ThirdPartyClassContext).() -> Unit): Unit'
            // cannot be called implicitly in this context due to the receiver restriction
            third { }
        }
    }
}

This is a common use case and is often covered—for example, as described here.

However, these are the only two usage scenarios for @MyDslMarker that are commonly discussed, with no reliable sources addressing its application to functions, properties, or other targets.

Why This Seems Important

  1. Since it is neither prohibited nor specifically mentioned in the documentation, it is possible that using such annotations on functions, properties, or even other targets could have valid use cases, similar to the scenario of annotating a Type of ThirdPartyClass.

  2. When I apply @MyDslMarker to a method in a Context class and then use that method, it appears in purple (once again, more details about the colors here). This suggests that:

fun third(init: (@MyDslMarker ThirdPartyClassContext).() -> Unit) {
    ThirdPartyClassContext().init()
}

How it looks in the IDE

and

@MyDslMarker
fun third(init: (@MyDslMarker ThirdPartyClassContext).() -> Unit) {
    ThirdPartyClassContext().init()
}

How it looks in the IDE

are somehow different.

Final Questions

  1. Does anything actually change in the example above, apart from the color?
  2. If something does change, what are the use cases?
  3. While we could theoretically apply it to any available target (e.g., functions, properties, property getters, etc.), would this be practical, and if so, in what way?
// At a minimum, these targets are available:
@Target(
    AnnotationTarget.CLASS, 
    AnnotationTarget.TYPE, 
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY, 
    AnnotationTarget.PROPERTY_GETTER
)
@DslMarker
annotation class MyDslMarker

Unfortunately, the official documentation and related examples are, at best, sparse and uninformative.

Thank you for any insights! I believe answers to these questions will be helpful not only to me but to many others who are searching for relevant information—especially since the JetBrains team itself has not provided much clarity.



Solution

  • Does anything actually change in the example above, apart from the color?

    The coloring is just part of IntelliJ's syntax highlighting. Its purpose is just to highlight which calls are DSL method calls. It also allows you to differentiate between method calls from different DSLs, by allowing you to customise what color the DSL marker should use. You can do this by clicking on this gutter icon:

    enter image description here

    See also: Kotlin - DSL Color Style and the test data for this feature.

    While we could theoretically apply it to any available target (e.g., functions, properties, property getters, etc.), would this be practical, and if so, in what way?

    IntelliJ already makes use of this to do syntax highlighting, as I have just described. This IntelliJ feature is not just limited to functions, but also properties and object declarations. See the other files in this directory to see some examples.

    As far as the language is concerned however, only DSL markers that are applied on types have an effect (at least for now). You can find how the compiler handles this in DslMarkerUtils.kt. This file is the only file where the compiler looks for @DslMarker annotations. extractDslMarkerFqNames extracts DSL markers from a KotlinType only:

    fun extractDslMarkerFqNames(kotlinType: KotlinType): Set<FqName> {
        val result = mutableSetOf<FqName>()
        result.addAll(kotlinType.annotations.extractDslMarkerFqNames())
    
        kotlinType.getAbbreviation()?.constructor?.declarationDescriptor?.run {
            result.addAll(annotations.extractDslMarkerFqNames())
            (this as? TypeAliasDescriptor)?.run {
                result.addAll(extractDslMarkerFqNames(this.underlyingType))
            }
        }
    
        kotlinType.constructor.declarationDescriptor?.getAllSuperClassifiers()?.asIterable()
            ?.flatMapTo(result) { it.annotations.extractDslMarkerFqNames() }
    
        return result
    }
    

    You can see exactly how it finds @DslMarker annotations like @MyDslMarker:

    This method is called by another extractDslMarkerFqNames method that takes a ReceiverValue, which is then used by DslScopeViolationCallChecker.kt.