From d0f8a6bbe8c25652d4647fa42817ac1c2b9b8004 Mon Sep 17 00:00:00 2001 From: jisungbin Date: Fri, 14 Jul 2023 04:59:55 +0900 Subject: [PATCH 01/12] Restructuring a sugar project --- settings.gradle.kts | 23 +- .../build.gradle.kts | 11 +- sugar-compiler/version.txt | 1 + sugar-core/build.gradle.kts | 27 + sugar-core/codegen/build.gradle.kts | 19 + sugar-core/node/build.gradle.kts | 17 + sugar-core/visitor/build.gradle.kts | 23 + .../processor/SugarCommandLineProcessor.kt | 61 --- .../processor/SugarComponentRegistrar.kt | 73 --- .../sugar/processor/ir/SugarIrData.kt | 221 -------- .../sugar/processor/ir/SugarIrExtension.kt | 52 -- .../sugar/processor/ir/SugarIrTransformer.kt | 120 ----- .../sugar/processor/ir/SugarIrVisitor.kt | 215 -------- .../quackquack/sugar/processor/ir/errors.kt | 79 --- .../quackquack/sugar/processor/ir/names.kt | 58 -- .../sugar/processor/poet/PoetUtils.kt | 26 - .../quackquack/sugar/processor/poet/poet.kt | 150 ------ .../sugar/processor/SugarIrErrorTest.kt | 283 ---------- .../sugar/processor/SugarIrTransformTest.kt | 125 ----- .../sugar/processor/SugarPoetTest.kt | 505 ------------------ .../quackquack/sugar/processor/stubs.kt | 42 -- sugar-processor/version.txt | 1 - ui-sugar/build.gradle.kts | 20 + ui-sugar/src/main/AndroidManifest.xml | 8 + .../team/duckie/quackquack/ui/sugar/todo | 0 ui-sugar/version.txt | 1 + ui/build.gradle.kts | 4 +- 27 files changed, 133 insertions(+), 2032 deletions(-) rename {sugar-processor => sugar-compiler}/build.gradle.kts (65%) create mode 100644 sugar-compiler/version.txt create mode 100644 sugar-core/build.gradle.kts create mode 100644 sugar-core/codegen/build.gradle.kts create mode 100644 sugar-core/node/build.gradle.kts create mode 100644 sugar-core/visitor/build.gradle.kts delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/SugarCommandLineProcessor.kt delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/SugarComponentRegistrar.kt delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrData.kt delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrExtension.kt delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrTransformer.kt delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrVisitor.kt delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/errors.kt delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/names.kt delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/poet/PoetUtils.kt delete mode 100644 sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/poet/poet.kt delete mode 100644 sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarIrErrorTest.kt delete mode 100644 sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarIrTransformTest.kt delete mode 100644 sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarPoetTest.kt delete mode 100644 sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/stubs.kt delete mode 100644 sugar-processor/version.txt create mode 100644 ui-sugar/build.gradle.kts create mode 100644 ui-sugar/src/main/AndroidManifest.xml create mode 100644 ui-sugar/src/main/kotlin/team/duckie/quackquack/ui/sugar/todo create mode 100644 ui-sugar/version.txt diff --git a/settings.gradle.kts b/settings.gradle.kts index b391ddb42..5b95e2048 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,14 +30,6 @@ buildCache { include( ":catalog", - ":util", - ":util-modifier", - ":util-backend", - ":util-backend-ksp", - ":util-backend-kotlinc", - ":util-backend-test", - ":util-compose-runtime-test", - ":util-compose-snapshot-test", ":runtime", ":material", ":material-icon", @@ -50,11 +42,24 @@ include( ":ui-plugin:interceptor", ":ui-plugin:interceptor:textfield", ":ui-sample", + ":ui-sugar", ":sugar-material", - ":sugar-processor", + ":sugar-compiler", + ":sugar-core", + ":sugar-core:node", + ":sugar-core:visitor", + ":sugar-core:codegen", ":casa-ui", ":casa-annotation", ":casa-material", ":casa-processor", + ":util", + ":util-modifier", + ":util-backend", + ":util-backend-ksp", + ":util-backend-kotlinc", + ":util-backend-test", + ":util-compose-runtime-test", + ":util-compose-snapshot-test", ":bom", ) diff --git a/sugar-processor/build.gradle.kts b/sugar-compiler/build.gradle.kts similarity index 65% rename from sugar-processor/build.gradle.kts rename to sugar-compiler/build.gradle.kts index 5cbcab61f..ac65fe138 100644 --- a/sugar-processor/build.gradle.kts +++ b/sugar-compiler/build.gradle.kts @@ -5,11 +5,8 @@ * 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) } @@ -21,14 +18,8 @@ ksp { dependencies { compileOnly(libs.kotlin.embeddable.compiler) - implementations( - libs.google.autoservice.annotation, - libs.kotlin.kotlinpoet.core, - projects.casaAnnotation.orArtifact(), - projects.sugarMaterial.orArtifact(), - projects.utilBackendKotlinc.orArtifact(), - ) ksp(libs.google.autoservice.ksp.processor) + implementation(libs.google.autoservice.annotation) testImplementations( libs.test.kotlin.compilation.core, projects.utilBackendTest, diff --git a/sugar-compiler/version.txt b/sugar-compiler/version.txt new file mode 100644 index 000000000..311be597a --- /dev/null +++ b/sugar-compiler/version.txt @@ -0,0 +1 @@ +2.0.0-alpha01 diff --git a/sugar-core/build.gradle.kts b/sugar-core/build.gradle.kts new file mode 100644 index 000000000..9d38e3628 --- /dev/null +++ b/sugar-core/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +plugins { + quackquack("jvm-kotlin") + alias(libs.plugins.kotlin.ksp) +} + +ksp { + arg("autoserviceKsp.verify", "true") + arg("autoserviceKsp.verbose", "true") +} + +dependencies { + compileOnly(libs.kotlin.embeddable.compiler) + implementations( + libs.google.autoservice.annotation, + projects.sugarCore.node, + projects.sugarCore.visitor, + projects.sugarCore.codegen, + ) + ksp(libs.google.autoservice.ksp.processor) +} diff --git a/sugar-core/codegen/build.gradle.kts b/sugar-core/codegen/build.gradle.kts new file mode 100644 index 000000000..2ea44cc14 --- /dev/null +++ b/sugar-core/codegen/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +plugins { + quackquack("jvm-kotlin") + quackquack("test-kotest") +} + +dependencies { + implementations( + libs.kotlin.embeddable.compiler, + libs.kotlin.kotlinpoet.core, + projects.sugarCore.node, + ) +} diff --git a/sugar-core/node/build.gradle.kts b/sugar-core/node/build.gradle.kts new file mode 100644 index 000000000..537dc7f92 --- /dev/null +++ b/sugar-core/node/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +plugins { + quackquack("jvm-kotlin") +} + +dependencies { + implementations( + libs.kotlin.embeddable.compiler, + libs.kotlin.kotlinpoet.core, + ) +} diff --git a/sugar-core/visitor/build.gradle.kts b/sugar-core/visitor/build.gradle.kts new file mode 100644 index 000000000..27fb65d61 --- /dev/null +++ b/sugar-core/visitor/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +plugins { + quackquack("jvm-kotlin") + quackquack("test-kotest") +} + +dependencies { + implementations( + libs.kotlin.embeddable.compiler, + projects.sugarMaterial, + projects.sugarCore.node, + ) + testImplementations( + libs.test.kotlin.compilation.core, + projects.utilBackendTest, + ) +} diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/SugarCommandLineProcessor.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/SugarCommandLineProcessor.kt deleted file mode 100644 index cfa4d41c1..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/SugarCommandLineProcessor.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:OptIn(ExperimentalCompilerApi::class) - -package team.duckie.quackquack.sugar.processor - -import com.google.auto.service.AutoService -import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption -import org.jetbrains.kotlin.compiler.plugin.CliOption -import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.config.CompilerConfigurationKey - -internal const val PluginId = "team.duckie.quackquack.sugar.processor" - -internal val KEY_SUGAR_PATH = CompilerConfigurationKey( - "Where the sugar components will be created - required", -) -internal val OPTION_SUGAR_PATH = CliOption( - optionName = "sugarPath", - valueDescription = "String", - description = KEY_SUGAR_PATH.toString(), - required = true, - allowMultipleOccurrences = false, -) - -internal val KEY_POET = CompilerConfigurationKey( - "Whether to enable sugar components generation - default is true", -) -internal val OPTION_POET = CliOption( - optionName = "poet", - valueDescription = "", - description = KEY_POET.toString(), - required = false, - allowMultipleOccurrences = false, -) - -@AutoService(CommandLineProcessor::class) -class SugarCommandLineProcessor : CommandLineProcessor { - override val pluginId = PluginId - - override val pluginOptions = listOf(OPTION_SUGAR_PATH, OPTION_POET) - - override fun processOption( - option: AbstractCliOption, - value: String, - configuration: CompilerConfiguration, - ) { - when (val optionName = option.optionName) { - OPTION_SUGAR_PATH.optionName -> configuration.put(KEY_SUGAR_PATH, value) - OPTION_POET.optionName -> configuration.put(KEY_POET, value) - else -> error("Unknown plugin option: $optionName") - } - } -} diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/SugarComponentRegistrar.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/SugarComponentRegistrar.kt deleted file mode 100644 index d4eff1121..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/SugarComponentRegistrar.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:Suppress("DEPRECATION") -@file:OptIn(ExperimentalCompilerApi::class) - -package team.duckie.quackquack.sugar.processor - -import com.google.auto.service.AutoService -import org.jetbrains.annotations.TestOnly -import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension -import org.jetbrains.kotlin.com.intellij.mock.MockProject -import org.jetbrains.kotlin.com.intellij.openapi.extensions.LoadingOrder -import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar -import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import org.jetbrains.kotlin.config.CompilerConfiguration -import team.duckie.quackquack.sugar.processor.ir.SugarIrExtension -import team.duckie.quackquack.sugar.processor.ir.SugarIrVisitor -import team.duckie.quackquack.util.backend.kotlinc.getLogger - -/** - * ### Deprecated된 메서드를 사용하는 이유 - * - * Compose Compiler의 [`Default Arguments Transform`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt;l=341-365)에 - * 의해 모든 컴포저블 함수에서 default argument의 값이 null로 변경됩니다. 하지만 sugar component를 - * 생성하기 위해선 default value의 값을 보존해야 합니다. 이를 위해 [SugarIrVisitor]가 Compose Compiler - * 보다 먼저 적용될 수 있도록 Compiler Plugin의 적용 순서를 조정할 수 있는 deprecated된 [registerProjectComponents] - * 메서드를 사용합니다. deprecated 되지 않은 [CompilerPluginRegistrar]를 사용하면 Compiler Plugin의 적용 - * 순서를 조정할 수 없습니다. - */ -@AutoService(ComponentRegistrar::class) -class SugarComponentRegistrar : ComponentRegistrar { - override val supportsK2 = false - - override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { - project.extensionArea - .getExtensionPoint(IrGenerationExtension.extensionPointName) - .registerExtension(configuration.getSugarIrExtension(), LoadingOrder.FIRST, project) - } - - internal companion object { - /** - * [ComponentRegistrar]의 complie test는 DeprecatedError 상태로 항상 테스트에 실패합니다. - * 이를 해결하기 위해 [SugarComponentRegistrar]의 [CompilerPluginRegistrar] 버전을 제공합니다. - * 이 함수는 오직 테스트 코드에서만 사용돼야 합니다. (테스트 환경에서는 Compose Compiler가 - * 적용되지 않으니 유효합니다.) - */ - @TestOnly - internal fun asPluginRegistrar() = object : CompilerPluginRegistrar() { - override val supportsK2 = false - - override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { - IrGenerationExtension.registerExtension(configuration.getSugarIrExtension()) - } - } - - private fun CompilerConfiguration.getSugarIrExtension(): SugarIrExtension { - val sugarPath = requireNotNull(this[KEY_SUGAR_PATH]) { "sugarPath was missing." } - val poet = this[KEY_POET]?.toBooleanStrict() ?: true - - return SugarIrExtension( - logger = getLogger("sugar-processor"), - sugarPath = sugarPath, - poet = poet, - ) - } - } -} diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrData.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrData.kt deleted file mode 100644 index b6af86264..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrData.kt +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.sugar.processor.ir - -import com.squareup.kotlinpoet.AnnotationSpec -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.LambdaTypeName -import com.squareup.kotlinpoet.ParameterSpec -import org.jetbrains.kotlin.builtins.isFunctionOrSuspendFunctionType -import org.jetbrains.kotlin.ir.backend.js.utils.asString -import org.jetbrains.kotlin.ir.declarations.IrFunction -import org.jetbrains.kotlin.ir.declarations.IrValueParameter -import org.jetbrains.kotlin.ir.descriptors.toIrBasedKotlinType -import org.jetbrains.kotlin.ir.expressions.IrConstructorCall -import org.jetbrains.kotlin.ir.expressions.IrExpressionBody -import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.types.isMarkedNullable -import org.jetbrains.kotlin.ir.util.dumpKotlinLike -import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable -import org.jetbrains.kotlin.ir.util.isFunction -import org.jetbrains.kotlin.ir.util.isSuspendFunction -import org.jetbrains.kotlin.ir.util.parentAsClass -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.types.checker.SimpleClassicTypeSystemContext.getClassFqNameUnsafe -import org.jetbrains.kotlin.utils.addToStdlib.applyIf -import team.duckie.quackquack.casa.annotation.CasaValue -import team.duckie.quackquack.sugar.material.Imports -import team.duckie.quackquack.sugar.material.SugarName -import team.duckie.quackquack.sugar.material.SugarToken -import team.duckie.quackquack.util.backend.kotlinc.unsafeClassName - -/** - * [SugarIrVisitor]에서 IR을 방문하면서 수집할 정보들을 관리합니다. - * - * @param owner IR이 제공된 함수 - * @param referFqn IR이 제공된 함수의 [fully-qualified name][FqName]. [owner]에서 직접 - * 가져오는 방식보다 안전한 방식으로 fqn이 제공됩니다. - * @param kdocGetter IR이 제공된 함수의 Sugared-KDoc을 계산하는 람다. 사용된 Sugar Token의 - * 리터럴을 람다 인자로 제공해야 합니다. - * @param sugarName 생성할 sugar component의 네이밍 규칙. [`@SugarToken`][SugarName] - * 값을 가져옵니다. - * @param sugarToken 생성할 sugar component의 Sugar Token에 해당하는 [인자][IrValueParameter]. - * [`@SugarToken`][SugarToken]이 달린 인자를 가져옵니다. - * @param tokenFqExpressions Sugar Token의 expression 모음. 예를 들면 다음과 같습니다. - * - * ``` - * package team.duckie.theme - * - * @JvmInline - * value class Theme(val index: Int) { - * companion object { - * val Default = Theme(1) - * val Dark = Theme(2) - * val Light = Theme(3) - * val System = Theme(4) - * } - * } - * - * // ["team.duckie.theme.Theme.Default", "team.duckie.theme.Theme.Dark", "team.duckie.theme.Theme.Light", "team.duckie.theme.Theme.System"] - * ``` - * - * @param parameters IR이 제공된 함수의 인자 모음. sugar component 생성에 필요한 정보만 수집합니다. - * 자세한 수집 정보는 [SugarParameter]를 확인하세요. - */ -internal data class SugarIrData( - val owner: IrFunction, - val referFqn: FqName, - val kdocGetter: (usedTokenLiteral: String) -> String, - val sugarName: String?, - val sugarToken: IrValueParameter, - val tokenFqExpressions: List, - val parameters: List, - val optins: List, -) { - /** [parameters]에서 Sugar Token을 제외한 [요소][SugarParameter]만 불러옵니다. */ - val parametersWithoutToken: List = parameters.toMutableList().apply { - removeIf(SugarParameter::isToken) - } - - override fun toString(): String { - return """ - |owner: ${owner.name.asString()} - |referFqn: ${referFqn.asString()} - |kdoc: ${kdocGetter("SugarToken")} - |sugarName: $sugarName - |sugarToken: ${sugarToken.name.asString()} - |tokenExpressions: $tokenFqExpressions - |parameters: ${parameters.joinToString("\n\n", prefix = "\n")} - |optins: ${optins.joinToString(transform = IrConstructorCall::toFqnStringOrEmpty)} - """.trimMargin() - } -} - -/** 주어진 어노테이션의 fqn을 조회하여 반환하고, 만약 조회에 실패했다면 공백을 반환합니다. */ -internal fun IrConstructorCall.toFqnStringOrEmpty() = - symbol.owner.parentAsClass.fqNameWhenAvailable?.asString().orEmpty() - -/** - * [IrValueParameter]에서 sugar component 생성에 필요한 정보를 관리합니다. - * - * @param name 인자의 이름 - * @param type 인자의 타입 - * @param isToken 인자가 [Sugar Token][SugarToken]인지 여부 - * @param isComposable 인자에 [`@Composable`][ComposableFqn] 어노테이션이 달려있는지 - * 여부 - * @param imports [type] 외에 추가로 import가 필요한 클래스의 [fully-qualified name][FqName]으로 - * 구성된 목록. 자세한 정보는 [`@Imports`][Imports] 어노테이션을 확인하세요. - * @param casaValueLiteral 만약 인자에 [`@CasaValue`][CasaValue] 어노테이션이 달려있다면 - * [CasaValue.literal]로 제공된 값 - * @param defaultValue 인자의 기본 값 - */ -internal data class SugarParameter( - val owner: IrFunction, - val name: Name, - val type: IrType, - val isToken: Boolean, - val isComposable: Boolean, - val imports: List, - val casaValueLiteral: String?, - val defaultValue: IrExpressionBody?, -) { - /** 제공된 정보를 [ParameterSpec]으로 변환합니다. */ - fun toParameterSpec(): ParameterSpec { - val parameterTypedBuilder = ParameterSpec - .builder( - name = name.asString(), - type = when { - type.isFunction() || type.isSuspendFunction() -> { - val funArguments = type.toIrBasedKotlinType().arguments - - /* - * maintainer notes: 모든 **함수형 타입**은 `Function`으로 처리되며 이는 `IrFunction`과는 - * 다른 유형임. 코틀린의 **함수 정의**는 `IrFunction`으로 해석되고, **함수형 타입**은 코틀린 - * 네이티브 타입인 `KotlinType`으로 해석됨. - * - * `Function`은 value parameter의 타입과 return의 타입을 generic으로 받는 인터페이스임. - * 즉, `Function`은 메타데이터가 없어서 컴파일 시점에서는 인자의 타입만 조회 가능함. - * 따라서 람다의 인자명 정책으로 `P{$index}`를 사용함. - * - * receiver extension은 **첫 번째** value parameter로 치환됨. - * > `String.() -> Unit` == `(String) -> Unit` - * - * return type은 **마지막** value parameter로 치환됨. - */ - val referLambdaParameters = - if (funArguments.size >= 2) { - funArguments.dropLast(1).mapIndexed { index, argument -> - require(!argument.type.isFunctionOrSuspendFunctionType) { - NotSupportedError.nestedFunctionalType("${owner.name.asString()}#${name.asString()}") - } - - val argumentTypeFqn = argument.type.constructor.getClassFqNameUnsafe() - val argumentTypeCn = ClassName.bestGuess(argumentTypeFqn.toString()) - - ParameterSpec - .builder( - name = "P$index", - type = argumentTypeCn, - ) - .build() - } - } else { - emptyList() - } - val referLambdaReturnTypeFqn = funArguments.last().type.constructor.getClassFqNameUnsafe() - val referLambdaReturnTypeCn = ClassName.bestGuess(referLambdaReturnTypeFqn.toString()) - - LambdaTypeName - .get( - parameters = referLambdaParameters, - returnType = referLambdaReturnTypeCn, - ) - .copy(suspending = type.isSuspendFunction()) - } - else -> { - type.unsafeClassName - } - }.copy(nullable = type.isMarkedNullable()), - ) - - return parameterTypedBuilder - .applyIf(casaValueLiteral != null) { - addAnnotation( - AnnotationSpec - .builder(CasaValueCn) - .addMember("%S", casaValueLiteral!!) - .build(), - ) - } - .applyIf(isComposable) { - addAnnotation( - AnnotationSpec - .builder(ComposableCn) - .build(), - ) - } - .applyIf(defaultValue != null) { - defaultValue("%L()", "sugar") - } - .build() - } - - override fun toString(): String { - return """ - |owner: ${owner.name.asString()} - |name: ${name.asString()} - |type: ${type.asString()} - |isToken: $isToken - |isComposable: $isComposable - |imports: ${imports.joinToString(transform = FqName::asString)} - |casaValueLiteral: $casaValueLiteral - |defaultValue: ${defaultValue?.dumpKotlinLike()} - """.trimMargin() - } -} diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrExtension.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrExtension.kt deleted file mode 100644 index f54a07850..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrExtension.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.sugar.processor.ir - -import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.ir.declarations.IrModuleFragment -import team.duckie.quackquack.sugar.processor.poet.generateSugarComponentFiles -import team.duckie.quackquack.util.backend.kotlinc.Logger - -internal class SugarIrExtension( - private val logger: Logger, - private val sugarPath: String, - private val poet: Boolean, -) : IrGenerationExtension { - override fun generate( - moduleFragment: IrModuleFragment, - pluginContext: IrPluginContext, - ) { - val sugarIrDatas = mutableListOf() - val visitor = SugarIrVisitor( - context = pluginContext, - logger = logger, - addSugarIrData = sugarIrDatas::add, - ) - val transformer = SugarIrTransformer( - context = pluginContext, - logger = logger, - ) - moduleFragment.accept(visitor, null) - if (poet) { - generateSugarComponentFiles( - irDatas = sugarIrDatas, - sugarPath = sugarPath, - ) - } - moduleFragment.transform(transformer, sugarIrDatas.asMap()) - } -} - -private fun List.asMap(): Map { - return buildMap(capacity = size) { - this@asMap.forEach { sugarIrData -> - set(sugarIrData.referFqn.asString(), sugarIrData) - } - } -} diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrTransformer.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrTransformer.kt deleted file mode 100644 index c49c691b3..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrTransformer.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:OptIn(UnsafeCastFunction::class) - -package team.duckie.quackquack.sugar.processor.ir - -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.ir.IrStatement -import org.jetbrains.kotlin.ir.declarations.IrFile -import org.jetbrains.kotlin.ir.declarations.IrModuleFragment -import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction -import org.jetbrains.kotlin.ir.declarations.IrValueParameter -import org.jetbrains.kotlin.ir.expressions.IrConst -import org.jetbrains.kotlin.ir.expressions.IrConstructorCall -import org.jetbrains.kotlin.ir.expressions.IrExpressionBody -import org.jetbrains.kotlin.ir.util.file -import org.jetbrains.kotlin.ir.util.getAnnotation -import org.jetbrains.kotlin.ir.util.hasAnnotation -import org.jetbrains.kotlin.ir.visitors.IrElementTransformer -import org.jetbrains.kotlin.utils.addToStdlib.UnsafeCastFunction -import org.jetbrains.kotlin.utils.addToStdlib.cast -import team.duckie.quackquack.util.backend.kotlinc.Logger -import team.duckie.quackquack.util.backend.kotlinc.isQuackComponent -import team.duckie.quackquack.util.backend.kotlinc.locationOf - -internal class SugarIrTransformer( - @Suppress("unused") private val context: IrPluginContext, - private val logger: Logger, -) : IrElementTransformer> { - override fun visitModuleFragment( - declaration: IrModuleFragment, - data: Map, - ): IrModuleFragment { - declaration.files.forEach { file -> - file.accept(this, data) - } - return declaration - } - - override fun visitFile( - declaration: IrFile, - data: Map, - ): IrFile { - if (declaration.hasAnnotation(SugarGeneratedFileFqn)) { - declaration.declarations.forEach { item -> - item.accept(this, data) - } - } - return declaration - } - - override fun visitSimpleFunction( - declaration: IrSimpleFunction, - data: Map, - ): IrStatement { - if (declaration.isQuackComponent) { - if (declaration.hasAnnotation(NoSugarFqn)) { - return super.visitSimpleFunction(declaration, data) - } - - // run으로 throwError 하는 게 더 가독성이 좋음 - val referAnnotation = declaration.getAnnotation(SugarReferFqn) ?: run { - logger.throwError( - message = PoetError.sugarComponentButNoSugarRefer(declaration.name.asString()), - location = declaration.file.locationOf(declaration), - ) - } - val referFqn = referAnnotation.getReferFqName() - - data[referFqn]?.let { referIrData -> - declaration.valueParameters.forEach { parameter -> - parameter.defaultValue = referIrData.findMatchedDefaultValue( - sugarComponentName = declaration.name.asString(), - parameter = parameter, - error = { message -> - logger.throwError( - message = message, - location = declaration.file.locationOf(parameter), - ) - }, - ) - } - } ?: logger.throwError( - message = SugarVisitError.noMatchedSugarIrData(declaration.name.asString()), - location = declaration.file.locationOf(declaration), - ) - } - - return super.visitSimpleFunction(declaration, data) - } -} - -private fun IrConstructorCall.getReferFqName(): String { - // Assuming the first argument is always "fqn" - val referFqnExpression = getValueArgument(0) - return referFqnExpression.cast>().value -} - -private fun SugarIrData.findMatchedDefaultValue( - sugarComponentName: String, - parameter: IrValueParameter, - error: (message: String) -> Unit, -): IrExpressionBody? { - val matched = parameters.find { referParameter -> - referParameter.name.asString() == parameter.name.asString() - } - if (matched == null) { - error( - SugarTransformError.sugarComponentAndSugarReferHasDifferentParameters( - "(refer) ${owner.name.asString()} -> (sugar) $sugarComponentName#${parameter.name.asString()}", - ), - ) - } - return matched?.defaultValue -} diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrVisitor.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrVisitor.kt deleted file mode 100644 index f861155bf..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/SugarIrVisitor.kt +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:OptIn(UnsafeCastFunction::class) - -package team.duckie.quackquack.sugar.processor.ir - -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.backend.jvm.ir.psiElement -import org.jetbrains.kotlin.ir.backend.js.utils.asString -import org.jetbrains.kotlin.ir.declarations.IrFile -import org.jetbrains.kotlin.ir.declarations.IrFunction -import org.jetbrains.kotlin.ir.declarations.IrModuleFragment -import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction -import org.jetbrains.kotlin.ir.declarations.IrValueParameter -import org.jetbrains.kotlin.ir.expressions.IrClassReference -import org.jetbrains.kotlin.ir.expressions.IrConst -import org.jetbrains.kotlin.ir.expressions.IrConstructorCall -import org.jetbrains.kotlin.ir.expressions.IrVararg -import org.jetbrains.kotlin.ir.types.classFqName -import org.jetbrains.kotlin.ir.types.getClass -import org.jetbrains.kotlin.ir.util.companionObject -import org.jetbrains.kotlin.ir.util.file -import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable -import org.jetbrains.kotlin.ir.util.getAnnotation -import org.jetbrains.kotlin.ir.util.hasAnnotation -import org.jetbrains.kotlin.ir.util.parentAsClass -import org.jetbrains.kotlin.ir.util.properties -import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid -import org.jetbrains.kotlin.kdoc.psi.api.KDoc -import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection -import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.utils.addToStdlib.UnsafeCastFunction -import org.jetbrains.kotlin.utils.addToStdlib.cast -import team.duckie.quackquack.util.backend.kotlinc.Logger -import team.duckie.quackquack.util.backend.kotlinc.isQuackComponent -import team.duckie.quackquack.util.backend.kotlinc.locationOf - -internal class SugarIrVisitor( - @Suppress("unused") private val context: IrPluginContext, - private val logger: Logger, - private val addSugarIrData: (data: SugarIrData) -> Unit, -) : IrElementVisitorVoid { - override fun visitModuleFragment(declaration: IrModuleFragment) { - declaration.files.forEach { file -> - file.accept(this, null) - } - } - - override fun visitFile(declaration: IrFile) { - if (declaration.hasAnnotation(SugarGeneratedFileFqn)) return - declaration.declarations.forEach { item -> - item.accept(this, null) - } - } - - override fun visitSimpleFunction(declaration: IrSimpleFunction) { - if (declaration.isQuackComponent) { - val componentLocation = declaration.file.locationOf(declaration) - val componentFqn = declaration.fqNameWhenAvailable ?: logger.throwError( - message = SourceError.quackComponentFqnUnavailable(declaration.name.asString()), - location = componentLocation, - ) - - if (declaration.hasAnnotation(NoSugarFqn)) return - - val sugarNameAnnotation = declaration.getAnnotation(SugarNameFqn) - val sugarName = sugarNameAnnotation?.getSugarNameIfNotDefault(owner = declaration) - - var sugarToken: IrValueParameter? = null - val sugarParameters = declaration.valueParameters.map { parameter -> - val isSugarToken = parameter.hasAnnotation(SugarTokenFqn) - if (isSugarToken) { - check(sugarToken == null) { - SourceError.multipleSugarTokenIsNotAllowed(declaration.name.asString()) - } - sugarToken = parameter - } - parameter.toSugarParameter(owner = declaration, isToken = isSugarToken) - } - sugarToken ?: logger.throwError( - message = SourceError.quackComponentWithoutSugarToken(componentFqn.asString()), - location = componentLocation, - ) - - val optins = declaration.annotations.filter { annotation -> - annotation.symbol.owner.parentAsClass.hasAnnotation(RequiresOptInFqn) - } - - val sugarIrData = SugarIrData( - owner = declaration, - referFqn = componentFqn, - kdocGetter = { usedTokenLiteral -> - declaration.getSugarKDoc( - referFqn = componentFqn, - tokenName = sugarToken!!.name.asString(), - usedTokenLiteral = usedTokenLiteral, - ) - }, - sugarName = sugarName, - sugarToken = sugarToken!!, - tokenFqExpressions = sugarToken!!.getAllTokenFqExpressions(), - parameters = sugarParameters, - optins = optins, - ) - - // logger(with(logger) { sugarIrData.prependLogPrefix(withNewline = true) }) - addSugarIrData(sugarIrData) - } - } -} - -private fun IrConstructorCall.getSugarNameIfNotDefault(owner: IrFunction): String? { - // Assuming the first argument is always "name" - val sugarNameExpression = getValueArgument(0) - return sugarNameExpression.cast>().value.takeIf { name -> - (name != SugarDefaultName).also { isCustomSugarName -> - if (isCustomSugarName) checkCustomSugarNameIsValid(owner, name) - } - } -} - -private fun checkCustomSugarNameIsValid(owner: IrFunction, name: String) { - val cause = "${owner.name.asString()} ($name)" - - require(name.startsWith(QuackComponentPrefix)) { - SourceError.sugarNamePrefixIsNotQuack(cause) - } - require(name.contains(SugarTokenName)) { - SourceError.sugarNameWithoutTokenName(cause) - } -} - -private fun IrValueParameter.toSugarParameter(owner: IrFunction, isToken: Boolean): SugarParameter { - val casaValue = getAnnotation(CasaValueFqn)?.let { casaValueAnnotation -> - // Assuming the first argument is always "literal" - val casaValueExpression = casaValueAnnotation.getValueArgument(0) - casaValueExpression.cast>().value - } - val sugarImports = getAnnotation(ImportsFqn)?.let { sugarImportsAnnotation -> - // Assuming the first argument is always "clazz" - val sugarImportsExpression = sugarImportsAnnotation.getValueArgument(0) - sugarImportsExpression.cast().elements.map { element -> - element.cast().classType.classFqName ?: error( - SourceError.importClazzFqnUnavailable(element.cast().type.asString()), - ) - } - } - val isComposable = hasAnnotation(ComposableFqn) - - return SugarParameter( - owner = owner, - name = name, - type = type, - isToken = isToken, - isComposable = isComposable, - imports = sugarImports.orEmpty(), - casaValueLiteral = casaValue, - defaultValue = defaultValue, - ) -} - -// TODO(1): util-backend-kotlinc로 KDoc 추출 공통 로직 분리 -// TODO: default section은 없고 KDoc Tag만 있는 경우는 로직이 어떻게? -// TODO: subject의 fqn을 가져올 수는 없을까? -private fun IrSimpleFunction.getSugarKDoc( - referFqn: FqName, - tokenName: String, - usedTokenLiteral: String, -): String { - val usedTokenComment = "This component uses [$usedTokenLiteral] as the token value for `$tokenName`." - val generatedDocComment = "This document was automatically generated by [${referFqn.shortName().asString()}].\n" + - "If any contents are broken, please check the original document." - - val kdocArea = psiElement?.children?.firstOrNull { it is KDoc } as? KDoc - val kdocDefaultSection = kdocArea?.getDefaultSection() ?: return "" - - val kdocTags = kdocArea.children - .firstOrNull { it is KDocSection } - ?.children - ?.filterIsInstance() - .orEmpty() - - return buildString { - appendLine(kdocDefaultSection.getContent().trim()) - appendLine("\n$usedTokenComment\n\n$generatedDocComment\n") - for (tag in kdocTags) { - val tagName = tag.name?.let { "@$it " }.orEmpty() - var subjectName = tag.getSubjectName() - if (subjectName == tokenName) continue - subjectName = subjectName?.plus(" ").orEmpty() - - appendLine("${tagName}${subjectName}${tag.getContent()}") - } - } -} - -private fun IrValueParameter.getAllTokenFqExpressions(): List { - val tokenClass = type.getClass()!! - val tokenClassName = tokenClass.name.asString() - return tokenClass.companionObject()?.let { companion -> - val tokenableProperties = companion.properties.filter { property -> - property.visibility.isPublicAPI - } - val propertyFqExpressions = tokenableProperties.map { property -> - "$tokenClassName.${property.name.asString()}" - } - propertyFqExpressions.toList() - } ?: error(SourceError.sugarTokenButNoCompanionObject(tokenClassName)) -} diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/errors.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/errors.kt deleted file mode 100644 index cb07817d0..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/errors.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.sugar.processor.ir - -internal object NotSupportedError { - internal fun nestedFunctionalType(name: String?): String { - return "Nested functional types are not currently supported due to implementation complexity." + - " ($name)".getIfGivenIsNotNull(name) - } -} - -internal object SourceError { - internal fun quackComponentFqnUnavailable(name: String?): String { - return "A Quack component was detected, but unable to look up a fully qualified name. " + - "Is it an anonymous object?" + " ($name)".getIfGivenIsNotNull(name) - } - - internal fun importClazzFqnUnavailable(name: String?): String { - return "Can't look up the fully qualified name of the class given as `clazz` in `@Imports`. " + - "Is it an anonymous class?" + " ($name)".getIfGivenIsNotNull(name) - } - - internal fun quackComponentWithoutSugarToken(name: String?): String { - return "A Quack component was detected, but no SugarToken was applied." + - " ($name)".getIfGivenIsNotNull(name) - } - - internal fun multipleSugarTokenIsNotAllowed(name: String?): String { - return "A Sugar component can only contain one SugarToken." + - " ($name)".getIfGivenIsNotNull(name) - } - - internal fun sugarNamePrefixIsNotQuack(name: String?): String { - return "Quack component names must start with `SugarName.PREFIX_NAME" + - " (= $QuackComponentPrefix)`." + " ($name)".getIfGivenIsNotNull(name) - } - - internal fun sugarNameWithoutTokenName(name: String?): String { - return "When specifying the sugar component name directly, " + - "`SugarName.TOKEN_NAME (= $SugarTokenName)` must be used." + - " ($name)".getIfGivenIsNotNull(name) - } - - internal fun sugarTokenButNoCompanionObject(name: String?): String { - return "The SugarToken class must include a companion object. " + - "See the sugar component creation policy for more information." + - " ($name)".getIfGivenIsNotNull(name) - } -} - -internal object PoetError { - internal fun sugarComponentButNoSugarRefer(name: String?): String { - return "The SugarRefer for the Sugar component is missing." + - " ($name)".getIfGivenIsNotNull(name) - } -} - -internal object SugarVisitError { - internal fun noMatchedSugarIrData(name: String?): String { - return "No SugarIrData was found for the given SugarRefer. " + - "Please report it in a GitHub Issue. (https://link.duckie.team/quackquack-bug)" + - " ($name)".getIfGivenIsNotNull(name) - } -} - -internal object SugarTransformError { - internal fun sugarComponentAndSugarReferHasDifferentParameters(name: String?): String { - return "The Sugar component has a parameter that doesn't exist in the SugarRefer." + - " ($name)".getIfGivenIsNotNull(name) - } -} - -private fun String.getIfGivenIsNotNull(given: Any?) = - if (given == null) "" else this diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/names.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/names.kt deleted file mode 100644 index acc595bcb..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/ir/names.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) -@file:Suppress("OPT_IN_MARKER_CAN_ONLY_BE_USED_AS_ANNOTATION_OR_ARGUMENT_IN_OPT_IN") - -package team.duckie.quackquack.sugar.processor.ir - -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.asClassName -import org.jetbrains.kotlin.name.FqName -import team.duckie.quackquack.casa.annotation.Casa -import team.duckie.quackquack.casa.annotation.CasaValue -import team.duckie.quackquack.casa.annotation.SugarGeneratorUsage -import team.duckie.quackquack.sugar.material.Imports -import team.duckie.quackquack.sugar.material.NoSugar -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarName -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.SugarToken - -@Suppress("OPT_IN_CAN_ONLY_BE_USED_AS_ANNOTATION") -internal val RequiresOptInFqn = RequiresOptIn::class.qualifiedName!!.toFqnClass() - -internal val ComposableFqn = "androidx.compose.runtime.Composable".toFqnClass() -internal val ComposableCn = ClassName.bestGuess(ComposableFqn.asString()) -internal val NonRestartableComposableCn = ClassName.bestGuess("androidx.compose.runtime.NonRestartableComposable") - -internal const val QuackComponentPrefix = SugarName.PREFIX_NAME - -internal const val SugarDefaultName = SugarName.DEFAULT_NAME -internal const val SugarTokenName = SugarName.TOKEN_NAME - -internal val CasaCn = ClassName.bestGuess(Casa::class.qualifiedName!!) -internal val CasaValueCn = CasaValue::class.asClassName() -internal val CasaValueFqn = CasaValue::class.qualifiedName!!.toFqnClass() - -internal val SugarCompilerApiCn = SugarCompilerApi::class.asClassName() -internal val SugarGeneratorUsageCn = SugarGeneratorUsage::class.asClassName() -internal val SugarGeneratedFileCn = SugarGeneratedFile::class.asClassName() -internal val SugarGeneratedFileFqn = SugarGeneratedFile::class.qualifiedName!!.toFqnClass() - -// sugar 함수에 리플렉션으로 접근 불가 -internal val SugarFqn = "team.duckie.quackquack.sugar.material.sugar".toFqnClass() -internal val SugarNameFqn = SugarName::class.qualifiedName!!.toFqnClass() -internal val SugarTokenFqn = SugarToken::class.qualifiedName!!.toFqnClass() -internal val SugarReferCn = SugarRefer::class.asClassName() -internal val SugarReferFqn = SugarRefer::class.qualifiedName!!.toFqnClass() - -internal val ImportsFqn = Imports::class.qualifiedName!!.toFqnClass() -internal val NoSugarFqn = NoSugar::class.qualifiedName!!.toFqnClass() - -private fun String.toFqnClass() = FqName(this) diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/poet/PoetUtils.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/poet/PoetUtils.kt deleted file mode 100644 index 1aaf611f9..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/poet/PoetUtils.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.sugar.processor.poet - -import team.duckie.quackquack.sugar.processor.ir.QuackComponentPrefix -import team.duckie.quackquack.sugar.processor.ir.SugarIrData -import team.duckie.quackquack.sugar.processor.ir.SugarTokenName - -// TODO: Testing -internal fun SugarIrData.toSugarComponentName(tokenFqExpression: String): String { - val tokenExpression = tokenFqExpression - .substringAfterLast(".") - .replaceFirstChar(Char::titlecase) - return sugarName?.replace(SugarTokenName, tokenExpression) - ?: referFqn - .shortName() - .asString() - .toMutableList() - .apply { addAll(QuackComponentPrefix.length, tokenExpression.toList()) } - .joinToString("") -} diff --git a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/poet/poet.kt b/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/poet/poet.kt deleted file mode 100644 index e826fa259..000000000 --- a/sugar-processor/src/main/kotlin/team/duckie/quackquack/sugar/processor/poet/poet.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.sugar.processor.poet - -import com.squareup.kotlinpoet.AnnotationSpec -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.KModifier -import com.squareup.kotlinpoet.buildCodeBlock -import com.squareup.kotlinpoet.withIndent -import java.io.File -import org.jetbrains.kotlin.ir.declarations.name -import org.jetbrains.kotlin.ir.types.classFqName -import org.jetbrains.kotlin.ir.util.file -import org.jetbrains.kotlin.name.FqName -import team.duckie.quackquack.sugar.processor.ir.CasaCn -import team.duckie.quackquack.sugar.processor.ir.ComposableCn -import team.duckie.quackquack.sugar.processor.ir.NonRestartableComposableCn -import team.duckie.quackquack.sugar.processor.ir.SugarCompilerApiCn -import team.duckie.quackquack.sugar.processor.ir.SugarFqn -import team.duckie.quackquack.sugar.processor.ir.SugarGeneratedFileCn -import team.duckie.quackquack.sugar.processor.ir.SugarGeneratorUsageCn -import team.duckie.quackquack.sugar.processor.ir.SugarIrData -import team.duckie.quackquack.sugar.processor.ir.SugarParameter -import team.duckie.quackquack.sugar.processor.ir.SugarReferCn -import team.duckie.quackquack.sugar.processor.ir.toFqnStringOrEmpty -import team.duckie.quackquack.util.backend.FormatterOffComment -import team.duckie.quackquack.util.backend.SuppressAnnotation -import team.duckie.quackquack.util.backend.addAnnotations -import team.duckie.quackquack.util.backend.addFunctions -import team.duckie.quackquack.util.backend.bestGuessToKotlinPackageName -import team.duckie.quackquack.util.backend.getGeneratedFileComment -import team.duckie.quackquack.util.backend.kotlinc.addImports - -private val GeneratedComment = getGeneratedFileComment("sugar-processor") - -@Suppress("OPT_IN_CAN_ONLY_BE_USED_AS_ANNOTATION") -private val SugarCompilerOptInAnnotation = AnnotationSpec - .builder(OptIn::class) - .addMember( - "%T::class, %T::class", - SugarCompilerApiCn, - SugarGeneratorUsageCn, - ) - .useSiteTarget(AnnotationSpec.UseSiteTarget.FILE) - .build() - -private val SugarGeneratedFileMarkerAnnotation = AnnotationSpec - .builder(SugarGeneratedFileCn) - .useSiteTarget(AnnotationSpec.UseSiteTarget.FILE) - .build() - -internal fun generateSugarComponentFiles(irDatas: List, sugarPath: String) { - val fileGroupedIrDatas = irDatas.groupBy { irData -> - irData.owner.file.name - } - - fileGroupedIrDatas.forEach { (fileName, irDatas) -> - val (imports, funSpecs) = irDatas.toFunSpecsWithImports() - val ktSpec = FileSpec - .builder( - packageName = sugarPath.bestGuessToKotlinPackageName(), - fileName = fileName.substringBeforeLast("."), - ) - .addFileComment(GeneratedComment) - .addFileComment(FormatterOffComment) - .addAnnotations( - SuppressAnnotation, - SugarCompilerOptInAnnotation, - SugarGeneratedFileMarkerAnnotation, - ) - .addImports(imports.toMutableList().apply { add(SugarFqn) }) - .addFunctions(funSpecs) - .build() - - File(sugarPath, fileName).also { file -> - if (!file.exists()) { - file.parentFile?.mkdirs() - file.createNewFile() - } - }.writeText(ktSpec.toString()) - } -} - -private fun List.toFunSpecsWithImports(): Pair, List> { - val imports = mutableListOf() - val funSpecs = mutableListOf() - forEach { sugarIrData -> - imports += sugarIrData.referFqn - sugarIrData.tokenFqExpressions.forEach { tokenFqExpression -> - val (_imports, funSpec) = sugarIrData.toFunSpecWithImports(tokenFqExpression) - imports += _imports - funSpecs += funSpec - } - } - return imports to funSpecs -} - -private fun SugarIrData.toFunSpecWithImports(tokenFqExpression: String): Pair, FunSpec> { - val imports = mutableListOf() - - val sugarReferAnnotation = AnnotationSpec - .builder(SugarReferCn) - .addMember("%S", referFqn.asString()) - .build() - - val sugarName = toSugarComponentName(tokenFqExpression) - val sugarBody = buildCodeBlock { - addStatement("%L(", referFqn.shortName().asString()) - withIndent { - parameters.forEach { parameter -> - imports += parameter.type.classFqName!! - imports += parameter.imports - - val parameterName = parameter.name.asString() - val parameterValue = if (parameter.isToken) tokenFqExpression else parameterName - - addStatement("%L = %L,", parameterName, parameterValue) - } - } - addStatement(")") - } - - val optinCns = optins.map { irOptin -> - ClassName.bestGuess(irOptin.toFqnStringOrEmpty()) - } - - val funSpec = FunSpec - .builder(sugarName) - .addAnnotations( - CasaCn, - ComposableCn, - NonRestartableComposableCn, - *optinCns.toTypedArray(), - ) - .addAnnotation(sugarReferAnnotation) - .addModifiers(KModifier.PUBLIC) - .addParameters(parametersWithoutToken.map(SugarParameter::toParameterSpec)) - .addCode(sugarBody) - .addKdoc(kdocGetter(tokenFqExpression)) - .build() - - return imports to funSpec -} diff --git a/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarIrErrorTest.kt b/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarIrErrorTest.kt deleted file mode 100644 index f9b4723d5..000000000 --- a/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarIrErrorTest.kt +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:OptIn(ExperimentalCompilerApi::class) -@file:Suppress( - "RedundantUnitReturnType", - "RedundantVisibilityModifier", - "RedundantUnitExpression", - "RedundantSuppression", - "LongMethod", - "HasPlatformType", - "KDocUnresolvedReference", -) - -package team.duckie.quackquack.sugar.processor - -import com.tschuchort.compiletesting.KotlinCompilation -import com.tschuchort.compiletesting.PluginOption -import com.tschuchort.compiletesting.SourceFile -import com.tschuchort.compiletesting.SourceFile.Companion.kotlin -import io.kotest.core.spec.style.ExpectSpec -import io.kotest.core.test.Enabled -import io.kotest.engine.spec.tempdir -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import org.jetbrains.kotlin.config.JvmTarget -import team.duckie.quackquack.sugar.processor.ir.NotSupportedError.nestedFunctionalType -import team.duckie.quackquack.sugar.processor.ir.PoetError.sugarComponentButNoSugarRefer -import team.duckie.quackquack.sugar.processor.ir.SourceError.multipleSugarTokenIsNotAllowed -import team.duckie.quackquack.sugar.processor.ir.SourceError.quackComponentWithoutSugarToken -import team.duckie.quackquack.sugar.processor.ir.SourceError.sugarNamePrefixIsNotQuack -import team.duckie.quackquack.sugar.processor.ir.SourceError.sugarNameWithoutTokenName -import team.duckie.quackquack.sugar.processor.ir.SourceError.sugarTokenButNoCompanionObject -import team.duckie.quackquack.sugar.processor.ir.SugarTransformError.sugarComponentAndSugarReferHasDifferentParameters - -class SugarIrErrorTest : ExpectSpec() { - private val tempDir = tempdir() - - init { - context("NotSupportedError") { - expect("nestedFunctionalType").config( - enabledOrReasonIf = { - Enabled.disabled( - "테스트 코드는 실패하는데 실제 코드로 돌려보면 정상 작동함.." + - "추후 테스트 코드가 실패하는 원인을 찾아야 함.", - ) - }, - ) { - val result = compile( - kotlin( - "main.kt", - """ -import team.duckie.quackquack.sugar.material.SugarToken -import androidx.compose.runtime.Composable - -@Composable -fun QuackText( - @SugarToken style: AwesomeType, - lambda: (unit: Unit, unit2: Unit, unit3: () -> Unit) -> Unit, -) {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR - result.messages shouldContain nestedFunctionalType("QuackText#lambda") - } - } - - context("SourceError") { - expect("quackComponentWithoutSugarToken") { - val result = compile( - kotlin( - "main.kt", - """ -import androidx.compose.runtime.Composable - -@Composable -fun QuackText() {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR - result.messages shouldContain quackComponentWithoutSugarToken("QuackText") - } - - expect("quackComponentWithoutSugarToken - @NoSugar applied") { - val result = compile( - kotlin( - "main.kt", - """ -import team.duckie.quackquack.sugar.material.NoSugar -import androidx.compose.runtime.Composable - -@NoSugar -@Composable -fun QuackText() {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - } - - expect("multipleSugarTokenIsNotAllowed") { - val result = compile( - kotlin( - "main.kt", - """ -import team.duckie.quackquack.sugar.material.SugarToken -import androidx.compose.runtime.Composable - -@Composable -fun QuackText( - @SugarToken style: AwesomeType, - @SugarToken style2: AwesomeType2, -) {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR - result.messages shouldContain multipleSugarTokenIsNotAllowed("QuackText") - } - - expect("sugarNamePrefixIsNotQuack") { - val result = compile( - kotlin( - "main.kt", - """ -import androidx.compose.runtime.Composable -import team.duckie.quackquack.sugar.material.SugarName -import team.duckie.quackquack.sugar.material.SugarToken - -@SugarName("Text") -@Composable -fun QuackText(@SugarToken type: AwesomeType) {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR - result.messages shouldContain sugarNamePrefixIsNotQuack("QuackText (Text)") - } - - expect("sugarNameWithoutTokenName") { - val result = compile( - kotlin( - "main.kt", - """ -import androidx.compose.runtime.Composable -import team.duckie.quackquack.sugar.material.SugarName -import team.duckie.quackquack.sugar.material.SugarToken - -@SugarName("QuackText") -@Composable -fun QuackText(@SugarToken type: AwesomeType) {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR - result.messages shouldContain sugarNameWithoutTokenName("QuackText (QuackText)") - } - - expect("sugarTokenButNoCompanionObject") { - val result = compile( - kotlin( - "main.kt", - """ -import androidx.compose.runtime.Composable -import team.duckie.quackquack.sugar.material.SugarToken - -@Composable -fun QuackText(@SugarToken type: AwesomeType3) {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR - result.messages shouldContain sugarTokenButNoCompanionObject("AwesomeType3") - } - } - - context("PoetError") { - expect("sugarComponentButNoSugarRefer") { - val result = compile( - kotlin( - "main.kt", - """ -@file:OptIn(SugarCompilerApi::class) -@file:SugarGeneratedFile - -import androidx.compose.runtime.Composable -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile - -@Composable -fun QuackOneText() {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR - result.messages shouldContain sugarComponentButNoSugarRefer("QuackOneText") - } - } - - context("SugarTransformError") { - expect("sugarComponentAndSugarReferHasDifferentParameters") { - val result = compile( - kotlin( - "main.kt", - """ -import team.duckie.quackquack.sugar.material.SugarToken -import androidx.compose.runtime.Composable - -@Composable -fun QuackText(@SugarToken style: AwesomeType) {} - """, - ), - kotlin( - "main-generated.kt", - """ -@file:OptIn(SugarCompilerApi::class) -@file:SugarGeneratedFile - -import androidx.compose.runtime.Composable -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.sugar - -@Composable -@SugarRefer("QuackText") -fun QuackOneText(newNumber: Int = sugar()) {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR - result.messages shouldContain sugarComponentAndSugarReferHasDifferentParameters( - "(refer) QuackText -> (sugar) QuackOneText#newNumber", - ) - } - } - } - - private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result { - return prepareCompilation(*sourceFiles).compile() - } - - private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation { - return KotlinCompilation().apply { - workingDir = tempDir - sources = sourceFiles.asList() + stubs - jvmTarget = JvmTarget.JVM_17.toString() - inheritClassPath = true - supportsK2 = false - useK2 = false - pluginOptions = listOf( - PluginOption( - pluginId = PluginId, - optionName = OPTION_SUGAR_PATH.optionName, - optionValue = tempDir.path, - ), - PluginOption( - pluginId = PluginId, - optionName = OPTION_POET.optionName, - optionValue = "false", - ), - ) - compilerPluginRegistrars = listOf(SugarComponentRegistrar.asPluginRegistrar()) - commandLineProcessors = listOf(SugarCommandLineProcessor()) - } - } -} diff --git a/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarIrTransformTest.kt b/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarIrTransformTest.kt deleted file mode 100644 index 0650ed0c5..000000000 --- a/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarIrTransformTest.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:OptIn(ExperimentalCompilerApi::class) -@file:Suppress( - "RedundantUnitReturnType", - "RedundantVisibilityModifier", - "RedundantUnitExpression", - "RedundantSuppression", - "LongMethod", - "HasPlatformType", - "KDocUnresolvedReference", -) - -package team.duckie.quackquack.sugar.processor - -import com.tschuchort.compiletesting.KotlinCompilation -import com.tschuchort.compiletesting.PluginOption -import com.tschuchort.compiletesting.SourceFile -import com.tschuchort.compiletesting.SourceFile.Companion.kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.engine.spec.tempdir -import io.kotest.matchers.shouldBe -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import org.jetbrains.kotlin.config.JvmTarget -import org.jetbrains.kotlin.utils.addToStdlib.cast - -class SugarIrTransformTest : StringSpec() { - private val tempDir = tempdir() - - init { - "Default Argument에 SugarIrTransform이 정상 작동함" { - val result = compile( - kotlin( - "main.kt", - """ -import team.duckie.quackquack.sugar.material.SugarToken -import androidx.compose.runtime.Composable - -var number = 0 - -@Composable -fun QuackText( - @SugarToken style: AwesomeType, - newNumber: Int = Int.MAX_VALUE, -) { - number = newNumber -} - """, - ), - kotlin( - "text-sugar.kt", - """ -@file:OptIn(SugarCompilerApi::class) -@file:SugarGeneratedFile - -import androidx.compose.runtime.Composable -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.sugar - -@Composable -@SugarRefer("QuackText") -fun QuackOneText(newNumber: Int = sugar()) { - QuackText( - style = AwesomeType.One, - newNumber = newNumber, - ) -} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - - val sugarClass = result.classLoader.loadClass("Text_sugarKt") - val quackTextMethod = sugarClass.getMethod( - "QuackOneText\$default", - Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType, - java.lang.Object::class.java, - ) - quackTextMethod.invoke(sugarClass, 0, 1, null) - - val mainClass = result.classLoader.loadClass("MainKt") - val getNumberMethod = mainClass.getMethod("getNumber") - - getNumberMethod.invoke(mainClass).cast() shouldBe Int.MAX_VALUE - } - } - - private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result { - return prepareCompilation(*sourceFiles).compile() - } - - private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation { - return KotlinCompilation().apply { - workingDir = tempDir - sources = sourceFiles.asList() + stubs - jvmTarget = JvmTarget.JVM_17.toString() - inheritClassPath = true - supportsK2 = false - useK2 = false - pluginOptions = listOf( - PluginOption( - pluginId = PluginId, - optionName = OPTION_SUGAR_PATH.optionName, - optionValue = tempDir.path, - ), - PluginOption( - pluginId = PluginId, - optionName = OPTION_POET.optionName, - optionValue = "false", - ), - ) - compilerPluginRegistrars = listOf(SugarComponentRegistrar.asPluginRegistrar()) - commandLineProcessors = listOf(SugarCommandLineProcessor()) - } - } -} diff --git a/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarPoetTest.kt b/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarPoetTest.kt deleted file mode 100644 index 9d194586a..000000000 --- a/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/SugarPoetTest.kt +++ /dev/null @@ -1,505 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -@file:OptIn(ExperimentalCompilerApi::class) -@file:Suppress( - "RedundantUnitReturnType", - "RedundantVisibilityModifier", - "RedundantUnitExpression", - "RedundantSuppression", - "LongMethod", - "HasPlatformType", - "KDocUnresolvedReference", -) - -package team.duckie.quackquack.sugar.processor - -import com.tschuchort.compiletesting.KotlinCompilation -import com.tschuchort.compiletesting.PluginOption -import com.tschuchort.compiletesting.SourceFile -import com.tschuchort.compiletesting.SourceFile.Companion.kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.engine.spec.tempdir -import io.kotest.matchers.shouldBe -import org.intellij.lang.annotations.Language -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import org.jetbrains.kotlin.config.JvmTarget -import team.duckie.quackquack.util.backend.test.findGeneratedFileOrNull -import team.duckie.quackquack.util.backend.test.removePackageLine - -// TODO: @Imports 테스트 작성 -// TODO: nullable한 인자 테스트 작성 -class SugarPoetTest : StringSpec() { - private val tempDir = tempdir() - - init { - """ - - @SugarName이 없을 때는 기본 정책대로 sugar component가 생성됨 - - KDoc이 없는 대상은 KDoc을 생성하지 않음 - """ { - val result = compile( - kotlin( - "text.kt", - """ -import team.duckie.quackquack.sugar.material.SugarToken -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun QuackText( - modifier: Modifier = Modifier, - text: String, - @SugarToken style: AwesomeType2, - singleLine: Boolean = false, - softWrap: Boolean = true, -) {} - """, - ), - ) - - @Language("kotlin") - val expect = """ -// This file was automatically generated by sugar-processor. -// Do not modify it manually. -// @formatter:off -@file:Suppress("NoConsecutiveBlankLines", "PackageDirectoryMismatch", "Wrapping", - "TrailingCommaOnCallSite", "ArgumentListWrapping", "RedundantVisibilityModifier", - "UnusedImport", "NoUnusedImports", "SpacingAroundParens", "Indentation", "NoUnitReturn", - "RedundantUnitReturnType", "ModifierParameter", "KDocUnresolvedReference", "NoTrailingSpaces", - "NoMultipleSpaces", "ktlint") -@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) -@file:SugarGeneratedFile - - -import AwesomeType2 -import QuackText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.ui.Modifier -import kotlin.Boolean -import kotlin.OptIn -import kotlin.String -import kotlin.Suppress -import team.duckie.quackquack.casa.`annotation`.Casa -import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.sugar - -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("QuackText") -public fun QuackOneText( - modifier: Modifier = sugar(), - text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), -) { - QuackText( - modifier = modifier, - text = text, - style = AwesomeType2.One, - singleLine = singleLine, - softWrap = softWrap, - ) -} - -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("QuackText") -public fun QuackTwoText( - modifier: Modifier = sugar(), - text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), -) { - QuackText( - modifier = modifier, - text = text, - style = AwesomeType2.Two, - singleLine = singleLine, - softWrap = softWrap, - ) -} - -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("QuackText") -public fun QuackThreeText( - modifier: Modifier = sugar(), - text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), -) { - QuackText( - modifier = modifier, - text = text, - style = AwesomeType2.Three, - singleLine = singleLine, - softWrap = softWrap, - ) -} - - """.trimIndent() - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - tempDir.findGeneratedFileOrNull("text.kt")?.readText()?.removePackageLine() shouldBe expect - } - - """ - - PREFIX_NAME + Awesome + TOKEN_NAME 조합으로 sugar component를 생성함 - - KDoc이 있는 대상은 Generated KDoc을 생성함 - """ { - val result = compile( - kotlin( - "text.kt", - """ -import team.duckie.quackquack.sugar.material.SugarToken -import team.duckie.quackquack.sugar.material.SugarName -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -/** - * AWESOME! - * - * @param modifier 적용할 Modifier - * @param text 표시할 문자 - */ -@SugarName(SugarName.PREFIX_NAME + "Awesome" + SugarName.TOKEN_NAME) -@Composable -fun QuackText( - modifier: Modifier = Modifier, - text: String, - @SugarToken style: AwesomeType2, - singleLine: Boolean = false, - softWrap: Boolean = true, -) {} - """, - ), - ) - - @Language("kotlin") - val expect = """ -// This file was automatically generated by sugar-processor. -// Do not modify it manually. -// @formatter:off -@file:Suppress("NoConsecutiveBlankLines", "PackageDirectoryMismatch", "Wrapping", - "TrailingCommaOnCallSite", "ArgumentListWrapping", "RedundantVisibilityModifier", - "UnusedImport", "NoUnusedImports", "SpacingAroundParens", "Indentation", "NoUnitReturn", - "RedundantUnitReturnType", "ModifierParameter", "KDocUnresolvedReference", "NoTrailingSpaces", - "NoMultipleSpaces", "ktlint") -@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) -@file:SugarGeneratedFile - - -import AwesomeType2 -import QuackText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.ui.Modifier -import kotlin.Boolean -import kotlin.OptIn -import kotlin.String -import kotlin.Suppress -import team.duckie.quackquack.casa.`annotation`.Casa -import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.sugar - -/** - * AWESOME! - * - * This component uses [AwesomeType2.One] as the token value for `style`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param modifier 적용할 Modifier - * @param text 표시할 문자 - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("QuackText") -public fun QuackAwesomeOne( - modifier: Modifier = sugar(), - text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), -) { - QuackText( - modifier = modifier, - text = text, - style = AwesomeType2.One, - singleLine = singleLine, - softWrap = softWrap, - ) -} - -/** - * AWESOME! - * - * This component uses [AwesomeType2.Two] as the token value for `style`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param modifier 적용할 Modifier - * @param text 표시할 문자 - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("QuackText") -public fun QuackAwesomeTwo( - modifier: Modifier = sugar(), - text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), -) { - QuackText( - modifier = modifier, - text = text, - style = AwesomeType2.Two, - singleLine = singleLine, - softWrap = softWrap, - ) -} - -/** - * AWESOME! - * - * This component uses [AwesomeType2.Three] as the token value for `style`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param modifier 적용할 Modifier - * @param text 표시할 문자 - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("QuackText") -public fun QuackAwesomeThree( - modifier: Modifier = sugar(), - text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), -) { - QuackText( - modifier = modifier, - text = text, - style = AwesomeType2.Three, - singleLine = singleLine, - softWrap = softWrap, - ) -} - - """.trimIndent() - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - tempDir.findGeneratedFileOrNull("text.kt")?.readText()?.removePackageLine() shouldBe expect - } - - "DEFAULT_NAME을 사용하면 기본 정책대로 sugar component가 생성됨" { - val result = compile( - kotlin( - "text.kt", - """ -import team.duckie.quackquack.sugar.material.SugarToken -import team.duckie.quackquack.sugar.material.SugarName -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@SugarName(SugarName.DEFAULT_NAME) -@Composable -fun QuackText( - modifier: Modifier = Modifier, - text: String, - @SugarToken style: AwesomeType, - singleLine: Boolean = false, - softWrap: Boolean = true, -) {} - """, - ), - ) - - @Language("kotlin") - val expect = """ -// This file was automatically generated by sugar-processor. -// Do not modify it manually. -// @formatter:off -@file:Suppress("NoConsecutiveBlankLines", "PackageDirectoryMismatch", "Wrapping", - "TrailingCommaOnCallSite", "ArgumentListWrapping", "RedundantVisibilityModifier", - "UnusedImport", "NoUnusedImports", "SpacingAroundParens", "Indentation", "NoUnitReturn", - "RedundantUnitReturnType", "ModifierParameter", "KDocUnresolvedReference", "NoTrailingSpaces", - "NoMultipleSpaces", "ktlint") -@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) -@file:SugarGeneratedFile - - -import AwesomeType -import QuackText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.ui.Modifier -import kotlin.Boolean -import kotlin.OptIn -import kotlin.String -import kotlin.Suppress -import team.duckie.quackquack.casa.`annotation`.Casa -import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.sugar - -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("QuackText") -public fun QuackOneText( - modifier: Modifier = sugar(), - text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), -) { - QuackText( - modifier = modifier, - text = text, - style = AwesomeType.One, - singleLine = singleLine, - softWrap = softWrap, - ) -} - - """.trimIndent() - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - tempDir.findGeneratedFileOrNull("text.kt")?.readText()?.removePackageLine() shouldBe expect - } - - "람다 인자가 지원됨" { - val result = compile( - kotlin( - "checkbox.kt", - """ -import team.duckie.quackquack.sugar.material.SugarToken -import androidx.compose.runtime.Composable - -@Composable -fun QuackCheckbox( - @SugarToken style: AwesomeType, - onCheckChanged: (checked: Boolean) -> Unit, -) {} - -@Composable -fun QuackCheckbox2( - @SugarToken style: AwesomeType, - onCheckChanged: suspend Boolean.(checked: Boolean) -> Boolean, -) {} - """, - ), - ) - - @Language("kotlin") - val expect = """ -// This file was automatically generated by sugar-processor. -// Do not modify it manually. -// @formatter:off -@file:Suppress("NoConsecutiveBlankLines", "PackageDirectoryMismatch", "Wrapping", - "TrailingCommaOnCallSite", "ArgumentListWrapping", "RedundantVisibilityModifier", - "UnusedImport", "NoUnusedImports", "SpacingAroundParens", "Indentation", "NoUnitReturn", - "RedundantUnitReturnType", "ModifierParameter", "KDocUnresolvedReference", "NoTrailingSpaces", - "NoMultipleSpaces", "ktlint") -@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) -@file:SugarGeneratedFile - - -import AwesomeType -import QuackCheckbox -import QuackCheckbox2 -import androidx.compose.runtime.Composable -import androidx.compose.runtime.NonRestartableComposable -import kotlin.Boolean -import kotlin.Function1 -import kotlin.OptIn -import kotlin.Suppress -import kotlin.Unit -import kotlin.coroutines.SuspendFunction2 -import team.duckie.quackquack.casa.`annotation`.Casa -import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.sugar - -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("QuackCheckbox") -public fun QuackOneCheckbox(onCheckChanged: (P0: Boolean) -> Unit) { - QuackCheckbox( - style = AwesomeType.One, - onCheckChanged = onCheckChanged, - ) -} - -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("QuackCheckbox2") -public fun QuackOneCheckbox2(onCheckChanged: suspend (P0: Boolean, P1: Boolean) -> Boolean) { - QuackCheckbox2( - style = AwesomeType.One, - onCheckChanged = onCheckChanged, - ) -} - - """.trimIndent() - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - tempDir.findGeneratedFileOrNull("checkbox.kt")?.readText()?.removePackageLine() shouldBe expect - } - } - - private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result { - return prepareCompilation(*sourceFiles).compile() - } - - private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation { - return KotlinCompilation().apply { - workingDir = tempDir - sources = sourceFiles.asList() + stubs - jvmTarget = JvmTarget.JVM_17.toString() - inheritClassPath = true - supportsK2 = false - useK2 = false - pluginOptions = listOf( - PluginOption( - pluginId = PluginId, - optionName = OPTION_SUGAR_PATH.optionName, - optionValue = tempDir.path, - ), - PluginOption( - pluginId = PluginId, - optionName = OPTION_POET.optionName, - optionValue = "true", - ), - ) - compilerPluginRegistrars = listOf(SugarComponentRegistrar.asPluginRegistrar()) - commandLineProcessors = listOf(SugarCommandLineProcessor()) - } - } -} diff --git a/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/stubs.kt b/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/stubs.kt deleted file mode 100644 index c63da56fe..000000000 --- a/sugar-processor/src/test/kotlin/team/duckie/quackquack/sugar/processor/stubs.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.sugar.processor - -import com.tschuchort.compiletesting.SourceFile.Companion.kotlin -import team.duckie.quackquack.util.backend.test.stub.ComposeStub -import team.duckie.quackquack.util.backend.test.stub.SugarStub - -val stubs = listOf( - kotlin("Modifier.kt", ComposeStub.Modifier), - kotlin("Composable.kt", ComposeStub.Composable), - kotlin("annotations.kt", SugarStub.Annotations), - kotlin("typer.kt", SugarStub.Typer), - kotlin( - "AwesomeTypes.kt", - """ -@JvmInline -value class AwesomeType(val index: Int) { - companion object { - val One = AwesomeType(1) - } -} - -@JvmInline -value class AwesomeType2(val index: Int) { - companion object { - val One = AwesomeType2(1) - val Two = AwesomeType2(2) - val Three = AwesomeType2(3) - } -} - -@JvmInline -value class AwesomeType3(val index: Int) - """, - ), -) diff --git a/sugar-processor/version.txt b/sugar-processor/version.txt deleted file mode 100644 index 3bf51efdd..000000000 --- a/sugar-processor/version.txt +++ /dev/null @@ -1 +0,0 @@ -2.0.0-alpha02 diff --git a/ui-sugar/build.gradle.kts b/ui-sugar/build.gradle.kts new file mode 100644 index 000000000..09c875e0a --- /dev/null +++ b/ui-sugar/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +plugins { + quackquack("android-library") + quackquack("android-compose") + quackquack("quack-publishing") +} + +android { + namespace = "team.duckie.quackquack.ui.sugar" +} + +dependencies { + api(projects.ui.orArtifact()) +} diff --git a/ui-sugar/src/main/AndroidManifest.xml b/ui-sugar/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b1e11303c --- /dev/null +++ b/ui-sugar/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + diff --git a/ui-sugar/src/main/kotlin/team/duckie/quackquack/ui/sugar/todo b/ui-sugar/src/main/kotlin/team/duckie/quackquack/ui/sugar/todo new file mode 100644 index 000000000..e69de29bb diff --git a/ui-sugar/version.txt b/ui-sugar/version.txt new file mode 100644 index 000000000..0cc5bc23c --- /dev/null +++ b/ui-sugar/version.txt @@ -0,0 +1 @@ +0.1.0-2.0.0-alpha10 diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 26197c211..dfcbcbcff 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -99,12 +99,12 @@ dependencies { libs.test.kotest.assertion.core, ) - kotlinCompilerPlugin(projects.sugarProcessor.orArtifact()) + kotlinCompilerPlugin(projects.sugarCompiler.orArtifact()) safeRunWithinDevelopmentMode { ksps( // TODO: projects.casaProcessor, ) - // kotlinCompilerPlugin(projects.docusaurusIntegration) + kotlinCompilerPlugin(projects.sugarCore) } } From 52eb34be7ddf7db9d992068acbf85c6dae60f88c Mon Sep 17 00:00:00 2001 From: jisungbin Date: Fri, 14 Jul 2023 07:48:18 +0900 Subject: [PATCH 02/12] Migrating from a single-module structure to a multi-module structure --- .../duckie/quackquack/casa/processor/poet.kt | 6 +- gradle/libs.versions.toml | 4 + settings.gradle.kts | 5 +- sugar-compiler/build.gradle.kts | 11 +- .../sugar/compiler/SugarCompilerExtension.kt | 44 ++++ .../sugar/compiler/SugarCompilerRegistrar.kt | 64 +++++ .../sugar/compiler/ir/SugarIrTransformer.kt | 123 +++++++++ sugar-core/build.gradle.kts | 2 + sugar-core/codegen/build.gradle.kts | 5 +- .../quackquack/sugar/codegen/codegen.kt | 152 +++++++++++ .../duckie/quackquack/sugar/codegen/utils.kt | 131 ++++++++++ sugar-core/error/build.gradle.kts | 15 ++ .../quackquack/sugar/error/SugarErrors.kt | 73 ++++++ sugar-core/error/version.txt | 1 + sugar-core/names/build.gradle.kts | 20 ++ .../quackquack/sugar/names/SugarNames.kt | 62 +++++ sugar-core/names/version.txt | 1 + sugar-core/node/build.gradle.kts | 7 +- .../sugar/node/SugarComponentNode.kt | 78 ++++++ .../quackquack/sugar/node/SugarParameter.kt | 58 +++++ sugar-core/node/version.txt | 1 + .../core/SugarCoreCommandLineProcessor.kt | 54 ++++ .../sugar/core/SugarCoreExtension.kt | 39 +++ .../sugar/core/SugarCoreRegistrar.kt | 70 +++++ sugar-core/visitor/build.gradle.kts | 13 +- sugar-core/visitor/version.txt | 1 + sugar-test/build.gradle.kts | 23 ++ .../sugar/test/CompilerTestCompilation.kt | 44 ++++ .../sugar/test/SugarCompilerErrorTest.kt | 239 ++++++++++++++++++ .../sugar/test/SugarCompilerTransformTest.kt | 81 ++++++ .../duckie/quackquack/sugar/test/stubs.kt | 42 +++ ui-sugar/.nospotless | 0 util-backend-kotlinc/build.gradle.kts | 1 - .../util/backend/kotlinc/IrUtils.kt | 7 + .../build.gradle.kts | 1 - .../util/backend/kotlinpoet/utils.kt | 33 +-- util-backend-ksp/build.gradle.kts | 4 - util-backend-ksp/version.txt | 1 - .../quackquack/util/backend/test/stub/aide.kt | 21 -- .../quackquack/util/backend/test/stub/casa.kt | 3 +- .../util/backend/test/stub/compose.kt | 14 +- .../util/backend/test/stub/sugar.kt | 18 +- .../quackquack/util/backend/test/utils.kt | 3 +- .../duckie/quackquack/util/backend/utils.kt | 30 --- util-backend/version.txt | 1 - 45 files changed, 1481 insertions(+), 125 deletions(-) create mode 100644 sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/SugarCompilerExtension.kt create mode 100644 sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/SugarCompilerRegistrar.kt create mode 100644 sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/ir/SugarIrTransformer.kt create mode 100644 sugar-core/codegen/src/main/kotlin/team/duckie/quackquack/sugar/codegen/codegen.kt create mode 100644 sugar-core/codegen/src/main/kotlin/team/duckie/quackquack/sugar/codegen/utils.kt create mode 100644 sugar-core/error/build.gradle.kts create mode 100644 sugar-core/error/src/main/kotlin/team/duckie/quackquack/sugar/error/SugarErrors.kt create mode 100644 sugar-core/error/version.txt create mode 100644 sugar-core/names/build.gradle.kts create mode 100644 sugar-core/names/src/main/kotlin/team/duckie/quackquack/sugar/names/SugarNames.kt create mode 100644 sugar-core/names/version.txt create mode 100644 sugar-core/node/src/main/kotlin/team/duckie/quackquack/sugar/node/SugarComponentNode.kt create mode 100644 sugar-core/node/src/main/kotlin/team/duckie/quackquack/sugar/node/SugarParameter.kt create mode 100644 sugar-core/node/version.txt create mode 100644 sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreCommandLineProcessor.kt create mode 100644 sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreExtension.kt create mode 100644 sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreRegistrar.kt create mode 100644 sugar-core/visitor/version.txt create mode 100644 sugar-test/build.gradle.kts create mode 100644 sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/CompilerTestCompilation.kt create mode 100644 sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerErrorTest.kt create mode 100644 sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerTransformTest.kt create mode 100644 sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/stubs.kt create mode 100644 ui-sugar/.nospotless rename {util-backend => util-backend-kotlinpoet}/build.gradle.kts (91%) rename util-backend/src/main/kotlin/team/duckie/quackquack/util/backend/PoetUtils.kt => util-backend-kotlinpoet/src/main/kotlin/team/duckie/quackquack/util/backend/kotlinpoet/utils.kt (53%) delete mode 100644 util-backend-ksp/version.txt delete mode 100644 util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/aide.kt delete mode 100644 util-backend/src/main/kotlin/team/duckie/quackquack/util/backend/utils.kt delete mode 100644 util-backend/version.txt diff --git a/casa-processor/src/main/kotlin/team/duckie/quackquack/casa/processor/poet.kt b/casa-processor/src/main/kotlin/team/duckie/quackquack/casa/processor/poet.kt index 2c5b63705..b49f997c5 100644 --- a/casa-processor/src/main/kotlin/team/duckie/quackquack/casa/processor/poet.kt +++ b/casa-processor/src/main/kotlin/team/duckie/quackquack/casa/processor/poet.kt @@ -18,9 +18,9 @@ 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.FormatterOffComment +import team.duckie.quackquack.util.backend.kotlinpoet.SuppressAnnotation +import team.duckie.quackquack.util.backend.kotlinpoet.getGeneratedFileComment import team.duckie.quackquack.util.backend.ksp.generateBuildOrLocalFile import team.duckie.quackquack.util.backend.ksp.requireContainingFile diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fdab6a667..900abb27a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,8 @@ gradle-dependency-graph = "1.1.0" google-autoservice-standard = "1.1.1" google-autoservice-ksp = "1.1.0" +jetbrains-annotation = "24.0.0" + kotlin-core = "1.8.22" kotlin-coroutines = "1.7.2" kotlin-dokka = "1.8.20" @@ -73,6 +75,8 @@ gradle-publish-maven = { module = "com.vanniktech:gradle-maven-publish-plugin", google-autoservice-annotation = { module = "com.google.auto.service:auto-service-annotations", version.ref = "google-autoservice-standard" } google-autoservice-ksp-processor = { module = "dev.zacsweers.autoservice:auto-service-ksp", version.ref = "google-autoservice-ksp" } +jetbrains-annotation = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotation" } + kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-core" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin-core" } kotlin-embeddable-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin-core" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5b95e2048..72c9cab1f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,15 +49,18 @@ include( ":sugar-core:node", ":sugar-core:visitor", ":sugar-core:codegen", + ":sugar-core:names", + ":sugar-core:error", + ":sugar-test", ":casa-ui", ":casa-annotation", ":casa-material", ":casa-processor", ":util", ":util-modifier", - ":util-backend", ":util-backend-ksp", ":util-backend-kotlinc", + ":util-backend-kotlinpoet", ":util-backend-test", ":util-compose-runtime-test", ":util-compose-snapshot-test", diff --git a/sugar-compiler/build.gradle.kts b/sugar-compiler/build.gradle.kts index ac65fe138..34d10284f 100644 --- a/sugar-compiler/build.gradle.kts +++ b/sugar-compiler/build.gradle.kts @@ -19,9 +19,12 @@ ksp { dependencies { compileOnly(libs.kotlin.embeddable.compiler) ksp(libs.google.autoservice.ksp.processor) - implementation(libs.google.autoservice.annotation) - testImplementations( - libs.test.kotlin.compilation.core, - projects.utilBackendTest, + implementations( + libs.google.autoservice.annotation, + projects.sugarCore.names.orArtifact(), + projects.sugarCore.error.orArtifact(), + projects.sugarCore.node.orArtifact(), + projects.sugarCore.visitor.orArtifact(), + projects.utilBackendKotlinc.orArtifact(), ) } diff --git a/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/SugarCompilerExtension.kt b/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/SugarCompilerExtension.kt new file mode 100644 index 000000000..9c2eafbf5 --- /dev/null +++ b/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/SugarCompilerExtension.kt @@ -0,0 +1,44 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.sugar.compiler + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import team.duckie.quackquack.sugar.compiler.ir.SugarIrTransformer +import team.duckie.quackquack.sugar.node.SugarComponentNode +import team.duckie.quackquack.sugar.visitor.SugarCoreVisitor +import team.duckie.quackquack.util.backend.kotlinc.Logger + +internal class SugarCompilerExtension(private val logger: Logger) : IrGenerationExtension { + override fun generate( + moduleFragment: IrModuleFragment, + pluginContext: IrPluginContext, + ) { + val nodes = mutableListOf() + val visitor = SugarCoreVisitor( + context = pluginContext, + logger = logger, + addSugarComponentNode = nodes::add, + ) + val transformer = SugarIrTransformer( + context = pluginContext, + logger = logger, + ) + + moduleFragment.accept(visitor, null) + moduleFragment.transform(transformer, nodes.asMap()) + } +} + +private fun List.asMap() = + buildMap(capacity = size) { + this@asMap.forEach { SugarComponentNode -> + set(SugarComponentNode.referFqn.asString(), SugarComponentNode) + } + } diff --git a/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/SugarCompilerRegistrar.kt b/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/SugarCompilerRegistrar.kt new file mode 100644 index 000000000..8f86947c3 --- /dev/null +++ b/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/SugarCompilerRegistrar.kt @@ -0,0 +1,64 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:Suppress("DEPRECATION", "unused") +@file:OptIn(ExperimentalCompilerApi::class) + +package team.duckie.quackquack.sugar.compiler + +import com.google.auto.service.AutoService +import org.jetbrains.annotations.TestOnly +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.extensions.LoadingOrder +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import team.duckie.quackquack.sugar.visitor.SugarCoreVisitor +import team.duckie.quackquack.util.backend.kotlinc.getLogger + +/** + * ### Deprecated된 메서드를 사용하는 이유 + * + * Compose Compiler의 [`Default Arguments Transform`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt;l=341-365)에 + * 의해 모든 컴포저블 함수에서 default argument의 값이 null로 변경됩니다. 하지만 sugar component를 + * 생성하기 위해선 default value의 값을 보존해야 합니다. 이를 위해 [SugarCoreVisitor]가 Compose Compiler + * 보다 먼저 적용될 수 있도록 Compiler Plugin의 적용 순서를 조정할 수 있는 deprecated된 [registerProjectComponents] + * 메서드를 사용합니다. deprecated 되지 않은 [CompilerPluginRegistrar]를 사용하면 Compiler Plugin의 적용 + * 순서를 조정할 수 없습니다. + */ +@AutoService(ComponentRegistrar::class) +class SugarCompilerRegistrar : ComponentRegistrar { + override val supportsK2 = false + + override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { + project.extensionArea + .getExtensionPoint(IrGenerationExtension.extensionPointName) + .registerExtension(configuration.getSugarIrExtension(), LoadingOrder.FIRST, project) + } + + companion object { + /** + * [ComponentRegistrar]의 complie test는 DeprecatedError 상태로 항상 테스트에 실패합니다. + * 이를 해결하기 위해 [SugarCompilerRegistrar]의 [CompilerPluginRegistrar] 버전을 제공합니다. + * 이 함수는 오직 테스트 코드에서만 사용돼야 합니다. (테스트 환경에서는 Compose Compiler가 + * 적용되지 않으니 유효합니다.) + */ + @TestOnly + fun asPluginRegistrar() = object : CompilerPluginRegistrar() { + override val supportsK2 = false + + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + IrGenerationExtension.registerExtension(configuration.getSugarIrExtension()) + } + } + + private fun CompilerConfiguration.getSugarIrExtension() = + SugarCompilerExtension(logger = getLogger("sugar-compiler")) + } +} diff --git a/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/ir/SugarIrTransformer.kt b/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/ir/SugarIrTransformer.kt new file mode 100644 index 000000000..3677fc62b --- /dev/null +++ b/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/ir/SugarIrTransformer.kt @@ -0,0 +1,123 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(UnsafeCastFunction::class) + +package team.duckie.quackquack.sugar.compiler.ir + +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.expressions.IrExpressionBody +import org.jetbrains.kotlin.ir.util.file +import org.jetbrains.kotlin.ir.util.getAnnotation +import org.jetbrains.kotlin.ir.util.hasAnnotation +import org.jetbrains.kotlin.ir.visitors.IrElementTransformer +import org.jetbrains.kotlin.utils.addToStdlib.UnsafeCastFunction +import org.jetbrains.kotlin.utils.addToStdlib.cast +import team.duckie.quackquack.sugar.error.PoetError +import team.duckie.quackquack.sugar.error.SugarTransformError +import team.duckie.quackquack.sugar.error.SugarVisitError +import team.duckie.quackquack.sugar.names.NoSugarFqn +import team.duckie.quackquack.sugar.names.SugarGeneratedFileFqn +import team.duckie.quackquack.sugar.names.SugarReferFqn +import team.duckie.quackquack.sugar.node.SugarComponentNode +import team.duckie.quackquack.util.backend.kotlinc.Logger +import team.duckie.quackquack.util.backend.kotlinc.isQuackComponent +import team.duckie.quackquack.util.backend.kotlinc.locationOf + +internal class SugarIrTransformer( + @Suppress("unused") private val context: IrPluginContext, + private val logger: Logger, +) : IrElementTransformer> { + override fun visitModuleFragment( + declaration: IrModuleFragment, + data: Map, + ): IrModuleFragment { + declaration.files.forEach { file -> file.accept(this, data) } + return declaration + } + + override fun visitFile( + declaration: IrFile, + data: Map, + ): IrFile { + if (declaration.hasAnnotation(SugarGeneratedFileFqn)) { + declaration.declarations.forEach { item -> item.accept(this, data) } + } + return declaration + } + + override fun visitSimpleFunction( + declaration: IrSimpleFunction, + data: Map, + ): IrStatement { + if (declaration.isQuackComponent) { + if (declaration.hasAnnotation(NoSugarFqn)) { + return super.visitSimpleFunction(declaration, data) + } + + val referAnnotation = + declaration.getAnnotation(SugarReferFqn) + ?: logger.throwError( + message = PoetError.sugarComponentButNoSugarRefer(declaration.name.asString()), + location = declaration.file.locationOf(declaration), + ) + val referFqn = referAnnotation.getReferFqName() + + data[referFqn]?.let { referIrData -> + declaration.valueParameters.forEach { parameter -> + parameter.defaultValue = referIrData.findMatchedDefaultValue( + sugarComponentName = declaration.name.asString(), + parameter = parameter, + error = { message -> + logger.throwError( + message = message, + location = declaration.file.locationOf(parameter), + ) + }, + ) + } + } ?: logger.throwError( + message = SugarVisitError.noMatchedSugarComponentNode(declaration.name.asString()), + location = declaration.file.locationOf(declaration), + ) + } + + return super.visitSimpleFunction(declaration, data) + } +} + +private fun IrConstructorCall.getReferFqName(): String { + // Assuming the first argument is always "fqn" + val referFqnExpression = getValueArgument(0) + return referFqnExpression.cast>().value +} + +private fun SugarComponentNode.findMatchedDefaultValue( + sugarComponentName: String, + parameter: IrValueParameter, + error: (message: String) -> Unit, +): IrExpressionBody? { + val matched = + parameters.find { referParameter -> + referParameter.name.asString() == parameter.name.asString() + } + if (matched == null) { + error( + SugarTransformError.sugarComponentAndSugarReferHasDifferentParameters( + "(refer) ${owner.name.asString()} -> (sugar) $sugarComponentName#${parameter.name.asString()}", + ), + ) + } + return matched?.defaultValue +} diff --git a/sugar-core/build.gradle.kts b/sugar-core/build.gradle.kts index 9d38e3628..0a67b386d 100644 --- a/sugar-core/build.gradle.kts +++ b/sugar-core/build.gradle.kts @@ -19,9 +19,11 @@ dependencies { compileOnly(libs.kotlin.embeddable.compiler) implementations( libs.google.autoservice.annotation, + libs.jetbrains.annotation, projects.sugarCore.node, projects.sugarCore.visitor, projects.sugarCore.codegen, + projects.utilBackendKotlinc, ) ksp(libs.google.autoservice.ksp.processor) } diff --git a/sugar-core/codegen/build.gradle.kts b/sugar-core/codegen/build.gradle.kts index 2ea44cc14..c22407141 100644 --- a/sugar-core/codegen/build.gradle.kts +++ b/sugar-core/codegen/build.gradle.kts @@ -7,7 +7,6 @@ plugins { quackquack("jvm-kotlin") - quackquack("test-kotest") } dependencies { @@ -15,5 +14,9 @@ dependencies { libs.kotlin.embeddable.compiler, libs.kotlin.kotlinpoet.core, projects.sugarCore.node, + projects.sugarCore.names, + projects.sugarCore.error, + projects.utilBackendKotlinc, + projects.utilBackendKotlinpoet, ) } diff --git a/sugar-core/codegen/src/main/kotlin/team/duckie/quackquack/sugar/codegen/codegen.kt b/sugar-core/codegen/src/main/kotlin/team/duckie/quackquack/sugar/codegen/codegen.kt new file mode 100644 index 000000000..5bb55e043 --- /dev/null +++ b/sugar-core/codegen/src/main/kotlin/team/duckie/quackquack/sugar/codegen/codegen.kt @@ -0,0 +1,152 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.sugar.codegen + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.buildCodeBlock +import com.squareup.kotlinpoet.withIndent +import java.io.File +import org.jetbrains.kotlin.ir.declarations.name +import org.jetbrains.kotlin.ir.types.classFqName +import org.jetbrains.kotlin.ir.util.file +import org.jetbrains.kotlin.name.FqName +import team.duckie.quackquack.sugar.names.CasaCn +import team.duckie.quackquack.sugar.names.ComposableCn +import team.duckie.quackquack.sugar.names.NonRestartableComposableCn +import team.duckie.quackquack.sugar.names.SugarCompilerApiCn +import team.duckie.quackquack.sugar.names.SugarFqn +import team.duckie.quackquack.sugar.names.SugarGeneratedFileCn +import team.duckie.quackquack.sugar.names.SugarGeneratorUsageCn +import team.duckie.quackquack.sugar.names.SugarReferCn +import team.duckie.quackquack.sugar.node.SugarComponentNode +import team.duckie.quackquack.sugar.node.SugarParameter +import team.duckie.quackquack.util.backend.kotlinc.addImports +import team.duckie.quackquack.util.backend.kotlinc.toFqnStringOrEmpty +import team.duckie.quackquack.util.backend.kotlinpoet.addAnnotations +import team.duckie.quackquack.util.backend.kotlinpoet.addFunctions +import team.duckie.quackquack.util.backend.kotlinpoet.getGeneratedFileComment + +private val GeneratedComment = getGeneratedFileComment("sugar-core") + +@Suppress("OPT_IN_CAN_ONLY_BE_USED_AS_ANNOTATION") +private val SugarCompilerOptInAnnotation = + AnnotationSpec + .builder(OptIn::class) + .addMember( + "%T::class, %T::class", + SugarCompilerApiCn, + SugarGeneratorUsageCn, + ) + .useSiteTarget(AnnotationSpec.UseSiteTarget.FILE) + .build() + +private val SugarGeneratedFileMarkerAnnotation = + AnnotationSpec + .builder(SugarGeneratedFileCn) + .useSiteTarget(AnnotationSpec.UseSiteTarget.FILE) + .build() + +fun generateSugarComponentFiles(sugarComponentNodes: List, sugarPath: String) { + val fileGroupedNodeDatas = sugarComponentNodes.groupBy { node -> node.owner.file.name } + + fileGroupedNodeDatas.forEach { (fileName, componentNode) -> + val (imports, funSpecs) = componentNode.toFunSpecsWithImports() + val ktSpec = + FileSpec + .builder( + packageName = sugarPath.bestGuessToKotlinPackageName(), + fileName = fileName.substringBeforeLast("."), + ) + .addFileComment(GeneratedComment) + .addAnnotations( + SugarCompilerOptInAnnotation, + SugarGeneratedFileMarkerAnnotation, + ) + .addImports(imports.toMutableList().apply { add(SugarFqn) }) + .addFunctions(funSpecs) + .build() + + File(sugarPath, fileName) + .also { file -> + if (!file.exists()) { + file.parentFile?.mkdirs() + file.createNewFile() + } + } + .writeText(ktSpec.toString()) + } +} + +private fun List.toFunSpecsWithImports(): Pair, List> { + val imports = mutableListOf() + val funSpecs = mutableListOf() + forEach { sugarIrData -> + imports += sugarIrData.referFqn + sugarIrData.tokenFqExpressions.forEach { tokenFqExpression -> + val (_imports, funSpec) = sugarIrData.toFunSpecWithImports(tokenFqExpression) + imports += _imports + funSpecs += funSpec + } + } + return imports to funSpecs +} + +private fun SugarComponentNode.toFunSpecWithImports(tokenFqExpression: String): Pair, FunSpec> { + val imports = mutableListOf() + + val sugarReferAnnotation = + AnnotationSpec + .builder(SugarReferCn) + .addMember("%S", referFqn.asString()) + .build() + + val sugarName = toSugarComponentName(tokenFqExpression) + val sugarBody = + buildCodeBlock { + addStatement("%L(", referFqn.shortName().asString()) + withIndent { + parameters.forEach { parameter -> + imports += parameter.type.classFqName!! + imports += parameter.imports + + val parameterName = parameter.name.asString() + val parameterValue = if (parameter.isToken) tokenFqExpression else parameterName + + addStatement("%L = %L,", parameterName, parameterValue) + } + } + addStatement(")") + } + + val optinCns = + optins.map { irOptin -> + ClassName.bestGuess(irOptin.toFqnStringOrEmpty()) + } + + val funSpec = + FunSpec + .builder(sugarName) + .addAnnotations( + CasaCn, + ComposableCn, + NonRestartableComposableCn, + *optinCns.toTypedArray(), + ) + .addAnnotation(sugarReferAnnotation) + .addModifiers(KModifier.PUBLIC) + .addParameters(parametersWithoutToken.map(SugarParameter::toParameterSpec)) + .addCode(sugarBody) + .addKdoc(kdocGetter(tokenFqExpression)) + .build() + + return imports to funSpec +} diff --git a/sugar-core/codegen/src/main/kotlin/team/duckie/quackquack/sugar/codegen/utils.kt b/sugar-core/codegen/src/main/kotlin/team/duckie/quackquack/sugar/codegen/utils.kt new file mode 100644 index 000000000..786c72a60 --- /dev/null +++ b/sugar-core/codegen/src/main/kotlin/team/duckie/quackquack/sugar/codegen/utils.kt @@ -0,0 +1,131 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.sugar.codegen + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.ParameterSpec +import org.jetbrains.kotlin.builtins.isFunctionOrSuspendFunctionType +import org.jetbrains.kotlin.ir.descriptors.toIrBasedKotlinType +import org.jetbrains.kotlin.ir.types.isMarkedNullable +import org.jetbrains.kotlin.ir.util.isFunction +import org.jetbrains.kotlin.ir.util.isSuspendFunction +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaAnnotation.Companion.addAnnotation +import org.jetbrains.kotlin.types.checker.SimpleClassicTypeSystemContext.getClassFqNameUnsafe +import org.jetbrains.kotlin.utils.addToStdlib.applyIf +import team.duckie.quackquack.sugar.error.NotSupportedError +import team.duckie.quackquack.sugar.names.CasaValueCn +import team.duckie.quackquack.sugar.names.ComposableCn +import team.duckie.quackquack.sugar.names.QuackComponentPrefix +import team.duckie.quackquack.sugar.names.SugarTokenName +import team.duckie.quackquack.sugar.node.SugarComponentNode +import team.duckie.quackquack.sugar.node.SugarParameter +import team.duckie.quackquack.util.backend.kotlinc.unsafeClassName + +internal fun String.bestGuessToKotlinPackageName(): String { + // make sure testable + // require(contains("src/main/kotlin")) { "The given package is not a Kotlin package." } + return substringAfterLast("src/main/kotlin/").replace("/", ".") +} + +// TODO: Testing +internal fun SugarComponentNode.toSugarComponentName(tokenFqExpression: String): String { + val tokenExpression = tokenFqExpression + .substringAfterLast(".") + .replaceFirstChar(Char::titlecase) + return sugarName?.replace(SugarTokenName, tokenExpression) + ?: referFqn + .shortName() + .asString() + .toMutableList() + .apply { addAll(QuackComponentPrefix.length, tokenExpression.toList()) } + .joinToString("") +} + +/** 제공된 정보를 [ParameterSpec]으로 변환합니다. */ +internal fun SugarParameter.toParameterSpec(): ParameterSpec { + val parameterTypedBuilder = + ParameterSpec + .builder( + name = name.asString(), + type = when { + type.isFunction() || type.isSuspendFunction() -> { + val funArguments = type.toIrBasedKotlinType().arguments + + /* + * maintainer notes: 모든 **함수형 타입**은 `Function`으로 처리되며 이는 `IrFunction`과는 + * 다른 유형임. 코틀린의 **함수 정의**는 `IrFunction`으로 해석되고, **함수형 타입**은 코틀린 + * 네이티브 타입인 `KotlinType`으로 해석됨. + * + * `Function`은 value parameter의 타입과 return의 타입을 generic으로 받는 인터페이스임. + * 즉, `Function`은 메타데이터가 없어서 컴파일 시점에서는 인자의 타입만 조회 가능함. + * 따라서 람다의 인자명 정책으로 `P{$index}`를 사용함. + * + * receiver extension은 **첫 번째** value parameter로 치환됨. + * > `String.() -> Unit` == `(String) -> Unit` + * + * return type은 **마지막** value parameter로 치환됨. + */ + val referLambdaParameters = + if (funArguments.size >= 2) { + funArguments.dropLast(1).mapIndexed { index, argument -> + require(!argument.type.isFunctionOrSuspendFunctionType) { + NotSupportedError.nestedFunctionalType("${owner.name.asString()}#${name.asString()}") + } + + val argumentTypeFqn = argument.type.constructor.getClassFqNameUnsafe() + val argumentTypeCn = ClassName.bestGuess(argumentTypeFqn.toString()) + + ParameterSpec + .builder( + name = "P$index", + type = argumentTypeCn, + ) + .build() + } + } else { + emptyList() + } + val referLambdaReturnTypeFqn = funArguments.last().type.constructor.getClassFqNameUnsafe() + val referLambdaReturnTypeCn = ClassName.bestGuess(referLambdaReturnTypeFqn.toString()) + + LambdaTypeName + .get( + parameters = referLambdaParameters, + returnType = referLambdaReturnTypeCn, + ) + .copy(suspending = type.isSuspendFunction()) + } + else -> { + type.unsafeClassName + } + }.copy(nullable = type.isMarkedNullable()), + ) + + return parameterTypedBuilder + .applyIf(casaValueLiteral != null) { + addAnnotation( + AnnotationSpec + .builder(CasaValueCn) + .addMember("%S", casaValueLiteral!!) + .build(), + ) + } + .applyIf(isComposable) { + addAnnotation( + AnnotationSpec + .builder(ComposableCn) + .build(), + ) + } + .applyIf(defaultValue != null) { + defaultValue("%L()", "sugar") + } + .build() +} diff --git a/sugar-core/error/build.gradle.kts b/sugar-core/error/build.gradle.kts new file mode 100644 index 000000000..0d6ac202f --- /dev/null +++ b/sugar-core/error/build.gradle.kts @@ -0,0 +1,15 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +plugins { + quackquack("jvm-kotlin") + quackquack("quack-publishing") +} + +dependencies { + implementation(projects.sugarCore.names.orArtifact()) +} diff --git a/sugar-core/error/src/main/kotlin/team/duckie/quackquack/sugar/error/SugarErrors.kt b/sugar-core/error/src/main/kotlin/team/duckie/quackquack/sugar/error/SugarErrors.kt new file mode 100644 index 000000000..90052a834 --- /dev/null +++ b/sugar-core/error/src/main/kotlin/team/duckie/quackquack/sugar/error/SugarErrors.kt @@ -0,0 +1,73 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:Suppress("unused") + +package team.duckie.quackquack.sugar.error + +import team.duckie.quackquack.sugar.names.QuackComponentPrefix +import team.duckie.quackquack.sugar.names.SugarTokenName + +object NotSupportedError { + fun nestedFunctionalType(name: String?) = + "Nested functional types are not currently supported due to implementation complexity." + + " ($name)".getIfGivenIsNotNull(name) +} + +object SourceError { + fun quackComponentFqnUnavailable(name: String?) = + "A Quack component was detected, but unable to look up a fully qualified name. " + + "Is it an anonymous object?" + " ($name)".getIfGivenIsNotNull(name) + + fun importClazzFqnUnavailable(name: String?) = + "Can't look up the fully qualified name of the class given as `clazz` in `@Imports`. " + + "Is it an anonymous class?" + " ($name)".getIfGivenIsNotNull(name) + + fun quackComponentWithoutSugarToken(name: String?) = + "A Quack component was detected, but no SugarToken was applied." + + " ($name)".getIfGivenIsNotNull(name) + + fun multipleSugarTokenIsNotAllowed(name: String?) = + "A Sugar component can only contain one SugarToken." + + " ($name)".getIfGivenIsNotNull(name) + + fun sugarNamePrefixIsNotQuack(name: String?) = + "Quack component names must start with `SugarName.PREFIX_NAME" + + " (= $QuackComponentPrefix)`." + " ($name)".getIfGivenIsNotNull(name) + + fun sugarNameWithoutTokenName(name: String?) = + "When specifying the sugar component name directly, " + + "`SugarName.TOKEN_NAME (= $SugarTokenName)` must be used." + + " ($name)".getIfGivenIsNotNull(name) + + fun sugarTokenButNoCompanionObject(name: String?) = + "The SugarToken class must include a companion object. " + + "See the sugar component creation policy for more information." + + " ($name)".getIfGivenIsNotNull(name) +} + +object PoetError { + fun sugarComponentButNoSugarRefer(name: String?) = + "The SugarRefer for the Sugar component is missing." + + " ($name)".getIfGivenIsNotNull(name) +} + +object SugarVisitError { + fun noMatchedSugarComponentNode(name: String?) = + "No SugarComponentNode was found for the given SugarRefer. " + + "Please report it in a GitHub Issue. (https://link.duckie.team/quackquack-bug)" + + " ($name)".getIfGivenIsNotNull(name) +} + +object SugarTransformError { + fun sugarComponentAndSugarReferHasDifferentParameters(name: String?) = + "The Sugar component has a parameter that doesn't exist in the SugarRefer." + + " ($name)".getIfGivenIsNotNull(name) +} + +private fun String.getIfGivenIsNotNull(given: Any?) = + if (given == null) "" else this diff --git a/sugar-core/error/version.txt b/sugar-core/error/version.txt new file mode 100644 index 000000000..311be597a --- /dev/null +++ b/sugar-core/error/version.txt @@ -0,0 +1 @@ +2.0.0-alpha01 diff --git a/sugar-core/names/build.gradle.kts b/sugar-core/names/build.gradle.kts new file mode 100644 index 000000000..daec655c1 --- /dev/null +++ b/sugar-core/names/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +plugins { + quackquack("jvm-kotlin") + quackquack("quack-publishing") +} + +dependencies { + implementations( + libs.kotlin.embeddable.compiler, + libs.kotlin.kotlinpoet.core, + projects.casaAnnotation.orArtifact(), + projects.sugarMaterial.orArtifact(), + ) +} diff --git a/sugar-core/names/src/main/kotlin/team/duckie/quackquack/sugar/names/SugarNames.kt b/sugar-core/names/src/main/kotlin/team/duckie/quackquack/sugar/names/SugarNames.kt new file mode 100644 index 000000000..17d74d40c --- /dev/null +++ b/sugar-core/names/src/main/kotlin/team/duckie/quackquack/sugar/names/SugarNames.kt @@ -0,0 +1,62 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) +@file:Suppress( + "OPT_IN_CAN_ONLY_BE_USED_AS_ANNOTATION", + "OPT_IN_MARKER_CAN_ONLY_BE_USED_AS_ANNOTATION_OR_ARGUMENT_IN_OPT_IN", + "unused", +) + +package team.duckie.quackquack.sugar.names + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.asClassName +import org.jetbrains.kotlin.name.FqName +import team.duckie.quackquack.casa.annotation.Casa +import team.duckie.quackquack.casa.annotation.CasaValue +import team.duckie.quackquack.casa.annotation.SugarGeneratorUsage +import team.duckie.quackquack.sugar.material.Imports +import team.duckie.quackquack.sugar.material.NoSugar +import team.duckie.quackquack.sugar.material.SugarCompilerApi +import team.duckie.quackquack.sugar.material.SugarGeneratedFile +import team.duckie.quackquack.sugar.material.SugarName +import team.duckie.quackquack.sugar.material.SugarRefer +import team.duckie.quackquack.sugar.material.SugarToken + +val RequiresOptInFqn = RequiresOptIn::class.qualifiedName!!.toFqnClass() + +val ComposableFqn = "androidx.compose.runtime.Composable".toFqnClass() +val ComposableCn = ComposableFqn.asString().toCnClass() +val NonRestartableComposableCn = "androidx.compose.runtime.NonRestartableComposable".toCnClass() + +const val QuackComponentPrefix = SugarName.PREFIX_NAME + +const val SugarDefaultName = SugarName.DEFAULT_NAME +const val SugarTokenName = SugarName.TOKEN_NAME + +val CasaCn = Casa::class.qualifiedName!!.toCnClass() +val CasaValueCn = CasaValue::class.asClassName() +val CasaValueFqn = CasaValue::class.qualifiedName!!.toFqnClass() + +val SugarCompilerApiCn = SugarCompilerApi::class.asClassName() +val SugarGeneratorUsageCn = SugarGeneratorUsage::class.asClassName() +val SugarGeneratedFileCn = SugarGeneratedFile::class.asClassName() +val SugarGeneratedFileFqn = SugarGeneratedFile::class.qualifiedName!!.toFqnClass() + +// sugar 함수에 리플렉션으로 접근 불가 +val SugarFqn = "team.duckie.quackquack.sugar.material.sugar".toFqnClass() +val SugarNameFqn = SugarName::class.qualifiedName!!.toFqnClass() +val SugarTokenFqn = SugarToken::class.qualifiedName!!.toFqnClass() +val SugarReferCn = SugarRefer::class.asClassName() +val SugarReferFqn = SugarRefer::class.qualifiedName!!.toFqnClass() + +val ImportsFqn = Imports::class.qualifiedName!!.toFqnClass() +val NoSugarFqn = NoSugar::class.qualifiedName!!.toFqnClass() + +private fun String.toFqnClass() = FqName(this) +private fun String.toCnClass() = ClassName.bestGuess(this) diff --git a/sugar-core/names/version.txt b/sugar-core/names/version.txt new file mode 100644 index 000000000..311be597a --- /dev/null +++ b/sugar-core/names/version.txt @@ -0,0 +1 @@ +2.0.0-alpha01 diff --git a/sugar-core/node/build.gradle.kts b/sugar-core/node/build.gradle.kts index 537dc7f92..c73be038e 100644 --- a/sugar-core/node/build.gradle.kts +++ b/sugar-core/node/build.gradle.kts @@ -7,11 +7,16 @@ plugins { quackquack("jvm-kotlin") + quackquack("quack-publishing") } dependencies { implementations( libs.kotlin.embeddable.compiler, - libs.kotlin.kotlinpoet.core, + projects.sugarCore.names.orArtifact(), + projects.sugarCore.error.orArtifact(), + projects.sugarMaterial.orArtifact(), + projects.casaAnnotation.orArtifact(), + projects.utilBackendKotlinc.orArtifact(), ) } diff --git a/sugar-core/node/src/main/kotlin/team/duckie/quackquack/sugar/node/SugarComponentNode.kt b/sugar-core/node/src/main/kotlin/team/duckie/quackquack/sugar/node/SugarComponentNode.kt new file mode 100644 index 000000000..c772adfff --- /dev/null +++ b/sugar-core/node/src/main/kotlin/team/duckie/quackquack/sugar/node/SugarComponentNode.kt @@ -0,0 +1,78 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:Suppress("unused") + +package team.duckie.quackquack.sugar.node + +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.name.FqName +import team.duckie.quackquack.sugar.material.SugarName +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.util.backend.kotlinc.toFqnStringOrEmpty + +/** + * 소스 파일의 IR을 방문하면서 수집할 정보들을 관리합니다. + * + * @param owner IR이 제공된 함수 + * @param referFqn IR이 제공된 함수의 [fully-qualified name][FqName]. + * [owner]에서 직접 가져오는 방식보다 안전한 방식으로 fqn이 제공됩니다. + * @param kdocGetter IR이 제공된 함수의 Sugared-KDoc을 계산하는 람다. + * 사용된 Sugar Token의 리터럴을 람다 인자로 제공해야 합니다. + * @param sugarName 생성할 sugar component의 네이밍 규칙. + * [`@SugarToken`][SugarName] 값을 가져옵니다. + * @param sugarToken 생성할 sugar component의 Sugar Token에 해당하는 [인자][IrValueParameter]. + * [`@SugarToken`][SugarToken]이 달린 인자를 가져옵니다. + * @param tokenFqExpressions Sugar Token의 expression 모음. 예를 들면 다음과 같습니다. + * + * ``` + * package team.duckie.theme + * + * @JvmInline + * value class Theme(val index: Int) { + * companion object { + * val Default = Theme(1) + * val Dark = Theme(2) + * val Light = Theme(3) + * val System = Theme(4) + * } + * } + * + * // ["team.duckie.theme.Theme.Default", "team.duckie.theme.Theme.Dark", "team.duckie.theme.Theme.Light", "team.duckie.theme.Theme.System"] + * ``` + * + * @param parameters IR이 제공된 함수의 인자 모음. sugar component 생성에 필요한 정보만 수집합니다. + * 자세한 수집 정보는 [SugarParameter]를 확인하세요. + */ +data class SugarComponentNode( + val owner: IrFunction, + val referFqn: FqName, + val kdocGetter: (usedTokenLiteral: String) -> String, + val sugarName: String?, + val sugarToken: IrValueParameter, + val tokenFqExpressions: List, + val parameters: List, + val optins: List, +) { + /** [parameters]에서 Sugar Token을 제외한 [요소][SugarParameter]만 불러옵니다. */ + val parametersWithoutToken: List = + parameters.toMutableList().apply { removeIf(SugarParameter::isToken) } + + override fun toString() = + """ + owner: ${owner.name.asString()} + referFqn: ${referFqn.asString()} + kdoc: ${kdocGetter("SugarToken")} + sugarName: $sugarName + sugarToken: ${sugarToken.name.asString()} + tokenExpressions: $tokenFqExpressions + parameters: ${parameters.joinToString("\n\n", prefix = "\n")} + optins: ${optins.joinToString(transform = IrConstructorCall::toFqnStringOrEmpty)} + """.trimIndent() +} diff --git a/sugar-core/node/src/main/kotlin/team/duckie/quackquack/sugar/node/SugarParameter.kt b/sugar-core/node/src/main/kotlin/team/duckie/quackquack/sugar/node/SugarParameter.kt new file mode 100644 index 000000000..553919134 --- /dev/null +++ b/sugar-core/node/src/main/kotlin/team/duckie/quackquack/sugar/node/SugarParameter.kt @@ -0,0 +1,58 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.sugar.node + +import org.jetbrains.kotlin.ir.backend.js.utils.asString +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.expressions.IrExpressionBody +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.util.dumpKotlinLike +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import team.duckie.quackquack.casa.annotation.CasaValue +import team.duckie.quackquack.sugar.material.Imports +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.names.ComposableFqn + +/** + * [IrValueParameter]에서 sugar component 생성에 필요한 정보를 관리합니다. + * + * @param name 인자의 이름 + * @param type 인자의 타입 + * @param isToken 인자가 [Sugar Token][SugarToken]인지 여부 + * @param isComposable 인자에 [`@Composable`][ComposableFqn] 어노테이션이 달려있는지 + * 여부 + * @param imports [type] 외에 추가로 import가 필요한 클래스의 [fully-qualified name][FqName]으로 + * 구성된 목록. 자세한 정보는 [`@Imports`][Imports] 어노테이션을 확인하세요. + * @param casaValueLiteral 만약 인자에 [`@CasaValue`][CasaValue] 어노테이션이 달려있다면 + * [CasaValue.literal]로 제공된 값 + * @param defaultValue 인자의 기본 값 + */ +data class SugarParameter( + val owner: IrFunction, + val name: Name, + val type: IrType, + val isToken: Boolean, + val isComposable: Boolean, + val imports: List, + val casaValueLiteral: String?, + val defaultValue: IrExpressionBody?, +) { + override fun toString() = + """ + owner: ${owner.name.asString()} + name: ${name.asString()} + type: ${type.asString()} + isToken: $isToken + isComposable: $isComposable + imports: ${imports.joinToString(transform = FqName::asString)} + casaValueLiteral: $casaValueLiteral + defaultValue: ${defaultValue?.dumpKotlinLike()} + """.trimIndent() +} diff --git a/sugar-core/node/version.txt b/sugar-core/node/version.txt new file mode 100644 index 000000000..311be597a --- /dev/null +++ b/sugar-core/node/version.txt @@ -0,0 +1 @@ +2.0.0-alpha01 diff --git a/sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreCommandLineProcessor.kt b/sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreCommandLineProcessor.kt new file mode 100644 index 000000000..9e598aed7 --- /dev/null +++ b/sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreCommandLineProcessor.kt @@ -0,0 +1,54 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(ExperimentalCompilerApi::class) +@file:Suppress("unused") + +package team.duckie.quackquack.sugar.core + +import com.google.auto.service.AutoService +import org.jetbrains.annotations.VisibleForTesting +import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption +import org.jetbrains.kotlin.compiler.plugin.CliOption +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.CompilerConfigurationKey + +@VisibleForTesting +const val PluginId = "team.duckie.quackquack.sugar.core" + +internal val KEY_SUGAR_PATH = + CompilerConfigurationKey("Where the sugar components will be created - required") + +@VisibleForTesting +val OPTION_SUGAR_PATH = + CliOption( + optionName = "sugarPath", + valueDescription = "String", + description = KEY_SUGAR_PATH.toString(), + required = true, + allowMultipleOccurrences = false, + ) + +@AutoService(CommandLineProcessor::class) +class SugarCoreCommandLineProcessor : CommandLineProcessor { + override val pluginId = PluginId + + override val pluginOptions = listOf(OPTION_SUGAR_PATH) + + override fun processOption( + option: AbstractCliOption, + value: String, + configuration: CompilerConfiguration, + ) { + when (val optionName = option.optionName) { + OPTION_SUGAR_PATH.optionName -> configuration.put(KEY_SUGAR_PATH, value) + else -> error("Unknown plugin option: $optionName") + } + } +} diff --git a/sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreExtension.kt b/sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreExtension.kt new file mode 100644 index 000000000..e9cfafb6b --- /dev/null +++ b/sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreExtension.kt @@ -0,0 +1,39 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.sugar.core + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import team.duckie.quackquack.sugar.codegen.generateSugarComponentFiles +import team.duckie.quackquack.sugar.node.SugarComponentNode +import team.duckie.quackquack.sugar.visitor.SugarCoreVisitor +import team.duckie.quackquack.util.backend.kotlinc.Logger + +internal class SugarCoreExtension( + private val logger: Logger, + private val sugarPath: String, +) : IrGenerationExtension { + override fun generate( + moduleFragment: IrModuleFragment, + pluginContext: IrPluginContext, + ) { + val nodes = mutableListOf() + val visitor = SugarCoreVisitor( + context = pluginContext, + logger = logger, + addSugarComponentNode = nodes::add, + ) + + moduleFragment.accept(visitor, null) + generateSugarComponentFiles( + sugarComponentNodes = nodes, + sugarPath = sugarPath, + ) + } +} diff --git a/sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreRegistrar.kt b/sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreRegistrar.kt new file mode 100644 index 000000000..0d548bf7f --- /dev/null +++ b/sugar-core/src/main/kotlin/team/duckie/quackquack/sugar/core/SugarCoreRegistrar.kt @@ -0,0 +1,70 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:Suppress("DEPRECATION", "unused", "UnstableApiUsage") +@file:OptIn(ExperimentalCompilerApi::class) + +package team.duckie.quackquack.sugar.core + +import com.google.auto.service.AutoService +import org.jetbrains.annotations.TestOnly +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.extensions.LoadingOrder +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import team.duckie.quackquack.sugar.visitor.SugarCoreVisitor +import team.duckie.quackquack.util.backend.kotlinc.getLogger + +/** + * ### Deprecated된 메서드를 사용하는 이유 + * + * Compose Compiler의 [`Default Arguments Transform`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt;l=341-365)에 + * 의해 모든 컴포저블 함수에서 default argument의 값이 null로 변경됩니다. 하지만 sugar component를 + * 생성하기 위해선 default value의 값을 보존해야 합니다. 이를 위해 [SugarCoreVisitor]가 Compose Compiler + * 보다 먼저 적용될 수 있도록 Compiler Plugin의 적용 순서를 조정할 수 있는 deprecated된 [registerProjectComponents] + * 메서드를 사용합니다. deprecated 되지 않은 [CompilerPluginRegistrar]를 사용하면 Compiler Plugin의 적용 + * 순서를 조정할 수 없습니다. + */ +@AutoService(ComponentRegistrar::class) +class SugarCoreRegistrar : ComponentRegistrar { + override val supportsK2 = false + + override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { + project.extensionArea + .getExtensionPoint(IrGenerationExtension.extensionPointName) + .registerExtension(configuration.getSugarIrExtension(), LoadingOrder.FIRST, project) + } + + companion object { + /** + * [ComponentRegistrar]의 complie test는 DeprecatedError 상태로 항상 테스트에 실패합니다. + * 이를 해결하기 위해 [SugarCoreRegistrar]의 [CompilerPluginRegistrar] 버전을 제공합니다. + * 이 함수는 오직 테스트 코드에서만 사용돼야 합니다. (테스트 환경에서는 Compose Compiler가 + * 적용되지 않으니 유효합니다.) + */ + @TestOnly + fun asPluginRegistrar() = object : CompilerPluginRegistrar() { + override val supportsK2 = false + + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + IrGenerationExtension.registerExtension(configuration.getSugarIrExtension()) + } + } + + private fun CompilerConfiguration.getSugarIrExtension(): SugarCoreExtension { + val sugarPath = requireNotNull(this[KEY_SUGAR_PATH]) { "sugarPath was missing." } + + return SugarCoreExtension( + logger = getLogger("sugar-core"), + sugarPath = sugarPath, + ) + } + } +} diff --git a/sugar-core/visitor/build.gradle.kts b/sugar-core/visitor/build.gradle.kts index 27fb65d61..9139ef276 100644 --- a/sugar-core/visitor/build.gradle.kts +++ b/sugar-core/visitor/build.gradle.kts @@ -7,17 +7,16 @@ plugins { quackquack("jvm-kotlin") - quackquack("test-kotest") + quackquack("quack-publishing") } dependencies { implementations( libs.kotlin.embeddable.compiler, - projects.sugarMaterial, - projects.sugarCore.node, - ) - testImplementations( - libs.test.kotlin.compilation.core, - projects.utilBackendTest, + projects.sugarMaterial.orArtifact(), + projects.sugarCore.error.orArtifact(), + projects.sugarCore.names.orArtifact(), + projects.sugarCore.node.orArtifact(), + projects.utilBackendKotlinc.orArtifact(), ) } diff --git a/sugar-core/visitor/version.txt b/sugar-core/visitor/version.txt new file mode 100644 index 000000000..311be597a --- /dev/null +++ b/sugar-core/visitor/version.txt @@ -0,0 +1 @@ +2.0.0-alpha01 diff --git a/sugar-test/build.gradle.kts b/sugar-test/build.gradle.kts new file mode 100644 index 000000000..fe9c598a1 --- /dev/null +++ b/sugar-test/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +plugins { + quackquack("jvm-kotlin") + quackquack("test-kotest") +} + +dependencies { + testImplementations( + libs.test.kotlin.compilation.core, + projects.sugarCompiler, + projects.sugarCore, + projects.sugarCore.error, + projects.sugarCore.visitor, + projects.sugarCore.codegen, + projects.utilBackendTest, + ) +} diff --git a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/CompilerTestCompilation.kt b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/CompilerTestCompilation.kt new file mode 100644 index 000000000..0078326e0 --- /dev/null +++ b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/CompilerTestCompilation.kt @@ -0,0 +1,44 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(ExperimentalCompilerApi::class) +@file:Suppress("MemberVisibilityCanBePrivate") + +package team.duckie.quackquack.sugar.test + +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import java.io.File +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.JvmTarget +import team.duckie.quackquack.sugar.compiler.SugarCompilerRegistrar +import team.duckie.quackquack.util.backend.test.findGeneratedFileOrNull + +class CompilerTestCompilation(private val tempDir: File) { + private var defaultPrepareSetting: (KotlinCompilation.(tempDir: File) -> Unit)? = null + + fun compile(vararg sourceFiles: SourceFile) = + prepare(*sourceFiles).compile() + + fun prepare(vararg sourceFiles: SourceFile) = + KotlinCompilation().apply { + workingDir = tempDir + sources = sourceFiles.asList() + stubs + jvmTarget = JvmTarget.JVM_17.toString() + inheritClassPath = true + supportsK2 = false + useK2 = false + compilerPluginRegistrars = listOf(SugarCompilerRegistrar.asPluginRegistrar()) + defaultPrepareSetting?.invoke(this, tempDir) + } + + fun defaultPrepareSetting(block: KotlinCompilation.(tempDir: File) -> Unit) { + defaultPrepareSetting = block + } + + fun findGeneratedFileOrNull(fileName: String) = tempDir.findGeneratedFileOrNull(fileName) +} diff --git a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerErrorTest.kt b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerErrorTest.kt new file mode 100644 index 000000000..36611d024 --- /dev/null +++ b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerErrorTest.kt @@ -0,0 +1,239 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.sugar.test + +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile.Companion.kotlin +import io.kotest.core.spec.style.ExpectSpec +import io.kotest.core.test.Enabled +import io.kotest.engine.spec.tempdir +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import team.duckie.quackquack.sugar.error.NotSupportedError.nestedFunctionalType +import team.duckie.quackquack.sugar.error.PoetError.sugarComponentButNoSugarRefer +import team.duckie.quackquack.sugar.error.SourceError.multipleSugarTokenIsNotAllowed +import team.duckie.quackquack.sugar.error.SourceError.quackComponentWithoutSugarToken +import team.duckie.quackquack.sugar.error.SourceError.sugarNamePrefixIsNotQuack +import team.duckie.quackquack.sugar.error.SourceError.sugarNameWithoutTokenName +import team.duckie.quackquack.sugar.error.SourceError.sugarTokenButNoCompanionObject +import team.duckie.quackquack.sugar.error.SugarTransformError.sugarComponentAndSugarReferHasDifferentParameters + +class SugarCompilerErrorTest : ExpectSpec() { + private val testCompilation = CompilerTestCompilation(tempdir()) + + init { + context("NotSupportedError") { + expect("nestedFunctionalType").config( + enabledOrReasonIf = { + Enabled.disabled( + "테스트 코드는 실패하는데 실제 코드로 돌려보면 정상 작동함.." + + "추후 테스트 코드가 실패하는 원인을 찾아야 함.", + ) + }, + ) { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +import team.duckie.quackquack.sugar.material.SugarToken +import androidx.compose.runtime.Composable + +@Composable +fun QuackText( + @SugarToken style: AwesomeType, + lambda: (unit: Unit, unit2: Unit, unit3: () -> Unit) -> Unit, +) {} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR + result.messages shouldContain nestedFunctionalType("QuackText#lambda") + } + } + + context("SourceError") { + expect("quackComponentWithoutSugarToken") { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +import androidx.compose.runtime.Composable + +@Composable +fun QuackText() {} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR + result.messages shouldContain quackComponentWithoutSugarToken("QuackText") + } + + expect("quackComponentWithoutSugarToken - @NoSugar applied") { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +import team.duckie.quackquack.sugar.material.NoSugar +import androidx.compose.runtime.Composable + +@NoSugar +@Composable +fun QuackText() {} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.OK + } + + expect("multipleSugarTokenIsNotAllowed") { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +import team.duckie.quackquack.sugar.material.SugarToken +import androidx.compose.runtime.Composable + +@Composable +fun QuackText( + @SugarToken style: AwesomeType, + @SugarToken style2: AwesomeType2, +) {} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR + result.messages shouldContain multipleSugarTokenIsNotAllowed("QuackText") + } + + expect("sugarNamePrefixIsNotQuack") { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarName +import team.duckie.quackquack.sugar.material.SugarToken + +@SugarName("Text") +@Composable +fun QuackText(@SugarToken type: AwesomeType) {} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR + result.messages shouldContain sugarNamePrefixIsNotQuack("QuackText (Text)") + } + + expect("sugarNameWithoutTokenName") { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarName +import team.duckie.quackquack.sugar.material.SugarToken + +@SugarName("QuackText") +@Composable +fun QuackText(@SugarToken type: AwesomeType) {} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR + result.messages shouldContain sugarNameWithoutTokenName("QuackText (QuackText)") + } + + expect("sugarTokenButNoCompanionObject") { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarToken + +@Composable +fun QuackText(@SugarToken type: AwesomeType3) {} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR + result.messages shouldContain sugarTokenButNoCompanionObject("AwesomeType3") + } + } + + context("PoetError") { + expect("sugarComponentButNoSugarRefer") { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +@file:OptIn(SugarCompilerApi::class) +@file:SugarGeneratedFile + +import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarCompilerApi +import team.duckie.quackquack.sugar.material.SugarGeneratedFile + +@Composable +fun QuackOneText() {} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR + result.messages shouldContain sugarComponentButNoSugarRefer("QuackOneText") + } + } + + context("SugarTransformError") { + expect("sugarComponentAndSugarReferHasDifferentParameters") { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +import team.duckie.quackquack.sugar.material.SugarToken +import androidx.compose.runtime.Composable + +@Composable +fun QuackText(@SugarToken style: AwesomeType) {} + """, + ), + kotlin( + "main-generated.kt", + """ +@file:OptIn(SugarCompilerApi::class) +@file:SugarGeneratedFile + +import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarCompilerApi +import team.duckie.quackquack.sugar.material.SugarGeneratedFile +import team.duckie.quackquack.sugar.material.SugarRefer +import team.duckie.quackquack.sugar.material.sugar + +@Composable +@SugarRefer("QuackText") +fun QuackOneText(newNumber: Int = sugar()) {} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR + result.messages shouldContain sugarComponentAndSugarReferHasDifferentParameters( + "(refer) QuackText -> (sugar) QuackOneText#newNumber", + ) + } + } + } +} diff --git a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerTransformTest.kt b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerTransformTest.kt new file mode 100644 index 000000000..72ce6bdf1 --- /dev/null +++ b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerTransformTest.kt @@ -0,0 +1,81 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.sugar.test + +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile.Companion.kotlin +import io.kotest.core.spec.style.StringSpec +import io.kotest.engine.spec.tempdir +import io.kotest.matchers.shouldBe +import org.jetbrains.kotlin.utils.addToStdlib.cast + +class SugarCompilerTransformTest : StringSpec() { + private val testCompilation = CompilerTestCompilation(tempdir()) + + init { + "Default Argument에 SugarIrTransform이 정상 작동함" { + val result = testCompilation.compile( + kotlin( + "main.kt", + """ +import team.duckie.quackquack.sugar.material.SugarToken +import androidx.compose.runtime.Composable + +var number = 0 + +@Composable +fun QuackText( + @SugarToken style: AwesomeType, + newNumber: Int = Int.MAX_VALUE, +) { + number = newNumber +} + """, + ), + kotlin( + "text-sugar.kt", + """ +@file:OptIn(SugarCompilerApi::class) +@file:SugarGeneratedFile + +import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarCompilerApi +import team.duckie.quackquack.sugar.material.SugarGeneratedFile +import team.duckie.quackquack.sugar.material.SugarRefer +import team.duckie.quackquack.sugar.material.sugar + +@Composable +@SugarRefer("QuackText") +fun QuackOneText(newNumber: Int = sugar()) { + QuackText( + style = AwesomeType.One, + newNumber = newNumber, + ) +} + """, + ), + ) + + result.exitCode shouldBe KotlinCompilation.ExitCode.OK + + val sugarClass = result.classLoader.loadClass("Text_sugarKt") + val quackTextMethod = sugarClass.getMethod( + "QuackOneText\$default", + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + java.lang.Object::class.java, + ) + quackTextMethod.invoke(sugarClass, 0, 1, null) + + val mainClass = result.classLoader.loadClass("MainKt") + val getNumberMethod = mainClass.getMethod("getNumber") + + getNumberMethod.invoke(mainClass).cast() shouldBe Int.MAX_VALUE + } + } +} diff --git a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/stubs.kt b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/stubs.kt new file mode 100644 index 000000000..fd5502317 --- /dev/null +++ b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/stubs.kt @@ -0,0 +1,42 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.sugar.test + +import com.tschuchort.compiletesting.SourceFile.Companion.kotlin +import team.duckie.quackquack.util.backend.test.stub.ComposeStub +import team.duckie.quackquack.util.backend.test.stub.SugarStub + +val stubs = listOf( + kotlin("Modifier.kt", ComposeStub.Modifier), + kotlin("Composable.kt", ComposeStub.Composable), + kotlin("annotations.kt", SugarStub.Annotations), + kotlin("typer.kt", SugarStub.Typer), + kotlin( + "AwesomeTypes.kt", + """ +@JvmInline +value class AwesomeType(val index: Int) { + companion object { + val One = AwesomeType(1) + } +} + +@JvmInline +value class AwesomeType2(val index: Int) { + companion object { + val One = AwesomeType2(1) + val Two = AwesomeType2(2) + val Three = AwesomeType2(3) + } +} + +@JvmInline +value class AwesomeType3(val index: Int) + """, + ), +) diff --git a/ui-sugar/.nospotless b/ui-sugar/.nospotless new file mode 100644 index 000000000..e69de29bb diff --git a/util-backend-kotlinc/build.gradle.kts b/util-backend-kotlinc/build.gradle.kts index d66436d21..8edabcfcb 100644 --- a/util-backend-kotlinc/build.gradle.kts +++ b/util-backend-kotlinc/build.gradle.kts @@ -14,7 +14,6 @@ plugins { } dependencies { - api(projects.utilBackend.orArtifact()) implementations( libs.kotlin.kotlinpoet.core, libs.kotlin.embeddable.compiler, diff --git a/util-backend-kotlinc/src/main/kotlin/team/duckie/quackquack/util/backend/kotlinc/IrUtils.kt b/util-backend-kotlinc/src/main/kotlin/team/duckie/quackquack/util/backend/kotlinc/IrUtils.kt index a70142412..33889feb6 100644 --- a/util-backend-kotlinc/src/main/kotlin/team/duckie/quackquack/util/backend/kotlinc/IrUtils.kt +++ b/util-backend-kotlinc/src/main/kotlin/team/duckie/quackquack/util/backend/kotlinc/IrUtils.kt @@ -13,11 +13,14 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.declarations.IrFile import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.classFqName import org.jetbrains.kotlin.ir.types.isUnit import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET +import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable import org.jetbrains.kotlin.ir.util.hasAnnotation +import org.jetbrains.kotlin.ir.util.parentAsClass /** * 주어진 [함수][IrSimpleFunction]가 꽥꽥 컴포넌트인지 조회합니다. @@ -57,3 +60,7 @@ public fun IrFile.locationOf(irElement: IrElement?): CompilerMessageSourceLocati lineContent = null, )!! } + +/** 주어진 어노테이션의 fqn을 조회하여 반환하고, 만약 조회에 실패했다면 공백을 반환합니다. */ +public fun IrConstructorCall.toFqnStringOrEmpty(): String = + symbol.owner.parentAsClass.fqNameWhenAvailable?.asString().orEmpty() diff --git a/util-backend/build.gradle.kts b/util-backend-kotlinpoet/build.gradle.kts similarity index 91% rename from util-backend/build.gradle.kts rename to util-backend-kotlinpoet/build.gradle.kts index 026a6d3b0..21f2b7797 100644 --- a/util-backend/build.gradle.kts +++ b/util-backend-kotlinpoet/build.gradle.kts @@ -10,7 +10,6 @@ plugins { quackquack("jvm-kotlin") quackquack("kotlin-explicit-api") - quackquack("quack-publishing") } dependencies { diff --git a/util-backend/src/main/kotlin/team/duckie/quackquack/util/backend/PoetUtils.kt b/util-backend-kotlinpoet/src/main/kotlin/team/duckie/quackquack/util/backend/kotlinpoet/utils.kt similarity index 53% rename from util-backend/src/main/kotlin/team/duckie/quackquack/util/backend/PoetUtils.kt rename to util-backend-kotlinpoet/src/main/kotlin/team/duckie/quackquack/util/backend/kotlinpoet/utils.kt index a6da8ae00..dda74f9e2 100644 --- a/util-backend/src/main/kotlin/team/duckie/quackquack/util/backend/PoetUtils.kt +++ b/util-backend-kotlinpoet/src/main/kotlin/team/duckie/quackquack/util/backend/kotlinpoet/utils.kt @@ -5,7 +5,7 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -package team.duckie.quackquack.util.backend +package team.duckie.quackquack.util.backend.kotlinpoet import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName @@ -15,40 +15,9 @@ import com.squareup.kotlinpoet.FunSpec public fun getGeneratedFileComment(source: String): String = "This file was automatically generated by $source.\nDo not modify it manually." -public const val FormatterOffComment: String = "\n@formatter:off" - -public val SuppressAnnotation: AnnotationSpec = AnnotationSpec - .builder(Suppress::class) - .addMember( - // FIXME: How to suppress all lints? (detekt, compose, ... etc) - "%S, %S, %S, %S, %S, %S, %S, %S, %S, %S, %S, %S, %S, %S, %S, %S, %S", - "NoConsecutiveBlankLines", - "PackageDirectoryMismatch", - "Wrapping", - "TrailingCommaOnCallSite", - "ArgumentListWrapping", - "RedundantVisibilityModifier", - "UnusedImport", - "NoUnusedImports", - "SpacingAroundParens", - "Indentation", - "NoUnitReturn", - "RedundantUnitReturnType", - "ModifierParameter", - "KDocUnresolvedReference", - "NoTrailingSpaces", - "NoMultipleSpaces", - "ktlint", - ) - .useSiteTarget(AnnotationSpec.UseSiteTarget.FILE) - .build() - public fun FileSpec.Builder.addAnnotations(vararg annotations: AnnotationSpec): FileSpec.Builder = addAnnotations(annotations.asList()) -public fun FileSpec.Builder.addAnnotations(annotations: List): FileSpec.Builder = - apply { annotations.forEach(::addAnnotation) } - public fun FunSpec.Builder.addAnnotations(vararg annotations: ClassName): FunSpec.Builder = addAnnotations(annotations.asList()) diff --git a/util-backend-ksp/build.gradle.kts b/util-backend-ksp/build.gradle.kts index e0983e967..ce121eb97 100644 --- a/util-backend-ksp/build.gradle.kts +++ b/util-backend-ksp/build.gradle.kts @@ -5,16 +5,12 @@ * 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("kotlin-explicit-api") - quackquack("quack-publishing") } dependencies { - api(projects.utilBackend.orArtifact()) implementations( libs.kotlin.ksp.api, libs.kotlin.kotlinpoet.core, diff --git a/util-backend-ksp/version.txt b/util-backend-ksp/version.txt deleted file mode 100644 index 3a3bf97af..000000000 --- a/util-backend-ksp/version.txt +++ /dev/null @@ -1 +0,0 @@ -2.0.0-alpha01 \ No newline at end of file diff --git a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/aide.kt b/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/aide.kt deleted file mode 100644 index 7bc791d08..000000000 --- a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/aide.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.util.backend.test.stub - -import org.intellij.lang.annotations.Language - -public object AideStub { - @Language("kotlin") - public const val Annotation: String = """ -package team.duckie.quackquack.aide.annotation - -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.SOURCE) -annotation class DecorateModifier - """ -} diff --git a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/casa.kt b/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/casa.kt index 42abfe10e..5dda26f13 100644 --- a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/casa.kt +++ b/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/casa.kt @@ -11,7 +11,8 @@ import org.intellij.lang.annotations.Language public object CasaStub { @Language("kotlin") - public const val Annotations: String = """ + public const val Annotations: String = + """ package team.duckie.quackquack.casa.annotation @Target(AnnotationTarget.FUNCTION) diff --git a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/compose.kt b/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/compose.kt index e58363bde..e490b72d3 100644 --- a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/compose.kt +++ b/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/compose.kt @@ -11,22 +11,24 @@ import org.intellij.lang.annotations.Language public object ComposeStub { @Language("kotlin") - public const val Modifier: String = """ + public const val Modifier: String = + """ package androidx.compose.ui interface Modifier { companion object : Modifier } """ @Language("kotlin") - public const val Composable: String = """ + public const val Composable: String = + """ package androidx.compose.runtime @Retention(AnnotationRetention.BINARY) @Target( - AnnotationTarget.FUNCTION, - AnnotationTarget.TYPE, - AnnotationTarget.TYPE_PARAMETER, - AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPE, + AnnotationTarget.TYPE_PARAMETER, + AnnotationTarget.PROPERTY_GETTER, ) annotation class Composable """ diff --git a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/sugar.kt b/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/sugar.kt index 09118b54f..9e0fb06fd 100644 --- a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/sugar.kt +++ b/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/stub/sugar.kt @@ -11,16 +11,18 @@ import org.intellij.lang.annotations.Language public object SugarStub { @Language("kotlin") - public const val Typer: String = """ + public const val Typer: String = + """ package team.duckie.quackquack.sugar.material fun sugar(): T { - throw NotImplementedError() + throw NotImplementedError() } """ @Language("kotlin") - public const val Annotations: String = """ + public const val Annotations: String = + """ package team.duckie.quackquack.sugar.material import kotlin.reflect.KClass @@ -28,11 +30,11 @@ import kotlin.reflect.KClass @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.BINARY) annotation class SugarName(val name: String = DEFAULT_NAME) { - companion object { - const val PREFIX_NAME: String = "Quack" - const val DEFAULT_NAME: String = "<>" - const val TOKEN_NAME: String = "<>" - } + companion object { + const val PREFIX_NAME: String = "Quack" + const val DEFAULT_NAME: String = "<>" + const val TOKEN_NAME: String = "<>" + } } @Target(AnnotationTarget.VALUE_PARAMETER) diff --git a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/utils.kt b/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/utils.kt index 73a8ed2ac..eb0d02522 100644 --- a/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/utils.kt +++ b/util-backend-test/src/main/kotlin/team/duckie/quackquack/util/backend/test/utils.kt @@ -10,8 +10,7 @@ package team.duckie.quackquack.util.backend.test import java.io.File public fun File.findGeneratedFileOrNull(fileName: String): File? = - walkTopDown() - .find { it.name == fileName } + walkTopDown().find { it.name == fileName } public fun String.removePackageLine(): String = split("\n") diff --git a/util-backend/src/main/kotlin/team/duckie/quackquack/util/backend/utils.kt b/util-backend/src/main/kotlin/team/duckie/quackquack/util/backend/utils.kt deleted file mode 100644 index 0dfc04800..000000000 --- a/util-backend/src/main/kotlin/team/duckie/quackquack/util/backend/utils.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -package team.duckie.quackquack.util.backend - -public fun String.bestGuessToKotlinPackageName(): String { - // make sure testable - // require(contains("src/main/kotlin")) { "The given package is not a Kotlin package." } - return substringAfterLast("src/main/kotlin/").replace("/", ".") -} - -/** - * 주어진 문자열 리스트를 문자열 [리터럴](https://en.wikipedia.org/wiki/Literal_(computer_programming))로 - * 반환합니다. - * - * ``` - * Input: listOf("1", "2", "3") - * Output: "listOf(\"1\", \"2\", \"3\")" - * ``` - */ -public fun Collection.toLiteralListString(): String = - joinToString( - prefix = "listOf(", - postfix = ")", - transform = { "\"$it\"" }, - ) diff --git a/util-backend/version.txt b/util-backend/version.txt deleted file mode 100644 index 3a3bf97af..000000000 --- a/util-backend/version.txt +++ /dev/null @@ -1 +0,0 @@ -2.0.0-alpha01 \ No newline at end of file From 8a0ccba52e271b2e1071e8285a4b75651e8c9262 Mon Sep 17 00:00:00 2001 From: jisungbin Date: Fri, 14 Jul 2023 07:48:54 +0900 Subject: [PATCH 03/12] Improvements to sugared-kdoc --- .../sugar/visitor/SugarCoreVisitor.kt | 246 +++++++++ .../quackquack/sugar/test/SugarCoreTest.kt | 505 ++++++++++++++++++ 2 files changed, 751 insertions(+) create mode 100644 sugar-core/visitor/src/main/kotlin/team/duckie/quackquack/sugar/visitor/SugarCoreVisitor.kt create mode 100644 sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCoreTest.kt diff --git a/sugar-core/visitor/src/main/kotlin/team/duckie/quackquack/sugar/visitor/SugarCoreVisitor.kt b/sugar-core/visitor/src/main/kotlin/team/duckie/quackquack/sugar/visitor/SugarCoreVisitor.kt new file mode 100644 index 000000000..4f39123c3 --- /dev/null +++ b/sugar-core/visitor/src/main/kotlin/team/duckie/quackquack/sugar/visitor/SugarCoreVisitor.kt @@ -0,0 +1,246 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(UnsafeCastFunction::class) + +package team.duckie.quackquack.sugar.visitor + +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.jvm.ir.psiElement +import org.jetbrains.kotlin.ir.backend.js.utils.asString +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.expressions.IrClassReference +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.expressions.IrVararg +import org.jetbrains.kotlin.ir.types.classFqName +import org.jetbrains.kotlin.ir.types.getClass +import org.jetbrains.kotlin.ir.util.companionObject +import org.jetbrains.kotlin.ir.util.file +import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable +import org.jetbrains.kotlin.ir.util.getAnnotation +import org.jetbrains.kotlin.ir.util.hasAnnotation +import org.jetbrains.kotlin.ir.util.parentAsClass +import org.jetbrains.kotlin.ir.util.properties +import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid +import org.jetbrains.kotlin.kdoc.psi.api.KDoc +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.utils.addToStdlib.UnsafeCastFunction +import org.jetbrains.kotlin.utils.addToStdlib.cast +import team.duckie.quackquack.sugar.error.SourceError +import team.duckie.quackquack.sugar.names.CasaValueFqn +import team.duckie.quackquack.sugar.names.ComposableFqn +import team.duckie.quackquack.sugar.names.ImportsFqn +import team.duckie.quackquack.sugar.names.NoSugarFqn +import team.duckie.quackquack.sugar.names.QuackComponentPrefix +import team.duckie.quackquack.sugar.names.RequiresOptInFqn +import team.duckie.quackquack.sugar.names.SugarDefaultName +import team.duckie.quackquack.sugar.names.SugarGeneratedFileFqn +import team.duckie.quackquack.sugar.names.SugarNameFqn +import team.duckie.quackquack.sugar.names.SugarTokenFqn +import team.duckie.quackquack.sugar.names.SugarTokenName +import team.duckie.quackquack.sugar.node.SugarComponentNode +import team.duckie.quackquack.sugar.node.SugarParameter +import team.duckie.quackquack.util.backend.kotlinc.Logger +import team.duckie.quackquack.util.backend.kotlinc.isQuackComponent +import team.duckie.quackquack.util.backend.kotlinc.locationOf + +class SugarCoreVisitor( + @Suppress("unused") private val context: IrPluginContext, + private val logger: Logger, + private val addSugarComponentNode: (data: SugarComponentNode) -> Unit, +) : IrElementVisitorVoid { + override fun visitModuleFragment(declaration: IrModuleFragment) { + declaration.files.forEach { file -> + file.accept(this, null) + } + } + + override fun visitFile(declaration: IrFile) { + if (declaration.hasAnnotation(SugarGeneratedFileFqn)) return + declaration.declarations.forEach { item -> + item.accept(this, null) + } + } + + override fun visitSimpleFunction(declaration: IrSimpleFunction) { + if (declaration.isQuackComponent) { + val componentLocation = declaration.file.locationOf(declaration) + val componentFqn = + declaration.fqNameWhenAvailable + ?: logger.throwError( + message = SourceError.quackComponentFqnUnavailable(declaration.name.asString()), + location = componentLocation, + ) + + if (declaration.hasAnnotation(NoSugarFqn)) return + + val sugarNameAnnotation = declaration.getAnnotation(SugarNameFqn) + val sugarName = sugarNameAnnotation?.getSugarNameIfNotDefault(owner = declaration) + + var sugarToken: IrValueParameter? = null + val sugarParameters = + declaration.valueParameters.map { parameter -> + val isSugarToken = parameter.hasAnnotation(SugarTokenFqn) + if (isSugarToken) { + check(sugarToken == null) { + SourceError.multipleSugarTokenIsNotAllowed(declaration.name.asString()) + } + sugarToken = parameter + } + parameter.toSugarParameter(owner = declaration, isToken = isSugarToken) + } + + sugarToken + ?: logger.throwError( + message = SourceError.quackComponentWithoutSugarToken(componentFqn.asString()), + location = componentLocation, + ) + + val optins = + declaration.annotations.filter { annotation -> + annotation.symbol.owner.parentAsClass.hasAnnotation(RequiresOptInFqn) + } + + val sugarComponentNode = + SugarComponentNode( + owner = declaration, + referFqn = componentFqn, + kdocGetter = { usedTokenLiteral -> + declaration.getSugareKDoc( + referFqn = componentFqn, + tokenName = sugarToken!!.name.asString(), + usedTokenLiteral = usedTokenLiteral, + ) + }, + sugarName = sugarName, + sugarToken = sugarToken!!, + tokenFqExpressions = sugarToken!!.getAllTokenFqExpressions(), + parameters = sugarParameters, + optins = optins, + ) + + addSugarComponentNode(sugarComponentNode) + } + } +} + +private fun IrConstructorCall.getSugarNameIfNotDefault(owner: IrFunction): String? { + // Assuming the first argument is always "name" + val sugarNameExpression = getValueArgument(0) + return sugarNameExpression.cast>().value.takeIf { name -> + (name != SugarDefaultName).also { isCustomSugarName -> + if (isCustomSugarName) checkCustomSugarNameIsValid(owner, name) + } + } +} + +private fun checkCustomSugarNameIsValid(owner: IrFunction, name: String) { + val cause = "${owner.name.asString()} ($name)" + + require(name.startsWith(QuackComponentPrefix)) { + SourceError.sugarNamePrefixIsNotQuack(cause) + } + require(name.contains(SugarTokenName)) { + SourceError.sugarNameWithoutTokenName(cause) + } +} + +private fun IrValueParameter.toSugarParameter(owner: IrFunction, isToken: Boolean): SugarParameter { + val casaValue = getAnnotation(CasaValueFqn)?.let { casaValueAnnotation -> + // Assuming the first argument is always "literal" + val casaValueExpression = casaValueAnnotation.getValueArgument(0) + casaValueExpression.cast>().value + } + val sugarImports = getAnnotation(ImportsFqn)?.let { sugarImportsAnnotation -> + // Assuming the first argument is always "clazz" + val sugarImportsExpression = sugarImportsAnnotation.getValueArgument(0) + sugarImportsExpression + .cast() + .elements + .map { element -> + element.cast().classType.classFqName + ?: error(SourceError.importClazzFqnUnavailable(element.cast().type.asString())) + } + } + val isComposable = hasAnnotation(ComposableFqn) + + return SugarParameter( + owner = owner, + name = name, + type = type, + isToken = isToken, + isComposable = isComposable, + imports = sugarImports.orEmpty(), + casaValueLiteral = casaValue, + defaultValue = defaultValue, + ) +} + +private fun IrSimpleFunction.getSugareKDoc( + referFqn: FqName, + tokenName: String, + usedTokenLiteral: String, +): String { + val usedTokenComment = "This component uses [$usedTokenLiteral] as the token value for `$tokenName`." + val generatedDocComment = + "This document was automatically generated by [${referFqn.shortName().asString()}].\n" + + "If any contents are broken or wanna see the entire contents, please check the original document." + + val kdocArea = psiElement?.children?.firstOrNull { it is KDoc } as? KDoc + val kdocDefaultSection = kdocArea?.getDefaultSection() ?: "" + + val kdocTags = + kdocArea + ?.children + ?.firstOrNull { it is KDocSection } + ?.children + ?.filterIsInstance() + .orEmpty() + + val kdocFirstSentence = + (kdocDefaultSection as? KDocSection)?.getContent()?.trim()?.let { defaultContent -> + if (defaultContent.contains(".")) defaultContent.split(".").first() + "." + else defaultContent + } + + return buildString { + append(kdocFirstSentence?.plus("\n\n").orEmpty()) + appendLine("$usedTokenComment\n\n$generatedDocComment\n") + for (tag in kdocTags) { + var subjectName = tag.getSubjectName() + if (subjectName == tokenName) continue + + val tagName = tag.name?.let { "@$it " }.orEmpty() + subjectName = subjectName?.plus(" ").orEmpty() + + appendLine("${tagName}${subjectName}${tag.getContent()}") + } + } +} + +private fun IrValueParameter.getAllTokenFqExpressions(): List { + val tokenClass = type.getClass()!! + val tokenClassName = tokenClass.name.asString() + return tokenClass.companionObject()?.let { companion -> + val tokenableProperties = + companion.properties.filter { property -> + property.visibility.isPublicAPI + } + val propertyFqExpressions = + tokenableProperties.map { property -> + "$tokenClassName.${property.name.asString()}" + } + propertyFqExpressions.toList() + } ?: error(SourceError.sugarTokenButNoCompanionObject(tokenClassName)) +} diff --git a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCoreTest.kt b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCoreTest.kt new file mode 100644 index 000000000..9b17db297 --- /dev/null +++ b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCoreTest.kt @@ -0,0 +1,505 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:Suppress("RedundantVisibilityModifier", "KDocUnresolvedReference") +@file:OptIn(ExperimentalCompilerApi::class) + +package team.duckie.quackquack.sugar.test + +import team.duckie.quackquack.sugar.core.PluginId as SugarCorePluginId +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.PluginOption +import com.tschuchort.compiletesting.SourceFile.Companion.kotlin +import io.kotest.core.spec.style.StringSpec +import io.kotest.engine.spec.tempdir +import io.kotest.matchers.shouldBe +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import team.duckie.quackquack.sugar.core.OPTION_SUGAR_PATH +import team.duckie.quackquack.sugar.core.SugarCoreCommandLineProcessor +import team.duckie.quackquack.sugar.core.SugarCoreRegistrar +import team.duckie.quackquack.util.backend.test.removePackageLine + +// TODO: @Imports 테스트 작성 +// TODO: nullable한 인자 테스트 작성 +class SugarCoreTest : StringSpec() { + private val testCompilation = + CompilerTestCompilation(tempdir()).apply { + defaultPrepareSetting { tempDir -> + compilerPluginRegistrars = listOf(SugarCoreRegistrar.asPluginRegistrar()) + pluginOptions = listOf( + PluginOption( + pluginId = SugarCorePluginId, + optionName = OPTION_SUGAR_PATH.optionName, + optionValue = tempDir.path, + ), + ) + commandLineProcessors = listOf(SugarCoreCommandLineProcessor()) + } + } + + init { + """ + - @SugarName이 없을 때는 기본 정책대로 sugar component를 생성함 + - KDoc이 없는 대상은 sugared kdoc만 생성함 + """ { + val result = testCompilation.compile( + kotlin( + "text.kt", + """ +import team.duckie.quackquack.sugar.material.SugarToken +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun QuackText( + modifier: Modifier = Modifier, + text: String, + @SugarToken style: AwesomeType2, + singleLine: Boolean = false, + softWrap: Boolean = true, +) {} + """, + ), + ) + + @Language("kotlin") + val expect = """ +// This file was automatically generated by sugar-core. +// Do not modify it manually. +@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) +@file:SugarGeneratedFile + + +import AwesomeType2 +import QuackText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.ui.Modifier +import kotlin.Boolean +import kotlin.OptIn +import kotlin.String +import team.duckie.quackquack.casa.`annotation`.Casa +import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage +import team.duckie.quackquack.sugar.material.SugarCompilerApi +import team.duckie.quackquack.sugar.material.SugarGeneratedFile +import team.duckie.quackquack.sugar.material.SugarRefer +import team.duckie.quackquack.sugar.material.sugar + +/** + * This component uses [AwesomeType2.One] as the token value for `style`. + * + * This document was automatically generated by [QuackText]. + * If any contents are broken or wanna see the entire contents, please check the original document. + */ +@Casa +@Composable +@NonRestartableComposable +@SugarRefer("QuackText") +public fun QuackOneText( + modifier: Modifier = sugar(), + text: String, + singleLine: Boolean = sugar(), + softWrap: Boolean = sugar(), +) { + QuackText( + modifier = modifier, + text = text, + style = AwesomeType2.One, + singleLine = singleLine, + softWrap = softWrap, + ) +} + +/** + * This component uses [AwesomeType2.Two] as the token value for `style`. + * + * This document was automatically generated by [QuackText]. + * If any contents are broken or wanna see the entire contents, please check the original document. + */ +@Casa +@Composable +@NonRestartableComposable +@SugarRefer("QuackText") +public fun QuackTwoText( + modifier: Modifier = sugar(), + text: String, + singleLine: Boolean = sugar(), + softWrap: Boolean = sugar(), +) { + QuackText( + modifier = modifier, + text = text, + style = AwesomeType2.Two, + singleLine = singleLine, + softWrap = softWrap, + ) +} + +/** + * This component uses [AwesomeType2.Three] as the token value for `style`. + * + * This document was automatically generated by [QuackText]. + * If any contents are broken or wanna see the entire contents, please check the original document. + */ +@Casa +@Composable +@NonRestartableComposable +@SugarRefer("QuackText") +public fun QuackThreeText( + modifier: Modifier = sugar(), + text: String, + singleLine: Boolean = sugar(), + softWrap: Boolean = sugar(), +) { + QuackText( + modifier = modifier, + text = text, + style = AwesomeType2.Three, + singleLine = singleLine, + softWrap = softWrap, + ) +} + + """.trimIndent() + + result.exitCode shouldBe KotlinCompilation.ExitCode.OK + testCompilation.findGeneratedFileOrNull("text.kt")?.readText()?.removePackageLine() shouldBe expect + } + + """ + - PREFIX_NAME + Awesome + TOKEN_NAME 조합으로 sugar component를 생성함 + - KDoc이 있는 대상은 SugarToken이 제외된 sugared fully-kdoc을 생성함 + """ { + val result = testCompilation.compile( + kotlin( + "text.kt", + """ +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.SugarName +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * AWESOME! + * + * @param modifier 적용할 Modifier + * @param text 표시할 문자 + */ +@SugarName(SugarName.PREFIX_NAME + "Awesome" + SugarName.TOKEN_NAME) +@Composable +fun QuackText( + modifier: Modifier = Modifier, + text: String, + @SugarToken style: AwesomeType2, + singleLine: Boolean = false, + softWrap: Boolean = true, +) {} + """, + ), + ) + + @Language("kotlin") + val expect = """ +// This file was automatically generated by sugar-core. +// Do not modify it manually. +@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) +@file:SugarGeneratedFile + + +import AwesomeType2 +import QuackText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.ui.Modifier +import kotlin.Boolean +import kotlin.OptIn +import kotlin.String +import team.duckie.quackquack.casa.`annotation`.Casa +import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage +import team.duckie.quackquack.sugar.material.SugarCompilerApi +import team.duckie.quackquack.sugar.material.SugarGeneratedFile +import team.duckie.quackquack.sugar.material.SugarRefer +import team.duckie.quackquack.sugar.material.sugar + +/** + * AWESOME! + * + * This component uses [AwesomeType2.One] as the token value for `style`. + * + * This document was automatically generated by [QuackText]. + * If any contents are broken or wanna see the entire contents, please check the original document. + * + * @param modifier 적용할 Modifier + * @param text 표시할 문자 + */ +@Casa +@Composable +@NonRestartableComposable +@SugarRefer("QuackText") +public fun QuackAwesomeOne( + modifier: Modifier = sugar(), + text: String, + singleLine: Boolean = sugar(), + softWrap: Boolean = sugar(), +) { + QuackText( + modifier = modifier, + text = text, + style = AwesomeType2.One, + singleLine = singleLine, + softWrap = softWrap, + ) +} + +/** + * AWESOME! + * + * This component uses [AwesomeType2.Two] as the token value for `style`. + * + * This document was automatically generated by [QuackText]. + * If any contents are broken or wanna see the entire contents, please check the original document. + * + * @param modifier 적용할 Modifier + * @param text 표시할 문자 + */ +@Casa +@Composable +@NonRestartableComposable +@SugarRefer("QuackText") +public fun QuackAwesomeTwo( + modifier: Modifier = sugar(), + text: String, + singleLine: Boolean = sugar(), + softWrap: Boolean = sugar(), +) { + QuackText( + modifier = modifier, + text = text, + style = AwesomeType2.Two, + singleLine = singleLine, + softWrap = softWrap, + ) +} + +/** + * AWESOME! + * + * This component uses [AwesomeType2.Three] as the token value for `style`. + * + * This document was automatically generated by [QuackText]. + * If any contents are broken or wanna see the entire contents, please check the original document. + * + * @param modifier 적용할 Modifier + * @param text 표시할 문자 + */ +@Casa +@Composable +@NonRestartableComposable +@SugarRefer("QuackText") +public fun QuackAwesomeThree( + modifier: Modifier = sugar(), + text: String, + singleLine: Boolean = sugar(), + softWrap: Boolean = sugar(), +) { + QuackText( + modifier = modifier, + text = text, + style = AwesomeType2.Three, + singleLine = singleLine, + softWrap = softWrap, + ) +} + + """.trimIndent() + + result.exitCode shouldBe KotlinCompilation.ExitCode.OK + testCompilation.findGeneratedFileOrNull("text.kt")?.readText()?.removePackageLine() shouldBe expect + } + + """ + - DEFAULT_NAME을 사용하면 기본 정책대로 sugar component를 생성함 + - 여러줄의 KDoc default content가 적용됐다면 첫 번째 줄만 default section으로 사용함 + """ { + val result = testCompilation.compile( + kotlin( + "text.kt", + """ +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.SugarName +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * AWESOME!. AWESOME!!. AWESOME!!!. + * AWESOME!!!!. AWESOME!!!!!. AWESOME!!!!!!. + * + * @param modifier 적용할 Modifier + * @param text 표시할 문자 + */ +@SugarName(SugarName.DEFAULT_NAME) +@Composable +fun QuackText( + modifier: Modifier = Modifier, + text: String, + @SugarToken style: AwesomeType, + singleLine: Boolean = false, + softWrap: Boolean = true, +) {} + """, + ), + ) + + @Language("kotlin") + val expect = """ +// This file was automatically generated by sugar-core. +// Do not modify it manually. +@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) +@file:SugarGeneratedFile + + +import AwesomeType +import QuackText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.ui.Modifier +import kotlin.Boolean +import kotlin.OptIn +import kotlin.String +import team.duckie.quackquack.casa.`annotation`.Casa +import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage +import team.duckie.quackquack.sugar.material.SugarCompilerApi +import team.duckie.quackquack.sugar.material.SugarGeneratedFile +import team.duckie.quackquack.sugar.material.SugarRefer +import team.duckie.quackquack.sugar.material.sugar + +/** + * AWESOME!. + * + * This component uses [AwesomeType.One] as the token value for `style`. + * + * This document was automatically generated by [QuackText]. + * If any contents are broken or wanna see the entire contents, please check the original document. + * + * @param modifier 적용할 Modifier + * @param text 표시할 문자 + */ +@Casa +@Composable +@NonRestartableComposable +@SugarRefer("QuackText") +public fun QuackOneText( + modifier: Modifier = sugar(), + text: String, + singleLine: Boolean = sugar(), + softWrap: Boolean = sugar(), +) { + QuackText( + modifier = modifier, + text = text, + style = AwesomeType.One, + singleLine = singleLine, + softWrap = softWrap, + ) +} + + """.trimIndent() + + result.exitCode shouldBe KotlinCompilation.ExitCode.OK + testCompilation.findGeneratedFileOrNull("text.kt")?.readText()?.removePackageLine() shouldBe expect + } + + "람다 인자가 지원됨" { + val result = testCompilation.compile( + kotlin( + "checkbox.kt", + """ +import team.duckie.quackquack.sugar.material.SugarToken +import androidx.compose.runtime.Composable + +@Composable +fun QuackCheckbox( + @SugarToken style: AwesomeType, + onCheckChanged: (checked: Boolean) -> Unit, +) {} + +@Composable +fun QuackCheckbox2( + @SugarToken style: AwesomeType, + onCheckChanged: suspend Boolean.(checked: Boolean) -> Boolean, +) {} + """, + ), + ) + + @Language("kotlin") + val expect = """ +// This file was automatically generated by sugar-core. +// Do not modify it manually. +@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) +@file:SugarGeneratedFile + + +import AwesomeType +import QuackCheckbox +import QuackCheckbox2 +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import kotlin.Boolean +import kotlin.Function1 +import kotlin.OptIn +import kotlin.Unit +import kotlin.coroutines.SuspendFunction2 +import team.duckie.quackquack.casa.`annotation`.Casa +import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage +import team.duckie.quackquack.sugar.material.SugarCompilerApi +import team.duckie.quackquack.sugar.material.SugarGeneratedFile +import team.duckie.quackquack.sugar.material.SugarRefer +import team.duckie.quackquack.sugar.material.sugar + +/** + * This component uses [AwesomeType.One] as the token value for `style`. + * + * This document was automatically generated by [QuackCheckbox]. + * If any contents are broken or wanna see the entire contents, please check the original document. + */ +@Casa +@Composable +@NonRestartableComposable +@SugarRefer("QuackCheckbox") +public fun QuackOneCheckbox(onCheckChanged: (P0: Boolean) -> Unit) { + QuackCheckbox( + style = AwesomeType.One, + onCheckChanged = onCheckChanged, + ) +} + +/** + * This component uses [AwesomeType.One] as the token value for `style`. + * + * This document was automatically generated by [QuackCheckbox2]. + * If any contents are broken or wanna see the entire contents, please check the original document. + */ +@Casa +@Composable +@NonRestartableComposable +@SugarRefer("QuackCheckbox2") +public fun QuackOneCheckbox2(onCheckChanged: suspend (P0: Boolean, P1: Boolean) -> Boolean) { + QuackCheckbox2( + style = AwesomeType.One, + onCheckChanged = onCheckChanged, + ) +} + + """.trimIndent() + + result.exitCode shouldBe KotlinCompilation.ExitCode.OK + testCompilation.findGeneratedFileOrNull("checkbox.kt")?.readText()?.removePackageLine() shouldBe expect + } + } +} From f296f103b16a7883ccd5f298477e54dfd439d6f3 Mon Sep 17 00:00:00 2001 From: jisungbin Date: Fri, 14 Jul 2023 08:27:08 +0900 Subject: [PATCH 04/12] Change sugar component creation conditions (introduced @Sugarable) --- .../src/main/kotlin/SugarPoetConfig.kt | 9 - .../sugar/compiler/ir/SugarIrTransformer.kt | 32 +- .../quackquack/sugar/error/SugarErrors.kt | 6 - .../quackquack/sugar/names/SugarNames.kt | 4 +- .../sugar/visitor/SugarCoreVisitor.kt | 6 +- .../quackquack/sugar/material/annotations.kt | 52 +- .../duckie/quackquack/sugar/material/optin.kt | 1 - .../duckie/quackquack/sugar/material/typer.kt | 11 +- .../sugar/test/SugarCompilerErrorTest.kt | 74 +- .../sugar/test/SugarCompilerTransformTest.kt | 15 +- .../quackquack/sugar/test/SugarCoreTest.kt | 21 +- ...rTestCompilation.kt => TestCompilation.kt} | 14 +- ui/build.gradle.kts | 18 +- .../team/duckie/quackquack/ui/button.kt | 4 +- .../kotlin/team/duckie/quackquack/ui/icon.kt | 2 - .../kotlin/team/duckie/quackquack/ui/image.kt | 4 - .../team/duckie/quackquack/ui/sugar/button.kt | 760 ------------------ .../team/duckie/quackquack/ui/sugar/tag.kt | 368 --------- .../team/duckie/quackquack/ui/sugar/text.kt | 421 ---------- .../team/duckie/quackquack/ui/switch.kt | 2 - .../kotlin/team/duckie/quackquack/ui/tab.kt | 2 - .../kotlin/team/duckie/quackquack/ui/tag.kt | 4 +- .../kotlin/team/duckie/quackquack/ui/text.kt | 2 + .../team/duckie/quackquack/ui/textfield.kt | 10 +- .../util/backend/test/stub/sugar.kt | 8 +- 25 files changed, 118 insertions(+), 1732 deletions(-) delete mode 100644 build-logic/src/main/kotlin/SugarPoetConfig.kt rename sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/{CompilerTestCompilation.kt => TestCompilation.kt} (62%) delete mode 100644 ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/button.kt delete mode 100644 ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/tag.kt delete mode 100644 ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/text.kt diff --git a/build-logic/src/main/kotlin/SugarPoetConfig.kt b/build-logic/src/main/kotlin/SugarPoetConfig.kt deleted file mode 100644 index f313e5787..000000000 --- a/build-logic/src/main/kotlin/SugarPoetConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Designed and developed by Duckie Team 2023. - * - * Licensed under the MIT. - * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE - */ - -// TODO: 작동 방식 변경 (gradle task) -const val sugarPoet = false diff --git a/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/ir/SugarIrTransformer.kt b/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/ir/SugarIrTransformer.kt index 3677fc62b..d92bf2439 100644 --- a/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/ir/SugarIrTransformer.kt +++ b/sugar-compiler/src/main/kotlin/team/duckie/quackquack/sugar/compiler/ir/SugarIrTransformer.kt @@ -24,10 +24,8 @@ import org.jetbrains.kotlin.ir.util.hasAnnotation import org.jetbrains.kotlin.ir.visitors.IrElementTransformer import org.jetbrains.kotlin.utils.addToStdlib.UnsafeCastFunction import org.jetbrains.kotlin.utils.addToStdlib.cast -import team.duckie.quackquack.sugar.error.PoetError import team.duckie.quackquack.sugar.error.SugarTransformError import team.duckie.quackquack.sugar.error.SugarVisitError -import team.duckie.quackquack.sugar.names.NoSugarFqn import team.duckie.quackquack.sugar.names.SugarGeneratedFileFqn import team.duckie.quackquack.sugar.names.SugarReferFqn import team.duckie.quackquack.sugar.node.SugarComponentNode @@ -62,30 +60,24 @@ internal class SugarIrTransformer( data: Map, ): IrStatement { if (declaration.isQuackComponent) { - if (declaration.hasAnnotation(NoSugarFqn)) { - return super.visitSimpleFunction(declaration, data) - } - val referAnnotation = declaration.getAnnotation(SugarReferFqn) - ?: logger.throwError( - message = PoetError.sugarComponentButNoSugarRefer(declaration.name.asString()), - location = declaration.file.locationOf(declaration), - ) + ?: return super.visitSimpleFunction(declaration, data) val referFqn = referAnnotation.getReferFqName() data[referFqn]?.let { referIrData -> declaration.valueParameters.forEach { parameter -> - parameter.defaultValue = referIrData.findMatchedDefaultValue( - sugarComponentName = declaration.name.asString(), - parameter = parameter, - error = { message -> - logger.throwError( - message = message, - location = declaration.file.locationOf(parameter), - ) - }, - ) + parameter.defaultValue = + referIrData.findMatchedDefaultValue( + sugarComponentName = declaration.name.asString(), + parameter = parameter, + error = { message -> + logger.throwError( + message = message, + location = declaration.file.locationOf(parameter), + ) + }, + ) } } ?: logger.throwError( message = SugarVisitError.noMatchedSugarComponentNode(declaration.name.asString()), diff --git a/sugar-core/error/src/main/kotlin/team/duckie/quackquack/sugar/error/SugarErrors.kt b/sugar-core/error/src/main/kotlin/team/duckie/quackquack/sugar/error/SugarErrors.kt index 90052a834..d0187bf16 100644 --- a/sugar-core/error/src/main/kotlin/team/duckie/quackquack/sugar/error/SugarErrors.kt +++ b/sugar-core/error/src/main/kotlin/team/duckie/quackquack/sugar/error/SugarErrors.kt @@ -50,12 +50,6 @@ object SourceError { " ($name)".getIfGivenIsNotNull(name) } -object PoetError { - fun sugarComponentButNoSugarRefer(name: String?) = - "The SugarRefer for the Sugar component is missing." + - " ($name)".getIfGivenIsNotNull(name) -} - object SugarVisitError { fun noMatchedSugarComponentNode(name: String?) = "No SugarComponentNode was found for the given SugarRefer. " + diff --git a/sugar-core/names/src/main/kotlin/team/duckie/quackquack/sugar/names/SugarNames.kt b/sugar-core/names/src/main/kotlin/team/duckie/quackquack/sugar/names/SugarNames.kt index 17d74d40c..1dcf09430 100644 --- a/sugar-core/names/src/main/kotlin/team/duckie/quackquack/sugar/names/SugarNames.kt +++ b/sugar-core/names/src/main/kotlin/team/duckie/quackquack/sugar/names/SugarNames.kt @@ -21,12 +21,12 @@ import team.duckie.quackquack.casa.annotation.Casa import team.duckie.quackquack.casa.annotation.CasaValue import team.duckie.quackquack.casa.annotation.SugarGeneratorUsage import team.duckie.quackquack.sugar.material.Imports -import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.sugar.material.SugarCompilerApi import team.duckie.quackquack.sugar.material.SugarGeneratedFile import team.duckie.quackquack.sugar.material.SugarName import team.duckie.quackquack.sugar.material.SugarRefer import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable val RequiresOptInFqn = RequiresOptIn::class.qualifiedName!!.toFqnClass() @@ -56,7 +56,7 @@ val SugarReferCn = SugarRefer::class.asClassName() val SugarReferFqn = SugarRefer::class.qualifiedName!!.toFqnClass() val ImportsFqn = Imports::class.qualifiedName!!.toFqnClass() -val NoSugarFqn = NoSugar::class.qualifiedName!!.toFqnClass() +val SugarableFqn = Sugarable::class.qualifiedName!!.toFqnClass() private fun String.toFqnClass() = FqName(this) private fun String.toCnClass() = ClassName.bestGuess(this) diff --git a/sugar-core/visitor/src/main/kotlin/team/duckie/quackquack/sugar/visitor/SugarCoreVisitor.kt b/sugar-core/visitor/src/main/kotlin/team/duckie/quackquack/sugar/visitor/SugarCoreVisitor.kt index 4f39123c3..33cdc12b2 100644 --- a/sugar-core/visitor/src/main/kotlin/team/duckie/quackquack/sugar/visitor/SugarCoreVisitor.kt +++ b/sugar-core/visitor/src/main/kotlin/team/duckie/quackquack/sugar/visitor/SugarCoreVisitor.kt @@ -41,7 +41,6 @@ import team.duckie.quackquack.sugar.error.SourceError import team.duckie.quackquack.sugar.names.CasaValueFqn import team.duckie.quackquack.sugar.names.ComposableFqn import team.duckie.quackquack.sugar.names.ImportsFqn -import team.duckie.quackquack.sugar.names.NoSugarFqn import team.duckie.quackquack.sugar.names.QuackComponentPrefix import team.duckie.quackquack.sugar.names.RequiresOptInFqn import team.duckie.quackquack.sugar.names.SugarDefaultName @@ -49,6 +48,7 @@ import team.duckie.quackquack.sugar.names.SugarGeneratedFileFqn import team.duckie.quackquack.sugar.names.SugarNameFqn import team.duckie.quackquack.sugar.names.SugarTokenFqn import team.duckie.quackquack.sugar.names.SugarTokenName +import team.duckie.quackquack.sugar.names.SugarableFqn import team.duckie.quackquack.sugar.node.SugarComponentNode import team.duckie.quackquack.sugar.node.SugarParameter import team.duckie.quackquack.util.backend.kotlinc.Logger @@ -74,7 +74,7 @@ class SugarCoreVisitor( } override fun visitSimpleFunction(declaration: IrSimpleFunction) { - if (declaration.isQuackComponent) { + if (declaration.isQuackComponent && declaration.hasAnnotation(SugarableFqn)) { val componentLocation = declaration.file.locationOf(declaration) val componentFqn = declaration.fqNameWhenAvailable @@ -83,8 +83,6 @@ class SugarCoreVisitor( location = componentLocation, ) - if (declaration.hasAnnotation(NoSugarFqn)) return - val sugarNameAnnotation = declaration.getAnnotation(SugarNameFqn) val sugarName = sugarNameAnnotation?.getSugarNameIfNotDefault(owner = declaration) diff --git a/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/annotations.kt b/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/annotations.kt index f27b0318c..83836546f 100644 --- a/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/annotations.kt +++ b/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/annotations.kt @@ -11,8 +11,7 @@ import kotlin.reflect.KClass /** * sugar component의 이름을 직접 명시합니다. 만약 직접 명시하지 않으면 기본 정책에 - * 맞게 네이밍이 진행됩니다. sugar component의 기본 네이밍 정책은 `sugar-processor` - * 모듈을 참고하세요. + * 맞게 네이밍이 진행됩니다. sugar component의 기본 네이밍 정책은 꽥꽥 웹 문서를 참고하세요. * * sugar component의 이름을 직접 명시할 때는 다음과 같은 규칙이 보장돼야 합니다. * @@ -69,8 +68,8 @@ public annotation class SugarName(val name: String = DEFAULT_NAME) { } /** - * sugar component에 사용할 sugar token을 나타냅니다. sugar token은 다음과 같은 - * 규칙을 보장해야 합니다. + * sugar component에 사용할 sugar token을 나타냅니다. + * sugar token은 다음과 같은 규칙을 보장해야 합니다. * * 1. companion object가 있어야 합니다. * 2. sugar token의 값들은 companion object 안에 public variable로 정의돼야 합니다. @@ -190,11 +189,11 @@ public annotation class SugarToken * } * ``` * - * 이 정보는 `sugar-processor`에서 원래 함수의 IR 정보를 조회하기 위해 추가됩니다. - * 자세한 정보는 `sugar-processor` 모듈을 참고하세요. + * 이 정보는 sugar component 처리 과정에서 원래 함수의 IR 정보를 조회하기 위해 추가됩니다. + * 자세한 정보는 꽥꽥 웹 문서를 참고하세요. * - * *이 어노테이션은 꽥꽥 컴파일러에서만 사용될 목적으로 설계됐습니다. 임의로 사용할 - * 경우 예상치 못한 버그가 발생할 수 있습니다.* + * 이 어노테이션은 꽥꽥 컴파일러에서만 사용될 목적으로 설계됐습니다. + * 임의로 사용할 경우 예상치 못한 버그가 발생할 수 있습니다. */ @SugarCompilerApi @MustBeDocumented @@ -203,37 +202,21 @@ public annotation class SugarToken public annotation class SugarRefer(val fqn: String) /** - * 이 컴포넌트의 sugar 생성을 무시합니다. 예를 들어 다음과 같은 코드가 있습니다. - * - * ``` - * @JvmInline - * value class Theme(val index: Int) { - * companion object { - * val Default = Theme(1) - * } - * } - * - * @NoSugar - * @Composable - * fun QuackAwesome(@SugarToken theme: Theme) { - * QuackTheme(theme = theme) - * } - * ``` - * - * 이 컴포넌트는 [`@NoSugar`][NoSugar]의 영향으로 sugar component가 생성되지 않습니다. + * 이 컴포넌트의 sugar 생성을 활성화합니다. + * 이 어노테이션이 붙은 컴포넌트만 sugar component가 생성됩니다. */ @MustBeDocumented @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.BINARY) -public annotation class NoSugar +public annotation class Sugarable /** - * `sugar-processor`에 의해 생성된 sugar components 파일임을 나타냅니다. 이 어노테이션이 - * 부착된 파일만 `SugarIrTransform`이 진행됩니다. 자세한 정보는 `sugar-processor` 모듈을 - * 참고하세요. + * 자동 생성된 sugar component 파일임을 나타냅니다. + * 이 어노테이션이 붙은 파일만 sugar component 처리가 진행됩니다. + * 자세한 정보는 꽥꽥 웹 문서를 참고하세요. * - * *이 어노테이션은 꽥꽥 컴파일러에서만 사용될 목적으로 설계됐습니다. 임의로 사용할 - * 경우 예상치 못한 버그가 발생할 수 있습니다.* + * 이 어노테이션은 꽥꽥 컴파일러에서만 사용될 목적으로 설계됐습니다. + * 임의로 사용할 경우 예상치 못한 버그가 발생할 수 있습니다. */ @SugarCompilerApi @MustBeDocumented @@ -242,9 +225,8 @@ public annotation class NoSugar public annotation class SugarGeneratedFile /** - * `sugar-processor`가 생성하는 sugar component 코드에 추가로 import 돼야 하는 클래스를 - * 나타냅니다. 함수의 인자에 적용될 수 있으며, 인자의 타입과 인자의 기본 값 타입이 다를 - * 경우 사용할 수 있습니다. + * 자동 생성되는 sugar component 코드에 추가로 import 돼야 하는 클래스를 나타냅니다. + * 함수의 인자에 적용될 수 있으며, 인자의 타입과 인자의 기본 값 타입이 다를 경우 사용할 수 있습니다. * * ``` * // file: flaver.kt diff --git a/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/optin.kt b/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/optin.kt index fd3e6657c..dd7c2e4db 100644 --- a/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/optin.kt +++ b/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/optin.kt @@ -7,7 +7,6 @@ package team.duckie.quackquack.sugar.material -/** `sugar-processor` 모듈에서만 사용돼야 함을 나타내는 optin 어노테이션입니다. */ @MustBeDocumented @RequiresOptIn( message = "This indicates that the feature should only be used in the Sugar Compiler. " + diff --git a/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/typer.kt b/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/typer.kt index e6ca545d3..e39dc64ad 100644 --- a/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/typer.kt +++ b/sugar-material/src/main/kotlin/team/duckie/quackquack/sugar/material/typer.kt @@ -8,7 +8,7 @@ package team.duckie.quackquack.sugar.material /** - * `sugar-processor`로 sugar component를 생성할 때 함수의 default argument가 있는 인자에 + * sugar component를 생성할 때 함수의 default argument가 있는 인자에 * 기본값으로 사용됩니다. * * ``` @@ -52,15 +52,12 @@ package team.duckie.quackquack.sugar.material * } * ``` * - * 자세한 내용은 `sugar-processor` 모듈을 참고하세요. - * - * *이 어노테이션은 꽥꽥 컴파일러에서만 사용될 목적으로 설계됐습니다. 임의로 사용할 - * 경우 예상치 못한 버그가 발생할 수 있습니다.* + * 이 어노테이션은 꽥꽥 컴파일러에서만 사용될 목적으로 설계됐습니다. + * 임의로 사용할 경우 예상치 못한 버그가 발생할 수 있습니다. */ @SugarCompilerApi -public fun sugar(): T { +public fun sugar(): T = throw NotImplementedError( "SugarIrTransform did not proceed. " + "Is the sugar-processor compiler plugin applied?", ) -} diff --git a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerErrorTest.kt b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerErrorTest.kt index 36611d024..a4223ffa0 100644 --- a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerErrorTest.kt +++ b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerErrorTest.kt @@ -5,6 +5,8 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ +@file:OptIn(ExperimentalCompilerApi::class) + package team.duckie.quackquack.sugar.test import com.tschuchort.compiletesting.KotlinCompilation @@ -14,8 +16,9 @@ import io.kotest.core.test.Enabled import io.kotest.engine.spec.tempdir import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import team.duckie.quackquack.sugar.compiler.SugarCompilerRegistrar import team.duckie.quackquack.sugar.error.NotSupportedError.nestedFunctionalType -import team.duckie.quackquack.sugar.error.PoetError.sugarComponentButNoSugarRefer import team.duckie.quackquack.sugar.error.SourceError.multipleSugarTokenIsNotAllowed import team.duckie.quackquack.sugar.error.SourceError.quackComponentWithoutSugarToken import team.duckie.quackquack.sugar.error.SourceError.sugarNamePrefixIsNotQuack @@ -24,7 +27,12 @@ import team.duckie.quackquack.sugar.error.SourceError.sugarTokenButNoCompanionOb import team.duckie.quackquack.sugar.error.SugarTransformError.sugarComponentAndSugarReferHasDifferentParameters class SugarCompilerErrorTest : ExpectSpec() { - private val testCompilation = CompilerTestCompilation(tempdir()) + private val testCompilation = + TestCompilation(tempdir()).apply { + prepareSetting { + compilerPluginRegistrars = listOf(SugarCompilerRegistrar.asPluginRegistrar()) + } + } init { context("NotSupportedError") { @@ -41,8 +49,10 @@ class SugarCompilerErrorTest : ExpectSpec() { "main.kt", """ import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable import androidx.compose.runtime.Composable +@Sugarable @Composable fun QuackText( @SugarToken style: AwesomeType, @@ -64,7 +74,9 @@ fun QuackText( "main.kt", """ import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.Sugarable +@Sugarable @Composable fun QuackText() {} """, @@ -75,32 +87,16 @@ fun QuackText() {} result.messages shouldContain quackComponentWithoutSugarToken("QuackText") } - expect("quackComponentWithoutSugarToken - @NoSugar applied") { - val result = testCompilation.compile( - kotlin( - "main.kt", - """ -import team.duckie.quackquack.sugar.material.NoSugar -import androidx.compose.runtime.Composable - -@NoSugar -@Composable -fun QuackText() {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.OK - } - expect("multipleSugarTokenIsNotAllowed") { val result = testCompilation.compile( kotlin( "main.kt", """ -import team.duckie.quackquack.sugar.material.SugarToken import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable +@Sugarable @Composable fun QuackText( @SugarToken style: AwesomeType, @@ -122,7 +118,9 @@ fun QuackText( import androidx.compose.runtime.Composable import team.duckie.quackquack.sugar.material.SugarName import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable +@Sugarable @SugarName("Text") @Composable fun QuackText(@SugarToken type: AwesomeType) {} @@ -142,7 +140,9 @@ fun QuackText(@SugarToken type: AwesomeType) {} import androidx.compose.runtime.Composable import team.duckie.quackquack.sugar.material.SugarName import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable +@Sugarable @SugarName("QuackText") @Composable fun QuackText(@SugarToken type: AwesomeType) {} @@ -161,7 +161,9 @@ fun QuackText(@SugarToken type: AwesomeType) {} """ import androidx.compose.runtime.Composable import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable +@Sugarable @Composable fun QuackText(@SugarToken type: AwesomeType3) {} """, @@ -173,39 +175,17 @@ fun QuackText(@SugarToken type: AwesomeType3) {} } } - context("PoetError") { - expect("sugarComponentButNoSugarRefer") { - val result = testCompilation.compile( - kotlin( - "main.kt", - """ -@file:OptIn(SugarCompilerApi::class) -@file:SugarGeneratedFile - -import androidx.compose.runtime.Composable -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile - -@Composable -fun QuackOneText() {} - """, - ), - ) - - result.exitCode shouldBe KotlinCompilation.ExitCode.INTERNAL_ERROR - result.messages shouldContain sugarComponentButNoSugarRefer("QuackOneText") - } - } - context("SugarTransformError") { expect("sugarComponentAndSugarReferHasDifferentParameters") { val result = testCompilation.compile( kotlin( "main.kt", """ -import team.duckie.quackquack.sugar.material.SugarToken import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable +@Sugarable @Composable fun QuackText(@SugarToken style: AwesomeType) {} """, @@ -222,8 +202,8 @@ import team.duckie.quackquack.sugar.material.SugarGeneratedFile import team.duckie.quackquack.sugar.material.SugarRefer import team.duckie.quackquack.sugar.material.sugar -@Composable @SugarRefer("QuackText") +@Composable fun QuackOneText(newNumber: Int = sugar()) {} """, ), diff --git a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerTransformTest.kt b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerTransformTest.kt index 72ce6bdf1..1c7dd531b 100644 --- a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerTransformTest.kt +++ b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCompilerTransformTest.kt @@ -5,6 +5,8 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ +@file:OptIn(ExperimentalCompilerApi::class) + package team.duckie.quackquack.sugar.test import com.tschuchort.compiletesting.KotlinCompilation @@ -12,10 +14,17 @@ import com.tschuchort.compiletesting.SourceFile.Companion.kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.engine.spec.tempdir import io.kotest.matchers.shouldBe +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.jetbrains.kotlin.utils.addToStdlib.cast +import team.duckie.quackquack.sugar.compiler.SugarCompilerRegistrar class SugarCompilerTransformTest : StringSpec() { - private val testCompilation = CompilerTestCompilation(tempdir()) + private val testCompilation = + TestCompilation(tempdir()).apply { + prepareSetting { + compilerPluginRegistrars = listOf(SugarCompilerRegistrar.asPluginRegistrar()) + } + } init { "Default Argument에 SugarIrTransform이 정상 작동함" { @@ -23,11 +32,13 @@ class SugarCompilerTransformTest : StringSpec() { kotlin( "main.kt", """ -import team.duckie.quackquack.sugar.material.SugarToken import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable var number = 0 +@Sugarable @Composable fun QuackText( @SugarToken style: AwesomeType, diff --git a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCoreTest.kt b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCoreTest.kt index 9b17db297..2c2dbe1ea 100644 --- a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCoreTest.kt +++ b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/SugarCoreTest.kt @@ -28,8 +28,8 @@ import team.duckie.quackquack.util.backend.test.removePackageLine // TODO: nullable한 인자 테스트 작성 class SugarCoreTest : StringSpec() { private val testCompilation = - CompilerTestCompilation(tempdir()).apply { - defaultPrepareSetting { tempDir -> + TestCompilation(tempdir()).apply { + prepareSetting { tempDir -> compilerPluginRegistrars = listOf(SugarCoreRegistrar.asPluginRegistrar()) pluginOptions = listOf( PluginOption( @@ -51,10 +51,12 @@ class SugarCoreTest : StringSpec() { kotlin( "text.kt", """ -import team.duckie.quackquack.sugar.material.SugarToken import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable +@Sugarable @Composable fun QuackText( modifier: Modifier = Modifier, @@ -181,6 +183,7 @@ public fun QuackThreeText( """ import team.duckie.quackquack.sugar.material.SugarToken import team.duckie.quackquack.sugar.material.SugarName +import team.duckie.quackquack.sugar.material.Sugarable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -191,6 +194,7 @@ import androidx.compose.ui.Modifier * @param text 표시할 문자 */ @SugarName(SugarName.PREFIX_NAME + "Awesome" + SugarName.TOKEN_NAME) +@Sugarable @Composable fun QuackText( modifier: Modifier = Modifier, @@ -330,10 +334,11 @@ public fun QuackAwesomeThree( kotlin( "text.kt", """ -import team.duckie.quackquack.sugar.material.SugarToken -import team.duckie.quackquack.sugar.material.SugarName import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.SugarName +import team.duckie.quackquack.sugar.material.Sugarable /** * AWESOME!. AWESOME!!. AWESOME!!!. @@ -343,6 +348,7 @@ import androidx.compose.ui.Modifier * @param text 표시할 문자 */ @SugarName(SugarName.DEFAULT_NAME) +@Sugarable @Composable fun QuackText( modifier: Modifier = Modifier, @@ -419,15 +425,18 @@ public fun QuackOneText( kotlin( "checkbox.kt", """ -import team.duckie.quackquack.sugar.material.SugarToken import androidx.compose.runtime.Composable +import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable +@Sugarable @Composable fun QuackCheckbox( @SugarToken style: AwesomeType, onCheckChanged: (checked: Boolean) -> Unit, ) {} +@Sugarable @Composable fun QuackCheckbox2( @SugarToken style: AwesomeType, diff --git a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/CompilerTestCompilation.kt b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/TestCompilation.kt similarity index 62% rename from sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/CompilerTestCompilation.kt rename to sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/TestCompilation.kt index 0078326e0..72e94a1a2 100644 --- a/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/CompilerTestCompilation.kt +++ b/sugar-test/src/test/kotlin/team/duckie/quackquack/sugar/test/TestCompilation.kt @@ -5,7 +5,6 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE */ -@file:OptIn(ExperimentalCompilerApi::class) @file:Suppress("MemberVisibilityCanBePrivate") package team.duckie.quackquack.sugar.test @@ -13,13 +12,11 @@ package team.duckie.quackquack.sugar.test import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile import java.io.File -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.jetbrains.kotlin.config.JvmTarget -import team.duckie.quackquack.sugar.compiler.SugarCompilerRegistrar import team.duckie.quackquack.util.backend.test.findGeneratedFileOrNull -class CompilerTestCompilation(private val tempDir: File) { - private var defaultPrepareSetting: (KotlinCompilation.(tempDir: File) -> Unit)? = null +class TestCompilation(private val tempDir: File) { + private var prepareSetting: (KotlinCompilation.(tempDir: File) -> Unit)? = null fun compile(vararg sourceFiles: SourceFile) = prepare(*sourceFiles).compile() @@ -32,12 +29,11 @@ class CompilerTestCompilation(private val tempDir: File) { inheritClassPath = true supportsK2 = false useK2 = false - compilerPluginRegistrars = listOf(SugarCompilerRegistrar.asPluginRegistrar()) - defaultPrepareSetting?.invoke(this, tempDir) + prepareSetting?.invoke(this, tempDir) } - fun defaultPrepareSetting(block: KotlinCompilation.(tempDir: File) -> Unit) { - defaultPrepareSetting = block + fun prepareSetting(block: KotlinCompilation.(tempDir: File) -> Unit) { + prepareSetting = block } fun findGeneratedFileOrNull(fileName: String) = tempDir.findGeneratedFileOrNull(fileName) diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index dfcbcbcff..a0c6540e8 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -24,16 +24,12 @@ plugins { } tasks.withType { - val sugarProcessorKotlinCompilerPluginId = "team.duckie.quackquack.sugar.processor" - val sugarPath = "$projectDir/src/main/kotlin/team/duckie/quackquack/ui/sugar" + val sugarCorePluginId = "team.duckie.quackquack.sugar.core" + val sugarPath = "${projects.uiSugar.dependencyProject.projectDir}/src/main/kotlin/team/duckie/quackquack/ui/sugar" kotlinOptions { freeCompilerArgs = freeCompilerArgs + listOf( "-P", - "plugin:$sugarProcessorKotlinCompilerPluginId:sugarPath=$sugarPath", - ) - freeCompilerArgs = freeCompilerArgs + listOf( - "-P", - "plugin:$sugarProcessorKotlinCompilerPluginId:poet=$sugarPoet", + "plugin:$sugarCorePluginId:sugarPath=$sugarPath", ) } } @@ -99,12 +95,10 @@ dependencies { libs.test.kotest.assertion.core, ) - kotlinCompilerPlugin(projects.sugarCompiler.orArtifact()) + kotlinCompilerPlugin(projects.sugarCompiler.orArtifact()) safeRunWithinDevelopmentMode { - ksps( - // TODO: projects.casaProcessor, - ) - kotlinCompilerPlugin(projects.sugarCore) + // TODO: ksp(projects.casaProcessor) + kotlinCompilerPlugin(projects.sugarCore) } } diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt index a515804b3..ffc2aa000 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt @@ -49,8 +49,8 @@ import team.duckie.quackquack.material.QuackTypography import team.duckie.quackquack.material.quackSurface import team.duckie.quackquack.runtime.QuackDataModifierModel import team.duckie.quackquack.runtime.quackMaterializeOf -import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi import team.duckie.quackquack.ui.util.QuackDsl @@ -758,6 +758,7 @@ public fun Modifier.icons( * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 * @param onClick 클릭했을 때 실행할 람다식. [enabled]이 true일 때만 작동합니다. */ +@Sugarable @MustBeTested(passed = true) @Composable @NonRestartableComposable @@ -868,7 +869,6 @@ private const val TrailingIconLayoutId = "QuackBaseButtonTrailingIcon" * * 이 컴포넌트는 [QuackButtonStyle]의 필드를 개별 인자로 받습니다. */ -@NoSugar @Composable public fun QuackBaseButton( modifier: Modifier, diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/icon.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/icon.kt index 926efd97e..84bf9778f 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/icon.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/icon.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import team.duckie.quackquack.material.QuackColor import team.duckie.quackquack.material.icon.QuackIcon -import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.ui.util.fontScaleAwareIconSize import team.duckie.quackquack.util.applyIf @@ -38,7 +37,6 @@ import team.duckie.quackquack.util.applyIf * @param contentScale [icon]에 적용할 [contentScale][ContentScale] * @param contentDescription [icon]을 설명하는 문구. 접근성 서비스에 사용됩니다. */ -@NoSugar @NonRestartableComposable @Composable public fun QuackIcon( diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/image.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/image.kt index 7d4287ee3..c927f4f79 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/image.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/image.kt @@ -27,7 +27,6 @@ import coil.ImageLoader import coil.compose.AsyncImage import coil.compose.LocalImageLoader import team.duckie.quackquack.material.QuackColor -import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.ui.plugin.EmptyQuackPlugins import team.duckie.quackquack.ui.plugin.LocalQuackPlugins import team.duckie.quackquack.ui.plugin.QuackPluginLocal @@ -51,7 +50,6 @@ import team.duckie.quackquack.util.modifier.getElementByTypeOrNull replaceWith = ReplaceWith("QuackIcon"), level = DeprecationLevel.ERROR, ) -@NoSugar @NonRestartableComposable @Composable public fun QuackImage( @@ -76,7 +74,6 @@ public fun QuackImage( * @param contentScale drawable 리소스에 적용할 [contentScale][ContentScale] * @param contentDescription 접근성 서비스에서 이 이미지가 무엇을 나타내는지 설명할 문구 */ -@NoSugar @NonRestartableComposable @Composable public fun QuackImage( @@ -116,7 +113,6 @@ public fun QuackImage( * @param contentScale 이미지 리소스에 적용할 [contentScale][ContentScale] * @param contentDescription 접근성 서비스에서 이 이미지가 무엇을 나타내는지 설명할 문구 */ -@NoSugar @NonRestartableComposable @Composable public fun QuackImage( diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/button.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/button.kt deleted file mode 100644 index 90c87ea42..000000000 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/button.kt +++ /dev/null @@ -1,760 +0,0 @@ -// This file was automatically generated by sugar-processor. -// Do not modify it manually. -// @formatter:off -@file:Suppress("NoConsecutiveBlankLines", "PackageDirectoryMismatch", "Wrapping", - "TrailingCommaOnCallSite", "ArgumentListWrapping", "RedundantVisibilityModifier", - "UnusedImport", "NoUnusedImports", "SpacingAroundParens", "Indentation", "NoUnitReturn", - "RedundantUnitReturnType", "ModifierParameter", "KDocUnresolvedReference", "NoTrailingSpaces", - "NoMultipleSpaces", "ktlint") -@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) -@file:SugarGeneratedFile - -package team.duckie.quackquack.ui.sugar - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.ui.Modifier -import kotlin.Boolean -import kotlin.Function0 -import kotlin.OptIn -import kotlin.String -import kotlin.Suppress -import kotlin.Unit -import team.duckie.quackquack.casa.`annotation`.Casa -import team.duckie.quackquack.casa.`annotation`.CasaValue -import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.sugar -import team.duckie.quackquack.ui.QuackButton -import team.duckie.quackquack.ui.QuackButtonStyle -import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi - -/** - * 버튼을 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - 이 컴포넌트는 자체의 배치 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [버튼의 스타일][QuackButtonStyle]에서 [contentPadding][QuackButtonStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 버튼의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackButtonStyle.contentPadding]은 버튼의 - * 텍스트를 기준으로 패딩이 적용됩니다. 이 부분의 자세한 내용은 배치 정책 세션을 참고하세요. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackButtonStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackButtonStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackButtonStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackButtonStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackButtonStyle.contentPadding]이 - * 무시되고 버튼의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackButtonStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackButtonStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 배치 정책 - * - * [style.contentPadding][QuackButtonStyle.contentPadding]은 항상 버튼의 텍스트를 기준으로 - * 적용됩니다. 예를 들어 버튼의 아이콘을 leading과 trailing을 모두 제공했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(horizontal=10.dp)`를 제공했다면 양끝의 horizontal 패딩이 각각 아이콘을 기준으로 - * 적용되는 게 아닌 버튼의 텍스트를 기준으로 적용됩니다. 따라서 개발자는 [contentPadding][QuackButtonStyle.contentPadding]의 값을 - * 제공할 때 양끝 아이콘을 기준으로 제공하는 게 아닌 가운데 텍스트를 기준으로 제공해야 합니다. - * 이 정책은 양끝 아이콘이 동적으로 적용될 때 의도하지 않는 버튼 사이즈 변경을 예방하기 위해 - * 고안됐습니다. 예를 들어 `contentPadding: QuackPadding(horizontal=10.dp)`을 양끝 아이콘 기준으로 - * 적용했다고 해봅시다. 처음에는 양끝에 아이콘이 없어서 가운데 텍스트를 기준으로 패딩이 적용됩니다. - * 이 시점에는 버튼의 너비가 25dp입니다. (왼쪽 패딩 10dp, 텍스트 5dp, 오른쪽 패딩 10dp) 사용자 - * 요청에 의해 양쪽 모두에 5dp의 너비를 갖는 아이콘이 추가되었습니다. 이 시점에서는 양쪽 아이콘이 - * 존재하므로 [contentPadding][QuackButtonStyle.contentPadding]이 양쪽 아이콘을 기준으로 적용되어 - * 버튼의 너비가 35dp입니다. (왼쪽 패딩 10dp, 왼쪽 아이콘 5dp, 텍스트 5dp, 오른쪽 아이콘 5dp, - * 오른쪽 패딩 10dp) 즉, 의도하지 않게 버튼의 너비가 10dp 증가하였습니다. 이러한 상황을 예방하기 - * 위해 이 정책이 사용됩니다. - * - * ### 사용 가능 데코레이터 - * - * | style | [icons][Modifier.icons] | description - * | - * | :-------------------------------: | :---------------------: | - * :-----------------------------------------------------: | - * | [Large][QuackLargeButtonStyle] | ⭕ | - * | - * | [Medium][QuackMediumButtonStyle] | ⭕ | - * | - * | [Small][QuackSmallButtonStyle] | ❌ | 버튼의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. - * | - * - * This component uses [QuackButtonStyle.PrimaryLarge] as the token value for `style`. - * - * This document was automatically generated by [QuackButton]. - * If any contents are broken, please check the original document. - * - * @param enabled 활성화 상태 여부 - * @param text 중앙에 표시할 텍스트 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. [enabled]이 true일 때만 작동합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackButton") -public fun QuackPrimaryLargeButton( - modifier: Modifier = sugar(), - enabled: Boolean = sugar(), - @CasaValue("\"QuackButton is experimental\"") text: String, - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackButton( - modifier = modifier, - enabled = enabled, - style = QuackButtonStyle.PrimaryLarge, - text = text, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 버튼을 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - 이 컴포넌트는 자체의 배치 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [버튼의 스타일][QuackButtonStyle]에서 [contentPadding][QuackButtonStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 버튼의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackButtonStyle.contentPadding]은 버튼의 - * 텍스트를 기준으로 패딩이 적용됩니다. 이 부분의 자세한 내용은 배치 정책 세션을 참고하세요. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackButtonStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackButtonStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackButtonStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackButtonStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackButtonStyle.contentPadding]이 - * 무시되고 버튼의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackButtonStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackButtonStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 배치 정책 - * - * [style.contentPadding][QuackButtonStyle.contentPadding]은 항상 버튼의 텍스트를 기준으로 - * 적용됩니다. 예를 들어 버튼의 아이콘을 leading과 trailing을 모두 제공했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(horizontal=10.dp)`를 제공했다면 양끝의 horizontal 패딩이 각각 아이콘을 기준으로 - * 적용되는 게 아닌 버튼의 텍스트를 기준으로 적용됩니다. 따라서 개발자는 [contentPadding][QuackButtonStyle.contentPadding]의 값을 - * 제공할 때 양끝 아이콘을 기준으로 제공하는 게 아닌 가운데 텍스트를 기준으로 제공해야 합니다. - * 이 정책은 양끝 아이콘이 동적으로 적용될 때 의도하지 않는 버튼 사이즈 변경을 예방하기 위해 - * 고안됐습니다. 예를 들어 `contentPadding: QuackPadding(horizontal=10.dp)`을 양끝 아이콘 기준으로 - * 적용했다고 해봅시다. 처음에는 양끝에 아이콘이 없어서 가운데 텍스트를 기준으로 패딩이 적용됩니다. - * 이 시점에는 버튼의 너비가 25dp입니다. (왼쪽 패딩 10dp, 텍스트 5dp, 오른쪽 패딩 10dp) 사용자 - * 요청에 의해 양쪽 모두에 5dp의 너비를 갖는 아이콘이 추가되었습니다. 이 시점에서는 양쪽 아이콘이 - * 존재하므로 [contentPadding][QuackButtonStyle.contentPadding]이 양쪽 아이콘을 기준으로 적용되어 - * 버튼의 너비가 35dp입니다. (왼쪽 패딩 10dp, 왼쪽 아이콘 5dp, 텍스트 5dp, 오른쪽 아이콘 5dp, - * 오른쪽 패딩 10dp) 즉, 의도하지 않게 버튼의 너비가 10dp 증가하였습니다. 이러한 상황을 예방하기 - * 위해 이 정책이 사용됩니다. - * - * ### 사용 가능 데코레이터 - * - * | style | [icons][Modifier.icons] | description - * | - * | :-------------------------------: | :---------------------: | - * :-----------------------------------------------------: | - * | [Large][QuackLargeButtonStyle] | ⭕ | - * | - * | [Medium][QuackMediumButtonStyle] | ⭕ | - * | - * | [Small][QuackSmallButtonStyle] | ❌ | 버튼의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. - * | - * - * This component uses [QuackButtonStyle.SecondaryLarge] as the token value for `style`. - * - * This document was automatically generated by [QuackButton]. - * If any contents are broken, please check the original document. - * - * @param enabled 활성화 상태 여부 - * @param text 중앙에 표시할 텍스트 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. [enabled]이 true일 때만 작동합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackButton") -public fun QuackSecondaryLargeButton( - modifier: Modifier = sugar(), - enabled: Boolean = sugar(), - @CasaValue("\"QuackButton is experimental\"") text: String, - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackButton( - modifier = modifier, - enabled = enabled, - style = QuackButtonStyle.SecondaryLarge, - text = text, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 버튼을 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - 이 컴포넌트는 자체의 배치 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [버튼의 스타일][QuackButtonStyle]에서 [contentPadding][QuackButtonStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 버튼의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackButtonStyle.contentPadding]은 버튼의 - * 텍스트를 기준으로 패딩이 적용됩니다. 이 부분의 자세한 내용은 배치 정책 세션을 참고하세요. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackButtonStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackButtonStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackButtonStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackButtonStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackButtonStyle.contentPadding]이 - * 무시되고 버튼의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackButtonStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackButtonStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 배치 정책 - * - * [style.contentPadding][QuackButtonStyle.contentPadding]은 항상 버튼의 텍스트를 기준으로 - * 적용됩니다. 예를 들어 버튼의 아이콘을 leading과 trailing을 모두 제공했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(horizontal=10.dp)`를 제공했다면 양끝의 horizontal 패딩이 각각 아이콘을 기준으로 - * 적용되는 게 아닌 버튼의 텍스트를 기준으로 적용됩니다. 따라서 개발자는 [contentPadding][QuackButtonStyle.contentPadding]의 값을 - * 제공할 때 양끝 아이콘을 기준으로 제공하는 게 아닌 가운데 텍스트를 기준으로 제공해야 합니다. - * 이 정책은 양끝 아이콘이 동적으로 적용될 때 의도하지 않는 버튼 사이즈 변경을 예방하기 위해 - * 고안됐습니다. 예를 들어 `contentPadding: QuackPadding(horizontal=10.dp)`을 양끝 아이콘 기준으로 - * 적용했다고 해봅시다. 처음에는 양끝에 아이콘이 없어서 가운데 텍스트를 기준으로 패딩이 적용됩니다. - * 이 시점에는 버튼의 너비가 25dp입니다. (왼쪽 패딩 10dp, 텍스트 5dp, 오른쪽 패딩 10dp) 사용자 - * 요청에 의해 양쪽 모두에 5dp의 너비를 갖는 아이콘이 추가되었습니다. 이 시점에서는 양쪽 아이콘이 - * 존재하므로 [contentPadding][QuackButtonStyle.contentPadding]이 양쪽 아이콘을 기준으로 적용되어 - * 버튼의 너비가 35dp입니다. (왼쪽 패딩 10dp, 왼쪽 아이콘 5dp, 텍스트 5dp, 오른쪽 아이콘 5dp, - * 오른쪽 패딩 10dp) 즉, 의도하지 않게 버튼의 너비가 10dp 증가하였습니다. 이러한 상황을 예방하기 - * 위해 이 정책이 사용됩니다. - * - * ### 사용 가능 데코레이터 - * - * | style | [icons][Modifier.icons] | description - * | - * | :-------------------------------: | :---------------------: | - * :-----------------------------------------------------: | - * | [Large][QuackLargeButtonStyle] | ⭕ | - * | - * | [Medium][QuackMediumButtonStyle] | ⭕ | - * | - * | [Small][QuackSmallButtonStyle] | ❌ | 버튼의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. - * | - * - * This component uses [QuackButtonStyle.Medium] as the token value for `style`. - * - * This document was automatically generated by [QuackButton]. - * If any contents are broken, please check the original document. - * - * @param enabled 활성화 상태 여부 - * @param text 중앙에 표시할 텍스트 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. [enabled]이 true일 때만 작동합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackButton") -public fun QuackMediumButton( - modifier: Modifier = sugar(), - enabled: Boolean = sugar(), - @CasaValue("\"QuackButton is experimental\"") text: String, - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackButton( - modifier = modifier, - enabled = enabled, - style = QuackButtonStyle.Medium, - text = text, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 버튼을 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - 이 컴포넌트는 자체의 배치 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [버튼의 스타일][QuackButtonStyle]에서 [contentPadding][QuackButtonStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 버튼의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackButtonStyle.contentPadding]은 버튼의 - * 텍스트를 기준으로 패딩이 적용됩니다. 이 부분의 자세한 내용은 배치 정책 세션을 참고하세요. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackButtonStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackButtonStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackButtonStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackButtonStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackButtonStyle.contentPadding]이 - * 무시되고 버튼의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackButtonStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackButtonStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 배치 정책 - * - * [style.contentPadding][QuackButtonStyle.contentPadding]은 항상 버튼의 텍스트를 기준으로 - * 적용됩니다. 예를 들어 버튼의 아이콘을 leading과 trailing을 모두 제공했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(horizontal=10.dp)`를 제공했다면 양끝의 horizontal 패딩이 각각 아이콘을 기준으로 - * 적용되는 게 아닌 버튼의 텍스트를 기준으로 적용됩니다. 따라서 개발자는 [contentPadding][QuackButtonStyle.contentPadding]의 값을 - * 제공할 때 양끝 아이콘을 기준으로 제공하는 게 아닌 가운데 텍스트를 기준으로 제공해야 합니다. - * 이 정책은 양끝 아이콘이 동적으로 적용될 때 의도하지 않는 버튼 사이즈 변경을 예방하기 위해 - * 고안됐습니다. 예를 들어 `contentPadding: QuackPadding(horizontal=10.dp)`을 양끝 아이콘 기준으로 - * 적용했다고 해봅시다. 처음에는 양끝에 아이콘이 없어서 가운데 텍스트를 기준으로 패딩이 적용됩니다. - * 이 시점에는 버튼의 너비가 25dp입니다. (왼쪽 패딩 10dp, 텍스트 5dp, 오른쪽 패딩 10dp) 사용자 - * 요청에 의해 양쪽 모두에 5dp의 너비를 갖는 아이콘이 추가되었습니다. 이 시점에서는 양쪽 아이콘이 - * 존재하므로 [contentPadding][QuackButtonStyle.contentPadding]이 양쪽 아이콘을 기준으로 적용되어 - * 버튼의 너비가 35dp입니다. (왼쪽 패딩 10dp, 왼쪽 아이콘 5dp, 텍스트 5dp, 오른쪽 아이콘 5dp, - * 오른쪽 패딩 10dp) 즉, 의도하지 않게 버튼의 너비가 10dp 증가하였습니다. 이러한 상황을 예방하기 - * 위해 이 정책이 사용됩니다. - * - * ### 사용 가능 데코레이터 - * - * | style | [icons][Modifier.icons] | description - * | - * | :-------------------------------: | :---------------------: | - * :-----------------------------------------------------: | - * | [Large][QuackLargeButtonStyle] | ⭕ | - * | - * | [Medium][QuackMediumButtonStyle] | ⭕ | - * | - * | [Small][QuackSmallButtonStyle] | ❌ | 버튼의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. - * | - * - * This component uses [QuackButtonStyle.PrimaryFilledSmall] as the token value for `style`. - * - * This document was automatically generated by [QuackButton]. - * If any contents are broken, please check the original document. - * - * @param enabled 활성화 상태 여부 - * @param text 중앙에 표시할 텍스트 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. [enabled]이 true일 때만 작동합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackButton") -public fun QuackPrimaryFilledSmallButton( - modifier: Modifier = sugar(), - enabled: Boolean = sugar(), - @CasaValue("\"QuackButton is experimental\"") text: String, - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackButton( - modifier = modifier, - enabled = enabled, - style = QuackButtonStyle.PrimaryFilledSmall, - text = text, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 버튼을 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - 이 컴포넌트는 자체의 배치 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [버튼의 스타일][QuackButtonStyle]에서 [contentPadding][QuackButtonStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 버튼의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackButtonStyle.contentPadding]은 버튼의 - * 텍스트를 기준으로 패딩이 적용됩니다. 이 부분의 자세한 내용은 배치 정책 세션을 참고하세요. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackButtonStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackButtonStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackButtonStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackButtonStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackButtonStyle.contentPadding]이 - * 무시되고 버튼의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackButtonStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackButtonStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 배치 정책 - * - * [style.contentPadding][QuackButtonStyle.contentPadding]은 항상 버튼의 텍스트를 기준으로 - * 적용됩니다. 예를 들어 버튼의 아이콘을 leading과 trailing을 모두 제공했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(horizontal=10.dp)`를 제공했다면 양끝의 horizontal 패딩이 각각 아이콘을 기준으로 - * 적용되는 게 아닌 버튼의 텍스트를 기준으로 적용됩니다. 따라서 개발자는 [contentPadding][QuackButtonStyle.contentPadding]의 값을 - * 제공할 때 양끝 아이콘을 기준으로 제공하는 게 아닌 가운데 텍스트를 기준으로 제공해야 합니다. - * 이 정책은 양끝 아이콘이 동적으로 적용될 때 의도하지 않는 버튼 사이즈 변경을 예방하기 위해 - * 고안됐습니다. 예를 들어 `contentPadding: QuackPadding(horizontal=10.dp)`을 양끝 아이콘 기준으로 - * 적용했다고 해봅시다. 처음에는 양끝에 아이콘이 없어서 가운데 텍스트를 기준으로 패딩이 적용됩니다. - * 이 시점에는 버튼의 너비가 25dp입니다. (왼쪽 패딩 10dp, 텍스트 5dp, 오른쪽 패딩 10dp) 사용자 - * 요청에 의해 양쪽 모두에 5dp의 너비를 갖는 아이콘이 추가되었습니다. 이 시점에서는 양쪽 아이콘이 - * 존재하므로 [contentPadding][QuackButtonStyle.contentPadding]이 양쪽 아이콘을 기준으로 적용되어 - * 버튼의 너비가 35dp입니다. (왼쪽 패딩 10dp, 왼쪽 아이콘 5dp, 텍스트 5dp, 오른쪽 아이콘 5dp, - * 오른쪽 패딩 10dp) 즉, 의도하지 않게 버튼의 너비가 10dp 증가하였습니다. 이러한 상황을 예방하기 - * 위해 이 정책이 사용됩니다. - * - * ### 사용 가능 데코레이터 - * - * | style | [icons][Modifier.icons] | description - * | - * | :-------------------------------: | :---------------------: | - * :-----------------------------------------------------: | - * | [Large][QuackLargeButtonStyle] | ⭕ | - * | - * | [Medium][QuackMediumButtonStyle] | ⭕ | - * | - * | [Small][QuackSmallButtonStyle] | ❌ | 버튼의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. - * | - * - * This component uses [QuackButtonStyle.PrimaryOutlinedSmall] as the token value for `style`. - * - * This document was automatically generated by [QuackButton]. - * If any contents are broken, please check the original document. - * - * @param enabled 활성화 상태 여부 - * @param text 중앙에 표시할 텍스트 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. [enabled]이 true일 때만 작동합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackButton") -public fun QuackPrimaryOutlinedSmallButton( - modifier: Modifier = sugar(), - enabled: Boolean = sugar(), - @CasaValue("\"QuackButton is experimental\"") text: String, - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackButton( - modifier = modifier, - enabled = enabled, - style = QuackButtonStyle.PrimaryOutlinedSmall, - text = text, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 버튼을 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - 이 컴포넌트는 자체의 배치 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [버튼의 스타일][QuackButtonStyle]에서 [contentPadding][QuackButtonStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 버튼의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackButtonStyle.contentPadding]은 버튼의 - * 텍스트를 기준으로 패딩이 적용됩니다. 이 부분의 자세한 내용은 배치 정책 세션을 참고하세요. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackButtonStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackButtonStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackButtonStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackButtonStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackButtonStyle.contentPadding]이 - * 무시되고 버튼의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackButtonStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackButtonStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 배치 정책 - * - * [style.contentPadding][QuackButtonStyle.contentPadding]은 항상 버튼의 텍스트를 기준으로 - * 적용됩니다. 예를 들어 버튼의 아이콘을 leading과 trailing을 모두 제공했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(horizontal=10.dp)`를 제공했다면 양끝의 horizontal 패딩이 각각 아이콘을 기준으로 - * 적용되는 게 아닌 버튼의 텍스트를 기준으로 적용됩니다. 따라서 개발자는 [contentPadding][QuackButtonStyle.contentPadding]의 값을 - * 제공할 때 양끝 아이콘을 기준으로 제공하는 게 아닌 가운데 텍스트를 기준으로 제공해야 합니다. - * 이 정책은 양끝 아이콘이 동적으로 적용될 때 의도하지 않는 버튼 사이즈 변경을 예방하기 위해 - * 고안됐습니다. 예를 들어 `contentPadding: QuackPadding(horizontal=10.dp)`을 양끝 아이콘 기준으로 - * 적용했다고 해봅시다. 처음에는 양끝에 아이콘이 없어서 가운데 텍스트를 기준으로 패딩이 적용됩니다. - * 이 시점에는 버튼의 너비가 25dp입니다. (왼쪽 패딩 10dp, 텍스트 5dp, 오른쪽 패딩 10dp) 사용자 - * 요청에 의해 양쪽 모두에 5dp의 너비를 갖는 아이콘이 추가되었습니다. 이 시점에서는 양쪽 아이콘이 - * 존재하므로 [contentPadding][QuackButtonStyle.contentPadding]이 양쪽 아이콘을 기준으로 적용되어 - * 버튼의 너비가 35dp입니다. (왼쪽 패딩 10dp, 왼쪽 아이콘 5dp, 텍스트 5dp, 오른쪽 아이콘 5dp, - * 오른쪽 패딩 10dp) 즉, 의도하지 않게 버튼의 너비가 10dp 증가하였습니다. 이러한 상황을 예방하기 - * 위해 이 정책이 사용됩니다. - * - * ### 사용 가능 데코레이터 - * - * | style | [icons][Modifier.icons] | description - * | - * | :-------------------------------: | :---------------------: | - * :-----------------------------------------------------: | - * | [Large][QuackLargeButtonStyle] | ⭕ | - * | - * | [Medium][QuackMediumButtonStyle] | ⭕ | - * | - * | [Small][QuackSmallButtonStyle] | ❌ | 버튼의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. - * | - * - * This component uses [QuackButtonStyle.PrimaryOutlinedRoundSmall] as the token value for `style`. - * - * This document was automatically generated by [QuackButton]. - * If any contents are broken, please check the original document. - * - * @param enabled 활성화 상태 여부 - * @param text 중앙에 표시할 텍스트 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. [enabled]이 true일 때만 작동합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackButton") -public fun QuackPrimaryOutlinedRoundSmallButton( - modifier: Modifier = sugar(), - enabled: Boolean = sugar(), - @CasaValue("\"QuackButton is experimental\"") text: String, - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackButton( - modifier = modifier, - enabled = enabled, - style = QuackButtonStyle.PrimaryOutlinedRoundSmall, - text = text, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 버튼을 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - 이 컴포넌트는 자체의 배치 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [버튼의 스타일][QuackButtonStyle]에서 [contentPadding][QuackButtonStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 버튼의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackButtonStyle.contentPadding]은 버튼의 - * 텍스트를 기준으로 패딩이 적용됩니다. 이 부분의 자세한 내용은 배치 정책 세션을 참고하세요. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackButtonStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackButtonStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackButtonStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackButtonStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackButtonStyle.contentPadding]이 - * 무시되고 버튼의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackButtonStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackButtonStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 배치 정책 - * - * [style.contentPadding][QuackButtonStyle.contentPadding]은 항상 버튼의 텍스트를 기준으로 - * 적용됩니다. 예를 들어 버튼의 아이콘을 leading과 trailing을 모두 제공했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(horizontal=10.dp)`를 제공했다면 양끝의 horizontal 패딩이 각각 아이콘을 기준으로 - * 적용되는 게 아닌 버튼의 텍스트를 기준으로 적용됩니다. 따라서 개발자는 [contentPadding][QuackButtonStyle.contentPadding]의 값을 - * 제공할 때 양끝 아이콘을 기준으로 제공하는 게 아닌 가운데 텍스트를 기준으로 제공해야 합니다. - * 이 정책은 양끝 아이콘이 동적으로 적용될 때 의도하지 않는 버튼 사이즈 변경을 예방하기 위해 - * 고안됐습니다. 예를 들어 `contentPadding: QuackPadding(horizontal=10.dp)`을 양끝 아이콘 기준으로 - * 적용했다고 해봅시다. 처음에는 양끝에 아이콘이 없어서 가운데 텍스트를 기준으로 패딩이 적용됩니다. - * 이 시점에는 버튼의 너비가 25dp입니다. (왼쪽 패딩 10dp, 텍스트 5dp, 오른쪽 패딩 10dp) 사용자 - * 요청에 의해 양쪽 모두에 5dp의 너비를 갖는 아이콘이 추가되었습니다. 이 시점에서는 양쪽 아이콘이 - * 존재하므로 [contentPadding][QuackButtonStyle.contentPadding]이 양쪽 아이콘을 기준으로 적용되어 - * 버튼의 너비가 35dp입니다. (왼쪽 패딩 10dp, 왼쪽 아이콘 5dp, 텍스트 5dp, 오른쪽 아이콘 5dp, - * 오른쪽 패딩 10dp) 즉, 의도하지 않게 버튼의 너비가 10dp 증가하였습니다. 이러한 상황을 예방하기 - * 위해 이 정책이 사용됩니다. - * - * ### 사용 가능 데코레이터 - * - * | style | [icons][Modifier.icons] | description - * | - * | :-------------------------------: | :---------------------: | - * :-----------------------------------------------------: | - * | [Large][QuackLargeButtonStyle] | ⭕ | - * | - * | [Medium][QuackMediumButtonStyle] | ⭕ | - * | - * | [Small][QuackSmallButtonStyle] | ❌ | 버튼의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. - * | - * - * This component uses [QuackButtonStyle.SecondarySmall] as the token value for `style`. - * - * This document was automatically generated by [QuackButton]. - * If any contents are broken, please check the original document. - * - * @param enabled 활성화 상태 여부 - * @param text 중앙에 표시할 텍스트 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. [enabled]이 true일 때만 작동합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackButton") -public fun QuackSecondarySmallButton( - modifier: Modifier = sugar(), - enabled: Boolean = sugar(), - @CasaValue("\"QuackButton is experimental\"") text: String, - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackButton( - modifier = modifier, - enabled = enabled, - style = QuackButtonStyle.SecondarySmall, - text = text, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 버튼을 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - 이 컴포넌트는 자체의 배치 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [버튼의 스타일][QuackButtonStyle]에서 [contentPadding][QuackButtonStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 버튼의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackButtonStyle.contentPadding]은 버튼의 - * 텍스트를 기준으로 패딩이 적용됩니다. 이 부분의 자세한 내용은 배치 정책 세션을 참고하세요. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackButtonStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackButtonStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackButtonStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackButtonStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackButtonStyle.contentPadding]이 - * 무시되고 버튼의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackButtonStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackButtonStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 배치 정책 - * - * [style.contentPadding][QuackButtonStyle.contentPadding]은 항상 버튼의 텍스트를 기준으로 - * 적용됩니다. 예를 들어 버튼의 아이콘을 leading과 trailing을 모두 제공했고, - * [contentPadding][QuackButtonStyle.contentPadding]으로 - * `QuackPadding(horizontal=10.dp)`를 제공했다면 양끝의 horizontal 패딩이 각각 아이콘을 기준으로 - * 적용되는 게 아닌 버튼의 텍스트를 기준으로 적용됩니다. 따라서 개발자는 [contentPadding][QuackButtonStyle.contentPadding]의 값을 - * 제공할 때 양끝 아이콘을 기준으로 제공하는 게 아닌 가운데 텍스트를 기준으로 제공해야 합니다. - * 이 정책은 양끝 아이콘이 동적으로 적용될 때 의도하지 않는 버튼 사이즈 변경을 예방하기 위해 - * 고안됐습니다. 예를 들어 `contentPadding: QuackPadding(horizontal=10.dp)`을 양끝 아이콘 기준으로 - * 적용했다고 해봅시다. 처음에는 양끝에 아이콘이 없어서 가운데 텍스트를 기준으로 패딩이 적용됩니다. - * 이 시점에는 버튼의 너비가 25dp입니다. (왼쪽 패딩 10dp, 텍스트 5dp, 오른쪽 패딩 10dp) 사용자 - * 요청에 의해 양쪽 모두에 5dp의 너비를 갖는 아이콘이 추가되었습니다. 이 시점에서는 양쪽 아이콘이 - * 존재하므로 [contentPadding][QuackButtonStyle.contentPadding]이 양쪽 아이콘을 기준으로 적용되어 - * 버튼의 너비가 35dp입니다. (왼쪽 패딩 10dp, 왼쪽 아이콘 5dp, 텍스트 5dp, 오른쪽 아이콘 5dp, - * 오른쪽 패딩 10dp) 즉, 의도하지 않게 버튼의 너비가 10dp 증가하였습니다. 이러한 상황을 예방하기 - * 위해 이 정책이 사용됩니다. - * - * ### 사용 가능 데코레이터 - * - * | style | [icons][Modifier.icons] | description - * | - * | :-------------------------------: | :---------------------: | - * :-----------------------------------------------------: | - * | [Large][QuackLargeButtonStyle] | ⭕ | - * | - * | [Medium][QuackMediumButtonStyle] | ⭕ | - * | - * | [Small][QuackSmallButtonStyle] | ❌ | 버튼의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. - * | - * - * This component uses [QuackButtonStyle.SecondaryRoundSmall] as the token value for `style`. - * - * This document was automatically generated by [QuackButton]. - * If any contents are broken, please check the original document. - * - * @param enabled 활성화 상태 여부 - * @param text 중앙에 표시할 텍스트 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. [enabled]이 true일 때만 작동합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackButton") -public fun QuackSecondaryRoundSmallButton( - modifier: Modifier = sugar(), - enabled: Boolean = sugar(), - @CasaValue("\"QuackButton is experimental\"") text: String, - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackButton( - modifier = modifier, - enabled = enabled, - style = QuackButtonStyle.SecondaryRoundSmall, - text = text, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/tag.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/tag.kt deleted file mode 100644 index 71f02684d..000000000 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/tag.kt +++ /dev/null @@ -1,368 +0,0 @@ -// This file was automatically generated by sugar-processor. -// Do not modify it manually. -// @formatter:off -@file:Suppress("NoConsecutiveBlankLines", "PackageDirectoryMismatch", "Wrapping", - "TrailingCommaOnCallSite", "ArgumentListWrapping", "RedundantVisibilityModifier", - "UnusedImport", "NoUnusedImports", "SpacingAroundParens", "Indentation", "NoUnitReturn", - "RedundantUnitReturnType", "ModifierParameter", "KDocUnresolvedReference", "NoTrailingSpaces", - "NoMultipleSpaces", "ktlint") -@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) -@file:SugarGeneratedFile - -package team.duckie.quackquack.ui.sugar - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.ui.Modifier -import kotlin.Boolean -import kotlin.Function0 -import kotlin.OptIn -import kotlin.String -import kotlin.Suppress -import kotlin.Unit -import team.duckie.quackquack.casa.`annotation`.Casa -import team.duckie.quackquack.casa.`annotation`.CasaValue -import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.sugar -import team.duckie.quackquack.ui.QuackTag -import team.duckie.quackquack.ui.QuackTagStyle -import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi - -/** - * 태그를 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [태그의 스타일][QuackTagStyle]에서 [contentPadding][QuackTagStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 태그의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackTagStyle.contentPadding]은 태그의 - * 텍스트와 후행 아이콘을 기준으로 적용됩니다. 태그 컴포넌트는 [trailingIcon][Modifier.trailingIcon] 데코레이터로 - * 후행 아이콘을 추가할 수 있고, 후행 아이콘 여부에 따라 패딩 정책이 결정됩니다. 후행 아이콘이 있다면 세로와 - * 가로에 따라 패딩을 적용하는 방식이 달라집니다. 세로의 경우는 태그 텍스트를 기준으로 적용되고, 가로의 경우는 - * 후행 아이콘의 터치 영역을 증가시키는 식으로 적용됩니다. 기본적으로 후행 아이콘은 16px의 사이즈를 갖습니다. - * 유저 입장에서 16px의 터치 영역은 좋은 경험을 제공하지 못할 것으로 예상하여, [전체 가로 패딩][QuackPadding.vertical]의 - * 오른쪽 영역을 후행 아이콘의 오른쪽 패딩으로 적용합니다. 이때, [전체 가로 패딩][QuackPadding.vertical]의 오른쪽 - * 영역을 그대로 적용하는 게 아니라 해당 값에서 [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy]을 뺀 - * 값을 적용합니다. 이는 디자인 가이드라인에 의거합니다. 그리고 [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy]의 - * 반을 후행 아이콘의 왼쪽 패딩으로 적용합니다. [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy] 반의 - * 나머지 부분은 태그 텍스트의 오른쪽 패딩으로 적용됩니다. 후행 아이콘이 없다면 단순히 태그 텍스트를 기준으로 패딩이 - * 적용됩니다. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackTagStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackTagStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackTagStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackTagStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackTagStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackTagStyle.contentPadding]이 - * 무시되고 태그의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackTagStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackTagStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 사용 가능 데코레이터 - * - * | style | [trailingIcon][Modifier.trailingIcon] - * | description | - * |:------------------------------------------------------:|:-------------------------------------:|:----------------------------------:| - * | [Outlined][QuackOutlinedTagDefaults] | ⭕ - * | | - * | [Filled][QuackFilledTagDefaults] | ⭕ - * | | - * | [GrayscaleFlat][QuackGrayscaleFlatTagDefaults] | ❌ - * | 태그의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. | - * | [GrayscaleOutlined][QuackGrayscaleOutlinedTagDefaults] | ⭕ - * | | - * - * This component uses [QuackTagStyle.Outlined] as the token value for `style`. - * - * This document was automatically generated by [QuackTag]. - * If any contents are broken, please check the original document. - * - * @param text 중앙에 표시할 텍스트 - * @param selected 선택 상태 여부 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. 태그는 토글이 자유로워야 하므로 [selected]와 관계 없이 - * 항상 클릭 가능합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackTag") -public fun QuackOutlinedTag( - @CasaValue("\"QuackTagPreview\"") text: String, - modifier: Modifier = sugar(), - selected: Boolean = sugar(), - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackTag( - text = text, - style = QuackTagStyle.Outlined, - modifier = modifier, - selected = selected, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 태그를 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [태그의 스타일][QuackTagStyle]에서 [contentPadding][QuackTagStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 태그의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackTagStyle.contentPadding]은 태그의 - * 텍스트와 후행 아이콘을 기준으로 적용됩니다. 태그 컴포넌트는 [trailingIcon][Modifier.trailingIcon] 데코레이터로 - * 후행 아이콘을 추가할 수 있고, 후행 아이콘 여부에 따라 패딩 정책이 결정됩니다. 후행 아이콘이 있다면 세로와 - * 가로에 따라 패딩을 적용하는 방식이 달라집니다. 세로의 경우는 태그 텍스트를 기준으로 적용되고, 가로의 경우는 - * 후행 아이콘의 터치 영역을 증가시키는 식으로 적용됩니다. 기본적으로 후행 아이콘은 16px의 사이즈를 갖습니다. - * 유저 입장에서 16px의 터치 영역은 좋은 경험을 제공하지 못할 것으로 예상하여, [전체 가로 패딩][QuackPadding.vertical]의 - * 오른쪽 영역을 후행 아이콘의 오른쪽 패딩으로 적용합니다. 이때, [전체 가로 패딩][QuackPadding.vertical]의 오른쪽 - * 영역을 그대로 적용하는 게 아니라 해당 값에서 [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy]을 뺀 - * 값을 적용합니다. 이는 디자인 가이드라인에 의거합니다. 그리고 [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy]의 - * 반을 후행 아이콘의 왼쪽 패딩으로 적용합니다. [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy] 반의 - * 나머지 부분은 태그 텍스트의 오른쪽 패딩으로 적용됩니다. 후행 아이콘이 없다면 단순히 태그 텍스트를 기준으로 패딩이 - * 적용됩니다. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackTagStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackTagStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackTagStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackTagStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackTagStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackTagStyle.contentPadding]이 - * 무시되고 태그의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackTagStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackTagStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 사용 가능 데코레이터 - * - * | style | [trailingIcon][Modifier.trailingIcon] - * | description | - * |:------------------------------------------------------:|:-------------------------------------:|:----------------------------------:| - * | [Outlined][QuackOutlinedTagDefaults] | ⭕ - * | | - * | [Filled][QuackFilledTagDefaults] | ⭕ - * | | - * | [GrayscaleFlat][QuackGrayscaleFlatTagDefaults] | ❌ - * | 태그의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. | - * | [GrayscaleOutlined][QuackGrayscaleOutlinedTagDefaults] | ⭕ - * | | - * - * This component uses [QuackTagStyle.Filled] as the token value for `style`. - * - * This document was automatically generated by [QuackTag]. - * If any contents are broken, please check the original document. - * - * @param text 중앙에 표시할 텍스트 - * @param selected 선택 상태 여부 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. 태그는 토글이 자유로워야 하므로 [selected]와 관계 없이 - * 항상 클릭 가능합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackTag") -public fun QuackFilledTag( - @CasaValue("\"QuackTagPreview\"") text: String, - modifier: Modifier = sugar(), - selected: Boolean = sugar(), - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackTag( - text = text, - style = QuackTagStyle.Filled, - modifier = modifier, - selected = selected, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 태그를 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [태그의 스타일][QuackTagStyle]에서 [contentPadding][QuackTagStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 태그의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackTagStyle.contentPadding]은 태그의 - * 텍스트와 후행 아이콘을 기준으로 적용됩니다. 태그 컴포넌트는 [trailingIcon][Modifier.trailingIcon] 데코레이터로 - * 후행 아이콘을 추가할 수 있고, 후행 아이콘 여부에 따라 패딩 정책이 결정됩니다. 후행 아이콘이 있다면 세로와 - * 가로에 따라 패딩을 적용하는 방식이 달라집니다. 세로의 경우는 태그 텍스트를 기준으로 적용되고, 가로의 경우는 - * 후행 아이콘의 터치 영역을 증가시키는 식으로 적용됩니다. 기본적으로 후행 아이콘은 16px의 사이즈를 갖습니다. - * 유저 입장에서 16px의 터치 영역은 좋은 경험을 제공하지 못할 것으로 예상하여, [전체 가로 패딩][QuackPadding.vertical]의 - * 오른쪽 영역을 후행 아이콘의 오른쪽 패딩으로 적용합니다. 이때, [전체 가로 패딩][QuackPadding.vertical]의 오른쪽 - * 영역을 그대로 적용하는 게 아니라 해당 값에서 [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy]을 뺀 - * 값을 적용합니다. 이는 디자인 가이드라인에 의거합니다. 그리고 [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy]의 - * 반을 후행 아이콘의 왼쪽 패딩으로 적용합니다. [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy] 반의 - * 나머지 부분은 태그 텍스트의 오른쪽 패딩으로 적용됩니다. 후행 아이콘이 없다면 단순히 태그 텍스트를 기준으로 패딩이 - * 적용됩니다. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackTagStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackTagStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackTagStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackTagStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackTagStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackTagStyle.contentPadding]이 - * 무시되고 태그의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackTagStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackTagStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 사용 가능 데코레이터 - * - * | style | [trailingIcon][Modifier.trailingIcon] - * | description | - * |:------------------------------------------------------:|:-------------------------------------:|:----------------------------------:| - * | [Outlined][QuackOutlinedTagDefaults] | ⭕ - * | | - * | [Filled][QuackFilledTagDefaults] | ⭕ - * | | - * | [GrayscaleFlat][QuackGrayscaleFlatTagDefaults] | ❌ - * | 태그의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. | - * | [GrayscaleOutlined][QuackGrayscaleOutlinedTagDefaults] | ⭕ - * | | - * - * This component uses [QuackTagStyle.GrayscaleFlat] as the token value for `style`. - * - * This document was automatically generated by [QuackTag]. - * If any contents are broken, please check the original document. - * - * @param text 중앙에 표시할 텍스트 - * @param selected 선택 상태 여부 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. 태그는 토글이 자유로워야 하므로 [selected]와 관계 없이 - * 항상 클릭 가능합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackTag") -public fun QuackGrayscaleFlatTag( - @CasaValue("\"QuackTagPreview\"") text: String, - modifier: Modifier = sugar(), - selected: Boolean = sugar(), - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackTag( - text = text, - style = QuackTagStyle.GrayscaleFlat, - modifier = modifier, - selected = selected, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} - -/** - * 태그를 그립니다. - * - * - 이 컴포넌트는 자체의 패딩 정책을 구현합니다. - * - [스타일][style]별로 사용 가능한 데코레이터가 달라집니다. - * - * ### 패딩 정책 - * - * 1. [태그의 스타일][QuackTagStyle]에서 [contentPadding][QuackTagStyle.contentPadding] 옵션을 - * 별도로 제공하고 있습니다. 이는 [Modifier.padding]과 다른 패딩 정책을 사용합니다. [Modifier.padding]은 - * 태그의 루트 레이아웃을 기준으로 패딩이 적용되지만, [QuackTagStyle.contentPadding]은 태그의 - * 텍스트와 후행 아이콘을 기준으로 적용됩니다. 태그 컴포넌트는 [trailingIcon][Modifier.trailingIcon] 데코레이터로 - * 후행 아이콘을 추가할 수 있고, 후행 아이콘 여부에 따라 패딩 정책이 결정됩니다. 후행 아이콘이 있다면 세로와 - * 가로에 따라 패딩을 적용하는 방식이 달라집니다. 세로의 경우는 태그 텍스트를 기준으로 적용되고, 가로의 경우는 - * 후행 아이콘의 터치 영역을 증가시키는 식으로 적용됩니다. 기본적으로 후행 아이콘은 16px의 사이즈를 갖습니다. - * 유저 입장에서 16px의 터치 영역은 좋은 경험을 제공하지 못할 것으로 예상하여, [전체 가로 패딩][QuackPadding.vertical]의 - * 오른쪽 영역을 후행 아이콘의 오른쪽 패딩으로 적용합니다. 이때, [전체 가로 패딩][QuackPadding.vertical]의 오른쪽 - * 영역을 그대로 적용하는 게 아니라 해당 값에서 [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy]을 뺀 - * 값을 적용합니다. 이는 디자인 가이드라인에 의거합니다. 그리고 [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy]의 - * 반을 후행 아이콘의 왼쪽 패딩으로 적용합니다. [텍스트와 후행 아이콘 사이 공간][QuackTagStyle.iconSpacedBy] 반의 - * 나머지 부분은 태그 텍스트의 오른쪽 패딩으로 적용됩니다. 후행 아이콘이 없다면 단순히 태그 텍스트를 기준으로 패딩이 - * 적용됩니다. - * 2. [LayoutModifier]를 사용하여 컴포넌트의 사이즈가 명시됐다면 [QuackTagStyle.contentPadding] - * 옵션은 무시됩니다. [contentPadding][QuackTagStyle.contentPadding]은 컴포넌트 사이즈 하드코딩을 - * 대체하는 용도로 제공됩니다. 하지만 컴포넌트 사이즈가 하드코딩됐다면 [contentPadding][QuackTagStyle.contentPadding]을 - * 제공하는 의미가 없어집니다. 따라서 컴포넌트의 사이즈가 하드코딩됐다면 개발자의 의도를 존중한다는 원칙하에 - * 컴포넌트의 사이즈가 중첩으로 확장되는 일을 예방하고자 [contentPadding][QuackTagStyle.contentPadding] - * 옵션을 무시합니다. 예를 들어 `Modifier.height(10.dp)`로 컴포넌트 높이를 명시했고, - * [contentPadding][QuackTagStyle.contentPadding]으로 - * `QuackPadding(vertical=10.dp)`을 제공했다고 해봅시다. 이런 경우에는 - * [contentPadding][QuackTagStyle.contentPadding]이 - * 무시되고 태그의 높이가 10dp로 적용됩니다. 컴포넌트 사이즈를 명시하면서 패딩을 적용하고 싶다면 - * [contentPadding][QuackTagStyle.contentPadding] 대신에 [Modifier.padding]을 사용하세요. - * [LayoutModifier]를 사용하는 흔한 [Modifier]로는 [Modifier.size], [Modifier.height], [Modifier.width] 등이 - * 있습니다. [LayoutModifierNode]를 사용하는 [Modifier]는 [contentPadding][QuackTagStyle.contentPadding] 무시 - * 옵션이 아직 지원되지 않습니다. ([#636](https://github.com/duckie-team/quack-quack-android/issues/636)) - * - * ### 사용 가능 데코레이터 - * - * | style | [trailingIcon][Modifier.trailingIcon] - * | description | - * |:------------------------------------------------------:|:-------------------------------------:|:----------------------------------:| - * | [Outlined][QuackOutlinedTagDefaults] | ⭕ - * | | - * | [Filled][QuackFilledTagDefaults] | ⭕ - * | | - * | [GrayscaleFlat][QuackGrayscaleFlatTagDefaults] | ❌ - * | 태그의 너비가 좁기에 아이콘 데코레이터를 사용할 수 없습니다. | - * | [GrayscaleOutlined][QuackGrayscaleOutlinedTagDefaults] | ⭕ - * | | - * - * This component uses [QuackTagStyle.GrayscaleOutlined] as the token value for `style`. - * - * This document was automatically generated by [QuackTag]. - * If any contents are broken, please check the original document. - * - * @param text 중앙에 표시할 텍스트 - * @param selected 선택 상태 여부 - * @param rippleEnabled 클릭했을 때 리플 애니메이션을 적용할지 여부 - * @param onClick 클릭했을 때 실행할 람다식. 태그는 토글이 자유로워야 하므로 [selected]와 관계 없이 - * 항상 클릭 가능합니다. - */ -@Casa -@Composable -@NonRestartableComposable -@ExperimentalQuackQuackApi -@SugarRefer("team.duckie.quackquack.ui.QuackTag") -public fun QuackGrayscaleOutlinedTag( - @CasaValue("\"QuackTagPreview\"") text: String, - modifier: Modifier = sugar(), - selected: Boolean = sugar(), - rippleEnabled: Boolean = sugar(), - @CasaValue("{}") onClick: () -> Unit, -): Unit { - QuackTag( - text = text, - style = QuackTagStyle.GrayscaleOutlined, - modifier = modifier, - selected = selected, - rippleEnabled = rippleEnabled, - onClick = onClick, - ) -} diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/text.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/text.kt deleted file mode 100644 index 772d7a3a5..000000000 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/sugar/text.kt +++ /dev/null @@ -1,421 +0,0 @@ -// This file was automatically generated by sugar-processor. -// Do not modify it manually. -// @formatter:off -@file:Suppress("NoConsecutiveBlankLines", "PackageDirectoryMismatch", "Wrapping", - "TrailingCommaOnCallSite", "ArgumentListWrapping", "RedundantVisibilityModifier", - "UnusedImport", "NoUnusedImports", "SpacingAroundParens", "Indentation", "NoUnitReturn", - "RedundantUnitReturnType", "ModifierParameter", "KDocUnresolvedReference", "NoTrailingSpaces", - "NoMultipleSpaces", "ktlint") -@file:OptIn(SugarCompilerApi::class, SugarGeneratorUsage::class) -@file:SugarGeneratedFile - -package team.duckie.quackquack.ui.sugar - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import kotlin.Boolean -import kotlin.OptIn -import kotlin.String -import kotlin.Suppress -import kotlin.Unit -import team.duckie.quackquack.casa.`annotation`.Casa -import team.duckie.quackquack.casa.`annotation`.CasaValue -import team.duckie.quackquack.casa.`annotation`.SugarGeneratorUsage -import team.duckie.quackquack.material.QuackTypography -import team.duckie.quackquack.sugar.material.SugarCompilerApi -import team.duckie.quackquack.sugar.material.SugarGeneratedFile -import team.duckie.quackquack.sugar.material.SugarRefer -import team.duckie.quackquack.sugar.material.sugar -import team.duckie.quackquack.ui.QuackText - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.Body1] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackBody1( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.Body1, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.Body2] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackBody2( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.Body2, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.Body3] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackBody3( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.Body3, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.HeadLine1] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackHeadLine1( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.HeadLine1, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.HeadLine2] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackHeadLine2( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.HeadLine2, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.Large1] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackLarge1( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.Large1, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.Subtitle] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackSubtitle( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.Subtitle, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.Subtitle2] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackSubtitle2( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.Subtitle2, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.Title1] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackTitle1( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.Title1, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} - -/** - * 텍스트를 그리는 기본적인 컴포저블입니다. - * - * This component uses [QuackTypography.Title2] as the token value for `typography`. - * - * This document was automatically generated by [QuackText]. - * If any contents are broken, please check the original document. - * - * @param text 그릴 텍스트 - * @param singleLine 텍스트가 한 줄로 그려질 지 여부. 텍스트가 주어진 줄 수를 초과하면 - * [softWrap] 및 [overflow]에 따라 잘립니다. - * @param softWrap 텍스트에 softwrap break를 적용할지 여부. `false`이면 텍스트 글리프가 - * 가로 공간이 무제한인 것처럼 배치됩니다. 또한 [overflow] 및 [TextAlign]에 예기치 않은 - * 효과가 발생할 수 있습니다. - * @param overflow 시각적 overflow를 처리하는 방법 - * - * @sample team.duckie.quackquack.ui.sample.SampleTest - */ -@Casa -@Composable -@NonRestartableComposable -@SugarRefer("team.duckie.quackquack.ui.QuackText") -public fun QuackTitle2( - modifier: Modifier = sugar(), - @CasaValue("\"QuackText\"") text: String, - singleLine: Boolean = sugar(), - softWrap: Boolean = sugar(), - overflow: TextOverflow = sugar(), -): Unit { - QuackText( - modifier = modifier, - text = text, - typography = QuackTypography.Title2, - singleLine = singleLine, - softWrap = softWrap, - overflow = overflow, - ) -} diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/switch.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/switch.kt index 3f8b4a0e1..c0160ec70 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/switch.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/switch.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import team.duckie.quackquack.material.QuackColor import team.duckie.quackquack.material.quackClickable -import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.onDrawFront @@ -126,7 +125,6 @@ private val colorTweenSpec = tween(durationMillis = AnimationMillis) * @param onClick 스위치가 클릭됐을 때 실행할 람다식. 토글은 stable 릴리스 전에 지원될 예정입니다. */ // TODO(impl): anchored-draggable 지원 (toggle) -@NoSugar @Composable public fun QuackSwitch( enabled: Boolean, diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/tab.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/tab.kt index 1a05c0452..49b973c2b 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/tab.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/tab.kt @@ -48,7 +48,6 @@ import team.duckie.quackquack.animation.animatedQuackTypographyAsState import team.duckie.quackquack.material.QuackColor import team.duckie.quackquack.material.QuackTypography import team.duckie.quackquack.material.quackClickable -import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.fastFilterById import team.duckie.quackquack.ui.util.onDrawFront @@ -205,7 +204,6 @@ private val tabSnapSpec = snap() * * 자세한 내용은 [QuackTabScope]를 참고하세요. */ -@NoSugar @Composable public fun QuackTab( index: Int, diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt index e92df7ce2..7f0d8561f 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt @@ -52,8 +52,8 @@ import team.duckie.quackquack.material.quackClickable import team.duckie.quackquack.material.quackSurface import team.duckie.quackquack.runtime.QuackDataModifierModel import team.duckie.quackquack.runtime.quackMaterializeOf -import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi import team.duckie.quackquack.ui.util.QuackDsl @@ -542,6 +542,7 @@ internal object QuackTagErrors { * @param onClick 클릭했을 때 실행할 람다식. 태그는 토글이 자유로워야 하므로 [selected]와 관계 없이 * 항상 클릭 가능합니다. */ +@Sugarable @ExperimentalQuackQuackApi @NonRestartableComposable @Composable @@ -648,7 +649,6 @@ private const val FakeTrailingIconLayoutId = "QuackBaseTagFakeTrailingIcon" * 이 컴포넌트는 [QuackTagStyle]의 필드를 개별 인자로 받습니다. */ @ExperimentalQuackQuackApi -@NoSugar @Composable public fun QuackBaseTag( modifier: Modifier, diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/text.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/text.kt index f96f198d3..7bc457dda 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/text.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/text.kt @@ -39,6 +39,7 @@ import team.duckie.quackquack.runtime.quackComposed import team.duckie.quackquack.runtime.quackMaterializeOf import team.duckie.quackquack.sugar.material.SugarName import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.asLoose import team.duckie.quackquack.ui.util.rememberLtrTextMeasurer @@ -166,6 +167,7 @@ internal object QuackTextErrors { * * @sample team.duckie.quackquack.ui.sample.SampleTest */ +@Sugarable @SugarName(SugarName.PREFIX_NAME + SugarName.TOKEN_NAME) @Composable public fun QuackText( diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/textfield.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/textfield.kt index 7314f1bb5..0c8950a18 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/textfield.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/textfield.kt @@ -84,8 +84,8 @@ import team.duckie.quackquack.material.quackSurface import team.duckie.quackquack.material.theme.LocalQuackTextFieldTheme import team.duckie.quackquack.runtime.QuackDataModifierModel import team.duckie.quackquack.runtime.quackMaterializeOf -import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.sugar.material.Sugarable import team.duckie.quackquack.ui.optin.ExperimentalDesignToken import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.token.HorizontalDirection @@ -876,6 +876,7 @@ public fun Modifier.counter( * @param interactionSource 이 텍스트 필드의 인터랙션 스트림을 나타내는 변경 가능한 인터랙션 소스입니다. 인터랙션을 관찰하고 * 다른 인터랙션에서 이 텍스트 필드의 모양/동작을 커스터마이징하려면 자신만의 변경 가능한 인터랙션 소스를 생성하여 전달할 수 있습니다. */ +@Sugarable @ExperimentalDesignToken @ExperimentalQuackQuackApi @NonRestartableComposable @@ -1023,7 +1024,7 @@ public fun