FIR Plugin API

Right now, FIR provides five different extensions, which may be used for different purposes. Most of them use a special predicate-based API for accessing declarations, so firstly there will be an explanation of those predicates and only then each extension point will be explained

Annotations and predicates

Generally, FIR compiler supports search for declarations only with a specific classId/callableId. On the other hand, plugins usually want to perform some global lookup, because they don't know actual names of declarations in user code. To solve this problem, FIR plugin API provides a way to quickly lookup for specific declarations in the user code based on some specific predicate. Right now, the only way to communicate between user code and plugin is to mark something in code with some annotation, so the plugin will look at this annotation and do its magic. And for that, FIR provides a predicate API.

Note: there are plans to design some new syntax for passing information from code to plugins instead of annotations, because they have some problems and limitations due to the compiler design reasons.

There is a special service in FIR named FirPredicateBasedProvider. It allows to find all declarations in compiled code which match some DeclarationPredicate.

There are multiple types of predicates and each one has DSL functions to create them (in parenthesis):

  • AnnotatedWith matches all declarations which have on of annotations passed to it (has(vararg annotations: FqName))
  • UnderAnnotatedWith matches all declarations which are declared inside class, which is annotated with on of annotations passed to it (under(vararg annotations: FqName))
  • AnnotatedWithMeta and UnderMetaAnnotated -- same but for meta annotations (metaHas(vararg annotations: FqName) and metaUnder(vararg annotations: FqName))
  • And and Or are predicates for combining other types of predicates

There are also functions hasOrUnder and metaHasOrUnder as shortcuts for has(...) or under(...) and metaHas(...) or metaUnder(...)

Those predicates take fully qualified names of annotation classes and those annotations must be part of the plugin (shipped with it).

Meta annotations are annotations which users can use to mark their own annotations and then use them to mark declarations.

Example

// my-super-plugin-annotations.jar
package my.super.plugin

annotation class MyAnnotation
annotation class MyMetaAnnotation

// user code
package test

import my.super.plugin.*

@MyMetaAnnotation
annotation class UserAnnotation

@MyAnnotation
class A {
    fun foo() {}
}

@UserAnnotation
class B {
    fun foo() {}
}

// my-super-plugin code
...
val provider = session.predicateBasedProvider
val myAnn = "my.super.plugin.MyAnnotation".toFqn()
val myMeta = "my.super.plugin.MyMetaAnnotation".toFqn()

provider.getSymbolsByPredicate(has(myAnn)) // [test.A]
provider.getSymbolsByPredicate(under(myAnn)) // [test.A.foo]
provider.getSymbolsByPredicate(hasOrUnder(myAnn)) // [test.A, test.A.foo]

provider.getSymbolsByPredicate(has(myMeta)) // [test.B]
provider.getSymbolsByPredicate(under(myMeta)) // [test.B.foo]
provider.getSymbolsByPredicate(hasOrUnder(myMeta)) // [test.B, test.B.foo]

Important: if you want to use some predicates in your plugin then you need to explicitly declare them in the method FirExtension.registerPredicates. If you don't do this then there is no guarantee that predicate based provider will find annotated declarations even they match with predicate. This limitation exists because predicate based provider builds an index of declarations based on all registered predicates at ANNOTATIONS_FOR_PLUGINS, which allows it to lookup for declarations with specific predicate (which was already indexed) for almost O(1).

Also, there are two limitations about annotations which can be used for plugins:

  1. Only top-level annotations can be used in predicates. This limitation exists because plugin annotations are resolved before SUPERTYPES stage, so compiler won‘t be able to resolve access to annotation in some class if it is defined as nested annotation of it’s supertype
open class Base {
    annotation class Ann
} 

class Derived : Base() { // supertype Base not resolved yet
    @Ann // can not be resolved at plugin annotation phase
    fun foo() {}
}
  1. Despite that plugin annotations are resolved in very early stage, their arguments will be resolved only on ARGUMENTS_OF_ANNOTATIONS phase, so you can not rely on the fact that all arguments are resolved in some extensions, which work before ARGUMENTS_OF_ANNOTATIONS phase.

