diff --git a/CHANGELOG.md b/CHANGELOG.md index fad5de3..d60620c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,13 @@ ## [Unreleased] ### Added +- Support Anvil KSP mode ### Changed +- Updated `Anvil` to `2.5.0-beta09` +- Better support for incremental compilation using new `GeneratedFilesWithSources` API from `Anvil` +- Split `:samples` module to multi-module structure showcasing usage of KSP and non-KSP code generators. See `Module structure in the project` section in README.md for more details. +- Updated `README.md` with explanation on how to use KSP code generators. ### Deprecated @@ -24,5 +29,5 @@ -[Unreleased]: https://github.com/square/anvil/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/IlyaGulya/anvil-utils/compare/v0.1.0...HEAD [0.1.0]: https://github.com/IlyaGulya/anvil-utils/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 35afe55..d88373f 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,103 @@ # Anvil Utils -Anvil Utils is a library that provides a set of annotations to simplify the development of modular applications with Dagger and Square Anvil. +Anvil Utils is a library that provides a set of annotations to simplify the development of modular applications with +Dagger and Square Anvil. ## Features -- `@ContributesAssistedFactory` - automatically generates assisted factories for annotated classes and contributes them to the specified scope as bindings of the provided factory type. +- `@ContributesAssistedFactory` - automatically generates assisted factories for annotated classes and contributes them + to the specified scope as bindings of the provided factory type. -## Getting Started +## Compatibility Map -1. Add the following dependencies to your project: +| Anvil Utils Version | Anvil Version | +|---------------------|---------------| +| 0.1.0 | 2.4.2 | +| 0.2.0-beta01 | 2.5.0-beta09 | -```kotlin -dependencies { - implementation("me.gulya.anvil:annotations:0.1.0") - anvil("me.gulya.anvil:compiler:0.1.0") -} -``` +## Getting Started -2. Enable Anvil in your project by applying the Anvil Gradle plugin: +### KSP + +1. Follow Zac Sweer's [guide](https://www.zacsweers.dev/preparing-for-k2/#anvil) on how to prepare your project for + Anvil with KSP and K2. +2. Ensure you have at least `2.5.0-beta09` version of Anvil in your project. +3. Ensure you have `ksp` plugin applied to your project: + ```kotlin + plugins { + id("com.google.devtools.ksp") + } + ``` + +4. Add the following dependencies to your project: + ```kotlin + dependencies { + implementation("me.gulya.anvil:annotations:0.1.0") + ksp("me.gulya.anvil:compiler:0.1.0") + } + ``` + +5. Enable Anvil in your project by applying the Anvil Gradle plugin: + ```kotlin + plugins { + id("com.squareup.anvil") version "2.5.0-beta09" + } + ``` + +6. Enable KSP and Dagger factory generation in `Anvil`: + ```kotlin + anvil { + useKsp( + contributesAndFactoryGeneration = true, + ) + generateDaggerFactories = true + } + ``` + +### Without KSP -```kotlin -plugins { - id("com.squareup.anvil") version "2.4.2" -} -``` +1. Add the following dependencies to your project: + ```kotlin + dependencies { + implementation("me.gulya.anvil:annotations:0.1.0") + anvil("me.gulya.anvil:compiler:0.1.0") + } + ``` +2. Enable Anvil in your project by applying the Anvil Gradle plugin: + ```kotlin + plugins { + id("com.squareup.anvil") version "2.5.0-beta09" + } + ``` + +3. Enable Dagger factory generation in `Anvil`: + ```kotlin + anvil { + generateDaggerFactories = true + } + ``` ## @ContributesAssistedFactory -`@ContributesAssistedFactory` is an annotation that helps to automatically generate -assisted factories for annotated classes and contribute them to the specified scope +`@ContributesAssistedFactory` is an annotation that helps to automatically generate +assisted factories for annotated classes and contribute them to the specified scope as bindings of the provided factory type. ### Motivation -When building modular applications with Dagger, it's common to define an API module with public interfaces -and a separate implementation module with concrete classes. Assisted injection is a useful pattern for creating + +When building modular applications with Dagger, it's common to define an API module with public interfaces +and a separate implementation module with concrete classes. Assisted injection is a useful pattern for creating instances of classes with a mix of dependencies provided by Dagger and runtime parameters. However, using Dagger's @AssistedFactory requires the factory interface and the implementation class to be in the same module, which breaks the separation between API and implementation. -`@ContributesAssistedFactory` solves this problem by allowing to declare the bound type (factory interface) in the +`@ContributesAssistedFactory` solves this problem by allowing to declare the bound type (factory interface) in the API module and generate the actual factory implementation in the implementation module. ### Usage 1. Define your bound type (factory interface) in the API module: + ```kotlin interface MyClass @@ -53,6 +107,7 @@ interface MyClassFactory { ``` 2. Annotate your implementation class with `@ContributesAssistedFactory` in the implementation module: + ```kotlin @ContributesAssistedFactory(AppScope::class, MyClassFactory::class) class DefaultMyClass @AssistedInject constructor( @@ -62,6 +117,7 @@ class DefaultMyClass @AssistedInject constructor( ``` 3. The following factory will be generated, implementing MyClassFactory: + ```kotlin @ContributesBinding(AppScope::class, MyClassFactory::class) @AssistedFactory @@ -70,5 +126,23 @@ interface DefaultMyClass_AssistedFactory : MyClassFactory { } ``` +### Module structure in the project + +- `:compiler` - contains code generators + - Package `me.gulya.anvil.utils.ksp` - KSP code generators + - Package `me.gulya.anvil.utils.embedded` - non-KSP code generators +- `:annotations` - contains annotations supported by this code generator +- `:samples` - sample project with examples of usage + - `:samples:entrypoint` - entrypoint modules showcasing usage of KSP and non-KSP code generators. + - `:samples:embedded` - entrypoint module where component merging is done. Depends on non-KSP implementation + module. + - `:samples:ksp` - entrypoint module where component merging is done. Depends on KSP implementation module. + - `:samples:library` - library modules using this code generator. + - `:samples:library:api` - API module with factory interfaces. + - `:samples:library:impl:ksp` - Implementation module using KSP code generator. + - `:samples:library:impl:embedded` - Implementation module using non-KSP code generator. + ### Important notes -- The factory interface method parameters should be annotated with @AssistedKey instead of Dagger's @Assisted because Dagger disallow such usage of this annotation. \ No newline at end of file + +- The factory interface method parameters should be annotated with @AssistedKey instead of Dagger's @Assisted because + Dagger disallow such usage of this annotation. \ No newline at end of file diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts index e878730..9bcb6ce 100644 --- a/compiler/build.gradle.kts +++ b/compiler/build.gradle.kts @@ -18,8 +18,11 @@ dependencies { implementation(libs.anvil.compiler.utils) implementation(libs.kotlinpoet) + implementation(libs.kotlinpoet.ksp) implementation(libs.dagger) + implementation(libs.ksp.api) + compileOnly(libs.google.autoservice.annotations) kapt(libs.google.autoservice.compiler) diff --git a/compiler/src/main/kotlin/me/gulya/anvil/utils/Errors.kt b/compiler/src/main/kotlin/me/gulya/anvil/utils/Errors.kt new file mode 100644 index 0000000..d357b96 --- /dev/null +++ b/compiler/src/main/kotlin/me/gulya/anvil/utils/Errors.kt @@ -0,0 +1,57 @@ +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import me.gulya.anvil.assisted.AssistedKey + +internal object Errors { + fun missingBoundType(className: String): String { + return "The @ContributesAssistedFactory annotation on class '$className' " + + "must have a 'boundType' parameter" + } + + fun mustHaveSinglePrimaryConstructor(className: String): String { + return "Class '$className' annotated with @ContributesAssistedFactory " + + "must have a single primary constructor" + } + + fun primaryConstructorMustBeAnnotatedWithAssistedInject(className: String): String { + return "Class '$className' annotated with @ContributesAssistedFactory " + + "must have its primary constructor annotated with @AssistedInject" + } + + fun boundTypeMustBeAbstractOrInterface(boundTypeName: String, assistedFactoryName: String): String { + return "The bound type '$boundTypeName' for @ContributesAssistedFactory on class " + + "'$assistedFactoryName' must be an abstract class or interface" + } + + fun boundTypeMustHasSingleAbstractMethod(boundType: String): String { + return "The bound type '$boundType' for @ContributesAssistedFactory " + + "must have a single abstract method" + } + + fun parameterMismatch(boundTypeName: String, factoryMethodName: String, assistedFactoryName: String): String { + return "The assisted factory method parameters in '$boundTypeName.$factoryMethodName' " + + "must match the @Assisted parameters in the primary constructor of " + + "'$assistedFactoryName'" + } + + fun parameterMustBeAnnotatedWithAssistedKey( + factoryParameterName: String, + boundTypeName: String, + factoryMethodName: String + ): String { + return "The parameter '${factoryParameterName}' in the factory method " + + "'${boundTypeName}.${factoryMethodName}' must be annotated with " + + "@${AssistedKey::class.simpleName} instead of @${Assisted::class.simpleName} " + + "to avoid conflicts with Dagger's @${AssistedFactory::class.simpleName} annotation" + } + + fun parameterDoesNotMatchAssistedParameter(factoryParameterName: String, assistedFactoryName: String): String { + return "The factory method parameter '${factoryParameterName}' does not match any @Assisted parameter " + + "in the primary constructor of '${assistedFactoryName}'" + } + + fun boundTypeMustBeClassOrInterface(boundTypeName: String): String { + return "Bound type ${boundTypeName} must be a class or interface" + } + +} \ No newline at end of file diff --git a/compiler/src/main/kotlin/me/gulya/anvil/utils/ContributesAssistedFactoryCodeGenerator.kt b/compiler/src/main/kotlin/me/gulya/anvil/utils/embedded/ContributesAssistedFactoryCodeGenerator.kt similarity index 64% rename from compiler/src/main/kotlin/me/gulya/anvil/utils/ContributesAssistedFactoryCodeGenerator.kt rename to compiler/src/main/kotlin/me/gulya/anvil/utils/embedded/ContributesAssistedFactoryCodeGenerator.kt index b7b8293..32dbf7c 100644 --- a/compiler/src/main/kotlin/me/gulya/anvil/utils/ContributesAssistedFactoryCodeGenerator.kt +++ b/compiler/src/main/kotlin/me/gulya/anvil/utils/embedded/ContributesAssistedFactoryCodeGenerator.kt @@ -2,12 +2,30 @@ import com.google.auto.service.AutoService import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.compiler.api.AnvilContext import com.squareup.anvil.compiler.api.CodeGenerator -import com.squareup.anvil.compiler.api.GeneratedFile +import com.squareup.anvil.compiler.api.GeneratedFileWithSources import com.squareup.anvil.compiler.api.createGeneratedFile import com.squareup.anvil.compiler.internal.buildFile import com.squareup.anvil.compiler.internal.fqName -import com.squareup.anvil.compiler.internal.reference.* -import com.squareup.kotlinpoet.* +import com.squareup.anvil.compiler.internal.reference.AnnotatedReference +import com.squareup.anvil.compiler.internal.reference.AnnotationReference +import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionAnnotationReference +import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionClassReference +import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionFunctionReference +import com.squareup.anvil.compiler.internal.reference.AnvilCompilationExceptionParameterReference +import com.squareup.anvil.compiler.internal.reference.ClassReference +import com.squareup.anvil.compiler.internal.reference.MemberFunctionReference +import com.squareup.anvil.compiler.internal.reference.ParameterReference +import com.squareup.anvil.compiler.internal.reference.argumentAt +import com.squareup.anvil.compiler.internal.reference.asClassName +import com.squareup.anvil.compiler.internal.reference.asTypeName +import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -30,7 +48,7 @@ class ContributesAssistedFactoryCodeGenerator : CodeGenerator { codeGenDir: File, module: ModuleDescriptor, projectFiles: Collection, - ): Collection { + ): Collection { return projectFiles.classAndInnerClassReferences(module) .filter { it.isAnnotatedWith(contributesAssistedFactoryFqName) } .map { generateAssistedFactory(it, codeGenDir) } @@ -40,7 +58,7 @@ class ContributesAssistedFactoryCodeGenerator : CodeGenerator { private fun generateAssistedFactory( assistedFactoryClass: ClassReference, codeGenDir: File, - ): GeneratedFile { + ): GeneratedFileWithSources { val generatedPackage = assistedFactoryClass.packageFqName.toString() val factoryClassName = "${assistedFactoryClass.shortName}_AssistedFactory" @@ -102,43 +120,45 @@ class ContributesAssistedFactoryCodeGenerator : CodeGenerator { .build(), ) } - return createGeneratedFile(codeGenDir, generatedPackage, factoryClassName, content) + return createGeneratedFile( + codeGenDir = codeGenDir, + packageName = generatedPackage, + fileName = factoryClassName, + content = content, + sourceFile = assistedFactoryClass.containingFileAsJavaFile, + ) } internal class ContributesAssistedFactoryValidator( private val annotation: AnnotationReference, private val assistedFactoryClass: ClassReference, ) { - fun validate(): GenerationDetails { + fun validate(): KspGenerationDetails { val boundType = annotation.boundTypeOrNull() boundType ?: throw AnvilCompilationExceptionAnnotationReference( annotation, - "The @ContributesAssistedFactory annotation on class '${assistedFactoryClass.shortName}' " + - "must have a 'boundType' parameter", + Errors.missingBoundType(assistedFactoryClass.shortName), ) val primaryConstructor = assistedFactoryClass.constructors.singleOrNull() primaryConstructor ?: throw AnvilCompilationExceptionClassReference( assistedFactoryClass, - "Class '${assistedFactoryClass.shortName}' annotated with @ContributesAssistedFactory " + - "must have a single primary constructor", + Errors.mustHaveSinglePrimaryConstructor(assistedFactoryClass.shortName), ) if (!primaryConstructor.isAnnotatedWith(AssistedInject::class.fqName)) { throw AnvilCompilationExceptionFunctionReference( primaryConstructor, - "Class '${assistedFactoryClass.shortName}' annotated with @ContributesAssistedFactory " + - "must have its primary constructor annotated with @AssistedInject", + Errors.primaryConstructorMustBeAnnotatedWithAssistedInject(assistedFactoryClass.shortName), ) } if (!boundType.isAbstract() && !boundType.isInterface()) { throw AnvilCompilationExceptionAnnotationReference( annotation, - "The bound type '${boundType.shortName}' for @ContributesAssistedFactory on class " + - "'${assistedFactoryClass.shortName}' must be an abstract class or interface", + Errors.boundTypeMustBeAbstractOrInterface(boundType.shortName, assistedFactoryClass.shortName), ) } @@ -146,8 +166,7 @@ class ContributesAssistedFactoryCodeGenerator : CodeGenerator { factoryMethod ?: throw AnvilCompilationExceptionClassReference( boundType, - "The bound type '${boundType.shortName}' for @ContributesAssistedFactory " + - "must have a single abstract method", + Errors.boundTypeMustHasSingleAbstractMethod(boundType.shortName) ) val factoryMethodParameters = factoryMethod.parameters val constructorParameters = primaryConstructor.parameters @@ -157,9 +176,7 @@ class ContributesAssistedFactoryCodeGenerator : CodeGenerator { if (constructorParameters.size != factoryMethodParameters.size) { throw AnvilCompilationExceptionFunctionReference( factoryMethod, - "The assisted factory method parameters in '${boundType.shortName}.${factoryMethod.name}' " + - "must match the @Assisted parameters in the primary constructor of " + - "'${assistedFactoryClass.shortName}'", + Errors.parameterMismatch(boundType.shortName, factoryMethod.name, assistedFactoryClass.shortName), ) } @@ -169,10 +186,11 @@ class ContributesAssistedFactoryCodeGenerator : CodeGenerator { if (isAnnotatedWithDaggerAssisted && !isAnnotatedWithAssistedKey) { throw AnvilCompilationExceptionParameterReference( factoryParameter, - "The parameter '${factoryParameter.name}' in the factory method " + - "'${boundType.shortName}.${factoryMethod.name}' must be annotated with " + - "@${AssistedKey::class.simpleName} instead of @${Assisted::class.simpleName} " + - "to avoid conflicts with Dagger's @${AssistedFactory::class.simpleName} annotation", + Errors.parameterMustBeAnnotatedWithAssistedKey( + factoryParameter.name, + boundType.shortName, + factoryMethod.name + ), ) } @@ -181,44 +199,14 @@ class ContributesAssistedFactoryCodeGenerator : CodeGenerator { constructorParameter ?: throw AnvilCompilationExceptionParameterReference( factoryParameter, - "The factory method parameter '${factoryParameter.name}' does not match any @Assisted parameter " + - "in the primary constructor of '${assistedFactoryClass.shortName}'", + Errors.parameterDoesNotMatchAssistedParameter( + factoryParameter.name, + assistedFactoryClass.shortName + ), ) - - // TODO: Improve heuristics for better error messages -// val factoryParameterTypeName = factoryParameter.type().asTypeName() -// val constructorParameterTypeName = constructorParameter.type().asTypeName() -// if (constructorParameterTypeName != factoryParameterTypeName) { -// throw AnvilCompilationExceptionParameterReference( -// factoryParameter, -// "The type of factory method parameter '${factoryParameter.name}' in " + -// "'${boundType.shortName}.${factoryMethod.name}' must match the type of the corresponding " + -// "@Assisted parameter in the primary constructor of '${assistedFactoryClass.shortName}'. " + -// "Expected: $constructorParameterTypeName, Found: $factoryParameterTypeName", -// ) -// } - -// if (factoryParameter.assistedKeyValue() != constructorParameter.assistedValue()) { -// val clarification = -// when { -// factoryParameter.assistedKeyValue() == null -> "Expected @AssistedKey(\"$assistedKey\") annotation on the '${boundType.shortName}.${factoryMethod.name}' parameter '${factoryParameter.name}'" -// constructorParameter.assistedValue() == null -> "Expected @Assisted annotation on the '${assistedFactoryClass.shortName}' primary constructor parameter '${constructorParameter.name}'" -// else -> null -// } -// -// val actualClarification = clarification?.let { ". $it" } ?: "" -// -// throw AnvilCompilationExceptionParameterReference( -// factoryParameter, -// "The @Assisted annotation value for parameter '${factoryParameter.name}' in the primary constructor " + -// "of '${assistedFactoryClass.shortName}' must match the value on the corresponding parameter " + -// "in the factory method '${boundType.shortName}.${factoryMethod.name}'" + -// actualClarification, -// ) -// } } - return GenerationDetails( + return KspGenerationDetails( boundType = boundType, factoryMethod = factoryMethod, factoryParameters = constructorParameters, @@ -227,7 +215,7 @@ class ContributesAssistedFactoryCodeGenerator : CodeGenerator { } } -internal data class GenerationDetails( +internal data class KspGenerationDetails( val boundType: ClassReference, val factoryMethod: MemberFunctionReference, val factoryParameters: Map, diff --git a/compiler/src/main/kotlin/me/gulya/anvil/utils/ksp/ContributesAssistedFactorySymbolProcessor.kt b/compiler/src/main/kotlin/me/gulya/anvil/utils/ksp/ContributesAssistedFactorySymbolProcessor.kt new file mode 100644 index 0000000..c0559fa --- /dev/null +++ b/compiler/src/main/kotlin/me/gulya/anvil/utils/ksp/ContributesAssistedFactorySymbolProcessor.kt @@ -0,0 +1,304 @@ +import com.google.auto.service.AutoService +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getConstructors +import com.google.devtools.ksp.isAbstract +import com.google.devtools.ksp.isAnnotationPresent +import com.google.devtools.ksp.isDefault +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSValueArgument +import com.google.devtools.ksp.symbol.KSValueParameter +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.anvil.compiler.internal.createAnvilSpec +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import com.squareup.kotlinpoet.ksp.writeTo +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import me.gulya.anvil.assisted.AssistedKey +import me.gulya.anvil.assisted.ContributesAssistedFactory +import me.gulya.anvil.utils.ParameterKey +import me.gulya.anvil.utils.ksp.internal.ErrorLoggingSymbolProcessor +import me.gulya.anvil.utils.ksp.internal.SymbolProcessingException + +private val contributesAssistedFactoryFqName = ContributesAssistedFactory::class.asClassName() + +@AutoService(SymbolProcessorProvider::class) +class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return ContributesAssistedFactorySymbolProcessor(environment) + } +} + +@Suppress("unused") +internal class ContributesAssistedFactorySymbolProcessor( + override val env: SymbolProcessorEnvironment, +) : ErrorLoggingSymbolProcessor() { + + override fun processChecked(resolver: Resolver): List { + resolver.getSymbolsWithAnnotation(contributesAssistedFactoryFqName.reflectionName()) + .filterIsInstance() + .forEach { annotated -> + generateAssistedFactory(annotated) + .writeTo(env.codeGenerator, Dependencies(false, annotated.containingFile!!)) + } + + return emptyList() + } + + private fun generateAssistedFactory( + assistedFactoryClass: KSClassDeclaration, + ): FileSpec { + val generatedPackage = assistedFactoryClass.packageName.asString() + val factoryClassName = "${assistedFactoryClass.simpleName.asString()}_AssistedFactory" + + val annotation = assistedFactoryClass.annotations + .single { it.annotationType.resolve().toClassName() == contributesAssistedFactoryFqName } + val scope = + annotation + .scope() + .toClassName() + + val generationDetails = ContributesAssistedFactoryValidator( + annotation = annotation, + assistedFactoryClass = assistedFactoryClass + ).validate() + + val boundType = generationDetails.boundType + val boundTypeName = boundType.toClassName() + val typeBuilder = + if (boundType.classKind == ClassKind.INTERFACE) { + TypeSpec + .interfaceBuilder(factoryClassName) + .addSuperinterface(boundTypeName) + } else { + TypeSpec + .classBuilder(factoryClassName) + .addModifiers(KModifier.ABSTRACT) + .superclass(boundTypeName) + } + + val content = FileSpec.createAnvilSpec(generatedPackage, factoryClassName) { + addType( + typeBuilder + .addAnnotation( + AnnotationSpec + .builder(ContributesBinding::class) + .addClassMember(scope) + .addClassMember(boundTypeName) + .build(), + ) + .addAnnotation(AssistedFactory::class) + .addFunction( + FunSpec + .builder(generationDetails.factoryMethod.simpleName.asString()) + .addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT) + .apply { + generationDetails.factoryMethod.parameters.forEach { parameter -> + val type = parameter.type.toTypeName() + addParameter( + ParameterSpec + .builder(parameter.name!!.asString(), type) + .assisted(parameter.assistedKeyValue()) + .build(), + ) + } + } + .returns(assistedFactoryClass.toClassName()) + .build(), + ) + .build(), + ) + } + return content + } + + internal class ContributesAssistedFactoryValidator( + private val annotation: KSAnnotation, + private val assistedFactoryClass: KSClassDeclaration, + ) { + @OptIn(KspExperimental::class) + fun validate(): GenerationDetails { + val boundType = annotation.boundTypeOrNull()?.declaration + + boundType ?: throw SymbolProcessingException( + annotation, + Errors.missingBoundType(assistedFactoryClass.simpleName.asString()), + ) + + if (boundType !is KSClassDeclaration) { + throw SymbolProcessingException( + annotation, + Errors.boundTypeMustBeClassOrInterface(boundType.simpleName.asString()), + ) + } + + val primaryConstructor = assistedFactoryClass.primaryConstructor + val hasMoreThanOneConstructor = assistedFactoryClass.getConstructors().toList().size != 1 + + if (primaryConstructor == null || hasMoreThanOneConstructor) { + throw SymbolProcessingException( + assistedFactoryClass, + Errors.mustHaveSinglePrimaryConstructor(assistedFactoryClass.simpleName.asString()), + ) + } + + if (!primaryConstructor.isAnnotationPresent(AssistedInject::class)) { + throw SymbolProcessingException( + primaryConstructor, + Errors.primaryConstructorMustBeAnnotatedWithAssistedInject(assistedFactoryClass.simpleName.asString()), + ) + } + + if (!boundType.isAbstract()) { + throw SymbolProcessingException( + annotation, + Errors.boundTypeMustBeAbstractOrInterface( + boundType.simpleName.asString(), + assistedFactoryClass.simpleName.asString(), + ), + ) + } + + val factoryMethod = boundType.getAllFunctions().singleOrNull { it.isAbstract } + + factoryMethod ?: throw SymbolProcessingException( + boundType, + Errors.boundTypeMustHasSingleAbstractMethod(boundType.simpleName.asString()), + ) + val factoryMethodParameters = factoryMethod.parameters + val constructorParameters = primaryConstructor.parameters + .filter { it.isAnnotationPresent(Assisted::class) } + .associateBy { ParameterKey(it.type.resolve().toTypeName(), it.assistedValue()) } +// .associateBy { ParameterKey(it.type.toTypeName(), it.assistedValue()) } + + if (constructorParameters.size != factoryMethodParameters.size) { + throw SymbolProcessingException( + factoryMethod, + Errors.parameterMismatch( + boundType.simpleName.asString(), + factoryMethod.simpleName.asString(), + assistedFactoryClass.simpleName.asString(), + ), + ) + } + + factoryMethodParameters.forEach { factoryParameter -> + val isAnnotatedWithDaggerAssisted = factoryParameter.isAnnotationPresent(Assisted::class) + val isAnnotatedWithAssistedKey = factoryParameter.isAnnotationPresent(AssistedKey::class) + if (isAnnotatedWithDaggerAssisted && !isAnnotatedWithAssistedKey) { + throw SymbolProcessingException( + factoryParameter, + Errors.parameterMustBeAnnotatedWithAssistedKey( + factoryParameter.name!!.asString(), + boundType.simpleName.asString(), + factoryMethod.simpleName.asString(), + ), + ) + } + + val assistedKey = factoryParameter.assistedKeyValue() + val constructorParameter = constructorParameters[factoryParameter.asParameterKey { assistedKey }] + + constructorParameter ?: throw SymbolProcessingException( + factoryParameter, + Errors.parameterDoesNotMatchAssistedParameter( + factoryParameter.name!!.asString(), + assistedFactoryClass.simpleName.asString(), + ), + ) + } + + return GenerationDetails( + boundType = boundType, + factoryMethod = factoryMethod, + factoryParameters = constructorParameters, + ) + } + } +} + +internal data class GenerationDetails( + val boundType: KSClassDeclaration, + val factoryMethod: KSFunctionDeclaration, + val factoryParameters: Map, +) + +private fun AnnotationSpec.Builder.addClassMember( + member: TypeName, +): AnnotationSpec.Builder { + addMember("%T::class", member) + return this +} + +private fun ParameterSpec.Builder.assisted(value: String?): ParameterSpec.Builder { + if (value == null) return this + addAnnotation( + AnnotationSpec + .builder(Assisted::class) + .apply { + addMember("%S", value) + } + .build(), + ) + return this +} + +private fun KSValueParameter.asParameterKey(keyFactory: (KSValueParameter) -> String?): ParameterKey { + return ParameterKey(type.toTypeName(), keyFactory(this)) +} + +private fun KSValueParameter.assistedValue(): String? { + return annotationStringValue() +} + +private fun KSValueParameter.assistedKeyValue(): String? { + return annotationStringValue() +} + +private inline fun KSAnnotated.annotationStringValue(): String? { + val value = annotations + .singleOrNull { it.annotationType.resolve().toClassName() == T::class.asClassName() } + ?.argumentAt("value") + ?.value + return (value as String?)?.takeIf { it.isNotBlank() } +} + +internal fun KSAnnotation.scope(): KSType = + scopeOrNull() + ?: throw SymbolProcessingException( + this, + "Couldn't find scope for ${annotationType.resolve().declaration.qualifiedName}.", + ) + +internal fun KSAnnotation.scopeOrNull(): KSType? { + return argumentAt("scope")?.value as? KSType? +} + +internal fun KSAnnotation.argumentAt( + name: String, +): KSValueArgument? { + arguments + return arguments.find { it.name?.asString() == name } + ?.takeUnless { it.isDefault() } +} + +internal fun KSAnnotation.boundTypeOrNull(): KSType? = argumentAt("boundType")?.value as? KSType? diff --git a/compiler/src/main/kotlin/me/gulya/anvil/utils/ksp/internal/ErrorLoggingSymbolProcessor.kt b/compiler/src/main/kotlin/me/gulya/anvil/utils/ksp/internal/ErrorLoggingSymbolProcessor.kt new file mode 100644 index 0000000..932ebe4 --- /dev/null +++ b/compiler/src/main/kotlin/me/gulya/anvil/utils/ksp/internal/ErrorLoggingSymbolProcessor.kt @@ -0,0 +1,29 @@ +package me.gulya.anvil.utils.ksp.internal + +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSNode + +internal abstract class ErrorLoggingSymbolProcessor : SymbolProcessor { + abstract val env: SymbolProcessorEnvironment + + final override fun process(resolver: Resolver): List { + return try { + processChecked(resolver) + } catch (e: SymbolProcessingException) { + env.logger.error(e.message, e.node) + e.cause?.let(env.logger::exception) + emptyList() + } + } + + protected abstract fun processChecked(resolver: Resolver): List +} + +internal class SymbolProcessingException( + val node: KSNode, + override val message: String, + override val cause: Throwable? = null, +) : Exception() diff --git a/compiler/src/test/kotlin/ContributesAssistedFactoryCodeGeneratorTest.kt b/compiler/src/test/kotlin/ContributesAssistedFactoryCodeGeneratorTest.kt index 5495736..3b3cce3 100644 --- a/compiler/src/test/kotlin/ContributesAssistedFactoryCodeGeneratorTest.kt +++ b/compiler/src/test/kotlin/ContributesAssistedFactoryCodeGeneratorTest.kt @@ -1,17 +1,46 @@ import com.google.common.truth.Truth.assertThat import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode +import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode.Embedded +import com.squareup.anvil.compiler.internal.testing.AnvilCompilationMode.Ksp import com.squareup.anvil.compiler.internal.testing.compileAnvil +import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.COMPILATION_ERROR import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters import java.lang.reflect.AnnotatedElement import java.lang.reflect.Modifier +@RunWith(Parameterized::class) @OptIn(ExperimentalCompilerApi::class) -class ContributesAssistedFactoryCodeGeneratorTest { +class ContributesAssistedFactoryCodeGeneratorTest( + private val mode: AnvilCompilationMode, +) { + + companion object { + @Parameters(name = "mode: {0}") + @JvmStatic + fun params() = kspParams() + } + + private fun compileAnvil( + @org.intellij.lang.annotations.Language("kotlin") source: String, + previousCompilationResult: JvmCompilationResult? = null, + block: JvmCompilationResult.() -> Unit = {}, + ) { + compileAnvil( + source, + block = block, + mode = mode, + previousCompilationResult = previousCompilationResult, + ) + } @Test fun `an assisted factory with binding is generated`() { @@ -448,7 +477,8 @@ class ContributesAssistedFactoryCodeGeneratorTest { fun `generated factory method parameters match the order of the bound type factory method parameters`() { compileAnvil( """ - package com.test + @file:Suppress("UNUSED_PARAMETER") + package com.test import dagger.assisted.AssistedInject import dagger.assisted.Assisted @@ -472,7 +502,6 @@ class ContributesAssistedFactoryCodeGeneratorTest { @Assisted("param3") param3: Boolean ) : TestApi """, - allWarningsAsErrors = false, ) { assertThat(exitCode).isEqualTo(OK) @@ -492,10 +521,64 @@ class ContributesAssistedFactoryCodeGeneratorTest { ) } } + + @Test + fun `should not fail on lambda types from another modules`() { + compileAnvil( + """ + @file:Suppress("UNUSED_PARAMETER") + package com.test + + import me.gulya.anvil.assisted.AssistedKey + + interface TestApi + + interface TestApiFactory { + fun create( + @AssistedKey("param1") param1: () -> String + ): TestApi + } + """ + ) { + assertThat(exitCode).isEqualTo(OK) + + compileAnvil( + """ + @file:Suppress("UNUSED_PARAMETER") + package com.test + + import dagger.assisted.AssistedInject + import dagger.assisted.Assisted + import me.gulya.anvil.assisted.ContributesAssistedFactory + + @ContributesAssistedFactory(Any::class, TestApiFactory::class) + class DefaultTestApi @AssistedInject constructor( + @Assisted("param1") param1: () -> String + ) : TestApi + """.trimIndent(), + previousCompilationResult = this, + ) { + assertThat(exitCode).isEqualTo(OK) + } + } + } } inline fun AnnotatedElement.annotationOrNull(): T? = annotations.singleOrNull { it.annotationClass == T::class } as? T inline fun AnnotatedElement.requireAnnotation(): T = - requireNotNull(annotationOrNull()) { "Couldn't find annotation ${T::class}" } \ No newline at end of file + requireNotNull(annotationOrNull()) { "Couldn't find annotation ${T::class}" } + +/** + * Parameters for configuring [AnvilCompilationMode] and whether to run a full test run or not. + */ +internal fun kspParams( + embeddedCreator: () -> Embedded? = { Embedded() }, + kspCreator: () -> Ksp? = { Ksp() }, +): Collection { + return listOfNotNull( + embeddedCreator(), + kspCreator(), + ).distinct() +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fa87cde..0188b0f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,19 @@ [versions] -kotlin = "1.9.23" -anvil = "2.4.9" -dagger = "2.51" +kotlin = "2.0.0" +anvil = "2.5.0-beta09" +ksp = "2.0.0-1.0.21" +dagger = "2.51.1" kotlinx-binaryCompatibility = "0.14.0" dropbox-dependencyGuard = "0.5.0" mavenPublish = "0.28.0" dokka = "1.9.20" +kotlinpoet = "1.17.0" [libraries] anvil-compiler-api = { module = "com.squareup.anvil:compiler-api", version.ref = "anvil" } anvil-compiler-utils = { module = "com.squareup.anvil:compiler-utils", version.ref = "anvil" } -kotlinpoet = "com.squareup:kotlinpoet:1.16.0" +kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } +kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" } google-autoservice-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" google-autoservice-compiler = "com.google.auto.service:auto-service:1.1.1" @@ -20,6 +23,8 @@ dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = google-truth = "com.google.truth:truth:1.4.2" junit = "junit:junit:4.13.2" +ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } + kotlinx-binaryCompatibility = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "kotlinx-binaryCompatibility" } plugin-mavenPublish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "mavenPublish" } plugin-kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -30,6 +35,7 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kotlin-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } anvil = { id = "com.squareup.anvil", version.ref = "anvil" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlinx-binaryCompatibility = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "kotlinx-binaryCompatibility" } dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dropbox-dependencyGuard" } mavenPublish = { id = "com.vanniktech.maven.publish.base", version.ref = "mavenPublish" } \ No newline at end of file diff --git a/samples/build.gradle.kts b/samples/entrypoint/embedded/build.gradle.kts similarity index 64% rename from samples/build.gradle.kts rename to samples/entrypoint/embedded/build.gradle.kts index 0f737f8..20c376e 100644 --- a/samples/build.gradle.kts +++ b/samples/entrypoint/embedded/build.gradle.kts @@ -1,22 +1,24 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.kapt) alias(libs.plugins.anvil) } -group = "me.gulya.anvil" -version = "0.1.0" - kotlin { jvmToolchain(17) compilerOptions { freeCompilerArgs = listOf("-Xextended-compiler-checks") + languageVersion = KotlinVersion.KOTLIN_1_9 } } dependencies { anvil(projects.compiler) implementation(projects.annotations) + implementation(projects.samples.library.api) + implementation(projects.samples.library.impl.embedded) implementation(libs.dagger) kapt(libs.dagger.compiler) diff --git a/samples/entrypoint/embedded/src/main/kotlin/me/gulya/anvil/utils/sample/EmbeddedEntrypoint.kt b/samples/entrypoint/embedded/src/main/kotlin/me/gulya/anvil/utils/sample/EmbeddedEntrypoint.kt new file mode 100644 index 0000000..ca381c8 --- /dev/null +++ b/samples/entrypoint/embedded/src/main/kotlin/me/gulya/anvil/utils/sample/EmbeddedEntrypoint.kt @@ -0,0 +1,13 @@ +package me.gulya.anvil.utils.sample + +import com.squareup.anvil.annotations.MergeComponent + +@MergeComponent(SampleScope::class) +interface SampleComponent { + val factory: TestApi.Factory +} + +fun main(args: Array) { + val component: SampleComponent = DaggerSampleComponent.builder().build() + val testApi = component.factory.create(1, "arg2") +} \ No newline at end of file diff --git a/samples/entrypoint/ksp/build.gradle.kts b/samples/entrypoint/ksp/build.gradle.kts new file mode 100644 index 0000000..8f9e630 --- /dev/null +++ b/samples/entrypoint/ksp/build.gradle.kts @@ -0,0 +1,51 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.internal.KaptGenerateStubsTask +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.anvil) +} + +kotlin { + jvmToolchain(17) + compilerOptions { + freeCompilerArgs = listOf("-Xextended-compiler-checks") + } + + sourceSets { + main { + kotlin { + srcDir("build/anvil/main/generated") + } + } + } +} + + +tasks.withType().configureEach { + // TODO necessary until anvil supports something for K2 contribution merging + compilerOptions { + progressiveMode.set(false) + languageVersion.set(KotlinVersion.KOTLIN_1_9) + } +} + +tasks.withType().configureEach { + // TODO necessary until anvil supports something for K2 contribution merging + compilerOptions { + progressiveMode.set(false) + languageVersion.set(KotlinVersion.KOTLIN_1_9) + } +} + +dependencies { + anvil(projects.compiler) + implementation(projects.annotations) + implementation(projects.samples.library.api) + implementation(projects.samples.library.impl.embedded) + + implementation(libs.dagger) + kapt(libs.dagger.compiler) +} \ No newline at end of file diff --git a/samples/entrypoint/ksp/src/main/kotlin/me/gulya/anvil/utils/sample/KspEntrypoint.kt b/samples/entrypoint/ksp/src/main/kotlin/me/gulya/anvil/utils/sample/KspEntrypoint.kt new file mode 100644 index 0000000..ca381c8 --- /dev/null +++ b/samples/entrypoint/ksp/src/main/kotlin/me/gulya/anvil/utils/sample/KspEntrypoint.kt @@ -0,0 +1,13 @@ +package me.gulya.anvil.utils.sample + +import com.squareup.anvil.annotations.MergeComponent + +@MergeComponent(SampleScope::class) +interface SampleComponent { + val factory: TestApi.Factory +} + +fun main(args: Array) { + val component: SampleComponent = DaggerSampleComponent.builder().build() + val testApi = component.factory.create(1, "arg2") +} \ No newline at end of file diff --git a/samples/library/api/build.gradle.kts b/samples/library/api/build.gradle.kts new file mode 100644 index 0000000..327eb56 --- /dev/null +++ b/samples/library/api/build.gradle.kts @@ -0,0 +1,17 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + +plugins { + alias(libs.plugins.kotlin.jvm) +} + +kotlin { + jvmToolchain(17) + compilerOptions { + freeCompilerArgs = listOf("-Xextended-compiler-checks") + languageVersion = KotlinVersion.KOTLIN_1_9 + } +} + +dependencies { + implementation(projects.annotations) +} \ No newline at end of file diff --git a/samples/library/api/src/main/kotlin/me/gulya/anvil/utils/sample/TestApi.kt b/samples/library/api/src/main/kotlin/me/gulya/anvil/utils/sample/TestApi.kt new file mode 100644 index 0000000..b6425d8 --- /dev/null +++ b/samples/library/api/src/main/kotlin/me/gulya/anvil/utils/sample/TestApi.kt @@ -0,0 +1,14 @@ +package me.gulya.anvil.utils.sample + +import me.gulya.anvil.assisted.AssistedKey + +interface TestApi { + interface Factory { + fun create( + @AssistedKey("arg1") arg: Int, + @AssistedKey("arg2") arg1: String + ): TestApi + } +} + +abstract class SampleScope \ No newline at end of file diff --git a/samples/library/impl/embedded/build.gradle.kts b/samples/library/impl/embedded/build.gradle.kts new file mode 100644 index 0000000..8072c0b --- /dev/null +++ b/samples/library/impl/embedded/build.gradle.kts @@ -0,0 +1,27 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.anvil) +} + +anvil { + generateDaggerFactories = true +} + +kotlin { + jvmToolchain(17) + compilerOptions { + freeCompilerArgs = listOf("-Xextended-compiler-checks") + languageVersion = KotlinVersion.KOTLIN_1_9 + } + +} + +dependencies { + anvil(projects.compiler) + implementation(projects.annotations) + implementation(projects.samples.library.api) + + implementation(libs.dagger) +} \ No newline at end of file diff --git a/samples/library/impl/embedded/src/main/kotlin/me/gulya/anvil/utils/sample/DefaultTestApi.kt b/samples/library/impl/embedded/src/main/kotlin/me/gulya/anvil/utils/sample/DefaultTestApi.kt new file mode 100644 index 0000000..2a81e05 --- /dev/null +++ b/samples/library/impl/embedded/src/main/kotlin/me/gulya/anvil/utils/sample/DefaultTestApi.kt @@ -0,0 +1,11 @@ +package me.gulya.anvil.utils.sample + +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import me.gulya.anvil.assisted.ContributesAssistedFactory + +@ContributesAssistedFactory(SampleScope::class, TestApi.Factory::class) +class DefaultTestApi @AssistedInject constructor( + @Assisted("arg2") private val arg1: String, + @Assisted("arg1") private val arg: Int, +) : TestApi \ No newline at end of file diff --git a/samples/library/impl/ksp/build.gradle.kts b/samples/library/impl/ksp/build.gradle.kts new file mode 100644 index 0000000..bccd907 --- /dev/null +++ b/samples/library/impl/ksp/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) +} + +anvil { + useKsp( + contributesAndFactoryGeneration = true, + ) + generateDaggerFactories = true +} + +kotlin { + jvmToolchain(17) + compilerOptions { + freeCompilerArgs = listOf("-Xextended-compiler-checks") + } +} + +dependencies { + ksp(projects.compiler) + implementation(projects.annotations) + implementation(projects.samples.library.api) + + implementation(libs.dagger) +} \ No newline at end of file diff --git a/samples/library/impl/ksp/src/main/kotlin/me/gulya/anvil/utils/sample/DefaultTestApi.kt b/samples/library/impl/ksp/src/main/kotlin/me/gulya/anvil/utils/sample/DefaultTestApi.kt new file mode 100644 index 0000000..2a81e05 --- /dev/null +++ b/samples/library/impl/ksp/src/main/kotlin/me/gulya/anvil/utils/sample/DefaultTestApi.kt @@ -0,0 +1,11 @@ +package me.gulya.anvil.utils.sample + +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import me.gulya.anvil.assisted.ContributesAssistedFactory + +@ContributesAssistedFactory(SampleScope::class, TestApi.Factory::class) +class DefaultTestApi @AssistedInject constructor( + @Assisted("arg2") private val arg1: String, + @Assisted("arg1") private val arg: Int, +) : TestApi \ No newline at end of file diff --git a/samples/src/main/kotlin/me/gulya/anvil/utils/sample/TestApi.kt b/samples/src/main/kotlin/me/gulya/anvil/utils/sample/TestApi.kt deleted file mode 100644 index b271189..0000000 --- a/samples/src/main/kotlin/me/gulya/anvil/utils/sample/TestApi.kt +++ /dev/null @@ -1,34 +0,0 @@ -package me.gulya.anvil.utils.sample - -import com.squareup.anvil.annotations.MergeComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import me.gulya.anvil.assisted.ContributesAssistedFactory -import me.gulya.anvil.assisted.AssistedKey - -interface TestApi { - interface Factory { - fun create( - @AssistedKey("arg1") arg: Int, - @AssistedKey("arg2") arg1: String - ): TestApi - } -} - -abstract class SampleScope - -@ContributesAssistedFactory(SampleScope::class, TestApi.Factory::class) -class DefaultTestApi @AssistedInject constructor( - @Assisted("arg2") private val arg1: String, - @Assisted("arg1") private val arg: Int, -) : TestApi - -@MergeComponent(SampleScope::class) -interface SampleComponent { - val factory: TestApi.Factory -} - -fun main(args: Array) { - val component: SampleComponent = DaggerSampleComponent.builder().build() - val testApi = component.factory.create(1, "arg2") -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index e46e413..5b0c0d4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,4 +20,8 @@ dependencyResolutionManagement { include(":compiler") include(":annotations") -include(":samples") \ No newline at end of file +include(":samples:library:api") +include(":samples:library:impl:ksp") +include(":samples:library:impl:embedded") +include(":samples:entrypoint:embedded") +include(":samples:entrypoint:ksp") \ No newline at end of file