Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ rewrite our code generation setup to use Metro.
Please note that we are sharing this repository strictly as a practical demonstration of code generation with Metro.
This is a reference example and is not intended for public adoption.

> **⚠️ Metro's extensions API is highly experimental and does not accept any FRs and issues. The Kotlin compiler plugin
> API itself is also constantly changing and lacks documentation. You should be aware of the high-maintenance cost if
> you decide to follow the same approach.**
> **⚠️ [Metro's extensions API](https://github.com/ZacSweers/metro/blob/main/compiler/API.md) is highly experimental
> and does not accept any FRs and issues. The Kotlin compiler plugin API itself is also constantly changing and lacks
> documentation. You should be aware of the high-maintenance cost if you decide to follow the same approach.**

## Performance Benchmark

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
com.bandlab.metro.station.graph.MetroStationHintExtension$Factory
com.bandlab.metro.station.entry.StationEntryHintExtension$Factory
com.bandlab.metro.station.configselector.ContributesConfigSelectorHintExtension$Factory
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class MetroStationPluginComponentRegistrar : CompilerPluginRegistrar() {
override val supportsK2: Boolean get() = true

override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
val stationEntriesBaseline = configuration.get(MetroStationConfigurationKeys.STATION_ENTRIES_BASELINE)
val stationEntriesBaseline = configuration[MetroStationConfigurationKeys.STATION_ENTRIES_BASELINE]
FirExtensionRegistrarAdapter.registerExtension(
MetroStationPluginRegistrar(
includeBaselineChecker = stationEntriesBaseline != null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.bandlab.metro.station.configselector

import com.bandlab.metro.station.utils.ClassIds
import dev.zacsweers.metro.compiler.MetroOptions
import dev.zacsweers.metro.compiler.api.fir.MetroContributionHintExtension
import dev.zacsweers.metro.compiler.api.fir.MetroContributionHintExtension.ContributionHint
import dev.zacsweers.metro.compiler.compat.CompatContext
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar
import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol

/**
* Implements [MetroContributionHintExtension] to generate cross-module contribution hint files
* for `MultibindingContribution` interfaces generated by [ContributesConfigSelectorFir].
*
* These hint files allow Metro's `ContributionHintFirGenerator` to discover contributions from
* dependency modules during downstream compilation.
*/
public class ContributesConfigSelectorHintExtension(private val session: FirSession) :
MetroContributionHintExtension {

private val predicate = ContributesConfigSelectorIds.predicate

override fun FirDeclarationPredicateRegistrar.registerPredicates() {
register(predicate)
}

override fun getContributionHints(): List<ContributionHint> {
return session.predicateBasedProvider
.getSymbolsByPredicate(predicate)
.filterIsInstance<FirRegularClassSymbol>()
.map { classSymbol ->
val nestedInterfaceClassId = classSymbol.classId
.createNestedClassId(ContributesConfigSelectorIds.nestedContributionName)
ContributionHint(
contributingClassId = nestedInterfaceClassId,
scope = ClassIds.appScope
)
}
}

public class Factory : MetroContributionHintExtension.Factory {
override fun create(
session: FirSession,
options: MetroOptions,
compatContext: CompatContext,
): MetroContributionHintExtension {
return ContributesConfigSelectorHintExtension(session)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.bandlab.metro.station.entry

import com.bandlab.metro.station.graph.MetroStationIds
import com.bandlab.metro.station.utils.ClassIds
import com.bandlab.metro.station.utils.resolveParentScopeClassIdFromAnnotation
import dev.zacsweers.metro.compiler.MetroOptions
import dev.zacsweers.metro.compiler.api.fir.MetroContributionHintExtension
import dev.zacsweers.metro.compiler.api.fir.MetroContributionHintExtension.ContributionHint
import dev.zacsweers.metro.compiler.compat.CompatContext
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar
import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import org.jetbrains.kotlin.name.ClassId

/**
* Implements [MetroContributionHintExtension] to generate cross-module contribution hint files
* for `FeatureExtension.Factory` and `ExtensionFactoryContribution` interfaces generated by
* [StationEntryFir].
*
* These hint files allow Metro's `ContributionHintFirGenerator` to discover contributions from
* dependency modules during downstream compilation.
*/
public class StationEntryHintExtension(private val session: FirSession) : MetroContributionHintExtension {

private val predicate = MetroStationIds.stationEntryPredicate

override fun FirDeclarationPredicateRegistrar.registerPredicates() {
register(predicate)
}

override fun getContributionHints(): List<ContributionHint> {
return session.predicateBasedProvider
.getSymbolsByPredicate(predicate)
.filterIsInstance<FirRegularClassSymbol>()
.flatMap { classSymbol ->
val parentScope = resolveParentScopeClassId(classSymbol)
val factoryClassId = classSymbol.classId
.createNestedClassId(MetroStationIds.featureExtensionName)
.createNestedClassId(MetroStationIds.nestedFactoryName)
val extensionFactoryContributionClassId = classSymbol.classId
.createNestedClassId(MetroStationIds.extensionFactoryContributionName)

// Always emit both hints. If ExtensionFactoryContribution wasn't generated
// (non-Fragment types), Metro's hint resolver safely skips non-existent classes.
listOf(
ContributionHint(contributingClassId = factoryClassId, scope = parentScope),
ContributionHint(contributingClassId = extensionFactoryContributionClassId, scope = parentScope),
)
}
}

private fun resolveParentScopeClassId(owner: FirRegularClassSymbol): ClassId {
return resolveParentScopeClassIdFromAnnotation(
owner,
MetroStationIds.stationEntry,
session,
ClassIds.appScope
)
}

public class Factory : MetroContributionHintExtension.Factory {
override fun create(
session: FirSession,
options: MetroOptions,
compatContext: CompatContext,
): MetroContributionHintExtension {
return StationEntryHintExtension(session)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.bandlab.metro.station.graph

import com.bandlab.metro.station.utils.ClassIds
import dev.zacsweers.metro.compiler.MetroOptions
import dev.zacsweers.metro.compiler.api.fir.MetroContributionHintExtension
import dev.zacsweers.metro.compiler.api.fir.MetroContributionHintExtension.ContributionHint
import dev.zacsweers.metro.compiler.compat.CompatContext
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar
import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol

/**
* Implements [MetroContributionHintExtension] to generate cross-module contribution hint files
* for `FeatureServiceProvider` interfaces generated by [MetroStationFir].
*
* These hint files allow Metro's `ContributionHintFirGenerator` to discover contributions from
* dependency modules during downstream compilation.
*/
public class MetroStationHintExtension(private val session: FirSession) : MetroContributionHintExtension {

private val predicate = MetroStationIds.metroStationPredicate

override fun FirDeclarationPredicateRegistrar.registerPredicates() {
register(predicate)
}

override fun getContributionHints(): List<ContributionHint> {
return session.predicateBasedProvider
.getSymbolsByPredicate(predicate)
.filterIsInstance<FirRegularClassSymbol>()
.map { classSymbol ->
val serviceProvider = classSymbol.classId
.createNestedClassId(MetroStationIds.featureServiceProviderName)
ContributionHint(contributingClassId = serviceProvider, scope = ClassIds.appScope)
}
}

public class Factory : MetroContributionHintExtension.Factory {
override fun create(
session: FirSession,
options: MetroOptions,
compatContext: CompatContext,
): MetroContributionHintExtension {
return MetroStationHintExtension(session)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.bandlab.metro.station.services
import com.bandlab.metro.station.MetroStationPluginRegistrar
import com.bandlab.metro.station.entry.StationEntryIr
import com.bandlab.metro.station.graph.MetroStationIr
import dev.zacsweers.metro.compiler.MetroCommandLineProcessor
import dev.zacsweers.metro.compiler.MetroCompilerPluginRegistrar
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
Expand Down Expand Up @@ -53,12 +54,23 @@ fun TestConfigurationBuilder.configureImports(
}

private class ExtensionRegistrarConfigurator(testServices: TestServices) : EnvironmentConfigurator(testServices) {

private val metroCliProcessor = MetroCommandLineProcessor()
private val metroRegistrar = MetroCompilerPluginRegistrar()

override fun CompilerPluginRegistrar.ExtensionStorage.registerCompilerExtensions(
module: TestModule,
configuration: CompilerConfiguration
) {
// Configure Metro options from directives before registering
if (MetroDirectives.GENERATE_CLASSES_IN_IR in module.directives) {
val option =
metroCliProcessor.pluginOptions.first {
it.optionName == "generate-classes-in-ir"
}
metroCliProcessor.processOption(option, "true", configuration)
}

val includeBaselineChecker = MetroDirectives.ENABLE_STATION_ENTRIES_BASELINE in module.directives
FirExtensionRegistrarAdapter.registerExtension(
MetroStationPluginRegistrar(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer

object MetroDirectives : SimpleDirectivesContainer() {
val ENABLE_STATION_ENTRIES_BASELINE by directive("Enable the StationEntry baseline checker.")
val GENERATE_CLASSES_IN_IR by directive("Generate the Classes in ir.")
}
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ FILE fqName:<root> fileName:/activityParam.kt
VALUE_PARAMETER kind:DispatchReceiver name:<this> index:0 type:kotlin.Any
overridden:
public open fun toString (): kotlin.String declared in kotlin.Any
CLASS INTERFACE name:AppGraph modality:ABSTRACT visibility:public superTypes:[kotlin.Any; <root>.MyActivity.FeatureExtension.Factory.MetroContributionToDevzacsweersmetroappScopeocf15; com.bandlab.common.android.di.GraphExtensionFactoriesProvider; com.bandlab.common.android.di.GraphExtensionFactoriesProvider.MetroContributionToAppScope]
CLASS INTERFACE name:AppGraph modality:ABSTRACT visibility:public superTypes:[kotlin.Any; <root>.MyActivity.FeatureExtension.Factory.MetroContributionToDevzacsweersmetroappScopeocf15]
annotations:
DependencyGraph(scope = AppScope::class, additionalScopes = <null>, excludes = <null>, bindingContainers = <null>)
thisReceiver: VALUE_PARAMETER INSTANCE_RECEIVER kind:DispatchReceiver name:<this> type:<root>.AppGraph
Expand Down Expand Up @@ -702,21 +702,6 @@ FILE fqName:<root> fileName:/activityParam.kt
CONSTRUCTOR_CALL 'public constructor <init> (appGraphImpl: <root>.AppGraph.Impl, feature: <root>.MyActivity) declared in <root>.AppGraph.Impl.FeatureExtensionImpl' type=<root>.AppGraph.Impl.FeatureExtensionImpl origin=null
ARG appGraphImpl: GET_VAR '<this>: <root>.AppGraph.Impl declared in <root>.AppGraph.Impl.create' type=<root>.AppGraph.Impl origin=null
ARG feature: GET_VAR 'feature: <root>.MyActivity declared in <root>.AppGraph.Impl.create' type=<root>.MyActivity origin=null
PROPERTY name:graphExtensionFactories visibility:public modality:FINAL [val]
annotations:
Multibinds(allowEmpty = true)
overridden:
public abstract graphExtensionFactories: kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any> declared in <root>.AppGraph
FUN name:<get-graphExtensionFactories> visibility:public modality:FINAL returnType:kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any>
VALUE_PARAMETER INSTANCE_RECEIVER kind:DispatchReceiver name:<this> index:0 type:<root>.AppGraph.Impl
correspondingProperty: PROPERTY name:graphExtensionFactories visibility:public modality:FINAL [val]
overridden:
public abstract fun <get-graphExtensionFactories> (): kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any> declared in <root>.AppGraph
BLOCK_BODY
RETURN type=kotlin.Nothing from='public final fun <get-graphExtensionFactories> (): kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any> declared in <root>.AppGraph.Impl'
CALL 'public final fun emptyMap <K, V> (): kotlin.collections.Map<K of kotlin.collections.emptyMap, V of kotlin.collections.emptyMap> declared in kotlin.collections' type=kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any> origin=null
TYPE_ARG K: kotlin.reflect.KClass<*>
TYPE_ARG V: kotlin.Any
CLASS GENERATED[dev.zacsweers.metro.compiler.fir.Keys.MetroGraphCreatorsObjectDeclaration] OBJECT name:Companion modality:FINAL visibility:public [companion] superTypes:[kotlin.Any]
thisReceiver: VALUE_PARAMETER INSTANCE_RECEIVER kind:DispatchReceiver name:<this> type:<root>.AppGraph.Companion
CONSTRUCTOR GENERATED[dev.zacsweers.metro.compiler.fir.Keys.Default] visibility:private returnType:<root>.AppGraph.Companion [primary]
Expand Down Expand Up @@ -757,32 +742,14 @@ FILE fqName:<root> fileName:/activityParam.kt
VALUE_PARAMETER kind:Regular name:other index:1 type:kotlin.Any?
overridden:
public open fun equals (other: kotlin.Any?): kotlin.Boolean declared in kotlin.Any
public open fun equals (other: kotlin.Any?): kotlin.Boolean declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider
public open fun equals (other: kotlin.Any?): kotlin.Boolean declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider.MetroContributionToAppScope
FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN returnType:kotlin.Int [fake_override]
VALUE_PARAMETER kind:DispatchReceiver name:<this> index:0 type:kotlin.Any
overridden:
public open fun hashCode (): kotlin.Int declared in kotlin.Any
public open fun hashCode (): kotlin.Int declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider
public open fun hashCode (): kotlin.Int declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider.MetroContributionToAppScope
FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN returnType:kotlin.String [fake_override]
VALUE_PARAMETER kind:DispatchReceiver name:<this> index:0 type:kotlin.Any
overridden:
public open fun toString (): kotlin.String declared in kotlin.Any
public open fun toString (): kotlin.String declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider
public open fun toString (): kotlin.String declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider.MetroContributionToAppScope
PROPERTY FAKE_OVERRIDE name:graphExtensionFactories visibility:public modality:ABSTRACT [fake_override,val]
annotations:
Multibinds(allowEmpty = true)
overridden:
public abstract graphExtensionFactories: kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any> declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider
public abstract graphExtensionFactories: kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any> declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider.MetroContributionToAppScope
FUN FAKE_OVERRIDE name:<get-graphExtensionFactories> visibility:public modality:ABSTRACT returnType:kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any> [fake_override]
VALUE_PARAMETER kind:DispatchReceiver name:<this> index:0 type:com.bandlab.common.android.di.GraphExtensionFactoriesProvider
correspondingProperty: PROPERTY FAKE_OVERRIDE name:graphExtensionFactories visibility:public modality:ABSTRACT [fake_override,val]
overridden:
public abstract fun <get-graphExtensionFactories> (): kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any> declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider
public abstract fun <get-graphExtensionFactories> (): kotlin.collections.Map<kotlin.reflect.KClass<*>, kotlin.Any> declared in com.bandlab.common.android.di.GraphExtensionFactoriesProvider.MetroContributionToAppScope
FILE fqName:metro.hints fileName:/myActivityFeatureExtensionFactoryAppScope.kt
FUN GENERATED[dev.zacsweers.metro.compiler.fir.Keys.Default] name:AppScope visibility:public modality:FINAL returnType:kotlin.Unit
VALUE_PARAMETER kind:Regular name:contributed index:0 type:<root>.MyActivity.FeatureExtension.Factory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ class MyViewModel {
}

@DependencyGraph(scope = AppScope::class)
interface AppGraph : MetroContributionToDevzacsweersmetroappScopeocf15, GraphExtensionFactoriesProvider, MetroContributionToAppScope {
interface AppGraph : MetroContributionToDevzacsweersmetroappScopeocf15 {
@Deprecated(message = "This synthesized declaration should not be used directly", level = DeprecationLevel.HIDDEN)
@MetroImplMarker
class Impl : AppGraph {
Expand Down Expand Up @@ -329,12 +329,6 @@ interface AppGraph : MetroContributionToDevzacsweersmetroappScopeocf15, GraphExt
return FeatureExtensionImpl(appGraphImpl = <this>, feature = feature)
}

@Multibinds(allowEmpty = true)
override val graphExtensionFactories: Map<KClass<*>, Any>
override get(): Map<KClass<*>, Any> {
return emptyMap<KClass<*>, Any>()
}

}

companion object Companion {
Expand Down
Loading