diff options
author | Linnea Gräf <nea@nea.moe> | 2024-10-30 00:00:54 +0100 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2024-10-30 00:00:54 +0100 |
commit | 410f6a0dd1e5288df7c3fc90bd3937a97b2e6385 (patch) | |
tree | cc3db28a82d28dd59528aa3580878cf4fff8980a /kotlin-plugin | |
download | mcautotranslations-410f6a0dd1e5288df7c3fc90bd3937a97b2e6385.tar.gz mcautotranslations-410f6a0dd1e5288df7c3fc90bd3937a97b2e6385.tar.bz2 mcautotranslations-410f6a0dd1e5288df7c3fc90bd3937a97b2e6385.zip |
Init
Diffstat (limited to 'kotlin-plugin')
7 files changed, 348 insertions, 0 deletions
diff --git a/kotlin-plugin/build.gradle.kts b/kotlin-plugin/build.gradle.kts new file mode 100644 index 0000000..9cf4d36 --- /dev/null +++ b/kotlin-plugin/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + kotlin("jvm") + id("com.google.devtools.ksp") + `maven-publish` +} + +dependencies { + compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable") + implementation("com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.20") + implementation(project(":annotations")) + + ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") + compileOnly("com.google.auto.service:auto-service-annotations:1.0.1") + + testImplementation(kotlin("test-junit5")) + testImplementation("org.jetbrains.kotlin:kotlin-compiler-embeddable") + testImplementation("dev.zacsweers.kctfork:core:0.5.1") + testImplementation("dev.zacsweers.kctfork:ksp:0.5.1") +} + diff --git a/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsCallTransformerAndCollector.kt b/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsCallTransformerAndCollector.kt new file mode 100644 index 0000000..cc38b57 --- /dev/null +++ b/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsCallTransformerAndCollector.kt @@ -0,0 +1,166 @@ +package moe.nea.mcautotranslations.kotlin + +import moe.nea.mcautotranslations.annotations.GatheredTranslation +import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.getCompilerMessageLocation +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irCallConstructor +import org.jetbrains.kotlin.ir.builders.irVararg +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.expressions.IrConstKind +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrStringConcatenation +import org.jetbrains.kotlin.ir.types.makeNullable +import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET +import org.jetbrains.kotlin.ir.util.kotlinFqName +import org.jetbrains.kotlin.ir.util.toIrConst +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +class MCAutoTranslationsCallTransformerAndCollector( + val file: IrFile, + val irPluginContext: IrPluginContext, + val messageCollector: MessageCollector, + val translationNames: Map<FqName, CallableId>, +) : IrElementTransformerVoidWithContext() { + + override fun visitCall(expression: IrCall): IrExpression { + val function = expression.symbol.owner + val fqFunctionName = function.kotlinFqName + val translatedFunctionName = translationNames[fqFunctionName] + ?: return super.visitCall(expression) + if (expression.valueArgumentsCount != 2) { + messageCollector.report( + CompilerMessageSeverity.ERROR, + "Translation calls need to have exactly two arguments. Use $fqFunctionName(\"translation.key\", \"some \$template string\")", + expression.getCompilerMessageLocation(file) + ) + return super.visitCall(expression) + } + val translationKey = expression.getValueArgument(0).asStringConst() + if (translationKey == null) { + messageCollector.report( + CompilerMessageSeverity.ERROR, + "The key of a translation call needs to be a constant string. Use $fqFunctionName(\"translation.key\", \"some \$template string\")", + (expression.getValueArgument(0) ?: expression).getCompilerMessageLocation(file) + ) + return super.visitCall(expression) + } + val translationDefault = expression.getValueArgument(1).asStringDyn() + if (translationDefault == null) { + messageCollector.report( + CompilerMessageSeverity.ERROR, + "The default of a translation call needs to be a string template or constant. Use $fqFunctionName(\"translation.key\", \"some \$template string\")", + (expression.getValueArgument(1) ?: expression).getCompilerMessageLocation(file) + ) + return super.visitCall(expression) + + } + val symbol = currentScope!!.scope.scopeOwnerSymbol + val builder = DeclarationIrBuilder(irPluginContext, symbol, expression.startOffset, expression.endOffset) + return builder.generateTemplate( + translatedFunctionName, translationKey, + expression.getValueArgument(0)!!, translationDefault) + } + + fun DeclarationIrBuilder.generateTemplate( + translationName: CallableId, + key: String, + keySource: IrExpression, + template: StringTemplate, + ): IrExpression { + val replacementFunction = irPluginContext.referenceFunctions(translationName) + .single() // TODO: find proper overload + val arguments = splitTemplate(key, template) + val varArgs = irVararg(context.irBuiltIns.anyType.makeNullable(), arguments) + + return irCall( + replacementFunction, replacementFunction.owner.returnType, + valueArgumentsCount = 2, + ).apply { + putValueArgument(0, + constString(key, keySource.startOffset, keySource.endOffset)) + putValueArgument(1, varArgs) + } + } + + fun splitTemplate(key: String, template: StringTemplate): List<IrExpression> { + var templateString = "" + val arguments = mutableListOf<IrExpression>() + for (segment in template.segments) { + val const = segment.asStringConst() + if (const != null) { + templateString += const.replace("%", "%%") + } else { + templateString += "%s" + arguments.add(segment) + } + } + + messageCollector.report( + CompilerMessageSeverity.INFO, + "Generated template: $templateString" + ) + templates[key] = templateString + return arguments + } + + val templates: MutableMap<String, String> = mutableMapOf() + + + fun finish() { + val builder = DeclarationIrBuilder(irPluginContext, file.symbol, SYNTHETIC_OFFSET, SYNTHETIC_OFFSET) + val annotationCons = irPluginContext + .referenceConstructors(GatheredTranslation::class.java.toClassId()).single() + val annotations = templates.map { + builder.irCallConstructor(annotationCons, listOf()).apply { + putValueArgument(0, constString(it.key)) + putValueArgument(1, constString(it.value)) + } + } + file.annotations = annotations + file.annotations + } + + + fun constString( + text: String, + startOffset: Int = SYNTHETIC_OFFSET, + endOffset: Int = SYNTHETIC_OFFSET + ): IrConst<String> = + text.toIrConst(irPluginContext.irBuiltIns.stringType, startOffset, endOffset) as IrConst<String> +} + +data class StringTemplate( + val segments: List<IrExpression>, +) { + constructor(vararg segments: IrExpression) : this(segments.toList()) +} + +fun IrExpression?.asStringDyn(): StringTemplate? = when (this) { + is IrConst<*> -> if (kind == IrConstKind.String) StringTemplate(this) else null + is IrStringConcatenation -> StringTemplate(this.arguments) + else -> null +} + +fun IrExpression?.asStringConst(): String? = when (this) { + is IrConst<*> -> if (kind == IrConstKind.String) value as String else null + is IrStringConcatenation -> this.arguments.singleOrNull().asStringConst() + else -> null +} + + +fun Class<*>.toClassId(): ClassId { + require(!this.isArray) + require("." in this.name) + return ClassId( + FqName(this.name.substringBeforeLast(".")), + Name.identifier(this.name.substringAfterLast(".").replace("$", "."))) +} diff --git a/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsCommandLineProcessor.kt b/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsCommandLineProcessor.kt new file mode 100644 index 0000000..31088cf --- /dev/null +++ b/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsCommandLineProcessor.kt @@ -0,0 +1,23 @@ +@file:OptIn(ExperimentalCompilerApi::class) + +package moe.nea.mcautotranslations.kotlin + +import com.google.auto.service.AutoService +import moe.nea.mcautotranslation.`kotlin-plugin`.BuildConfig +import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration + + +@AutoService(CommandLineProcessor::class) +class MCAutoTranslationsCommandLineProcessor : CommandLineProcessor { + override val pluginId: String + get() = BuildConfig.KOTLIN_PLUGIN_ID + override val pluginOptions: Collection<AbstractCliOption> + get() = listOf() + + override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) { + // TODO: process options + } +}
\ No newline at end of file diff --git a/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsComponentRegistrar.kt b/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsComponentRegistrar.kt new file mode 100644 index 0000000..e1e22dd --- /dev/null +++ b/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsComponentRegistrar.kt @@ -0,0 +1,37 @@ +@file:OptIn(ExperimentalCompilerApi::class) + +package moe.nea.mcautotranslations.kotlin + +import com.google.auto.service.AutoService +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.messageCollector +import org.jetbrains.kotlin.extensions.ProjectExtensionDescriptor +import kotlin.collections.getOrPut + +@AutoService(CompilerPluginRegistrar::class) +class MCAutoTranslationsComponentRegistrar : CompilerPluginRegistrar() { + override val supportsK2: Boolean + get() = true + + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + val messageCollector = configuration.messageCollector + IrGenerationExtension.registerExtension( + extension = MCAutoTranslationsIrGenerationExtension(messageCollector)) + + messageCollector.report(CompilerMessageSeverity.INFO, "Registering stuff") + } + + fun <T : Any> ExtensionStorage.registerExtensionFirst( + descriptor: ProjectExtensionDescriptor<T>, + extension: T + ) { + val extensions = (registeredExtensions as MutableMap<Any, Any>) + .getOrPut(descriptor, { mutableListOf<Any>() }) as MutableList<T> + extensions.add(0, extension) + } + +}
\ No newline at end of file diff --git a/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsIrGenerationExtension.kt b/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsIrGenerationExtension.kt new file mode 100644 index 0000000..3b17e68 --- /dev/null +++ b/kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsIrGenerationExtension.kt @@ -0,0 +1,29 @@ +package moe.nea.mcautotranslations.kotlin + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.FqName + +class MCAutoTranslationsIrGenerationExtension( + private val messageCollector: MessageCollector, +) : IrGenerationExtension { + override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { + val translationNames: MutableMap<FqName, CallableId> = mutableMapOf() + val target = FqName("moe.nea.translatetest.tr") + val resolved = FqName("moe.nea.translatetest.trResolved") // TODO: make these names configurable + translationNames[target] = CallableId(resolved.parent(), resolved.shortName()) + moduleFragment.files.forEach { + val visitor = MCAutoTranslationsCallTransformerAndCollector( + it, + pluginContext, + messageCollector, + translationNames + ) + it.accept(visitor, null) + visitor.finish() + } + } +} diff --git a/kotlin-plugin/src/test/kotlin/moe/nea/mcautotranslations/TestTemplateReplacement.kt b/kotlin-plugin/src/test/kotlin/moe/nea/mcautotranslations/TestTemplateReplacement.kt new file mode 100644 index 0000000..b4a7f18 --- /dev/null +++ b/kotlin-plugin/src/test/kotlin/moe/nea/mcautotranslations/TestTemplateReplacement.kt @@ -0,0 +1,40 @@ +@file:OptIn(ExperimentalCompilerApi::class) + +package moe.nea.mcautotranslations + +import com.tschuchort.compiletesting.SourceFile +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import kotlin.test.Test + +class TestTemplateReplacement { + @Test + fun testX() { + val dollar = '$' + compile(listOf( + SourceFile.kotlin( + "test.kt", + """ + package moe.nea.translatetest + + data class Text(val text: String) + fun tr(key: String, value: String): Text { + error("This should not be executed at runtime. Compiler plugin did not run.") + } + + fun trResolved(key: String, vararg templateArgs: Any?): Text { + return Text("TODO: do a lookup here for $dollar{key} and make use of $dollar{templateArgs.toList()}") + } + + fun main() { + val test = 20 + val othertest = "test2" + + println(tr("hi", "aaa$dollar{test}rest$dollar{othertest}end")) + println(tr("hello", "just a regular strnig")) + println(trResolved("hi", test, othertest)) + } + """.trimIndent() + ) + )) + } +}
\ No newline at end of file diff --git a/kotlin-plugin/src/test/kotlin/moe/nea/mcautotranslations/compile_utils.kt b/kotlin-plugin/src/test/kotlin/moe/nea/mcautotranslations/compile_utils.kt new file mode 100644 index 0000000..48d9c6e --- /dev/null +++ b/kotlin-plugin/src/test/kotlin/moe/nea/mcautotranslations/compile_utils.kt @@ -0,0 +1,33 @@ +@file:OptIn(ExperimentalCompilerApi::class) + +package moe.nea.mcautotranslations + +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.configureKsp +import moe.nea.mcautotranslations.kotlin.MCAutoTranslationsComponentRegistrar +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi + +private val DEFAULT_PLUGINS = arrayOf( + MCAutoTranslationsComponentRegistrar() +) + + +fun compile( + list: List<SourceFile>, + vararg plugins: CompilerPluginRegistrar = DEFAULT_PLUGINS +): JvmCompilationResult { + return KotlinCompilation().apply { + sources = list + compilerPluginRegistrars = plugins.toList() + inheritClassPath = true + messageOutputStream = System.out // TODO: capture this output somehow for testing + }.compile() +} + + + + + |