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

Add support for generic mulit-interface mocks in Common #139

Merged
merged 6 commits into from
May 12, 2022
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 @@ -25,11 +25,13 @@ import com.squareup.kotlinpoet.ksp.toTypeVariableName
import tech.antibytes.kmock.processor.ProcessorContract.GenericDeclaration
import tech.antibytes.kmock.processor.ProcessorContract.GenericResolver
import tech.antibytes.kmock.processor.ProcessorContract.TemplateSource
import tech.antibytes.kmock.processor.utils.mapArgumentType

internal object KMockGenerics : GenericResolver {
private val any = Any::class.asTypeName()
private val nullableAnys = listOf(any.copy(nullable = true))
private val nonNullableAnys = listOf(any.copy(nullable = false))
private const val TYPE_PARAMETER = "KMockTypeParameter"

private fun resolveBound(type: KSTypeParameter): List<KSTypeReference> = type.bounds.toList()

Expand Down Expand Up @@ -59,6 +61,78 @@ internal object KMockGenerics : GenericResolver {
)
}

private fun mapTypes(
generics: Map<String, List<KSTypeReference>>,
suffix: Int,
): Map<String, String> {
var counter = 0 + suffix
return generics.map { (typeName, _) ->
Pair(typeName, "$TYPE_PARAMETER$counter").also { counter++ }
}.toMap()
}

private fun mapDeclaredGenericsWithSuffix(
generics: Map<String, List<KSTypeReference>>,
suffix: Int,
typeResolver: TypeParameterResolver
): List<TypeVariableName> {
var counter = 0 + suffix
val mapping = mapTypes(generics, suffix)
return generics.map { (_, bounds) ->
TypeVariableName(
"$TYPE_PARAMETER$counter",
bounds = bounds.map { ksReference ->
ksReference.mapArgumentType(
typeParameterResolver = typeResolver,
mapping = mapping,
)
}
).also { counter++ }
}
}

private fun resolveTypeParameter(
typeParameter: Map<String, List<KSTypeReference>>?,
typeResolver: TypeParameterResolver,
suffix: Int,
): List<TypeVariableName> {
return if (typeParameter == null) {
emptyList()
} else {
mapDeclaredGenericsWithSuffix(
generics = typeParameter,
typeResolver = typeResolver,
suffix = suffix,
)
}
}

override fun remapTypes(
templates: List<KSClassDeclaration>,
generics: List<Map<String, List<KSTypeReference>>?>
): Pair<List<TypeName>, List<TypeVariableName>> {
var counter = 0
val aggregatedTypeParameter: MutableList<TypeVariableName> = mutableListOf()
val parameterizedParents = templates.mapIndexed { idx, parent ->
val typeParameter = resolveTypeParameter(
typeParameter = generics[idx],
typeResolver = parent.typeParameters.toTypeParameterResolver(),
suffix = counter
)
val raw = parent.toClassName()
counter += generics[idx]?.size ?: 0

if (typeParameter.isNotEmpty()) {
aggregatedTypeParameter.addAll(typeParameter)
raw.parameterizedBy(typeParameter)
} else {
raw
}
}

return Pair(parameterizedParents, aggregatedTypeParameter)
}

private fun isNullable(type: KSType): Boolean = type.nullability == Nullability.NULLABLE

