Skip to content
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

Provide option to hide the merged interface #61

Merged
merged 1 commit into from
Oct 28, 2024
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 @@ -13,6 +13,7 @@ import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSValueParameter
import com.google.devtools.ksp.symbol.Visibility
import me.tatarka.inject.annotations.Qualifier
import me.tatarka.inject.annotations.Scope
Expand Down Expand Up @@ -191,6 +192,10 @@ internal interface ContextAware {

fun KSDeclaration.requireQualifiedName(): String = requireQualifiedName(this@ContextAware)

fun KSValueParameter.requireName(): String = requireNotNull(name, this) {
"The name of the parameter $this was null."
}.asString()

fun Resolver.getSymbolsWithAnnotation(annotation: KClass<*>): Sequence<KSAnnotated> =
getSymbolsWithAnnotation(annotation.requireQualifiedName())

Expand All @@ -206,4 +211,6 @@ internal interface ContextAware {
get() = requireQualifiedName()
.split(".")
.joinToString(separator = "") { it.capitalize() }

val KSClassDeclaration.mergedClassName get() = "${innerClassNames()}Merged"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import software.amazon.lastmile.kotlin.inject.anvil.processor.ContributesBinding
import software.amazon.lastmile.kotlin.inject.anvil.processor.ContributesSubcomponentFactoryProcessor
import software.amazon.lastmile.kotlin.inject.anvil.processor.ContributesSubcomponentProcessor
import software.amazon.lastmile.kotlin.inject.anvil.processor.ContributesToProcessor
import software.amazon.lastmile.kotlin.inject.anvil.processor.GenerateKotlinInjectComponentProcessor
import software.amazon.lastmile.kotlin.inject.anvil.processor.MergeComponentProcessor
import software.amazon.lastmile.kotlin.inject.anvil.processor.extend.ContributingAnnotationProcessor

Expand Down Expand Up @@ -64,6 +65,12 @@ class KotlinInjectExtensionSymbolProcessorProvider : SymbolProcessorProvider {
logger = environment.logger,
),
)
addIfEnabled(
GenerateKotlinInjectComponentProcessor(
codeGenerator = environment.codeGenerator,
logger = environment.logger,
),
)
}

return CompositeSymbolProcessor(symbolProcessors)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
@file:OptIn(KspExperimental::class)

package software.amazon.lastmile.kotlin.inject.anvil.processor

import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier.ABSTRACT
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
import com.squareup.kotlinpoet.ksp.toAnnotationSpec
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
import me.tatarka.inject.annotations.Component
import software.amazon.lastmile.kotlin.inject.anvil.ContextAware
import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent
import software.amazon.lastmile.kotlin.inject.anvil.addOriginAnnotation
import kotlin.reflect.KClass

