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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {

compileOnly(kotlin("compiler"))
compileOnly(kotlin("stdlib"))
compileOnly(libs.metro.compiler)

routeBindingRuntimeClasspath(project(":routebinding:routebinding-runtime"))
metroRuntimeClasspath(libs.metro.runtime)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ internal object ClassIds {
object RouteBinding {
val Annotation = ClassId.fromString("io/github/reactivecircus/routebinding/runtime/RouteBinding")
val NavEntryInstaller = ClassId.fromString("io/github/reactivecircus/routebinding/runtime/NavEntryInstaller")
val RouteMetadataProvider = ClassId.fromString(
"io/github/reactivecircus/routebinding/runtime/RouteMetadataProvider",
)
}

object Compose {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.github.reactivecircus.routebinding.compiler.fir

import io.github.reactivecircus.routebinding.compiler.ClassIds
import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name

/**
* Generates [ClassId] of the NavEntryInstaller class to be generated for the given source function
* annotated with `@Routebinding`.
*/
internal fun generateNavEntryInstallerClassId(sourceFunction: FirNamedFunctionSymbol): ClassId {
val packageFqName = sourceFunction.callableId.packageName
val classNameSuffix = ClassIds.RouteBinding.NavEntryInstaller.shortClassName.asString()
val className = Name.identifier("${sourceFunction.name.asString()}_$classNameSuffix")
return ClassId(packageFqName, className)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.github.reactivecircus.routebinding.compiler.fir

import dev.zacsweers.metro.compiler.MetroOptions
import dev.zacsweers.metro.compiler.api.fir.MetroContributionExtension
import dev.zacsweers.metro.compiler.api.fir.MetroContributions
import dev.zacsweers.metro.compiler.compat.CompatContext
import dev.zacsweers.metro.compiler.fir.MetroFirTypeResolver
import io.github.reactivecircus.routebinding.compiler.ClassIds
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar
import org.jetbrains.kotlin.fir.extensions.predicate.LookupPredicate
import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
import org.jetbrains.kotlin.fir.resolve.defaultType
import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider
import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import org.jetbrains.kotlin.name.ClassId

internal class RouteBindingContributionExtension(
private val session: FirSession,
) : MetroContributionExtension {
private val hasRouteBindingAnnotation = LookupPredicate.BuilderContext.annotated(
ClassIds.RouteBinding.Annotation.asSingleFqName(),
)

private val navEntryInstallerClassIds: List<ClassId> by lazy {
session.predicateBasedProvider
.getSymbolsByPredicate(hasRouteBindingAnnotation)
.filterIsInstance<FirNamedFunctionSymbol>()
.map { generateNavEntryInstallerClassId(it) }
}

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

override fun getContributions(
scopeClassId: ClassId,
typeResolverFactory: MetroFirTypeResolver.Factory,
): List<MetroContributionExtension.Contribution> {
if (scopeClassId != ClassIds.Metro.AppScope) return emptyList()
return navEntryInstallerClassIds.mapNotNull { classId ->
val metroContributionClassId = MetroContributions.metroContributionClassId(
contributingClassId = classId,
scopeClassId = scopeClassId,
)

val metroContributionSymbol = session.symbolProvider
.getClassLikeSymbolByClassId(metroContributionClassId) as? FirRegularClassSymbol
?: return@mapNotNull null

MetroContributionExtension.Contribution(
supertype = metroContributionSymbol.defaultType(),
replaces = emptyList(),
originClassId = classId,
)
}
}

internal class Factory : MetroContributionExtension.Factory {
override fun create(
session: FirSession,
options: MetroOptions,
compatContext: CompatContext,
): MetroContributionExtension = RouteBindingContributionExtension(session)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package io.github.reactivecircus.routebinding.compiler.fir

import dev.zacsweers.metro.compiler.MetroOptions
import dev.zacsweers.metro.compiler.api.fir.MetroContributionHintExtension
import dev.zacsweers.metro.compiler.api.fir.MetroFirDeclarationGenerationExtension
import dev.zacsweers.metro.compiler.compat.CompatContext
import io.github.reactivecircus.routebinding.compiler.ClassIds
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.declarations.declaredFunctions
import org.jetbrains.kotlin.fir.declarations.getDeprecationsProvider
import org.jetbrains.kotlin.fir.deserialization.toQualifiedPropertyAccessExpression
import org.jetbrains.kotlin.fir.expressions.FirAnnotation
import org.jetbrains.kotlin.fir.expressions.builder.buildAnnotation
import org.jetbrains.kotlin.fir.expressions.builder.buildAnnotationArgumentMapping
import org.jetbrains.kotlin.fir.expressions.builder.buildArgumentList
import org.jetbrains.kotlin.fir.expressions.builder.buildEnumEntryDeserializedAccessExpression
import org.jetbrains.kotlin.fir.expressions.builder.buildGetClassCall
import org.jetbrains.kotlin.fir.expressions.builder.buildLiteralExpression
import org.jetbrains.kotlin.fir.expressions.builder.buildResolvedQualifier
import org.jetbrains.kotlin.fir.extensions.ExperimentalTopLevelDeclarationsGenerationApi
import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar
import org.jetbrains.kotlin.fir.extensions.MemberGenerationContext
import org.jetbrains.kotlin.fir.extensions.predicate.LookupPredicate
import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
import org.jetbrains.kotlin.fir.plugin.createConstructor
import org.jetbrains.kotlin.fir.plugin.createMemberFunction
import org.jetbrains.kotlin.fir.plugin.createTopLevelClass
import org.jetbrains.kotlin.fir.resolve.defaultType
import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider
import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirConstructorSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import org.jetbrains.kotlin.fir.toFirResolvedTypeRef
import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef
import org.jetbrains.kotlin.fir.types.constructClassLikeType
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.types.ConstantValueKind

internal class RouteBindingDeclarationGenerationExtension(
session: FirSession,
) : MetroFirDeclarationGenerationExtension(session), MetroContributionHintExtension {
private val hasRouteBindingAnnotation = LookupPredicate.BuilderContext.annotated(
ClassIds.RouteBinding.Annotation.asSingleFqName(),
)

private val sourceFunctions: List<FirNamedFunctionSymbol> by lazy {
session.predicateBasedProvider
.getSymbolsByPredicate(hasRouteBindingAnnotation)
.filterIsInstance<FirNamedFunctionSymbol>()
}

private val navEntryInstallerClassIds: List<ClassId> by lazy {
sourceFunctions.map { generateNavEntryInstallerClassId(it) }
}

private val installFunctionName = Name.identifier("install")

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

override fun getContributionHints(): List<MetroContributionHintExtension.ContributionHint> {
return navEntryInstallerClassIds.map { classId ->
MetroContributionHintExtension.ContributionHint(
contributingClassId = classId,
scope = ClassIds.Metro.AppScope,
)
}
}

@ExperimentalTopLevelDeclarationsGenerationApi
override fun getTopLevelClassIds(): Set<ClassId> = navEntryInstallerClassIds.toSet()

@ExperimentalTopLevelDeclarationsGenerationApi
override fun generateTopLevelClassLikeDeclaration(classId: ClassId): FirClassLikeSymbol<*> {
return createTopLevelClass(
classId = classId,
key = RouteBindingKeys.NavEntryInstallerClassDeclaration,
) {
val navEntryInstallerSymbol = session.symbolProvider
.getClassLikeSymbolByClassId(ClassIds.RouteBinding.NavEntryInstaller) as FirRegularClassSymbol
superType(navEntryInstallerSymbol.defaultType().toFirResolvedTypeRef().coneType)
}.apply {
replaceAnnotations(annotations + buildDeprecatedHiddenAnnotation() + buildContributesIntoSetAnnotation())
replaceDeprecationsProvider(getDeprecationsProvider(session))
}.symbol
}

private fun buildContributesIntoSetAnnotation(): FirAnnotation = buildAnnotation {
val contributesIntoSetSymbol = session.symbolProvider
.getClassLikeSymbolByClassId(ClassIds.Metro.ContributesIntoSet) as FirRegularClassSymbol
annotationTypeRef = buildResolvedTypeRef {
coneType = contributesIntoSetSymbol.defaultType().toFirResolvedTypeRef().coneType
}
argumentMapping = buildAnnotationArgumentMapping {
mapping[Name.identifier("scope")] = buildGetClassCall {
val appScopeSymbol = session.symbolProvider
.getClassLikeSymbolByClassId(ClassIds.Metro.AppScope) as FirRegularClassSymbol
val appScopeConeType = appScopeSymbol.classId.constructClassLikeType()
val kClassSymbol = session.symbolProvider
.getClassLikeSymbolByClassId(StandardClassIds.KClass) as FirRegularClassSymbol
val kClassType = kClassSymbol.classId.constructClassLikeType(arrayOf(appScopeConeType))
coneTypeOrNull = kClassType
argumentList = buildArgumentList {
arguments += buildResolvedQualifier {
coneTypeOrNull = appScopeConeType
qualifierSymbol = appScopeSymbol
packageFqName = ClassIds.Metro.AppScope.packageFqName
relativeClassFqName = ClassIds.Metro.AppScope.relativeClassName
resolvedToCompanionObject = false
}
}
}
}
}

override fun getCallableNamesForClass(classSymbol: FirClassSymbol<*>, context: MemberGenerationContext): Set<Name> {
val classSymbol = context.owner
if (navEntryInstallerClassIds.none { it == classSymbol.classId }) return emptySet()
return setOf(SpecialNames.INIT, installFunctionName)
}

override fun generateConstructors(context: MemberGenerationContext): List<FirConstructorSymbol> {
val classSymbol = context.owner
if (navEntryInstallerClassIds.none { it == classSymbol.classId }) return emptyList()
return listOf(
createConstructor(
owner = classSymbol,
key = RouteBindingKeys.NavEntryInstallerClassDeclaration,
isPrimary = true,
generateDelegatedNoArgConstructorCall = true,
).symbol,
)
}

override fun generateFunctions(
callableId: CallableId,
context: MemberGenerationContext?,
): List<FirNamedFunctionSymbol> {
val classSymbol = context?.owner ?: return emptyList()
return when {
navEntryInstallerClassIds.none { it == classSymbol.classId } -> emptyList()
callableId.callableName != installFunctionName -> emptyList()
else -> listOf(generateInstallFunction(classSymbol))
}
}

private fun generateInstallFunction(classSymbol: FirClassSymbol<*>): FirNamedFunctionSymbol {
val navEntryInstallerSymbol = session.symbolProvider
.getClassLikeSymbolByClassId(ClassIds.RouteBinding.NavEntryInstaller) as FirRegularClassSymbol
val installFunctionSymbol = navEntryInstallerSymbol.declaredFunctions(session)
.first { it.name == installFunctionName }

return createMemberFunction(
owner = classSymbol,
key = RouteBindingKeys.InstallFunctionDeclaration,
name = installFunctionName,
returnType = installFunctionSymbol.resolvedReturnType,
) {
installFunctionSymbol.contextParameterSymbols.forEach {
contextReceiver(it.resolvedReturnType)
}
installFunctionSymbol.valueParameterSymbols.forEach {
valueParameter(it.name, it.resolvedReturnType)
}
status {
isOverride = true
}
}.symbol
}

private fun buildDeprecatedHiddenAnnotation(): FirAnnotation = buildAnnotation {
val deprecatedAnnotation = session.symbolProvider
.getClassLikeSymbolByClassId(StandardClassIds.Annotations.Deprecated) as FirRegularClassSymbol

annotationTypeRef = deprecatedAnnotation.defaultType().toFirResolvedTypeRef()
argumentMapping = buildAnnotationArgumentMapping {
mapping[Name.identifier("message")] = buildLiteralExpression(
source = null,
kind = ConstantValueKind.String,
value = "This synthesized declaration should not be used directly",
setType = true,
)
mapping[Name.identifier("level")] =
buildEnumEntryDeserializedAccessExpression {
enumClassId = StandardClassIds.DeprecationLevel
enumEntryName = Name.identifier("HIDDEN")
}.toQualifiedPropertyAccessExpression(session)
}
}

internal class Factory : MetroFirDeclarationGenerationExtension.Factory {
override fun create(
session: FirSession,
options: MetroOptions,
compatContext: CompatContext,
): MetroFirDeclarationGenerationExtension = RouteBindingDeclarationGenerationExtension(session)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.reactivecircus.routebinding.compiler.fir

import org.jetbrains.kotlin.GeneratedDeclarationKey

internal object RouteBindingKeys {
data object NavEntryInstallerClassDeclaration : GeneratedDeclarationKey()

data object InstallFunctionDeclaration : GeneratedDeclarationKey()
}
Loading