Extensions

All extensions to FIR compiler are inheritors of FirExtension. It has only one method, which can be overridden in custom extensions (registerPredicates) which was explained before.

For registering FIR extension you need to implement and register just one extension point named FirExtensionRegistrar. It has one method to implement (configurePlugin) in which you need register all your FIR extensions, using special DSL syntax:

class MySuperFirRegistrar : FirExtensionRegistrar() {
    override fun ExtensionRegistrarContext.configurePlugin() {
        // there is a unaryPlus function on callable reference to extension constructor
        //   which registers specific extension
        +::MyFirstFirExtension
        +::MySecondFirExtension
        +::MyFirCheckersExtension
    }
}

This extension point can be registered in compiler like all other existing extension points, using -Xplugin CLI argument or via META-INF.services.

FirSupertypeGenerationExtension

abstract class FirSupertypeGenerationExtension(session: FirSession) : FirExtension(session) {
    abstract fun needTransformSupertypes(declaration: FirClassLikeDeclaration): Boolean

    abstract fun computeAdditionalSupertypes(
        classLikeDeclaration: FirClassLikeDeclaration,
        resolvedSupertypes: List<FirResolvedTypeRef>
    ): List<FirResolvedTypeRef>
}

FirSupertypeGenerationExtension is an extension which allows you to add additional super types to classes and interfaces. This extension is called at SUPERTYPES stage right after types of some class (classLikeDeclaration) are resolved (resolvedSupertypes) but not written to class itself yet. Note that you can not modify explicitly declared classes, only add new one.

For example, if computeAdditionalSupertypes returned some [C, D] list for class Base : A, B, then class Base will have four supertypes: Base : A, B, C, D.

Also note that computeAdditionalSupertypes will be called only if needTransformSupertypes returned true for specific class.

FirStatusTransformerExtension

abstract class FirStatusTransformerExtension(session: FirSession) : FirExtension(session) {
    abstract fun needTransformStatus(declaration: FirDeclaration): Boolean

    open fun transformStatus(
        status: FirDeclarationStatus,
        declaration: FirDeclaration
    ): FirDeclarationStatus {
        return status
    }
    
    ...
    // overrides of `transformStatus` for different types of declarations
    ...
}

FirStatusTransformerExtension allows you to transform declaration status (visibility, modality, modifiers) for any non-local declaration. This extension called during STATUS phase right before inference of actual status from overrides. In transformStatus you may return new status with, for example, changed default modality. transformStatus will be called only if needTransformStatus returns true for specific declaration.

Example:

/*
 * transformStatus(function, status) {
 *   val newVisibility = if (status.visibility == Unknown) Public else status.visibility
 *   return status.withVisibility(newVisbility)
 * }
 */

abstract class Base {
    protected abstract fun foo()
}

@AllMembersPublic
class Derived : Base() {
    // without plugin `foo` is `protected` by default, 
    override fun foo() {}
}
/*
 * Status of Derived.foo before resolution:
 *   (visiblity = Unknown, modality = null, isOverride = true)
 * Status after resolution without plugin:
 *   (visiblity = Protected, modality = Final, isOverride = true)
 *
 * Status before resolution after plugin transformation:
 *   (visiblity = Public, modality = null, isOverride = true)
 * Status after resolution with plugin:
 *   (visiblity = Public, modality = Final, isOverride = true)
 */

Important: don‘t change visibility of classifiers (classes, objects, typealiases). This is not supported (and won’t be), and in future this won't be allowed by API itself or at least using assertions

FirDeclarationGenerationExtension

abstract class FirDeclarationGenerationExtension(session: FirSession) : FirExtension(session) {
    // Can be called on SUPERTYPES stage
    open fun generateClassLikeDeclaration(classId: ClassId): FirClassLikeSymbol<*>? = null