/**
* Generates the final kotlin-inject component when [MergeComponent] is found without the
* [Component] annotation, e.g.
* ```
* package software.amazon.test
*
* @MergeComponent(AppScope::class)
* @SingleIn(AppScope::class)
* interface TestComponent
* ```
* Will generate:
* ```
* package software.amazon.test
*
* @MergeComponent(AppScope::class)
* @Component
* @SingleIn(AppScope::class)
* interface KotlinInjectTestComponent : KotlinInjectTestComponentMerged
* ```
*
* Parameters are supported as well, e.g.
* ```
* package software.amazon.test
*
* @MergeComponent(AppScope::class)
* @SingleIn(AppScope::class)
* abstract class TestComponent(
* @get:Provides val string: String,
* )
* ```
* Will generate:
* ```
* package software.amazon.test
*
* @MergeComponent(AppScope::class)
* @Component
* @SingleIn(AppScope::class)
* abstract class KotlinInjectTestComponent(string: String) : KotlinInjectTestComponentMerged(string)
* ```
*
* This processor will also add a function to make instantiating the generated component easier.
* The function delegates the call to the final kotlin-inject component. For the example above
* the following function would be generated:
* ```
* fun KClass<TestComponent>.create(string: String): TestComponent {
* return KotlinInjectTestComponent::class.create(string)
* }
* ```
*/
internal class GenerateKotlinInjectComponentProcessor(
private val codeGenerator: CodeGenerator,
override val logger: KSPLogger,
) : SymbolProcessor, ContextAware {

private val processedComponents = mutableSetOf<String>()

@Suppress("ReturnCount")
override fun process(resolver: Resolver): List<KSAnnotated> {
resolver
.getSymbolsWithAnnotation(MergeComponent::class)
.filterIsInstance<KSClassDeclaration>()
.filter { it.requireQualifiedName() !in processedComponents }
.filter { !it.isAnnotationPresent(Component::class) }
.onEach {
checkIsPublic(it)
checkHasScope(it)
}
.forEach {
generateKotlinInjectComponent(it)

processedComponents += it.requireQualifiedName()
}

return emptyList()
}

@Suppress("LongMethod")
private fun generateKotlinInjectComponent(clazz: KSClassDeclaration) {
val className = ClassName(
packageName = clazz.packageName.asString(),
simpleNames = listOf("KotlinInject${clazz.innerClassNames()}"),
)

val isInterface = clazz.classKind == ClassKind.INTERFACE
val parameters = clazz.primaryConstructor?.parameters ?: emptyList()
val parametersAsSpec = parameters.map {
ParameterSpec
.builder(
name = it.requireName(),
type = it.type.toTypeName(),
)
.build()
}

val classBuilder = if (isInterface) {
TypeSpec
.interfaceBuilder(className)
.addSuperinterface(clazz.toClassName())
} else {
TypeSpec
.classBuilder(className)
.addModifiers(ABSTRACT)
.superclass(clazz.toClassName())
.apply {
if (parameters.isNotEmpty()) {
primaryConstructor(
FunSpec.constructorBuilder()
.addParameters(parametersAsSpec)
.build(),
)
addSuperclassConstructorParameter(
parameters.joinToString { it.requireName() },
)
}
}
}

val fileSpec = FileSpec.builder(className)
.addType(
classBuilder
.addOriginatingKSFile(clazz.requireContainingFile())
.addOriginAnnotation(clazz)
.addAnnotation(Component::class)
.addAnnotation(clazz.findAnnotation(MergeComponent::class).toAnnotationSpec())
.apply {
clazz.annotations
.filter { it.isKotlinInjectScopeAnnotation() }
.singleOrNull()
?.toAnnotationSpec()
?.let { addAnnotation(it) }
}
.addSuperinterface(
className.peerClass("KotlinInject${clazz.mergedClassName}"),
)
.build(),
)
.addFunction(
FunSpec
.builder("create")
.receiver(
KClass::class.asTypeName().parameterizedBy(clazz.toClassName()),
)
.addParameters(parametersAsSpec)
.returns(clazz.toClassName())
.addStatement(
"return %T::class.create(${parametersAsSpec.joinToString { it.name }})",
className,
)
.build(),
)
.build()

fileSpec.writeTo(codeGenerator, aggregating = false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.writeTo
import me.tatarka.inject.annotations.Component
import software.amazon.lastmile.kotlin.inject.anvil.ContextAware
import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding
import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent
Expand Down Expand Up @@ -95,6 +96,10 @@ internal class MergeComponentProcessor(
.filterIsInstance<KSClassDeclaration>()
.distinctBy { it.requireQualifiedName() }
.filter { it.requireQualifiedName() !in processedComponents }
.filter { it.isAnnotationPresent(Component::class) }
.onEach {
checkSuperTypeDeclared(it)
}
.toList()

// Nothing to do.
Expand Down Expand Up @@ -141,7 +146,7 @@ internal class MergeComponentProcessor(
) {
val className = ClassName(
packageName = clazz.packageName.asString(),
simpleNames = listOf("${clazz.innerClassNames()}Merged"),
simpleNames = listOf(clazz.mergedClassName),
)

val scope = clazz.scope()
Expand Down Expand Up @@ -234,4 +239,13 @@ internal class MergeComponentProcessor(
clazz.originOrNull()
}
}

private fun checkSuperTypeDeclared(clazz: KSClassDeclaration) {
check(clazz.superTypes.map { it.toString() }.any { it == clazz.mergedClassName }, clazz) {
"${clazz.simpleName.asString()} is annotated with @MergeComponent and @Component. " +
"It's required to add ${clazz.mergedClassName} as super type to " +
"${clazz.simpleName.asString()}. If you don't want to add the super manually, " +
"then you must remove the @Component annotation."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package software.amazon.lastmile.kotlin.inject.anvil

import com.tschuchort.compiletesting.JvmCompilationResult
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.jetbrains.kotlin.descriptors.runtime.structure.primitiveByWrapper
import software.amazon.lastmile.kotlin.inject.anvil.internal.Origin
import java.lang.reflect.Field
import java.lang.reflect.Modifier
Expand All @@ -26,11 +27,15 @@ internal val Class<*>.generatedComponent: Class<*>
internal val JvmCompilationResult.contributesRenderer: Class<*>
get() = classLoader.loadClass("software.amazon.test.ContributesRenderer")

internal fun <T : Any> Class<*>.newComponent(): T {
internal fun <T : Any> Class<*>.newComponent(vararg arguments: Any): T {
@Suppress("UNCHECKED_CAST")
return classLoader.loadClass("$packageName.Inject$simpleName")
.getDeclaredConstructor()
.newInstance() as T
.getDeclaredConstructor(
*arguments.map { arg ->
arg::class.java.primitiveByWrapper ?: arg::class.java
}.toTypedArray(),
)
.newInstance(*arguments) as T
}

internal val Class<*>.mergedComponent: Class<*>
Expand Down
Loading