Contributing to Metro¶
Metro welcomes contributions! Small contributions like documentation improvements, small obvious fixes, etc are always good and don’t need prior discussion. I liberally leave TODO comments in code that don’t quite meet the standard of an issue but are still things worth improving :). For larger functionality changes or features, please raise a discussion or issue first before starting work.
Development¶
Local development with Metro is fairly straightforward. You should be able to clone the repo and open it in IntelliJ as a standard Gradle project.
There are a few primary subprojects to consider.
:compiler
— Metro’s compiler plugin implementation lives. This includes compiler-supported interop features too.:runtime
— Metro’s core multiplatform runtime API. This is mostly annotations plus some small runtime APIs.:integration-tests
— self-explanatory.:interop-dagger
— An ancillary set of JVM-only Dagger-specific runtime APIs for interop with Dagger.:gradle-plugin
— Metro’s companion Gradle plugin implementation. Mostly just an extension API and compiler plugin wiring with KGP.samples/
— A separate gradle project that contains several sample projects. This includes the core artifacts as an included build. You can add this project in IntelliJ as another Gradle project to support developing both.:compiler-tests
— Compiler tests using JetBrains’ official compiler testing infrastructure.
There is a useful ./metrow
helper CLI that can perform a few common commands across the various subprojects. Before submitting a PR, it is useful to run the following:
./metrow regen
— This regenerates.api
files and runs all code formatters../metrow check
— This runs checks across all included Gradle projects (including samples and the Gradle plugin).
Testing¶
Tests are spread across a few areas.
compiler-tests/
— New compiler tests using JetBrains’ official compiler testing infrastructure. If possible, write new compiler tests in here! See this PR for more details on how they work.compiler/src/test/
— Core compiler tests. These should be focused primarily on error testing but can also perform limited functional testing. Note that while many tests are here, new tests should ideally usecompiler-tests
.integration-tests/
— Integration tests. These should only be functional in nature and not test error cases (error cases won’t compile!). Note that new integration tests should usually be written incompiler-tests
. Some scenarios, such as multi-compilation tests across Gradle, may make more sense to write here.samples/
— Some samples have tests! This is useful to assert that these samples work as expected.
Compiler Plugin Design¶
The compiler plugin is implemented primarily in two parts.
1. FIR¶
The FIR frontend generates declarations, generates supertypes, and performs diagnostic checks for Metro types. Any class or callable declaration generated by Metro should be done here as this is required for them to be visible in Kotlin metadata later.
Generators go in the dev.zacsweers.metro.compiler.fir.generators
package.
Checkers go in the dev.zacsweers.metro.compiler.fir.checkers
package.
New checker contributions are generally welcome. New generators almost always warrant prior discussion first!
2. IR¶
The IR backend performs two main functions:
- Implements declarations generated in FIR. This includes generated graphs, factories, member injectors, etc.
- Performs dependency graph construction and validation. This is primarily spread across
DependencyGraphTransformer
,BindingGraph
, andBinding
.
Most of this is implemented as transformers in the dev.zacsweers.metro.compiler.ir.transformers
package. Note that all transformers are run from the DependencyGraphTransformer
, which is the only true IrTransformer
of the bunch and just delegates out to the other transformers as needed.
Aggregation hint properties are also implemented in IR as a workaround to support incremental compilation. See ContributionHintIrTransformer
for more details.
TypeKey
and ContextualTypeKey
¶
TypeKey
and ContextualTypeKey
(and their FIR counterparts) deserve special mention. Most of the compiler’s dependency graph analysis thinks in terms of these two types.
A TypeKey
is the canonical representations of specific binding, composed of a type and optional qualifier.
A ContextualTypeKey
can be thought up as a TypeKey
with context of how it’s used. This is useful for a few reasons:
- Allows Metro’s compiler plugin to generate code accordingly for how the given
TypeKey
is used at runtime, for example wrapping inProvider
,Lazy
, etc. - Allows dependency graph resolution to understand if the type is deferrable, which is useful in breaking dependency cycles.
Misc Notes¶
- IR code should cache eagerly.
- FIR code should cache carefully (remember it runs in the IDE!).
- FIR code should be defensive. It may run continuously in the IDE and not all information may be available to the compiler as the user has written it. If you’ve ever written a custom lint check, your methodology should be similar.
- Inversely, IR code should be offensive. Assert expectations with clear error messages, report errors with useful error messages.
- FIR-generated declaration should use descriptive keys to declarations that can be referenced later in FIR and IR (as
origins
). SeeKeys.kt
for FIR declarations andOrigins.kt
for their IR analogs.