aboutsummaryrefslogtreecommitdiff
path: root/kotlin-plugin
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-10-30 00:00:54 +0100
committerLinnea Gräf <nea@nea.moe>2024-10-30 00:00:54 +0100
commit410f6a0dd1e5288df7c3fc90bd3937a97b2e6385 (patch)
treecc3db28a82d28dd59528aa3580878cf4fff8980a /kotlin-plugin
downloadmcautotranslations-410f6a0dd1e5288df7c3fc90bd3937a97b2e6385.tar.gz
mcautotranslations-410f6a0dd1e5288df7c3fc90bd3937a97b2e6385.tar.bz2
mcautotranslations-410f6a0dd1e5288df7c3fc90bd3937a97b2e6385.zip
Init
Diffstat (limited to 'kotlin-plugin')
-rw-r--r--kotlin-plugin/build.gradle.kts20
-rw-r--r--kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsCallTransformerAndCollector.kt166
-rw-r--r--kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsCommandLineProcessor.kt23
-rw-r--r--kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsComponentRegistrar.kt37
-rw-r--r--kotlin-plugin/src/main/kotlin/moe/nea/mcautotranslations/kotlin/MCAutoTranslationsIrGenerationExtension.kt29
-rw-r--r--kotlin-plugin/src/test/kotlin/moe/nea/mcautotranslations/TestTemplateReplacement.kt40
-rw-r--r--kotlin-plugin/src/test/kotlin/moe/nea/mcautotranslations/compile_utils.kt33
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()
+}
+
+
+
+
+