Skip to content

Commit

Permalink
Restructuring a sugar module (#804)
Browse files Browse the repository at this point in the history
* Restructuring a sugar project

* Migrating from a single-module structure to a multi-module structure

* Improvements to sugared-kdoc

* Change sugar component creation conditions (introduced @Sugarable)

* Regenerated sugar components

* Fix casa-processor build error

* Make spotless happy

* Cleanup

* Fix tests

* Rebuild

* Fix tests

* Fix tests
  • Loading branch information
jisungbin authored Jul 14, 2023
1 parent d9758f5 commit 1b70c01
Show file tree
Hide file tree
Showing 95 changed files with 3,619 additions and 2,951 deletions.
4 changes: 1 addition & 3 deletions casa-processor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@
* Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE
*/

@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")

plugins {
quackquack("jvm-kotlin")
quackquack("test-kotest")
quackquack("quack-publishing")
alias(libs.plugins.kotlin.ksp)
}

Expand All @@ -27,6 +24,7 @@ dependencies {
libs.kotlin.collections.immutable,
libs.google.autoservice.annotation,
projects.utilBackendKsp,
projects.utilBackendKotlinpoet,
)
testImplementations(
libs.test.kotlin.compilation.ksp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ 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.KSAnnotated

private const val CasaPathArg = "CasaPath"

Expand All @@ -35,13 +34,12 @@ private class CasaSymbolProcessor(
logger: KSPLogger,
options: Map<String, Any>,
) : SymbolProcessor {
private val processor = CasaProcessor(
codeGenerator = codeGenerator,
logger = logger,
casaPath = options[CasaPathArg]?.toString(),
)
private val processor =
CasaProcessor(
codeGenerator = codeGenerator,
logger = logger,
casaPath = options[CasaPathArg]?.toString(),
)

override fun process(resolver: Resolver): List<KSAnnotated> {
return processor.resolve(resolver)
}
override fun process(resolver: Resolver) = processor.resolve(resolver)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,91 +18,94 @@ import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.buildCodeBlock
import com.squareup.kotlinpoet.withIndent
import kotlinx.collections.immutable.ImmutableList
import team.duckie.quackquack.util.backend.FormatterOffComment
import team.duckie.quackquack.util.backend.SuppressAnnotation
import team.duckie.quackquack.util.backend.getGeneratedFileComment
import team.duckie.quackquack.util.backend.kotlinpoet.getGeneratedFileComment
import team.duckie.quackquack.util.backend.ksp.generateBuildOrLocalFile
import team.duckie.quackquack.util.backend.ksp.requireContainingFile

private val GeneratedComment = getGeneratedFileComment("casa-processor")
private val RequiredImports = listOf(
"kotlinx.collections.immutable.persistentListOf",
"kotlinx.collections.immutable.toImmutableList",
"androidx.compose.runtime.Composable",
)
private val RequiredImports =
listOf(
"kotlinx.collections.immutable.persistentListOf",
"kotlinx.collections.immutable.toImmutableList",
"androidx.compose.runtime.Composable",
)

internal fun generateCasaModels(
codeGenerator: CodeGenerator,
logger: KSPLogger,
casas: Sequence<KSFunctionDeclaration>,
casaPath: String?,
) {
val imports = mutableListOf<String>().also { imports ->
imports += RequiredImports
}
val casasWithDomainGroup = casas.groupBy { declaration ->
declaration.requireContainingFile.fileName.removeSuffix(".kt")
}
val groupedCasasWithNameGroup = buildMap(capacity = casasWithDomainGroup.size) {
casasWithDomainGroup.forEach { (domain, casas) ->
val casasWithNameGroup = casas.groupBy { casa ->
casa.parseSugarRefer().substringAfterLast(".")
val imports =
mutableListOf<String>().also { imports ->
imports += RequiredImports
}
val casasWithDomainGroup =
casas.groupBy { declaration ->
declaration.requireContainingFile.fileName.removeSuffix(".kt")
}
val groupedCasasWithNameGroup =
buildMap(capacity = casasWithDomainGroup.size) {
casasWithDomainGroup.forEach { (domain, casas) ->
val casasWithNameGroup = casas.groupBy { casa ->
casa.parseSugarRefer().substringAfterLast(".")
}
set(domain, casasWithNameGroup)
}
set(domain, casasWithNameGroup)
}
}
val casaModelPropertySpecs = buildList {
groupedCasasWithNameGroup.forEach { (domain, casasWithNameGroup) ->
casasWithNameGroup.forEach { (name, casas) ->
val (_imports, casaModelPropertySpec) = createCasaModelPropertySpecWithImports(
domain = domain,
name = name,
casas = casas,
)
imports += _imports
add(casaModelPropertySpec)
val casaModelPropertySpecs =
buildList {
groupedCasasWithNameGroup.forEach { (domain, casasWithNameGroup) ->
casasWithNameGroup.forEach { (name, casas) ->
val (_imports, casaModelPropertySpec) = createCasaModelPropertySpecWithImports(
domain = domain,
name = name,
casas = casas,
)
imports += _imports
add(casaModelPropertySpec)
}
}
}
}
val casaModelPropertySpecsAccessor = PropertySpec
.builder(
name = "casaModels",
type = ImmutableList::class.asClassName().parameterizedBy(CasaModelCn),
)
.initializer(
codeBlock = buildCodeBlock {
addStatement("persistentListOf(")
withIndent {
casaModelPropertySpecs.forEach { casaModelPropertySpec ->
addStatement("${casaModelPropertySpec.name},")
val casaModelPropertySpecsAccessor =
PropertySpec
.builder(
name = "casaModels",
type = ImmutableList::class.asClassName().parameterizedBy(CasaModelCn),
)
.initializer(
codeBlock = buildCodeBlock {
addStatement("persistentListOf(")
withIndent {
casaModelPropertySpecs.forEach { casaModelPropertySpec ->
addStatement("${casaModelPropertySpec.name},")
}
}
}
addStatement(")")
},
)
.addModifiers(KModifier.PUBLIC)
.build()
addStatement(")")
},
)
.addModifiers(KModifier.PUBLIC)
.build()

// TODO(3): OptIn 어노테이션 자동 추가
val casaModelFileSpec = FileSpec
.builder(
packageName = "",
fileName = "CasaModels",
)
.addFileComment(GeneratedComment)
.addFileComment(FormatterOffComment)
.addAnnotation(SuppressAnnotation)
.apply {
// memberImports += if (packageName.isNotEmpty()) {
// Import("$packageName.$import")
// } else {
// Import(import)
// }
addImport(packageName = "", imports)
casaModelPropertySpecs.forEach(::addProperty)
}
.addProperty(casaModelPropertySpecsAccessor)
.build()
val casaModelFileSpec =
FileSpec
.builder(
packageName = "",
fileName = "CasaModels",
)
.addFileComment(GeneratedComment)
.apply {
// memberImports += if (packageName.isNotEmpty()) {
// Import("$packageName.$import")
// } else {
// Import(import)
// }
addImport(packageName = "", imports)
casaModelPropertySpecs.forEach(::addProperty)
}
.addProperty(casaModelPropertySpecsAccessor)
.build()

generateBuildOrLocalFile(
codeGenerator = codeGenerator,
Expand All @@ -115,29 +118,32 @@ internal fun generateCasaModels(

private fun KSFunctionDeclaration.toCasaComponentLiteralWithImport(): Pair<String, String> {
val import = "team.duckie.quackquack.ui.sugar.${simpleName.asString()}"
val parameterValueMap = parameters.mapNotNull { parameter ->
if (parameter.hasDefault) {
return@mapNotNull null
}
val name = parameter.name!!.asString()
val value = parameter.annotations.singleOrNullStrict { annotation ->
annotation.shortName.asString() == CasaValueSn
}
if (value != null) {
name to value.arguments.single().value as String
} else if (parameter.type.resolve().isMarkedNullable) {
name to "null"
} else {
error("Argument $name is non-null and no `CasaValue` was provided.")
val parameterValueMap =
parameters.mapNotNull { parameter ->
if (parameter.hasDefault) {
return@mapNotNull null
}
val name = parameter.name!!.asString()
val value =
parameter.annotations.singleOrNullStrict { annotation ->
annotation.shortName.asString() == CasaValueSn
}
if (value != null) {
name to value.arguments.single().value as String
} else if (parameter.type.resolve().isMarkedNullable) {
name to "null"
} else {
error("Argument $name is non-null and no `CasaValue` was provided.")
}
}
}
val componentLiteral = buildString {
appendLine("${simpleName.asString()}(")
parameterValueMap.forEach { (name, value) ->
appendLineWithIndent("$name = $value,")
val componentLiteral =
buildString {
appendLine("${simpleName.asString()}(")
parameterValueMap.forEach { (name, value) ->
appendLineWithIndent("$name = $value,")
}
append(")")
}
append(")")
}

return import to componentLiteral
}
Expand All @@ -154,50 +160,63 @@ private fun createCasaModelPropertySpecWithImports(
name: String,
casas: List<KSFunctionDeclaration>,
): Pair<List<String>, PropertySpec> {
val imports = casas.first().parameters.map { parameter ->
parameter.type.resolve().declaration.qualifiedName!!.asString()
}.toMutableList()
val kdocString = casas.first().docString.orEmpty()
.split("This document was automatically generated")
.first()
.trimIndent()
val components = buildString {
appendLine("persistentListOf<Pair<String, @Composable () -> Unit>>(")
casas.forEach { casa ->
appendCasaComponentPairWithImport(casa = casa).let { (import, _) ->
imports += import
val imports =
casas
.first()
.parameters
.map { parameter ->
parameter.type.resolve().declaration.qualifiedName!!.asString()
}
appendLine(",")
.toMutableList()
val kdocString =
casas
.first()
.docString
.orEmpty()
.split("This document was automatically generated")
.first()
.trimIndent()
val components =
buildString {
appendLine("persistentListOf<Pair<String, @Composable () -> Unit>>(")
casas.forEach { casa ->
appendCasaComponentPairWithImport(casa = casa).let { (import, _) ->
imports += import
}
appendLine(",")
}
append(").toImmutableList()")
}
append(").toImmutableList()")
}

val casaModelPropertySpec = PropertySpec
.builder(
name = "${domain}${name}CasaModel",
type = CasaModelCn,
)
.addModifiers(KModifier.PRIVATE)
.initializer(
codeBlock = buildCodeBlock {
addStatement("CasaModel(")
withIndent {
addStatement("name = %S,", name)
addStatement("domain = %S,", domain)
addStatement("kdocDefaultSection = %S,", kdocString)
addStatement("components = %L,", components)
}
addStatement(")")
},
)
.build()
val casaModelPropertySpec =
PropertySpec
.builder(
name = "${domain}${name}CasaModel",
type = CasaModelCn,
)
.addModifiers(KModifier.PRIVATE)
.initializer(
codeBlock = buildCodeBlock {
addStatement("CasaModel(")
withIndent {
addStatement("name = %S,", name)
addStatement("domain = %S,", domain)
addStatement("kdocDefaultSection = %S,", kdocString)
addStatement("components = %L,", components)
}
addStatement(")")
},
)
.build()

return imports to casaModelPropertySpec
}

private fun KSFunctionDeclaration.parseSugarRefer(): String {
val sugarRefer = annotations.singleOrNullStrict { annotation ->
annotation.shortName.asString() == SugarReferSn
} ?: error("casa-processor only supports sugar components.")
val sugarRefer =
annotations.singleOrNullStrict { annotation ->
annotation.shortName.asString() == SugarReferSn
} ?: error("casa-processor only supports sugar components.")

return sugarRefer.arguments.first().value as String
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,5 @@ internal fun <T> Sequence<T>.singleOrNullStrict(predicate: (T) -> Boolean): T? {
return single
}

internal fun StringBuilder.appendLineWithIndent(
value: String?,
indentSize: Int = 2,
): StringBuilder {
return appendLine("${" ".repeat(indentSize)}$value")
}
internal fun StringBuilder.appendLineWithIndent(value: String?, indentSize: Int = 2) =
appendLine("${" ".repeat(indentSize)}$value")
Loading

0 comments on commit 1b70c01

Please sign in to comment.