private fun anys(rootNullability: Boolean): List<TypeName> {
Expand Down Expand Up @@ -357,7 +431,9 @@ internal object KMockGenerics : GenericResolver {
} else {
template.toClassName()
.parameterizedBy(
template.typeParameters.map { type -> type.toTypeVariableName(resolver) }
template.typeParameters.map { type ->
type.toTypeVariableName(resolver)
}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSFile
import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview
import tech.antibytes.kmock.processor.ProcessorContract.Aggregated
import tech.antibytes.kmock.processor.ProcessorContract.KmpCodeGenerator
import tech.antibytes.kmock.processor.ProcessorContract.MockFactoryEntryPointGenerator
Expand All @@ -32,6 +31,7 @@ import tech.antibytes.kmock.processor.ProcessorContract.TemplateSource
*/
internal class KMockProcessor(
private val logger: KSPLogger,
private val isUnderCompilerTest: Boolean, // TODO - Test Concern see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/263
private val isKmp: Boolean,
private val codeGenerator: KmpCodeGenerator,
private val interfaceGenerator: MultiInterfaceBinder,
Expand Down Expand Up @@ -118,6 +118,18 @@ internal class KMockProcessor(
commonMultiAggregated.extractedTemplates.isEmpty() // TODO - Test Concern see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/263
}

// TODO - Test Concern see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/263
private fun resolveMultiSources(
aggregated: Aggregated<TemplateMultiSource>,
stored: Aggregated<TemplateMultiSource>,
): List<TemplateMultiSource> {
return if (isUnderCompilerTest) {
aggregated.extractedTemplates
} else {
stored.extractedTemplates
}
}

private fun stubCommonSources(
resolver: Resolver,
relaxer: Relaxer?
Expand All @@ -127,7 +139,7 @@ internal class KMockProcessor(

mockGenerator.writeCommonMocks(
templateSources = singleCommonSources.extractedTemplates,
templateMultiSources = commonMultiAggregated,
templateMultiSources = resolveMultiSources(multiCommonSources, commonMultiAggregated),
relaxer = relaxer,
)

Expand Down Expand Up @@ -250,7 +262,6 @@ internal class KMockProcessor(
return relaxer
}

@OptIn(KotlinPoetKspPreview::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
val relaxer = extractRelaxer(resolver)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ import tech.antibytes.kmock.processor.utils.SourceFilter
import tech.antibytes.kmock.processor.utils.SourceSetValidator
import tech.antibytes.kmock.processor.utils.SpyContainer

class KMockProcessorProvider : SymbolProcessorProvider {
class KMockProcessorProvider(
private val isUnderCompilerTest: Boolean = false
) : SymbolProcessorProvider {
private fun determineFactoryGenerator(
options: Options,
logger: KSPLogger,
Expand Down Expand Up @@ -73,8 +75,10 @@ class KMockProcessorProvider : SymbolProcessorProvider {
genericResolver = KMockGenerics,
),
multiInterfaceGenerator = KMockFactoryMultiInterfaceGenerator(
isKmp = options.isKmp,
spyContainer = spyContainer,
utils = factoryUtils,
genericResolver = KMockGenerics,
),
spyContainer = spyContainer,
spiesOnly = options.spiesOnly,
Expand Down Expand Up @@ -155,12 +159,14 @@ class KMockProcessorProvider : SymbolProcessorProvider {

return KMockProcessor(
logger = logger,
isUnderCompilerTest = isUnderCompilerTest,
isKmp = options.isKmp,
codeGenerator = codeGenerator,
interfaceGenerator = KMockMultiInterfaceBinder(
logger = logger,
rootPackage = options.rootPackage,
codeGenerator = codeGenerator
genericResolver = KMockGenerics,
codeGenerator = codeGenerator,
),
mockGenerator = KMockGenerator(
logger = logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ internal interface ProcessorContract {
typeResolver: TypeParameterResolver
): List<TypeVariableName>

fun remapTypes(
templates: List<KSClassDeclaration>,
generics: List<Map<String, List<KSTypeReference>>?>
): Pair<List<TypeName>, List<TypeVariableName>>

fun mapProxyGenerics(
generics: Map<String, List<KSTypeReference>>,
typeResolver: TypeParameterResolver
Expand Down Expand Up @@ -323,6 +328,7 @@ internal interface ProcessorContract {
ksFunction: KSFunctionDeclaration,
typeResolver: TypeParameterResolver,
enableSpy: Boolean,
inherited: Boolean,
relaxer: Relaxer?,
): Pair<PropertySpec?, FunSpec>
}
Expand All @@ -348,16 +354,16 @@ internal interface ProcessorContract {

fun writeCommonMocks(
templateSources: List<TemplateSource>,
templateMultiSources: Aggregated<TemplateMultiSource>,
templateMultiSources: List<TemplateMultiSource>,
relaxer: Relaxer?
)
}

fun interface ParentFinder {
fun find(
templateSource: TemplateSource,
templateMultiSources: Aggregated<TemplateMultiSource>,
): List<KSClassDeclaration>
templateMultiSources: List<TemplateMultiSource>,
): TemplateMultiSource?
}

interface MultiInterfaceBinder {
Expand Down Expand Up @@ -396,8 +402,6 @@ internal interface ProcessorContract {
fun resolveGenerics(templateSource: TemplateSource): List<TypeVariableName>

fun resolveModifier(): KModifier?

fun toTypeNames(types: List<KSClassDeclaration>): List<TypeName>
}

interface MockFactoryWithoutGenerics {
Expand All @@ -417,6 +421,12 @@ internal interface ProcessorContract {
val shared: FunSpec
)

data class FactoryMultiBundle(
val kmock: FunSpec?,
val kspy: FunSpec?,
val shared: FunSpec?
)

interface MockFactoryWithGenerics {
fun buildGenericFactories(
templateSources: List<TemplateSource>,
Expand All @@ -425,10 +435,10 @@ internal interface ProcessorContract {
}

interface MockFactoryMultiInterface {
fun buildSpyFactory(
fun buildFactories(
templateMultiSources: List<TemplateMultiSource>,
relaxer: Relaxer?
): List<FunSpec>
): List<FactoryMultiBundle>
}

interface MockFactoryGenerator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import tech.antibytes.kmock.processor.ProcessorContract.MockFactoryGeneratorUtil
import tech.antibytes.kmock.processor.ProcessorContract.SpyContainer
import tech.antibytes.kmock.processor.ProcessorContract.TemplateMultiSource
import tech.antibytes.kmock.processor.ProcessorContract.TemplateSource
import tech.antibytes.kmock.processor.multi.hasGenerics

internal class KMockFactoryEntryPointGenerator(
private val isKmp: Boolean,
Expand Down Expand Up @@ -150,21 +151,75 @@ internal class KMockFactoryEntryPointGenerator(
).build()
}

private fun buildMultiInterfaceSpyFactory(
private fun buildMultiInterfaceGenericKMockFactory(
boundaries: List<TypeName>,
generics: List<TypeVariableName>
): FunSpec {
val mockType = TypeVariableName(KMOCK_FACTORY_TYPE_NAME).copy(bounds = boundaries)

return utils.generateKmockSignature(
type = mockType,
generics = emptyList(),
hasDefault = true,
modifier = KModifier.EXPECT
).addTypeVariables(generics).build()
}

private fun buildMultiInterfaceGenericKSpyFactory(
boundaries: List<TypeName>,
generics: List<TypeVariableName>
): FunSpec {
val spyType = TypeVariableName(
KSPY_FACTORY_TYPE_NAME,
bounds = boundaries,
)

val mockType = TypeVariableName(KMOCK_FACTORY_TYPE_NAME, bounds = listOf(spyType))

return utils.generateKspySignature(
mockType = mockType,
spyType = spyType,
generics = emptyList(),
hasDefault = true,
modifier = KModifier.EXPECT
).addTypeVariables(generics).build()
}

private fun TemplateMultiSource.isSpyable(): Boolean {
return spyContainer.isSpyable(null, this.packageName, this.templateName)
}

private fun resolveGenericMultiInterfaceFactories(
templateSource: TemplateMultiSource,
boundaries: List<TypeName>,
generics: List<TypeVariableName>
): FunSpec {
return if (templateSource.isSpyable()) {
buildMultiInterfaceGenericKSpyFactory(boundaries, generics)
} else {
buildMultiInterfaceGenericKMockFactory(boundaries, generics)
}
}

private fun buildMultiInterfaceFactory(
templateSource: TemplateMultiSource
): FunSpec? {
return if (spyContainer.isSpyable(null, templateSource.packageName, templateSource.templateName)) {
buildMultiInterfaceSpyFactory(utils.toTypeNames(templateSource.templates))
} else {
null
val (types, generics) = genericResolver.remapTypes(templateSource.templates, templateSource.generics)

return when {
templateSource.isSpyable() && !templateSource.hasGenerics() -> {
buildMultiInterfaceSpyFactory(types)
}
templateSource.hasGenerics() -> resolveGenericMultiInterfaceFactories(templateSource, types, generics)
else -> null
}
}

private fun FileSpec.Builder.generateMultiInterfaceEntryPoints(
templateSources: List<TemplateMultiSource>
) {
templateSources.forEach { source ->
val factory = buildMultiInterfaceSpyFactory(source)
val factory = buildMultiInterfaceFactory(source)

if (factory != null) {
this.addFunction(factory)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,23 @@ internal class KMockFactoryGenerator(
}
}

val multiInterfaceMocks = multiInterfaceGenerator.buildSpyFactory(
val multiInterfaceMocks = multiInterfaceGenerator.buildFactories(
templateMultiSources = templateMultiSources,
relaxer = relaxer
)

multiInterfaceMocks.forEach { factory ->
file.addFunction(factory)
multiInterfaceMocks.forEach { factories ->
if (factories.shared != null) {
file.addFunction(factories.shared)
}

if (factories.kmock != null && !spiesOnly) {
file.addFunction(factories.kmock)
}

if (factories.kspy != null) {
file.addFunction(factories.kspy)
}
}

file.build().writeTo(
Expand Down
Loading