There are six kinds of checkers:
The first three kinds are typed and may be restricted to checking only a specific type of declaration/expression/type ref. To simplify working with checkers for different FIR elements, there is a number of typed typealiases:
The next kind, FirLanguageVersionSettingsChecker, is to check language version settings independently of particular code pieces.
The last kind of checker, FirControlFlowChecker, is for checkers which perform Control Flow Analysis (CFA) and is supposed to work with every declaration that has its own Control Flow Graph (CFG)
All checkers are supposed to satisfy the following contracts:
Those contracts imply the following:
object without any stateFirDeclaration and check if the declaration is a FirSimpleFunction. Just parameterize the checker itself with FirSimpleFunctionFirRegularClassChecker it will never be run for a FirAnonymousObjectFirAnonymousInitializerChecker which will be separately run for each init block in the class rather than creating a FirClassChecker which will manually iterate over each init block in this class. There are several reasons for that:@Suppress annotation between the root element (passed to the checker) and the sub-element. There is a mechanism to fix it, but it's not recommended to useAll checkers are collected in special containers, named DeclarationCheckers, ExpressionCheckers and TypeCheckers. Those containers have fields with sets of checkers for each possible type of checker of corresponding kind
There is a number of different container groups:
:compiler:fir:checkers:checkers.platform modules)-Xuse-fir-extended-checkers compiler flag. This group includes experimental and not very performant checkers, which are not crucial for regular compilationAt the beginning of the compilation, in the initialization phase, all required checker containers are collected inside a session component named CheckersComponent. When the time of checker phase comes, the compiler creates an instance of AbstractDiagnosticCollector, which is responsible to run all checkers. DiagnosticCollector traverses the whole given FIR tree, collects CheckerContext during this traversal, and runs all checkers that suite the element type on each element.
CheckerContext contains all information which can be used by checkers, including
session and scopeSessioncontainingDeclarationsCheckerContext is meant to be read-only for checkers
All diagnostics which can be reported by the compiler are stored within the FirErrors, FirJvmErrors, FirJsErrors and FirNativeErrors objects. Those diagnostics are auto-generated based on the diagnostic description in one of a diagnostic list in checkers-component-generator.
The generation is needed, because Analysis API (AA), which is used in IDE, generates a separate class for each compiler diagnostic with proper conversions of arguments for parametrized diagnostics. And the goal of the code generator is to automatically generate those classes and conversions. To run the diagnostics generation use the Generators -> Generate FIR Checker Components and FIR/IDE Diagnostics run configuration.
Diagnostic messages must be added manually to FirErrorsDefaultMessages, FirJvmErrorsDefaultMessages, FirJsErrorsDefaultMessages and FirNativeErrorsDefaultMessages respectively. Guidelines for diagnostic messages are described in the header of FirErrorsDefaultMessages
To report diagnostics, each checker takes an instance of DiagnosticReporter as a parameter. To reduce the boilerplate needed to instantiate a diagnostic from the given factory and ensure it's not missed due to reporting on the null source, a one should use the utilities from KtDiagnosticReportHelpers
In CLI mode the compiler runs checkers only after it has analyzed the whole world up to the final FIR phase (BODY_RESOLVE). But the IDE uses lazy resolve, so there can be a situation when some files have been analyzed to BODY_RESOLVE and other files have not been analyzed at all. This means that in a checker one can not rely on the fact that some FIR elements should have been resolved to some specific phase. The only exception is the following: If some element was passed directly to the checker then it is guaranteed that this element is already resolved to the BODY_RESOLVE phase. If some declaration is received somewhere from outside (from a type, a symbol provider or a scope), then it could have been resolved up to an arbitrary phase.
So, to avoid possible problems with accessing some information from FIR elements which was not yet calculated in the AA mode, there are the following restrictions and recommendations:
FirBasedSymbol<*>.fir is prohibited. One can not extract any FIR element from the corresponding symbolWhile all checkers are run after resolution of the code is finished, some diagnostics can be actually detected only during resolution, such as
And at the same time, there is a contract that FIR resolution is side effect free (not very formal but still) and produces only a resolved FIR tree. So diagnostics can not be reported from resolution directly.
To support such diagnostics, there is the following mechanism:
Error in name, like FirResolvedErrorReference) have a property which contain a ConeDiagnosticConeDiagnostic for any possible problems, see ConeDiagnostics.ktConeDiagnostic is saved in the FIR tree, and then the special checker component (ErrorNodeDiagnosticCollectorComponent) checks all FIR nodes and report proper diagnostics based on the found ConeDiagnosticIn the MPP compilation, the same type may be resolved to different classes depending on the use-site session, if this type is based on the expect classifier. This implies that the same checker may produce different results depending on the use-site session:
// MODULE: common expect interface A class B : A // MODULE: platform()()(common) actual interface A { fun foo() }
In this example class B is located in the common module, and from this module POV there is no problems with this class. But after actualization supertype A is resolved to actual interface A, which brings an abstract fun foo() into the scope, so class B becomes incorrect, as it doesn't implement this abstract function.
To cover this problem, all checkers are split into two groups: Common and Platform (see the MppCheckerKind enum)
MppCheckerKind.Common means that this checker should run from the same session to which corresponding declaration belongsMppCheckerKind.Platform means that in case of MPP compilation this checker should run with session of leaf platform module for sources of all modulesSo the author of each new checker should decide in which session this checker should run and properly set the MppCheckerKind in the checker declaration. There are some hints that may help to decide:
CommonCommon// MODULE: common expect interface A expect class B : A class C : A // MODULE: platform()()(common) actual interface A { fun foo() } actual class B : A { override fun foo() {} }
In this example we want to report “abstract foo not implemented” on class C, but we don't want to report it on expect class B (as its supertype is always expect A, never actual A)
So to cover such cases, it's worth splitting the platform checker into two parts:
Regular, which is platform checkers and runs for everything except expect declarationForExpectClass, which is common checkers and runs only for expect declarationsAs an example, check the implementation of FirImplementationMismatchChecker checker