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
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 predicatesThere 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.
// 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:
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 supertypeopen class Base { annotation class Ann } class Derived : Base() { // supertype Base not resolved yet @Ann // can not be resolved at plugin annotation phase fun foo() {} }
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.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
.
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.
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
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.generateClassLikeDeclaration
then this class can have unresolved super typesgenerate...
methods should be fully resolved: status
should be FirResolvedDeclarationStatus
, all type refs should be FirResolvedTypeRef
, resolvePhase
field should be set to FirResolvePhase.BODY_RESOLVE
IrGenerationExtension
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)SpecialNames.INIT
in getCallableNamesForClass
for this classouterClassId
) then you need to return outerClassId.Companion
class id from generateClassLikeDeclaration
for this classorigin
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)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
WIP
There are plans to implement some more extensions which allow to
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.