    // Can be called on STATUS stage
    open fun generateFunctions(callableId: CallableId, owner: FirClassSymbol<*>?): List<FirNamedFunctionSymbol> = emptyList()
    open fun generateProperties(callableId: CallableId, owner: FirClassSymbol<*>?): List<FirPropertySymbol> = emptyList()
    open fun generateConstructors(owner: FirClassSymbol<*>): List<FirConstructorSymbol> = emptyList()

    // Can be called on IMPORTS stage
    open fun hasPackage(packageFqName: FqName): Boolean = false
  
    // Can be called after SUPERTYPES stage
    open fun getCallableNamesForClass(classSymbol: FirClassSymbol<*>): Set<Name> = emptySet()
    open fun getNestedClassifiersNames(classSymbol: FirClassSymbol<*>): Set<Name> = emptySet()
    open fun getTopLevelCallableIds(): Set<CallableId> = emptySet()
    open fun getTopLevelClassIds(): Set<ClassId> = emptySet()
}

FirDeclarationGenerationExtension is an extension for generating new declarations (classes, functions, properties). Unlike SyntheticResolveExtension, which generated all members and nested classes for specific class at once, FirDeclarationGenerationExtension have provider-like API: compiler came to extensions with some specific classId or callableId and plugin generates declaration(s) with this ID if it is needed.

Contracts and usage:

  • generate... functions will be called only if get...Names/get...Ids already returned corresponding ID.
  • Results of all functions are cached, so it's guaranteed that each function in extension with specific arguments will be called only once
  • Different methods of extensions can be called for the first time on different compiler stages (see comments), so be careful with that. This means that, for example, if you observe some class in generateClassLikeDeclaration then this class can have unresolved super types
  • All declarations you return in generate... methods should be fully resolved: status should be FirResolvedDeclarationStatus, all type refs should be FirResolvedTypeRef, resolvePhase field should be set to FirResolvePhase.BODY_RESOLVE
  • There is no need to generate body for functions and initializers for properties. They all can be filled in backed IR via IrGenerationExtension
  • If you generate some class using generateClassLikeDeclaration then you don‘t need to fill it’s declarations. Instead of that you need to generate members via generateProperties/Functions/Constructors methods of same generation extension (this is important note if you have multiple generation extensions in your plugin)
  • If you want to generate constructor in class (from source or generated) then you need to add SpecialNames.INIT in getCallableNamesForClass for this class
  • If you want to generate companion object in some class (with classId outerClassId) then you need to return outerClassId.Companion class id from generateClassLikeDeclaration for this class
  • All generated declarations will be automatically converted to backend IR, so there is no need to manually generate IR declarations and replace references in whole module. All you need is just fill bodies of generated declarations
  • For generated declarations you need to set special origin named FirDeclarationOrigin.Plugin. This origin takes object key: FirPluginKey, which will be saved in IrPluginDeclarationOrigin in IR for generated declaration, so you can use that key to pass data from frontend to backend (there are plans to migrate this mechanism to IR declaration attributes, but right now they don't exist)

FirAdditionalCheckersExtension

abstract class FirAdditionalCheckersExtension(session: FirSession) : FirExtension(session) {
    open val declarationCheckers: DeclarationCheckers = DeclarationCheckers.EMPTY
    open val expressionCheckers: ExpressionCheckers = ExpressionCheckers.EMPTY
    open val typeCheckers: TypeCheckers = TypeCheckers.EMPTY
}

FirAdditionalCheckersExtension is used to enable some additional checkers which can report diagnostics. There are three main kinds of checkers (for declarations, expressions and types) and multiple types of checkers for each specific type of declaration/expression/typeRef in every kind. All you need is just declare all those checkers inside extension

FirTypeAttributeExtension

WIP

Other extensions

There are plans to implement some more extensions which allow to

  • declare new kinds of contracts
  • modify bodies of functions
    • change return types and resolved references of function calls (and other resolvable entities)
    • replace some expressions with new ones

IDE integration

The whole FIR plugin API is designed in a way which provides IDE supports for plugins out of box, so there won't be any need to right separate IDE plugin for each compiler plugin. IDE integration right now in active development and shows very impressive results, but it is not ready for preview right now.

Examples