-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate Dagger modules for @ContributesBinding with basic configuration #1112
base: main-k2
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package com.squareup.anvil.compiler.k2.fir.contributions | ||
|
||
import com.squareup.anvil.compiler.k2.names.Names | ||
import com.squareup.anvil.compiler.k2.util.classId | ||
import com.squareup.anvil.compiler.k2.util.toFirAnnotation | ||
import org.jetbrains.kotlin.descriptors.ClassKind | ||
import org.jetbrains.kotlin.fir.FirSession | ||
import org.jetbrains.kotlin.fir.declarations.getKClassArgument | ||
import org.jetbrains.kotlin.fir.expressions.FirAnnotation | ||
import org.jetbrains.kotlin.fir.expressions.FirAnnotationCall | ||
import org.jetbrains.kotlin.fir.expressions.FirNamedArgumentExpression | ||
import org.jetbrains.kotlin.fir.expressions.builder.buildAnnotation | ||
import org.jetbrains.kotlin.fir.expressions.impl.FirAnnotationArgumentMappingImpl | ||
import org.jetbrains.kotlin.fir.extensions.ExperimentalTopLevelDeclarationsGenerationApi | ||
import org.jetbrains.kotlin.fir.extensions.FirExtension | ||
import org.jetbrains.kotlin.fir.plugin.createTopLevelClass | ||
import org.jetbrains.kotlin.fir.resolve.fqName | ||
import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol | ||
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol | ||
import org.jetbrains.kotlin.fir.types.ConeKotlinType | ||
import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef | ||
import org.jetbrains.kotlin.fir.types.classId | ||
import org.jetbrains.kotlin.fir.types.constructClassLikeType | ||
import org.jetbrains.kotlin.name.ClassId | ||
import org.jetbrains.kotlin.name.Name | ||
|
||
@OptIn(ExperimentalTopLevelDeclarationsGenerationApi::class) | ||
public class BindingModuleData( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kept the structure here mostly in-line with the POC branch until we can discuss concerns/alternatives (related https://github.com/square/anvil/pull/1110/files#r1951710072), and also because I'd like us to get the merging impl + related integration tests in before doing any heavier refactoring. |
||
public val generatedClassId: ClassId, | ||
public val matchedClassSymbol: FirClassSymbol<*>, | ||
firExtension: FirExtension, | ||
session: FirSession, | ||
) { | ||
public val generatedClassSymbol: FirClassLikeSymbol<*> by lazy { | ||
firExtension.createTopLevelClass( | ||
classId = generatedClassId, | ||
key = GeneratedBindingDeclarationKey, | ||
classKind = ClassKind.INTERFACE, | ||
).apply { | ||
replaceAnnotations( | ||
listOf( | ||
buildContributesToAnnotation(), | ||
Names.daggerModule.toFirAnnotation(), | ||
), | ||
) | ||
}.symbol | ||
} | ||
|
||
public val contributesBindingAnnotation: FirAnnotation by lazy { | ||
matchedClassSymbol.annotations.single { | ||
it.fqName(session)?.equals(Names.anvilContributesBinding) ?: false | ||
} | ||
} | ||
|
||
public val boundType: ConeKotlinType by lazy { | ||
contributesBindingAnnotation.getKClassArgument(Name.identifier("boundType"), session)!! | ||
} | ||
|
||
public val callableName: Name by lazy { | ||
"bind${boundType.classId!!.shortClassName.asString()}" | ||
.let(Name::identifier) | ||
} | ||
|
||
private fun buildContributesToAnnotation(): FirAnnotation = buildAnnotation { | ||
annotationTypeRef = buildResolvedTypeRef { | ||
coneType = Names.anvilContributesTo.classId().constructClassLikeType() | ||
} | ||
// Argument mapping may be empty if merging is also happening | ||
val newArgMapping = if (contributesBindingAnnotation.argumentMapping.mapping.isEmpty()) { | ||
val scopeArg = (contributesBindingAnnotation as FirAnnotationCall).argumentList | ||
.arguments | ||
.single { (it as FirNamedArgumentExpression).name.toString() == "scope" } | ||
|
||
mapOf(Name.identifier("scope") to scopeArg) | ||
} else { | ||
contributesBindingAnnotation.argumentMapping.mapping | ||
.filter { (key, _) -> | ||
key.asString() == "scope" | ||
} | ||
} | ||
|
||
argumentMapping = FirAnnotationArgumentMappingImpl(null, newArgMapping) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package com.squareup.anvil.compiler.k2.fir.contributions | ||
|
||
import com.squareup.anvil.annotations.ContributesBinding | ||
import com.squareup.anvil.compiler.k2.fir.AnvilFirContext | ||
import com.squareup.anvil.compiler.k2.fir.AnvilFirDeclarationGenerationExtension | ||
import com.squareup.anvil.compiler.k2.names.Names | ||
import com.squareup.anvil.compiler.k2.util.AnvilPredicates.hasContributesBindingAnnotation | ||
import com.squareup.anvil.compiler.k2.util.toFirAnnotation | ||
import org.jetbrains.kotlin.descriptors.Modality | ||
import org.jetbrains.kotlin.fir.FirSession | ||
import org.jetbrains.kotlin.fir.caches.contains | ||
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.plugin.createMemberFunction | ||
import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol | ||
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol | ||
import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol | ||
import org.jetbrains.kotlin.fir.types.constructType | ||
import org.jetbrains.kotlin.name.CallableId | ||
import org.jetbrains.kotlin.name.ClassId | ||
import org.jetbrains.kotlin.name.Name | ||
|
||
/** | ||
* Generates binding modules for every [ContributesBinding]-annotated class. | ||
* | ||
* Given a Kotlin source file like: | ||
* ``` | ||
* @ContributesBinding(Any::class) | ||
* class Foo @Inject constructor() : Bar | ||
* ``` | ||
* | ||
* We will generate the FIR equivalent of: | ||
* ``` | ||
* @ContributesTo(Any::class) | ||
* @Module | ||
* abstract class Foo_BindingModule { | ||
* @Binds | ||
* abstract fun bindBar(impl: Foo): Bar | ||
* } | ||
* ``` | ||
*/ | ||
@OptIn(ExperimentalTopLevelDeclarationsGenerationApi::class) | ||
public class ContributesBindingFirExtension( | ||
anvilFirContext: AnvilFirContext, | ||
session: FirSession, | ||
) : AnvilFirDeclarationGenerationExtension(anvilFirContext, session) { | ||
|
||
override fun FirDeclarationPredicateRegistrar.registerPredicates() { | ||
register(hasContributesBindingAnnotation) | ||
} | ||
|
||
override fun getTopLevelClassIds(): Set<ClassId> { | ||
return session.contributesBindingSessionComponent.generatedIdsToMatchedSymbols.keys | ||
} | ||
|
||
override fun generateTopLevelClassLikeDeclaration(classId: ClassId): FirClassLikeSymbol<*> { | ||
return session.contributesBindingSessionComponent | ||
.bindingModuleCache | ||
.getValue(classId, session) | ||
.generatedClassSymbol | ||
} | ||
|
||
override fun getCallableNamesForClass( | ||
classSymbol: FirClassSymbol<*>, | ||
context: MemberGenerationContext, | ||
): Set<Name> { | ||
val cache = session.contributesBindingSessionComponent.bindingModuleCache | ||
if (!cache.contains(classSymbol.classId)) { | ||
return emptySet() | ||
} | ||
val bindingData = cache.getValue(classSymbol.classId, session) | ||
|
||
return setOf(bindingData.callableName) | ||
} | ||
|
||
override fun generateFunctions( | ||
callableId: CallableId, | ||
context: MemberGenerationContext?, | ||
): List<FirNamedFunctionSymbol> { | ||
val bindingData = session.contributesBindingSessionComponent.bindingModuleCache.getValue( | ||
context!!.owner.classId, | ||
session, | ||
) | ||
|
||
return listOf( | ||
createMemberFunction( | ||
owner = context.owner, | ||
key = GeneratedBindingDeclarationKey, | ||
name = callableId.callableName, | ||
returnType = bindingData.boundType, | ||
) { | ||
modality = Modality.ABSTRACT | ||
valueParameter( | ||
name = Name.identifier("concreteType"), | ||
type = bindingData.matchedClassSymbol.constructType(), | ||
) | ||
}.apply { | ||
replaceAnnotations(listOf(Names.daggerBinds.toFirAnnotation())) | ||
}.symbol, | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.squareup.anvil.compiler.k2.fir.contributions | ||
|
||
import com.google.auto.service.AutoService | ||
import com.squareup.anvil.compiler.k2.fir.AnvilFirContext | ||
import com.squareup.anvil.compiler.k2.fir.AnvilFirDeclarationGenerationExtension | ||
import com.squareup.anvil.compiler.k2.fir.AnvilFirExtensionFactory | ||
import org.jetbrains.kotlin.fir.extensions.FirDeclarationGenerationExtension | ||
|
||
@AutoService(AnvilFirExtensionFactory::class) | ||
internal class ContributesBindingFirExtensionFactory : AnvilFirDeclarationGenerationExtension.Factory { | ||
|
||
override fun create(anvilFirContext: AnvilFirContext): FirDeclarationGenerationExtension.Factory { | ||
return FirDeclarationGenerationExtension.Factory { | ||
ContributesBindingFirExtension(anvilFirContext = anvilFirContext, session = it) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package com.squareup.anvil.compiler.k2.fir.contributions | ||
|
||
import com.squareup.anvil.compiler.k2.fir.AnvilFirContext | ||
import com.squareup.anvil.compiler.k2.fir.AnvilFirExtensionSessionComponent | ||
import com.squareup.anvil.compiler.k2.fir.utils.joinSimpleNames | ||
import com.squareup.anvil.compiler.k2.util.AnvilPredicates.hasContributesBindingAnnotation | ||
import org.jetbrains.kotlin.fir.FirSession | ||
import org.jetbrains.kotlin.fir.caches.FirCache | ||
import org.jetbrains.kotlin.fir.caches.firCachesFactory | ||
import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar | ||
import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider | ||
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol | ||
import org.jetbrains.kotlin.name.ClassId | ||
|
||
/** | ||
* Responsible for tracking the classes annotated with @ContributesBinding and creating + caching | ||
* their generated Dagger module metadata. | ||
*/ | ||
public class ContributesBindingSessionComponent( | ||
anvilFirContext: AnvilFirContext, | ||
session: FirSession, | ||
) : AnvilFirExtensionSessionComponent(anvilFirContext, session) { | ||
/** | ||
* A map to help us track the original annotated classes' bindings, and their | ||
* generated module IDs. | ||
* E.g. Key: "Foo_BindingModule", Value: ClassSymbol<Foo> | ||
*/ | ||
public val generatedIdsToMatchedSymbols: Map<ClassId, FirClassSymbol<*>> by lazy { | ||
session.predicateBasedProvider.getSymbolsByPredicate(hasContributesBindingAnnotation) | ||
.filterIsInstance<FirClassSymbol<*>>() | ||
.associateBy { | ||
it.classId.joinSimpleNames(suffix = "BindingModule") | ||
} | ||
} | ||
|
||
public val bindingModuleCache: FirCache<ClassId, BindingModuleData, FirSession> = | ||
session.firCachesFactory | ||
.createCache<ClassId, BindingModuleData, FirSession> { key: ClassId, context -> | ||
BindingModuleData( | ||
key, | ||
generatedIdsToMatchedSymbols[key] as FirClassSymbol<*>, | ||
this@ContributesBindingSessionComponent, | ||
session, | ||
) | ||
} | ||
|
||
override fun FirDeclarationPredicateRegistrar.registerPredicates() { | ||
register(hasContributesBindingAnnotation) | ||
} | ||
} | ||
|
||
public val FirSession.contributesBindingSessionComponent: ContributesBindingSessionComponent | ||
by FirSession.sessionComponentAccessor() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.squareup.anvil.compiler.k2.fir.contributions | ||
|
||
import com.google.auto.service.AutoService | ||
import com.squareup.anvil.compiler.k2.fir.AnvilFirContext | ||
import com.squareup.anvil.compiler.k2.fir.AnvilFirExtensionFactory | ||
import com.squareup.anvil.compiler.k2.fir.AnvilFirExtensionSessionComponent | ||
import org.jetbrains.kotlin.fir.extensions.FirExtensionSessionComponent | ||
|
||
@AutoService(AnvilFirExtensionFactory::class) | ||
internal class ContributesBindingSessionComponentFactory : AnvilFirExtensionSessionComponent.Factory { | ||
|
||
override fun create(anvilFirContext: AnvilFirContext): FirExtensionSessionComponent.Factory { | ||
return FirExtensionSessionComponent.Factory { | ||
ContributesBindingSessionComponent(anvilFirContext = anvilFirContext, session = it) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.squareup.anvil.compiler.k2.fir.contributions | ||
|
||
import org.jetbrains.kotlin.GeneratedDeclarationKey | ||
|
||
internal object GeneratedBindingDeclarationKey : GeneratedDeclarationKey() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.squareup.anvil.compiler.k2.util | ||
|
||
import com.squareup.anvil.compiler.k2.names.Names | ||
import org.jetbrains.kotlin.fir.extensions.predicate.LookupPredicate | ||
import org.jetbrains.kotlin.name.FqName | ||
|
||
internal object AnvilPredicates { | ||
val hasContributesBindingAnnotation | ||
get() = Names.anvilContributesBinding.toLookupPredicate() | ||
|
||
private fun FqName.toLookupPredicate(): LookupPredicate = | ||
LookupPredicate.create { annotated(this@toLookupPredicate) } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😭
Could you give the Kotest apis a shot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did initially (along with using ClassGraph instead of the reflection apis) but ended up reverting the changes; I feel like there's a lot of value in utilizing the same tools/apis as the existing tests here because I'm thinking about how it looks once we're ready to start running existing test suites against K2. Using Truth here allows the assertions to closely match the form as our existing test coverage and will make it a lot more obvious which tests are duplicates or have duplicate coverage, which then makes cleaning them up that much faster. It will also result in more consistent tests assuming we combine the new suite and existing suite at some point (if we even have new coverage at that point).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The majority of tests written within the last year have used Kotest assertions -- any time there was a new test class. All assertions in new test tooling use Kotest assertions. Going back to creating new test classes with Truth is an unnecessary step backwards.
Many of the existing tools make assumptions about hints, generated file names, KCT, and other K1 implementation details that don't really transfer over. The allegories I'm writing in the K2 tooling do not make those assumptions, specifically so that they can have the interop we need.