Annotation processor Implementation

This commit is contained in:
Gergely Hegedus 2021-07-30 22:36:48 +03:00
parent c1b8d92461
commit edf94325d8
19 changed files with 484 additions and 0 deletions

1
annotation/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

12
annotation/build.gradle Normal file
View file

@ -0,0 +1,12 @@
plugins {
id 'java-library'
id 'kotlin'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
sourceCompatibility = "8"
targetCompatibility = "8"

View file

@ -0,0 +1,19 @@
package org.fnives.library.reloadable.module.annotation
/**
* Annotate your custom Annotation with this to apply the annotation processor.
*
* The annotation processor will generate 2 classes for you.
*
* First will be an interface Reload<YourAnnotation>Module with only one method, reload.
* This Reload<YourAnnotation>Module can be injected anywhere where you want to reload the module.
*
* Second will be a class Reload<YourAnnotation>ModuleImpl which is the actual Module implementation for Hilt.
* This provides every class which constructor you annotate with YourAnnotation.
*
* Reload in this context means, every instance will be cleared and the next time Hilt accesses it, a new will be created.
* This newly created instance is reused until the next reload call.
*/
@Target(AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class ReloadableModule

View file

@ -1,5 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.5.21"
ext.hilt_version = "2.38.1"
repositories {
google()
mavenCentral()

1
processor/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

32
processor/build.gradle Normal file
View file

@ -0,0 +1,32 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
test {
useJUnitPlatform()
testLogging {
events 'started', 'passed', 'skipped', 'failed'
exceptionFormat "full"
showStandardStreams true
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.squareup:javapoet:1.13.0"
implementation project(":annotation")
implementation "com.google.dagger:hilt-core:$hilt_version"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.0"
testImplementation "org.junit.jupiter:junit-jupiter-engine:5.7.0"
testImplementation "com.github.tschuchortdev:kotlin-compile-testing:1.4.2"
testRuntimeOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version")
testRuntimeOnly("org.jetbrains.kotlin:kotlin-annotation-processing-embeddable:$kotlin_version")
}
sourceCompatibility = "8"
targetCompatibility = "8"

View file

@ -0,0 +1,201 @@
package org.fnives.library.reloadable.module.processor
import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.CodeBlock
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeSpec
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.fnives.library.reloadable.module.processor.GenerateReloadModuleUseCase.Companion.getModuleUseCaseName
import javax.annotation.processing.Filer
import javax.annotation.processing.RoundEnvironment
import javax.inject.Provider
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
class GenerateReloadModule(private val elementUtils: Elements, private val filer: Filer) {
fun createAndWrite(
typeElement: TypeElement,
roundEnvironment: RoundEnvironment
) {
val annotatedElements = roundEnvironment.getElementsAnnotatedWith(typeElement)
.filterIsInstance<ExecutableElement>()
val annotatedTypes = annotatedElements.map { it.enclosingElement as TypeElement }
val reloadFunctionSpec = createReloadFunctionSpec(annotatedTypes)
val cachedProperties = annotatedTypes.map(::getPropertySpec)
val provideMethods = annotatedElements.mapIndexed { index, it ->
createProvideMethodForDependency(it, annotatedTypes[index])
}
val typeSpec = createTypeSpec(typeElement, reloadFunctionSpec, provideMethods, cachedProperties)
val packageName = elementUtils.getPackageOf(typeElement).toString()
writeToSourceFile(typeSpec, packageName, filer)
}
private fun writeToSourceFile(typeSpec: TypeSpec, packageName: String, filer: Filer) {
JavaFile.builder(packageName, typeSpec).build().writeTo(filer)
}
private fun getPropertySpec(typeElement: TypeElement): FieldSpec =
FieldSpec.builder(TypeName.get(typeElement.asType()), typeElement.cachedPropertyName())
.initializer("null")
.addModifiers(Modifier.PRIVATE)
.build()
private fun createProvideMethodForDependency(
constructorElement: ExecutableElement,
dependencyType: TypeElement
): MethodSpec {
val constructorParameters = constructorElement.parameters.map { "${it.simpleName}.get()" }.joinToString(", ")
val functionElement = MethodSpec.methodBuilder("provide${dependencyType.simpleName}")
.addAnnotation(Provides::class.java)
.returns(TypeName.get(dependencyType.asType()))
.addModifiers(Modifier.PUBLIC)
.addCode(
CodeBlock.of(
"if (${dependencyType.cachedPropertyName()} == null) {\n" +
" ${dependencyType.cachedPropertyName()} = new ${"$"}T ($constructorParameters);\n" +
"}\n" +
"return ${dependencyType.cachedPropertyName()};",
dependencyType
)
)
return constructorElement.parameters.fold(functionElement) { methodSpecBuilder, variableElement ->
val className = TypeName.get(variableElement.asType())
val providerTypeSpec = ParameterizedTypeName.get(ClassName.get(Provider::class.java), className)
methodSpecBuilder.addParameter(
ParameterSpec.builder(providerTypeSpec, variableElement.simpleName.toString())
.addAnnotations(variableElement.annotationMirrors.map { AnnotationSpec.get(it) })
.build()
)
}.build()
}
private fun createProvideMethodForReloadModuleUseCase(typeElement: TypeElement): MethodSpec =
MethodSpec.methodBuilder("provide${getModuleUseCaseName(typeElement)}")
.addAnnotation(Provides::class.java)
.addModifiers(Modifier.PUBLIC)
.returns(GenerateReloadModuleUseCase.getTypeName(typeElement, elementUtils))
.addCode("return this;")
.build()
private fun createTypeSpec(typeElement: TypeElement, reloadFunctionSpec: MethodSpec, provideMethods: List<MethodSpec>, cacheProperties: List<FieldSpec>) =
TypeSpec.classBuilder(getModuleUseCaseName(typeElement) + "Impl")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(GenerateReloadModuleUseCase.getTypeName(typeElement, elementUtils))
.addAnnotation(Module::class.java)
.addAnnotation(
AnnotationSpec.builder(InstallIn::class.java)
.addMember("value", "\$T.class", SingletonComponent::class.java)
.build()
)
.addFields(cacheProperties)
.addMethod(createProvideMethodForReloadModuleUseCase(typeElement))
.addMethods(provideMethods)
.addMethod(reloadFunctionSpec)
.build()
private fun createReloadFunctionSpec(annotatedElements: List<TypeElement>): MethodSpec {
val reloadFunctionSpec = MethodSpec.methodBuilder(GenerateReloadModuleUseCase.getReloadMethodName())
.addAnnotation(Override::class.java)
.addModifiers(Modifier.PUBLIC)
return annotatedElements.fold(reloadFunctionSpec) { methodSpecBuilder, typeElement ->
methodSpecBuilder.addCode("${typeElement.cachedPropertyName()} = null;")
}.build()
}
// private fun createImplementation(interfaceTypeSpec: TypeSpec, typeElement: TypeElement, roundEnvironment: RoundEnvironment): TypeSpec {
// val annotatedElements = roundEnvironment.getElementsAnnotatedWith(typeElement)
// .filterIsInstance<ExecutableElement>()
//
// val reloadFunctionSpec = MethodSpec.methodBuilder("reload")
// .addAnnotation(Override::class.java)
// .addModifiers(Modifier.PUBLIC)
// val moduleImplName = "Reloadable${typeElement.simpleName}Impl"
// val typeSpecBuilder = TypeSpec.classBuilder(moduleImplName)
// .addModifiers(Modifier.PUBLIC)
// .addSuperinterface(interfaceTypeSpec.asTypeName(typeElement))
// .addAnnotation(Module::class.java)
// .addAnnotation(
// AnnotationSpec.builder(InstallIn::class.java)
// .addMember("value", "\$T.class", SingletonComponent::class.java)
// .build()
// )
// .addMethod(
// MethodSpec.methodBuilder("provideReloadable${typeElement.simpleName}")
// .addAnnotation(Provides::class.java)
// .addModifiers(Modifier.PUBLIC)
// .returns(interfaceTypeSpec.asTypeName(typeElement))
// .addCode("return this;")
// .build()
// )
//
// annotatedElements.forEach { constructorElement ->
// val annotatedType = constructorElement.enclosingElement as TypeElement
// val annotatedTypeClassName = ClassName.bestGuess(annotatedType.qualifiedName.toString())
// val memberElement = FieldSpec.builder(
// annotatedTypeClassName,
// "cached${annotatedType.simpleName}"
// )
// .initializer("null")
// .addModifiers(Modifier.PRIVATE)
// .build()
// reloadFunctionSpec.addCode("cached${annotatedType.simpleName} = null;")
// val constructorParameters = constructorElement.parameters.map { "${it.simpleName}.get()" }.joinToString(", ")
// val functionElement = MethodSpec.methodBuilder("provide${annotatedType.simpleName}")
// .addAnnotation(Provides::class.java)
// .returns(annotatedTypeClassName)
// .addModifiers(Modifier.PUBLIC)
// .addCode(
// CodeBlock.of(
// """if (cached${annotatedType.simpleName} == null) {
// cached${annotatedType.simpleName} = new ${"$"}T ($constructorParameters);
// }
// return cached${annotatedType.simpleName};
// """.trim(),
// annotatedType
// )
// )
//
// constructorElement.parameters.forEach { variableElement ->
// val className = TypeName.get(variableElement.asType())
// val providerTypeSpec = ParameterizedTypeName.get(ClassName.get(Provider::class.java), className)
// functionElement.addParameter(
// ParameterSpec.builder(providerTypeSpec, variableElement.simpleName.toString())
// .addAnnotations(variableElement.annotationMirrors.map { AnnotationSpec.get(it) })
// .build()
// )
// }
//
// typeSpecBuilder.addField(memberElement)
// typeSpecBuilder.addMethod(functionElement.build())
// }
//
//
// typeSpecBuilder.addMethod(reloadFunctionSpec.build())
//
// return typeSpecBuilder.build()
// }
//
// private fun TypeSpec.asTypeName(typeElement: TypeElement) =
// ClassName.bestGuess(elementUtils.getPackageOf(typeElement).toString() + "." + name.orEmpty())
companion object {
private fun TypeElement.cachedPropertyName() = "cached${simpleName}"
}
}

View file

@ -0,0 +1,43 @@
package org.fnives.library.reloadable.module.processor
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.TypeSpec
import javax.annotation.processing.Filer
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
class GenerateReloadModuleUseCase(private val elementUtils: Elements, private val filer: Filer) {
fun createAndWrite(typeElement: TypeElement) {
val typeSpec = createTypeSpec(getModuleUseCaseName(typeElement))
val packageName = elementUtils.getPackageOf(typeElement).toString()
writeToSourceFile(typeSpec, packageName, filer)
}
private fun createTypeSpec(moduleUseCaseName: String) =
TypeSpec.interfaceBuilder(moduleUseCaseName)
.addModifiers(Modifier.PUBLIC)
.addMethod(
MethodSpec.methodBuilder(getReloadMethodName())
.addModifiers(Modifier.ABSTRACT)
.addModifiers(Modifier.PUBLIC)
.build()
)
.build()
private fun writeToSourceFile(typeSpec: TypeSpec, packageName: String, filer: Filer) {
JavaFile.builder(packageName, typeSpec).build().writeTo(filer)
}
companion object {
fun getModuleUseCaseName(typeElement: TypeElement) = "Reload${typeElement.simpleName}Module"
fun getReloadMethodName() = "reload"
fun getTypeName(typeElement: TypeElement, elementUtils: Elements) =
ClassName.bestGuess("${elementUtils.getPackageOf(typeElement)}.${getModuleUseCaseName(typeElement)}")
}
}

View file

@ -0,0 +1,52 @@
package org.fnives.library.reloadable.module.processor
import org.fnives.library.reloadable.module.annotation.ReloadableModule
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Filer
import javax.annotation.processing.Messager
import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
class ReloadableModuleProcessor : AbstractProcessor() {
private lateinit var filer: Filer
private lateinit var messager: Messager
private lateinit var elementUtils: Elements
private lateinit var typeUtils: Types
private lateinit var generateReloadModuleUseCase: GenerateReloadModuleUseCase
private lateinit var generateReloadModule: GenerateReloadModule
private val supportedAnnotations = setOf(ReloadableModule::class.java)
@Synchronized
override fun init(processingEnvironment: ProcessingEnvironment) {
super.init(processingEnvironment)
filer = processingEnvironment.filer
messager = processingEnvironment.messager
elementUtils = processingEnvironment.elementUtils
typeUtils = processingEnvironment.typeUtils
generateReloadModuleUseCase = GenerateReloadModuleUseCase(elementUtils, filer)
generateReloadModule = GenerateReloadModule(elementUtils, filer)
}
override fun getSupportedAnnotationTypes(): Set<String> =
supportedAnnotations.map(Class<*>::getCanonicalName).toSet()
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
override fun process(set: Set<TypeElement>, roundEnvironment: RoundEnvironment): Boolean {
if (roundEnvironment.processingOver()) return false
val annotatedElements = supportedAnnotations.flatMap(roundEnvironment::getElementsAnnotatedWith).filterIsInstance<TypeElement>()
annotatedElements.forEach {
generateReloadModuleUseCase.createAndWrite(it)
generateReloadModule.createAndWrite(it, roundEnvironment)
}
return false
}
}

View file

@ -0,0 +1 @@
org.fnives.library.reloadable.module.processor.ReloadableModuleProcessor,isolating

View file

@ -0,0 +1 @@
org.fnives.library.reloadable.module.processor.ReloadableModuleProcessor

View file

@ -0,0 +1,12 @@
package org.fnives.library.reloadable.module.processor
import java.io.File
import java.nio.file.Paths
/**
+ * Helper class which read the file in the resources folder with the given [fileName] into a string, each line separated with [lineDelimiter].
+ */
fun Any.readResourceFileToString(fileName: String): String {
val path = this::class.java.classLoader.getResource(fileName).toURI().path
return File(Paths.get(path).toUri()).readText()
}

View file

@ -0,0 +1,50 @@
package org.fnives.library.reloadable.module.processor
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
class TestReloadableModuleProcessor {
@Test
fun testSimpleSetup() {
val userModuleInput = readResourceFileToString("simple/input/UserModuleInject.kt")
val providedByUserModule = readResourceFileToString("simple/input/ProvidedByUserModule.kt")
val fooDependency = readResourceFileToString("simple/input/FooDependency.kt")
val result = KotlinCompilation().apply {
sources = listOf(
SourceFile.kotlin("UserModule.kt", userModuleInput),
SourceFile.kotlin("ProvidedByUserModule.kt", providedByUserModule),
SourceFile.kotlin("FooDependency.kt", fooDependency)
)
annotationProcessors = listOf(ReloadableModuleProcessor())
inheritClassPath = true
messageOutputStream = System.out // see diagnostics in real time
}.compile()
val generatedFiles = result.generatedFiles.toList()
val reloadableUserModule = generatedFiles[0].readText()
val reloadableUserModuleImpl = generatedFiles[1].readText()
AssertionsAssertEqualsIgnoringMultipleSpaces(
readResourceFileToString("simple/expected/ReloadUserModuleInjectModule.java"),
reloadableUserModule
)
AssertionsAssertEqualsIgnoringMultipleSpaces(
readResourceFileToString("simple/expected/ReloadUserModuleInjectModuleImpl.java"),
reloadableUserModuleImpl
)
Assertions.assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode)
}
companion object {
private fun String.replaceSpacesWithOnlyOne(): String = replace(" *".toRegex(), " ")
@Suppress("TestFunctionName")
private fun AssertionsAssertEqualsIgnoringMultipleSpaces(expected: String, actual: String) =
Assertions.assertEquals(expected.replaceSpacesWithOnlyOne(), actual.replaceSpacesWithOnlyOne())
}
}

View file

@ -0,0 +1,5 @@
package simple.input;
public interface ReloadUserModuleInjectModule {
void reload();
}

View file

@ -0,0 +1,34 @@
package simple.input;
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
import dagger.hilt.components.SingletonComponent;
import java.lang.Override;
import javax.inject.Provider;
import org.jetbrains.annotations.NotNull;
@Module
@InstallIn(SingletonComponent.class)
public class ReloadUserModuleInjectModuleImpl implements ReloadUserModuleInjectModule {
private ProvidedByUserModule cachedProvidedByUserModule = null;
@Provides
public ReloadUserModuleInjectModule provideReloadUserModuleInjectModule() {
return this;
}
@Provides
public ProvidedByUserModule provideProvidedByUserModule(
@NotNull Provider<FooDependency> fooDependency) {
if (cachedProvidedByUserModule == null) {
cachedProvidedByUserModule = new ProvidedByUserModule (fooDependency.get());
}
return cachedProvidedByUserModule;
}
@Override
public void reload() {
cachedProvidedByUserModule = null;
}
}

View file

@ -0,0 +1,5 @@
package simple.input
import javax.inject.Inject
class FooDependency @Inject constructor()

View file

@ -0,0 +1,5 @@
package simple.input
import org.jetbrains.annotations.NotNull
class ProvidedByUserModule @UserModuleInject constructor(@NotNull fooDependency: FooDependency)

View file

@ -0,0 +1,6 @@
package simple.input
import org.fnives.library.reloadable.module.annotation.ReloadableModule
@ReloadableModule
annotation class UserModuleInject

View file

@ -8,3 +8,5 @@ dependencyResolutionManagement {
}
rootProject.name = "HiltReloadableModule"
include ':app'
include ':processor'
include ':annotation'