Mobile Development 13 min read

Understanding Compose Compiler Checkers: ComposableCallChecker, ComposableDeclarationChecker, and ComposeDiagnosticSuppressor

This article explains how the Compose compiler’s front‑end checkers—ComposableCallChecker, ComposableDeclarationChecker, and ComposeDiagnosticSuppressor—validate @Composable usage, handle inline lambdas, annotation retention, and named‑argument restrictions, providing code examples and visual diagrams to illustrate each rule.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Understanding Compose Compiler Checkers: ComposableCallChecker, ComposableDeclarationChecker, and ComposeDiagnosticSuppressor

The previous article introduced many extensions in the Compose Compiler, most of which are front‑end checkers that perform compile‑time validation of Compose code. The three main checkers covered here are ComposableCallChecker , ComposableDeclarationChecker , and ComposeDiagnosticSuppressor .

ComposableCallChecker

ComposableCallChecker verifies that calls to @Composable functions are legal. The checker works on PSI nodes using a visitor pattern; when a CALL_EXPRESSION node is encountered, the check method climbs the parent hierarchy to determine whether the call is inside a composable context. If the parent FUN node lacks the @Composable annotation, an error is reported.

The core implementation looks like this:

open class ComposableCallChecker : CallChecker, AdditionalTypeChecker, StorageComponentContainerContributor {
    // ...
    override fun check(
        resolvedCall: ResolvedCall<*>,
        reportOn: PsiElement,
        context: CallCheckerContext
    ) {
        if (!resolvedCall.isComposableInvocation()) return
        // Traverse parent nodes
        loop@ while (node != null) {
            when (node) {
                is KtFunction -> {
                    val descriptor = bindingContext[BindingContext.FUNCTION, node]
                    if (descriptor == null) {
                        illegalCall(context, reportOn)
                        return
                    }
                    val composable = descriptor.isComposableCallable(bindingContext)
                    if (!composable) {
                        illegalCall(context, reportOn, node.nameIdentifier ?: node)
                    }
                    return
                }
                // other cases ...
            }
            node = node.parent as? KtElement
        }
    }
}

When the call occurs inside a lambda, the checker also determines whether the lambda is inline . Inline lambdas may omit @Composable as long as the call site is composable; otherwise, an error is emitted.

@DisallowComposableCalls

If a lambda parameter is annotated with @DisallowComposableCalls , the checker records the lambda as incapable of capturing composable calls and reports an error if a composable is invoked inside it.

val arg = getArgumentDescriptor(node.functionLiteral, bindingContext)
if (arg?.type?.hasDisallowComposableCallsAnnotation() == true) {
    context.trace.record(ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE, descriptor, false)
    context.trace.report(ComposeErrors.CAPTURED_COMPOSABLE_INVOCATION.on(reportOn, arg, arg.containingDeclaration))
    return
}

ComposableDeclarationChecker

ComposableDeclarationChecker ensures that the placement of @Composable is valid. It checks function declarations, type parameters, property getters, and other allowed targets. It also forbids illegal combinations such as main functions, suspend functions, and mismatched overrides.

if (hasComposableAnnotation && descriptor.name.asString() == "main" && MainFunctionDetector(...).isMain(descriptor)) {
    context.trace.report(COMPOSABLE_FUN_MAIN.on(declaration.nameIdentifier ?: declaration))
}
if (descriptor.isSuspend && hasComposableAnnotation) {
    context.trace.report(COMPOSABLE_SUSPEND_FUN.on(declaration.nameIdentifier ?: declaration))
}
if (descriptor.overriddenDescriptors.isNotEmpty()) {
    val override = descriptor.overriddenDescriptors.first()
    if (override.hasComposableAnnotation() != hasComposableAnnotation) {
        context.trace.report(ComposeErrors.CONFLICTING_OVERLOADS.on(declaration, listOf(descriptor, override)))
    }
}

The checker also validates function‑type parameters, preventing a type from being both suspend and @Composable :

if (type.hasComposableAnnotation() && type.isSuspendFunctionType) {
    context.trace.report(COMPOSABLE_SUSPEND_FUN.on(element))
}

ComposeDiagnosticSuppressor

ComposeDiagnosticSuppressor extends DiagnosticSuppressor and selectively suppresses diagnostics that are irrelevant for Compose code. Two notable suppressed errors are:

NON_SOURCE_ANNOTATION_ON_INLINED_LAMBDA_EXPRESSION : Normally raised when a source‑retention annotation is placed on an inline lambda, but Compose allows @Composable on inline lambdas because the call site is already composable.

NAMED_ARGUMENTS_NOT_ALLOWED : Kotlin forbids named arguments for function‑type parameters, but when the function type is @Composable , the error is suppressed to accommodate Compose’s DSL style.

Implementation example for the first case:

override fun isSuppressed(diagnostic: Diagnostic, bindingContext: BindingContext?): Boolean {
    if (diagnostic.factory == Errors.NON_SOURCE_ANNOTATION_ON_INLINED_LAMBDA_EXPRESSION) {
        for (entry in (diagnostic.psiElement.parent as KtAnnotatedExpression).annotationEntries) {
            if (bindingContext != null) {
                val annotation = bindingContext.get(BindingContext.ANNOTATION, entry)
                if (annotation != null && annotation.isComposableAnnotation) return true
            } else if (entry.shortName?.identifier == "Composable") return true
        }
    }
    // other suppression logic ...
    return false
}

By suppressing these diagnostics, Compose provides a smoother developer experience, allowing inline composable lambdas and named arguments in composable DSLs without unnecessary compile‑time noise.

Overall, the article gives a concise overview of the front‑end responsibilities of the Compose compiler. Future articles will explore the back‑end code generation phase.

AndroidcompilerKotlinComposeCheckerComposable
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.