├── docs ├── dialog.png ├── right-click.png └── search-todo.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitignore ├── settings.gradle.kts ├── src └── main │ ├── resources │ ├── META-INF │ │ ├── lol.bai.ravel-kotlin.xml │ │ ├── lol.bai.ravel-json.xml │ │ ├── plugin.xml │ │ └── pluginIcon.svg │ ├── messages │ │ └── RavelBundle.properties │ └── icons │ │ └── pluginAction.svg │ ├── kotlin │ └── lol │ │ └── bai │ │ └── ravel │ │ ├── psi │ │ ├── JsonPsiExt.kt │ │ ├── KotlinPsiExt.kt │ │ └── JavaPsiExt.kt │ │ ├── mapping │ │ ├── BasicMapping.kt │ │ ├── downloader │ │ │ ├── MappingDownloader.kt │ │ │ ├── MojMapDownloader.kt │ │ │ └── YarnDownloader.kt │ │ ├── MappingNsVisitor.kt │ │ ├── MioMapping.kt │ │ └── Mapping.kt │ │ ├── util │ │ ├── MultiMap.kt │ │ ├── Cache.kt │ │ ├── Holder.kt │ │ ├── Extension.kt │ │ ├── Utils.kt │ │ ├── PathUtils.kt │ │ └── HttpUtils.kt │ │ ├── remapper │ │ ├── JsonRemapper.kt │ │ ├── PsiRemapper.kt │ │ ├── Remapper.kt │ │ ├── MixinJsonRemapper.kt │ │ ├── FabricModJsonRemapper.kt │ │ ├── JvmRemapper.kt │ │ ├── ClassTweakerRemapper.kt │ │ ├── JavaRemapper.kt │ │ ├── KotlinRemapper.kt │ │ └── MixinRemapper.kt │ │ └── ui │ │ ├── UiBindings.kt │ │ ├── ModuleAction.kt │ │ ├── RemapperDialog.kt │ │ ├── RemapperAction.kt │ │ └── MappingAction.kt │ └── java │ └── lol │ └── bai │ └── ravel │ └── util │ └── NoInline.java ├── gradle.properties ├── .run ├── Run Tests.run.xml ├── Run Plugin.run.xml └── Run Verifications.run.xml ├── LICENSE ├── CHANGELOG.md ├── README.md ├── gradlew.bat ├── .github └── workflows │ └── build.yml └── gradlew /docs/dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badasintended/ravel/HEAD/docs/dialog.png -------------------------------------------------------------------------------- /docs/right-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badasintended/ravel/HEAD/docs/right-click.png -------------------------------------------------------------------------------- /docs/search-todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badasintended/ravel/HEAD/docs/search-todo.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badasintended/ravel/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gradle 3 | .idea 4 | .intellijPlatform 5 | .kotlin 6 | .qodana 7 | build 8 | keys 9 | release.sh 10 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 3 | } 4 | 5 | rootProject.name = "Ravel Remapper" 6 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/lol.bai.ravel-kotlin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/psi/JsonPsiExt.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.psi 2 | 3 | import com.intellij.json.psi.JsonElementGenerator 4 | import com.intellij.psi.PsiComment 5 | 6 | fun JsonElementGenerator.createComment(comment: String): PsiComment { 7 | val file = createDummyFile(comment) 8 | return file.firstChild as PsiComment 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/lol.bai.ravel-json.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Opt-out flag for bundling Kotlin standard library 2 | # https://jb.gg/intellij-platform-kotlin-stdlib 3 | kotlin.stdlib.default.dependency = false 4 | 5 | # Enable Gradle Configuration Cache 6 | # https://docs.gradle.org/current/userguide/configuration_cache.html 7 | org.gradle.configuration-cache = true 8 | 9 | # Enable Gradle Build Cache 10 | # https://docs.gradle.org/current/userguide/build_cache.html 11 | org.gradle.caching = true 12 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/mapping/BasicMapping.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.mapping 2 | 3 | class BasicClassMapping( 4 | override val oldName: String, 5 | override val newName: String? 6 | ) : MutableClassMapping() 7 | 8 | class BasicFieldMapping( 9 | override val oldName: String, 10 | override val newName: String? 11 | ): FieldMapping() 12 | 13 | class BasicMethodMapping( 14 | override val oldName: String, 15 | override val oldDesc: String, 16 | override val newName: String? 17 | ) : MethodMapping() 18 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/psi/KotlinPsiExt.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.psi 2 | 3 | import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade 4 | import org.jetbrains.kotlin.asJava.toLightClass 5 | import org.jetbrains.kotlin.psi.KtClassOrObject 6 | import org.jetbrains.kotlin.psi.KtFile 7 | 8 | val KtClassOrObject.jvmName get() = toLightClass()?.jvmName 9 | 10 | val KtFile.jvmName: String? 11 | get() { 12 | for (pClass in classes) { 13 | if (pClass is KtLightClassForFacade) return pClass.jvmName 14 | } 15 | return null 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/util/MultiMap.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.util 2 | 3 | class MultiMap( 4 | private val map: MutableMap> = linkedMapOf(), 5 | private val factory: () -> MutableCollection, 6 | ) : MutableMap> by map { 7 | 8 | fun put(key: K, value: V) { 9 | map.getOrPut(key, factory).add(value) 10 | } 11 | 12 | } 13 | 14 | fun setMultiMap() = MultiMap { HashSet() } 15 | fun linkedSetMultiMap() = MultiMap { LinkedHashSet() } 16 | fun listMultiMap() = MultiMap { ArrayList() } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/mapping/downloader/MappingDownloader.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.mapping.downloader 2 | 3 | import lol.bai.ravel.util.Extension 4 | import java.nio.file.Path 5 | 6 | val MappingDownloaderExtension = Extension("lol.bai.ravel.mappingDownloader") 7 | 8 | abstract class MappingDownloader( 9 | val name: String 10 | ) { 11 | 12 | override fun toString() = name 13 | 14 | abstract fun resolveDest(version: String): Pair 15 | abstract suspend fun versions(): List 16 | abstract suspend fun download(version: String, dest: Path): Boolean 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/util/Cache.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.util 2 | 3 | class Cache { 4 | private val map = hashMapOf>() 5 | 6 | val values get() = map.values 7 | .filter { it.value != null } 8 | .map { it.value!! as V } 9 | 10 | fun has(key: K): Boolean = map.containsKey(key) 11 | fun get(key: K): V? = map.getOrPut(key) { null.held }.value 12 | fun put(key: K, value: V?) { 13 | map[key] = value.held 14 | } 15 | 16 | inline fun getOrPut(key: K, defaultValue: () -> V?): V? { 17 | if (has(key)) return get(key) 18 | val new = defaultValue() 19 | put(key, new) 20 | return new 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/util/Holder.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.util 2 | 3 | import com.intellij.openapi.util.Key 4 | import com.intellij.openapi.util.UserDataHolder 5 | 6 | typealias HolderKey = Key> 7 | 8 | data class Holder(val value: T) { 9 | companion object { 10 | val nullHolder = Holder(null) 11 | 12 | fun key(key: String): HolderKey { 13 | return Key.create>("lol.bai.ravel.${key}") 14 | } 15 | } 16 | } 17 | 18 | @Suppress("UNCHECKED_CAST") 19 | val T?.held: Holder 20 | get() = if (this == null) Holder.nullHolder as Holder else Holder(this) 21 | 22 | fun HolderKey.put(holder: UserDataHolder, value: T?): T? { 23 | holder.putUserData(this, value.held) 24 | return value 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/lol/bai/ravel/util/NoInline.java: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.util; 2 | 3 | import com.intellij.platform.util.progress.RawProgressReporter; 4 | import com.intellij.platform.util.progress.StepsKt; 5 | import java.util.function.Consumer; 6 | import kotlin.coroutines.Continuation; 7 | 8 | /** 9 | * Hack to suppress internal API usage errors because inlined functions. 10 | * 11 | * @see MP-7133 12 | */ 13 | @SuppressWarnings("UnstableApiUsage") 14 | public final class NoInline { 15 | public static void reportRawProgress(Continuation continuation, Consumer consumer) { 16 | StepsKt.reportRawProgress((progress) -> { 17 | consumer.accept(progress); 18 | return null; 19 | }, continuation); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/util/Extension.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.util 2 | 3 | import com.intellij.openapi.extensions.ExtensionPointName 4 | import com.intellij.openapi.extensions.RequiredElement 5 | import com.intellij.util.xmlb.annotations.Attribute 6 | 7 | class Extension(name: String) { 8 | private val ep = ExtensionPointName.create(name) 9 | 10 | @Suppress("UNCHECKED_CAST") 11 | fun createInstances(): List { 12 | val result = mutableListOf() 13 | ep.forEachExtensionSafe { 14 | result.add(Class.forName(it.implementation).getConstructor().newInstance() as T) 15 | } 16 | return result 17 | } 18 | } 19 | 20 | class ExtensionBean { 21 | 22 | @field:Attribute 23 | @field:RequiredElement 24 | lateinit var implementation: String 25 | 26 | } 27 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | ravel = "0.5.0" 3 | 4 | changelog = "2.4.0" 5 | github-api = "1.314" 6 | intellij-platform = "2.10.4" 7 | intellij-idea = "2025.1.5" 8 | junit = "4.13.2" 9 | kotlin = "2.1.20" 10 | mapping-io = "0.7.1" 11 | opentest4j = "1.3.0" 12 | 13 | [libraries] 14 | github-api = { group = "org.kohsuke", name = "github-api", version.ref = "github-api" } 15 | junit = { group = "junit", name = "junit", version.ref = "junit" } 16 | mapping-io = { group = "net.fabricmc", name = "mapping-io", version.ref = "mapping-io" } 17 | opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "opentest4j" } 18 | 19 | 20 | [plugins] 21 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 22 | intellij-platform = { id = "org.jetbrains.intellij.platform", version.ref = "intellij-platform" } 23 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/JsonRemapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.json.psi.JsonElementGenerator 4 | import com.intellij.json.psi.JsonFile 5 | import com.intellij.psi.PsiElement 6 | import lol.bai.ravel.psi.createComment 7 | 8 | abstract class JsonRemapper : PsiRemapper({ it as? JsonFile }) { 9 | protected lateinit var gen: JsonElementGenerator 10 | 11 | override fun init(): Boolean { 12 | if (!super.init()) return false 13 | gen = JsonElementGenerator(project) 14 | return true 15 | } 16 | 17 | override fun comment(pElt: PsiElement, comment: String) { 18 | var pAnchor: PsiElement? = null 19 | comment.split('\n').forEach { line -> 20 | val pComment = gen.createComment("// $line") 21 | pAnchor = 22 | if (pAnchor == null) pElt.addBefore(pComment, pElt.firstChild) 23 | else pElt.addAfter(pComment, pAnchor) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.util 2 | 3 | fun String.capitalizeFirstChar() = replaceFirstChar { it.uppercase() } 4 | fun String.decapitalizeFirstChar() = replaceFirstChar { it.lowercase() } 5 | 6 | fun wtf(): Nothing = throw UnsupportedOperationException() 7 | 8 | @Suppress("unused") 9 | fun mock(): T = throw UnsupportedOperationException() 10 | 11 | 12 | fun Collection.commonPrefix(): String { 13 | if (isEmpty()) return "" 14 | if (size == 1) return first() 15 | 16 | val list = this as? List ?: toList() 17 | val first = list.first() 18 | var prefixLen = first.length 19 | 20 | for (string in list) { 21 | var i = 0 22 | val limit = minOf(prefixLen, string.length) 23 | while (i < limit) { 24 | if (string[i] != first[i]) { 25 | prefixLen = i 26 | break 27 | } 28 | i++ 29 | } 30 | 31 | if (i == limit) prefixLen = limit 32 | if (prefixLen == 0) break 33 | } 34 | 35 | return first.take(prefixLen) 36 | } 37 | -------------------------------------------------------------------------------- /.run/Run Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | true 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Dimas Firmansyah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.run/Run Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | false 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /.run/Run Verifications.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/mapping/MappingNsVisitor.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.mapping 2 | 3 | import lol.bai.ravel.util.wtf 4 | import net.fabricmc.mappingio.MappedElementKind 5 | import net.fabricmc.mappingio.MappingVisitor 6 | 7 | object MappingNsVisitor : MappingVisitor { 8 | lateinit var src: String 9 | lateinit var dst: List 10 | 11 | override fun visitNamespaces(srcNamespace: String, dstNamespaces: List) { 12 | src = srcNamespace 13 | dst = dstNamespaces 14 | } 15 | 16 | override fun visitContent() = false 17 | 18 | // @formatter:off 19 | override fun visitClass(srcName: String) = wtf() 20 | override fun visitField(srcName: String?, srcDesc: String?) = wtf() 21 | override fun visitMethod(srcName: String?, srcDesc: String?) = wtf() 22 | override fun visitMethodArg(argPosition: Int, lvIndex: Int, srcName: String?) = wtf() 23 | override fun visitMethodVar(lvtRowIndex: Int, lvIndex: Int, startOpIdx: Int, endOpIdx: Int, srcName: String?) = wtf() 24 | override fun visitDstName(targetKind: MappedElementKind?, namespace: Int, name: String?) = wtf() 25 | override fun visitComment(targetKind: MappedElementKind?, comment: String?) = wtf() 26 | // @formatter:on 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/util/PathUtils.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.util 2 | 3 | import java.nio.file.Files 4 | import java.nio.file.Path 5 | import java.nio.file.Paths 6 | import kotlin.io.path.Path 7 | import kotlin.io.path.exists 8 | import kotlin.io.path.isDirectory 9 | 10 | fun getUserDownloadsDir(): Path { 11 | val home = System.getProperty("user.home") 12 | val userProfile = System.getProperty("USERPROFILE") 13 | 14 | val candidates = listOf( 15 | if (home != null) Paths.get(home, "Downloads") else null, 16 | if (userProfile != null) Paths.get(userProfile, "Downloads") else null, 17 | ) 18 | 19 | val found = candidates.firstOrNull { it != null && it.exists() && it.isDirectory() } 20 | return found ?: Path(".") 21 | } 22 | 23 | fun Path.resolveUnique(name: String, extension: String = ""): Path { 24 | val extWithDot = if (extension.isEmpty()) "" else ".$extension" 25 | var candidate = resolve(name + extWithDot) 26 | if (!Files.exists(candidate)) return candidate 27 | 28 | var i = 1 29 | while (true) { 30 | val numbered = "${name}-${i}${extWithDot}" 31 | candidate = resolve(numbered) 32 | if (!Files.exists(candidate)) return candidate 33 | i++ 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/util/HttpUtils.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.util 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.okhttp.* 5 | import io.ktor.client.request.* 6 | import io.ktor.client.statement.* 7 | import io.ktor.http.* 8 | import io.ktor.utils.io.* 9 | import java.nio.file.Path 10 | import kotlin.io.path.createParentDirectories 11 | import kotlin.io.path.outputStream 12 | 13 | val http = HttpClient(OkHttp) 14 | 15 | suspend fun downloadToFile(url: String, dest: Path, progress: ((downloaded: Long, total: Long?) -> Unit)? = null) { 16 | val response = http.get(url) 17 | if (!response.status.isSuccess()) throw Exception("HTTP ${response.status}") 18 | val contentLength = response.contentLength() 19 | 20 | dest.createParentDirectories() 21 | dest.outputStream().use { fos -> 22 | val channel = response.bodyAsChannel() 23 | val buffer = ByteArray(8 * 1024) 24 | var bytesCopied = 0L 25 | while (!channel.isClosedForRead) { 26 | val rc = channel.readAvailable(buffer, 0, buffer.size) 27 | if (rc <= 0) break 28 | fos.write(buffer, 0, rc) 29 | bytesCopied += rc 30 | progress?.invoke(bytesCopied, contentLength) 31 | } 32 | fos.flush() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/PsiRemapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.psi.PsiElement 4 | import com.intellij.psi.PsiFile 5 | import com.intellij.psi.PsiFileFactory 6 | import com.intellij.psi.PsiManager 7 | import com.intellij.psi.util.PsiTreeUtil 8 | 9 | abstract class PsiRemapper( 10 | val caster: (PsiFile?) -> F?, 11 | ) : Remapper() { 12 | protected lateinit var pFile: F 13 | protected lateinit var pFileFactory: PsiFileFactory 14 | 15 | override fun init(): Boolean { 16 | val psi = PsiManager.getInstance(project) 17 | pFile = caster(psi.findFile(file)) ?: return false 18 | pFileFactory = PsiFileFactory.getInstance(project) 19 | return true 20 | } 21 | 22 | abstract fun comment(pElt: PsiElement, comment: String) 23 | override fun fileComment(comment: String) = comment(pFile, comment) 24 | 25 | protected inline fun psiStage(crossinline action: (E) -> Unit): Stage = Stage { pFile.processChildren(action) } 26 | 27 | protected inline fun PsiElement.processChildren(crossinline action: (E) -> Unit) { 28 | PsiTreeUtil.processElements(this, E::class.java) { 29 | if (it != this) action(it) 30 | true 31 | } 32 | } 33 | 34 | protected inline fun PsiElement.parent(): E? { 35 | return PsiTreeUtil.getParentOfType(this, E::class.java) 36 | } 37 | 38 | protected val PsiElement.depth get() = PsiTreeUtil.getDepth(this, null) 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/messages/RavelBundle.properties: -------------------------------------------------------------------------------- 1 | action.lol.bai.ravel.ui.RemapperAction.text = Remap Using Ravel... 2 | action.lol.bai.ravel.ui.MarkAllModuleAction.text = Mark All Modules 3 | action.lol.bai.ravel.ui.MarkModuleAction.text = Mark Module(s) 4 | action.lol.bai.ravel.ui.UnmarkModuleAction.text = Unmark Module(s) 5 | 6 | group.lol.bai.ravel.ui.MappingActionGroup.text = Add Mapping 7 | action.lol.bai.ravel.ui.AddMappingAction.text = Add Local File Mapping... 8 | action.lol.bai.ravel.ui.DownloadMappingAction.text = Download Mapping... 9 | 10 | dialog.error.title = Error 11 | 12 | dialog.remapper.title = Ravel Remapper 13 | dialog.remapper.empty = Add at least one mapping 14 | dialog.remapper.mappings = Mappings 15 | dialog.remapper.modules = Modules to Remap ({0}/{1}) 16 | 17 | dialog.mapping.title = Mapping 18 | dialog.mapping.format = Format: 19 | dialog.mapping.unknownFormat = Unknown mapping file 20 | dialog.mapping.srcNs = Source Namespace: 21 | dialog.mapping.dstNs = Destination Namespace: 22 | 23 | dialog.mapping.download.title = Download Mapping 24 | dialog.mapping.download.directory = Download Location: 25 | dialog.mapping.download.type = Mapping Type: 26 | dialog.mapping.download.version.pending = Getting versions... 27 | dialog.mapping.download.mapping.pending = Downloading Mapping... 28 | dialog.mapping.download.version = Version: 29 | 30 | progress.readingMappings = Reading Mappings... 31 | progress.fileTraverse = Traversing Target Files... 32 | progress.resolving = Resolving {0} Changes in {1}/{2} Files{3}... 33 | progress.writing = Writing {0}/{1} Changes in {2}/{3} Files... 34 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/ui/UiBindings.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("FunctionName") 2 | 3 | package lol.bai.ravel.ui 4 | 5 | import com.intellij.DynamicBundle 6 | import com.intellij.openapi.actionSystem.ActionManager 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.DataKey 9 | import com.intellij.ui.CollectionListModel 10 | import com.intellij.util.application 11 | import lol.bai.ravel.mapping.MioMappingConfig 12 | import lol.bai.ravel.ui.RemapperDialog.ModuleList 13 | import org.jetbrains.annotations.NonNls 14 | import org.jetbrains.annotations.PropertyKey 15 | import javax.swing.JLabel 16 | 17 | internal fun A(clazz: Class): AnAction = 18 | ActionManager.getInstance().getAction(clazz.name) 19 | 20 | internal inline fun A() = A(T::class.java) 21 | 22 | @NonNls 23 | private const val BUNDLE = "messages.RavelBundle" 24 | 25 | internal object B : DynamicBundle(BUNDLE) { 26 | val error by lazy { B("dialog.error.title") } 27 | } 28 | 29 | internal fun B(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 30 | B.getMessage(key, *params) 31 | 32 | internal object K { 33 | val modelData = DataKey.create("RemapperDialogModel") 34 | val modulesLabel = DataKey.create("RemapperDialogModulesLabel") 35 | val modulesList = DataKey.create("RemapperDialogModulesList") 36 | val mappingsModel = DataKey.create>("RemapperDialogMappingsModel") 37 | val check = DataKey.create<() -> Unit>("RemapperDialogCheck") 38 | } 39 | 40 | internal inline fun S() = application.getService(T::class.java) 41 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/ui/ModuleAction.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.ui 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import lol.bai.ravel.ui.RemapperDialog.ModuleList 6 | 7 | abstract class ModuleAction : AnAction() { 8 | abstract fun act(model: RemapperModel, modules: ModuleList) 9 | 10 | override fun actionPerformed(e: AnActionEvent) { 11 | val model = e.getData(K.modelData) ?: return 12 | val modules = e.getData(K.modulesList) ?: return 13 | val modulesLabel = e.getData(K.modulesLabel) ?: return 14 | val check = e.getData(K.check) ?: return 15 | 16 | act(model, modules) 17 | modulesLabel.text = B("dialog.remapper.modules", model.modules.size, modules.model.size) 18 | check() 19 | } 20 | } 21 | 22 | class MarkAllModuleAction : ModuleAction() { 23 | override fun act(model: RemapperModel, modules: ModuleList) { 24 | modules.model.items.forEach { 25 | model.modules.add(it.module) 26 | it.selected = true 27 | } 28 | modules.repaint() 29 | } 30 | } 31 | 32 | class MarkModuleAction : ModuleAction() { 33 | override fun act(model: RemapperModel, modules: ModuleList) { 34 | modules.selectedValuesList.forEach { 35 | model.modules.add(it.module) 36 | it.selected = true 37 | } 38 | } 39 | } 40 | 41 | class UnmarkModuleAction : ModuleAction() { 42 | override fun act(model: RemapperModel, modules: ModuleList) { 43 | modules.selectedValuesList.forEach { 44 | model.modules.remove(it.module) 45 | it.selected = false 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/Remapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.vfs.VirtualFile 5 | import com.intellij.psi.search.GlobalSearchScope 6 | import lol.bai.ravel.mapping.MappingTree 7 | import lol.bai.ravel.mapping.MutableMappingTree 8 | import lol.bai.ravel.util.Extension 9 | 10 | val RemapperExtension = Extension("lol.bai.ravel.remapper") 11 | 12 | abstract class RemapperFactory( 13 | val create: () -> Remapper, 14 | val matches: (VirtualFile) -> Boolean, 15 | ) 16 | 17 | abstract class ExtensionRemapperFactory( 18 | create: () -> Remapper, 19 | extension: String, 20 | ) : RemapperFactory(create, { it.name.endsWith(".${extension}") }) 21 | 22 | abstract class Remapper { 23 | protected lateinit var project: Project 24 | protected lateinit var scope: GlobalSearchScope 25 | protected lateinit var mTree: MappingTree 26 | protected lateinit var file: VirtualFile 27 | protected lateinit var write: Write 28 | protected lateinit var rerun: Rerun 29 | 30 | protected abstract fun init(): Boolean 31 | fun init(project: Project, scope: GlobalSearchScope, mTree: MappingTree, file: VirtualFile, write: Write, rerun: Rerun): Boolean { 32 | this.project = project 33 | this.scope = scope 34 | this.mTree = mTree 35 | this.file = file 36 | this.write = write 37 | this.rerun = rerun 38 | return init() 39 | } 40 | 41 | abstract fun fileComment(comment: String) 42 | abstract fun stages(): List 43 | 44 | fun interface Stage { 45 | operator fun invoke() 46 | } 47 | 48 | fun interface Write { 49 | operator fun invoke(writer: () -> Unit) 50 | } 51 | 52 | fun interface Rerun { 53 | class Context( 54 | val mTree: MutableMappingTree 55 | ) 56 | 57 | operator fun invoke(ctx: Context.() -> Unit) 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/mapping/downloader/MojMapDownloader.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.mapping.downloader 2 | 3 | import com.google.gson.JsonArray 4 | import com.google.gson.JsonParser 5 | import com.intellij.openapi.diagnostic.thisLogger 6 | import io.ktor.client.request.* 7 | import io.ktor.client.statement.* 8 | import io.ktor.http.* 9 | import lol.bai.ravel.util.downloadToFile 10 | import lol.bai.ravel.util.http 11 | import java.nio.file.Path 12 | 13 | class MojMapDownloader : MappingDownloader("Mojang Mappings") { 14 | private val logger = thisLogger() 15 | 16 | private lateinit var versions: JsonArray 17 | 18 | override suspend fun versions(): List { 19 | val response = http.get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json") 20 | 21 | if (!response.status.isSuccess()) return emptyList() 22 | val json = response.bodyAsText() 23 | 24 | return try { 25 | versions = JsonParser.parseString(json).asJsonObject 26 | .getAsJsonArray("versions") 27 | versions.map { it.asJsonObject.get("id").asString } 28 | } catch (e: Exception) { 29 | logger.error(e) 30 | emptyList() 31 | } 32 | } 33 | 34 | override fun resolveDest(version: String) = "mojmap-${version}" to "txt" 35 | 36 | override suspend fun download(version: String, dest: Path): Boolean { 37 | val url = versions 38 | .find { it.asJsonObject.get("id").asString == version }?.asJsonObject 39 | ?.get("url")?.asString ?: return false 40 | 41 | val response = http.get(url) 42 | if (!response.status.isSuccess()) return false 43 | val json = response.bodyAsText() 44 | 45 | try { 46 | val clientTxt = JsonParser.parseString(json).asJsonObject 47 | .getAsJsonObject("downloads") 48 | .getAsJsonObject("client_mappings") 49 | .get("url").asString 50 | 51 | downloadToFile(clientTxt, dest) 52 | return true 53 | } catch (e: Exception) { 54 | logger.error(e) 55 | return false 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/MixinJsonRemapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.json.JsonUtil 4 | import com.intellij.json.psi.JsonArray 5 | import com.intellij.json.psi.JsonStringLiteral 6 | import com.intellij.openapi.diagnostic.thisLogger 7 | import lol.bai.ravel.util.commonPrefix 8 | 9 | class MixinJsonRemapperFactory : ExtensionRemapperFactory(::MixinJsonRemapper, "json") 10 | class MixinJsonRemapper : JsonRemapper() { 11 | private val logger = thisLogger() 12 | 13 | override fun stages() = listOf(remap) 14 | 15 | private val remap = Stage s@{ 16 | val root = JsonUtil.getTopLevelObject(pFile) ?: return@s 17 | 18 | val pkgProp = root.findProperty("package") ?: return@s 19 | val pkgLiteral = pkgProp.value as? JsonStringLiteral ?: return@s 20 | val pkg = pkgLiteral.value 21 | if (!pkg.contains("mixin")) return@s 22 | 23 | val keys = listOf("mixins", "client", "server") 24 | val newValues = hashMapOf() 25 | 26 | for (key in keys) { 27 | val array = root.findProperty(key)?.value as? JsonArray ?: continue 28 | 29 | for (value in array.valueList) { 30 | if (value !is JsonStringLiteral) continue 31 | val className = "${pkg}.${value.value}".replace('.', '/') 32 | val newClassName = mTree.getClass(className)?.newName ?: className 33 | newValues[value] = newClassName 34 | } 35 | } 36 | 37 | val newCommonPrefix = newValues.values.commonPrefix() 38 | if (!newCommonPrefix.endsWith('/')) { 39 | logger.warn("Does not have a concrete new package name") 40 | write { comment(root, "TODO(Ravel): Does not have a concrete package name") } 41 | return@s 42 | } 43 | 44 | val newPkg = newCommonPrefix.removeSuffix("/").replace('/', '.') 45 | if (pkg != newPkg) write { pkgLiteral.replace(gen.createStringLiteral(newPkg)) } 46 | 47 | newValues.forEach { (literal, newName) -> 48 | val newValue = newName.removePrefix(newCommonPrefix).replace('/', '.') 49 | if (literal.value != newValue) write { literal.replace(gen.createStringLiteral(newValue)) } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/mapping/downloader/YarnDownloader.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.mapping.downloader 2 | 3 | import com.google.gson.JsonParser 4 | import com.intellij.openapi.diagnostic.thisLogger 5 | import io.ktor.client.request.* 6 | import io.ktor.client.statement.* 7 | import io.ktor.http.* 8 | import io.ktor.utils.io.jvm.javaio.* 9 | import lol.bai.ravel.util.http 10 | import java.nio.file.Path 11 | import java.util.zip.ZipInputStream 12 | import kotlin.io.path.outputStream 13 | 14 | class YarnDownloader : MappingDownloader("Yarn") { 15 | private val logger = thisLogger() 16 | 17 | override fun resolveDest(version: String) = "yarn-${version}-merged" to "tiny" 18 | 19 | override suspend fun versions(): List { 20 | val response = http.get("https://meta.fabricmc.net/v2/versions/yarn") 21 | 22 | if (!response.status.isSuccess()) return emptyList() 23 | val json = response.bodyAsText() 24 | 25 | return try { 26 | JsonParser.parseString(json) 27 | .asJsonArray 28 | .map { it.asJsonObject.get("version").asString } 29 | } catch (e: Exception) { 30 | logger.error(e) 31 | emptyList() 32 | } 33 | } 34 | 35 | override suspend fun download(version: String, dest: Path): Boolean { 36 | val jarUrl = "https://maven.fabricmc.net/net/fabricmc/yarn/${version}/yarn-${version}-mergedv2.jar" 37 | val jarResponse = http.get(jarUrl) 38 | if (!jarResponse.status.isSuccess()) return false 39 | 40 | ZipInputStream(jarResponse.bodyAsChannel().toInputStream()).use { zis -> 41 | val buffer = ByteArray(8 * 1024) 42 | var entry = zis.nextEntry 43 | 44 | val zipDir = dest.parent.resolve("temp.zip") 45 | val mappingsPath = zipDir.resolve("mappings/mappings.tiny") 46 | 47 | while (entry != null) { 48 | val resolved = zipDir.resolve(entry.name.trimStart('/')) 49 | val normalized = resolved.normalize().toAbsolutePath() 50 | val destAbs = zipDir.toAbsolutePath().normalize() 51 | if (!normalized.startsWith(destAbs)) { 52 | throw SecurityException("Zip entry is outside the target dir: ${entry.name}") 53 | } 54 | 55 | if (normalized == mappingsPath) { 56 | dest.outputStream().use { fos -> 57 | var len: Int 58 | while (zis.read(buffer).also { len = it } > 0) { 59 | fos.write(buffer, 0, len) 60 | } 61 | fos.flush() 62 | return true 63 | } 64 | } 65 | 66 | zis.closeEntry() 67 | entry = zis.nextEntry 68 | } 69 | } 70 | return false 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Ravel Changelog 4 | 5 | ## [Unreleased] 6 | 7 | ## [0.5.0] 8 | 9 | - Java: added support for star imports, now adding new missing imports 10 | - Java: fixed class file not moved when only the package path changed 11 | - Kotlin: added support for renaming in-project classes 12 | 13 | ## [0.4.2] 14 | 15 | - Java: remap class constructor 16 | - Java/Kotlin: call super on element visitor, this fixed many issues 17 | - Mixin: handle `_` as inner class separator when renaming class 18 | 19 | ## [0.4.1] 20 | 21 | - Mixin: fixed `@At` target not being remapped 22 | 23 | ## [0.4.0] 24 | 25 | - UI: fixed progress modal not showing 26 | - Java: added support for remapping project classes 27 | - Mixin: remap mixin class name depending on new target class name 28 | - Mixin: also remap mixin configuration JSON 29 | - FMJ: remap entrypoints inside fabric.mod.json 30 | 31 | ## [0.3.3] 32 | 33 | - Mixin: remap accessor method directly instead of adding value parameter 34 | 35 | ## [0.3.2] 36 | 37 | - Java: fix parameter types not getting remapped 38 | - Mixin: remove `remap=false` check, try to remap the target anyway 39 | - Kotlin: more robust Java property access remap 40 | - UI: remapping progress modal now reports how many changes to do 41 | 42 | ## [0.3.1] 43 | 44 | - Java: fix parameter types not getting remapped 45 | - Mixin: remove `remap=false` check, try to remap the target anyway 46 | - Kotlin: more robust Java property access remap 47 | 48 | ## [0.3.0] 49 | 50 | - Added mappings downloader, currently supports downloading Yarn and Mojang Mappings 51 | 52 | ## [0.2.1] 53 | 54 | ### Fixed 55 | 56 | - A module can no longer be selected multiple times 57 | 58 | ## [0.2.0] 59 | 60 | - Initial support for remapping Kotlin sources 61 | 62 | ## [0.1.1] 63 | 64 | ### Fixed 65 | 66 | - Removed usage of internal fleet.util.Multimap 67 | 68 | ## [0.1.0] 69 | 70 | ### Added 71 | 72 | - Initial release 73 | 74 | [Unreleased]: https://github.com/badasintended/ravel/compare/0.5.0...HEAD 75 | [0.5.0]: https://github.com/badasintended/ravel/compare/0.4.2...0.5.0 76 | [0.4.2]: https://github.com/badasintended/ravel/compare/0.4.1...0.4.2 77 | [0.4.1]: https://github.com/badasintended/ravel/compare/0.4.0...0.4.1 78 | [0.4.0]: https://github.com/badasintended/ravel/compare/0.3.3...0.4.0 79 | [0.3.3]: https://github.com/badasintended/ravel/compare/0.3.2...0.3.3 80 | [0.3.2]: https://github.com/badasintended/ravel/compare/0.3.1...0.3.2 81 | [0.3.1]: https://github.com/badasintended/ravel/compare/0.3.0...0.3.1 82 | [0.3.0]: https://github.com/badasintended/ravel/compare/0.2.1...0.3.0 83 | [0.2.1]: https://github.com/badasintended/ravel/compare/0.2.0...0.2.1 84 | [0.2.0]: https://github.com/badasintended/ravel/compare/0.1.1...0.2.0 85 | [0.1.1]: https://github.com/badasintended/ravel/compare/0.1.0...0.1.1 86 | [0.1.0]: https://github.com/badasintended/ravel/commits/0.1.0 87 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/psi/JavaPsiExt.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.psi 2 | 3 | import com.intellij.psi.* 4 | import com.intellij.psi.PsiModifier.ModifierConstant 5 | import lol.bai.ravel.util.Holder 6 | import lol.bai.ravel.util.put 7 | 8 | private val jvmNameHolder = Holder.key("jvmName") 9 | val PsiClass.jvmName: String? 10 | get() { 11 | val cache = jvmNameHolder.get(this) 12 | if (cache != null) return cache.value 13 | 14 | val className = qualifiedName ?: return jvmNameHolder.put(this, null) 15 | val classOnlyName = className.substringAfterLast('.') 16 | 17 | val pOuterClass = containingClass 18 | val jvmName = if (pOuterClass != null) { 19 | pOuterClass.jvmName + "$" + classOnlyName 20 | } else { 21 | val packageName = className.substringBeforeLast('.').replace('.', '/') 22 | "$packageName/$classOnlyName" 23 | } 24 | 25 | return jvmNameHolder.put(this, jvmName) 26 | } 27 | 28 | fun PsiModifierListOwner.implicitly(@ModifierConstant modifier: String): Boolean { 29 | val modifierList = modifierList ?: return false 30 | return modifierList.hasModifierProperty(modifier) 31 | } 32 | 33 | fun PsiModifierListOwner.explicitly(@ModifierConstant modifier: String): Boolean { 34 | val modifierList = modifierList ?: return false 35 | return modifierList.hasExplicitModifier(modifier) 36 | } 37 | 38 | private val jvmDescHolder = Holder.key("jvmDesc") 39 | val PsiMethod.jvmDesc: String 40 | get() { 41 | val cache = jvmDescHolder.get(this) 42 | if (cache != null) return cache.value!! 43 | 44 | val mSignatureBuilder = StringBuilder() 45 | mSignatureBuilder.append("(") 46 | for (pParam in this.parameterList.parameters) { 47 | mSignatureBuilder.append(pParam.type.jvmRaw) 48 | } 49 | mSignatureBuilder.append(")") 50 | val pReturn = this.returnType ?: PsiTypes.voidType() 51 | mSignatureBuilder.append(pReturn.jvmRaw) 52 | val mSignature = mSignatureBuilder.toString() 53 | return mSignature 54 | } 55 | 56 | @Suppress("UnstableApiUsage") 57 | val PsiType.jvmRaw: String 58 | get() = when (this) { 59 | is PsiArrayType -> "[" + componentType.jvmRaw 60 | is PsiPrimitiveType -> kind.binaryName 61 | is PsiClassType -> { 62 | fun jvmName(cls: PsiClass): String? { 63 | if (cls is PsiTypeParameter) { 64 | val bounds = cls.extendsList.referencedTypes 65 | if (bounds.isEmpty()) return "java/lang/Object" 66 | return jvmName(bounds.first().resolve()!!) 67 | } 68 | 69 | return cls.jvmName 70 | } 71 | 72 | val name = jvmName(resolve()!!) 73 | "L${name};" 74 | } 75 | 76 | else -> { 77 | val ret = canonicalText 78 | if (ret.contains('<')) ret.substringBefore('<') else ret 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ravel Remapper Ravel Logo 2 | 3 | [![Version](https://img.shields.io/jetbrains/plugin/v/28938-ravel-remapper.svg)](https://plugins.jetbrains.com/plugin/28938-ravel-remapper) 4 | [![Downloads](https://img.shields.io/jetbrains/plugin/d/28938-ravel-remapper.svg)](https://plugins.jetbrains.com/plugin/28938-ravel-remapper) 5 | 6 | [![GitHub Release](https://img.shields.io/github/v/release/badasintended/ravel?label=github%20release)](https://github.com/badasintended/ravel/releases) 7 | [![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/badasintended/ravel/total)](https://github.com/badasintended/ravel/releases) 8 | 9 | > **ravel** _[rav-uhl]_ verb 10 | > _raveled, raveling, ravelled, ravelling_ 11 | > to disentangle or unravel the threads or fibers of (a woven or knitted fabric, rope, etc.). 12 | 13 | 14 | 15 | Ravel is a plugin for IntelliJ IDEA to remap source files, based on 16 | [PSI](https://plugins.jetbrains.com/docs/intellij/psi.html) and [Mapping-IO](https://github.com/FabricMC/mapping-io). 17 | 18 | Install it from [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/28938-ravel-remapper) 19 | or download it manually from [GitHub Release](https://github.com/badasintended/ravel/releases). 20 | 21 | Supports remapping: 22 | 23 | - [x] Java 24 | - [x] Kotlin 25 | - [x] [Mixin](https://github.com/FabricMC/Mixin) and [MixinExtras](https://github.com/LlamaLad7/MixinExtras)1 26 | - [x] [Class Tweaker / Access Widener](https://github.com/FabricMC/fabric-tooling/tree/main/class-tweaker) 27 | 28 | 1MixinExtras [Expression](https://github.com/LlamaLad7/MixinExtras/wiki/Expressions) is not supported. 29 | 30 | ## Usage 31 | 32 | ### See the page at [Fabric Docs](https://docs.fabricmc.net/develop/migrating-mappings/ravel) for remapping Fabric Mods! 33 | 34 | 1. **Commit any changes before attempting to remap your sources!** 35 | 36 | 2. Right-click the code editor and go to **Refactor** - **Remap Using Ravel** 37 | Right Click Action 38 | You can also find it inside the **Refactor** menu at the top menu 39 | 40 | 3. Select the mappings to use and modules to remap 41 | Remapper Dialog 42 | Here, I want to remap Fabric API from Yarn to Mojang Mappings, as there is no direct 43 | Yarn-to-Mojang mappings, I need to put both Yarn-merged TinyV2 mapping and Mojang ProGuard TXT 44 | mapping as the input. 45 | Select the source and destination namespace as you see fit. 46 | 47 | 4. Click OK and wait for the remapping to be done 48 | 49 | 5. Search for `TODO(Ravel)` for remapping errors and fix them manually 50 | Search TODO 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/mapping/MioMapping.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.mapping 2 | 3 | import java.nio.file.Path 4 | import net.fabricmc.mappingio.tree.MappingTree.ClassMapping as ClassMappingImpl 5 | import net.fabricmc.mappingio.tree.MappingTree.FieldMapping as FieldMappingImpl 6 | import net.fabricmc.mappingio.tree.MappingTree.MethodMapping as MethodMappingImpl 7 | import net.fabricmc.mappingio.tree.MemoryMappingTree as MappingTreeImpl 8 | 9 | class MioMappingConfig( 10 | val tree: MappingTreeImpl, 11 | val source: String, 12 | val dest: String, 13 | val path: Path, 14 | ) { 15 | override fun toString() = "$source -> $dest ($path)" 16 | } 17 | 18 | class MioClassMapping( 19 | private val configs: List, 20 | private val mio: ClassMappingImpl 21 | ) : MutableClassMapping() { 22 | 23 | override val oldName by lazy { mio.srcName!! } 24 | override val newName by lazy l@{ 25 | var className = mio.srcName 26 | 27 | for (config in configs) { 28 | val mClass = config.tree.getClass(className) ?: return@l null 29 | className = mClass.getName(config.dest) 30 | } 31 | 32 | if (className == mio.srcName) null else className 33 | } 34 | 35 | override fun getAllFieldsImpl() = mio.fields.map { MioFieldMapping(configs, it) } 36 | override fun getFieldImpl(name: String): FieldMapping? { 37 | val mField = mio.getField(name, null) ?: return null 38 | return MioFieldMapping(configs, mField) 39 | } 40 | 41 | override fun getAllMethodImpl() = mio.methods.map { MioMethodMapping(configs, it) } 42 | override fun getMethodImpl(name: String, desc: String): MethodMapping? { 43 | val mMethod = mio.getMethod(name, desc) ?: return null 44 | return MioMethodMapping(configs, mMethod) 45 | } 46 | 47 | } 48 | 49 | class MioFieldMapping( 50 | private val configs: List, 51 | private val mio: FieldMappingImpl 52 | ) : FieldMapping() { 53 | 54 | override val oldName by lazy { mio.srcName!! } 55 | override val newName by lazy l@{ 56 | var className = mio.owner.srcName 57 | var fieldName = mio.srcName 58 | 59 | for (config in configs) { 60 | val mClass = config.tree.getClass(className) ?: return@l null 61 | val mField = mClass.getField(fieldName, null) ?: return@l null 62 | className = mClass.getName(config.dest) 63 | fieldName = mField.getName(config.dest) 64 | } 65 | 66 | if (fieldName == oldName) null else fieldName 67 | } 68 | 69 | } 70 | 71 | class MioMethodMapping( 72 | private val configs: List, 73 | private val mio: MethodMappingImpl 74 | ) : MethodMapping() { 75 | 76 | override val oldName by lazy { mio.srcName!! } 77 | override val oldDesc by lazy { mio.srcDesc!! } 78 | override val newName by lazy l@{ 79 | var className = mio.owner.srcName 80 | var methodName = mio.srcName 81 | var methodDesc = mio.srcDesc 82 | 83 | for (config in configs) { 84 | val mClass = config.tree.getClass(className) ?: return@l null 85 | val mMethod = mClass.getMethod(methodName, methodDesc) ?: return@l null 86 | className = mClass.getName(config.dest) 87 | methodName = mMethod.getName(config.dest) 88 | methodDesc = mMethod.getDesc(config.dest) 89 | } 90 | 91 | if (methodName == oldName) null else methodName 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | lol.bai.ravel 4 | Ravel Remapper 5 | deirn.bai.lol 6 | 7 | com.intellij.modules.platform 8 | com.intellij.java 9 | 10 | org.jetbrains.kotlin 11 | com.intellij.modules.json 12 | 13 | messages.RavelBundle 14 | 15 | 16 | 19 | 20 | 21 | 22 | 26 | 27 | 30 | 31 | 34 | 35 | 36 | 39 | 40 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/FabricModJsonRemapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.json.JsonUtil 4 | import com.intellij.json.psi.* 5 | import com.intellij.openapi.diagnostic.thisLogger 6 | import lol.bai.ravel.mapping.Mapping 7 | 8 | class FabricModJsonRemapperFactory : RemapperFactory(::FabricModJsonRemapper, { it.name == "fabric.mod.json" }) 9 | class FabricModJsonRemapper : JsonRemapper() { 10 | private val logger = thisLogger() 11 | 12 | override fun stages() = listOf(remapEntrypoints) 13 | 14 | private val remapEntrypoints = Stage s@{ 15 | val root = JsonUtil.getTopLevelObject(pFile) ?: return@s 16 | 17 | val schemaVersionProp = root.findProperty("schemaVersion") 18 | if (schemaVersionProp == null) { 19 | logger.warn("Schema version not specified") 20 | write { comment(root, "TODO(Ravel): No schemaVersion found, only version 1 is supported") } 21 | return@s 22 | } 23 | 24 | val schemaVersion = (schemaVersionProp.value as? JsonNumberLiteral)?.value 25 | if (schemaVersion == null || schemaVersion.toInt() != 1) { 26 | logger.warn("Schema version != 1") 27 | write { comment(schemaVersionProp, "TODO(Ravel): only schemaVersion 1 is supported") } 28 | return@s 29 | } 30 | 31 | val entrypointsProp = root.findProperty("entrypoints") ?: return@s 32 | val entrypoints = entrypointsProp.value as? JsonObject ?: return@s 33 | 34 | fun remapEntrypoint(property: JsonProperty, literal: JsonStringLiteral) { 35 | val entrypoint = literal.value 36 | var newEntryPoint = entrypoint 37 | 38 | if (entrypoint.contains("::")) { 39 | val (className, memberName) = entrypoint.split("::", limit = 2) 40 | val classNameSlashed = className.replace('.', '/') 41 | 42 | val mClass = mTree.getClass(classNameSlashed) ?: return 43 | val newClassName = mClass.newPkgPeriodName ?: className 44 | 45 | val mMembers = arrayListOf() 46 | mClass.getField(memberName)?.let { mMembers.add(it) } 47 | mMembers.addAll(mClass.getMethods(memberName)) 48 | val newMemberNames = mMembers.map { it.newName ?: it.oldName }.toSet() 49 | 50 | if (newMemberNames.size != 1) { 51 | logger.warn("members have different new names") 52 | val comment = mMembers.joinToString(separator = "\n") { "${it.oldName} -> ${it.newName ?: it.oldName}" } 53 | write { comment(property, "TODO(Ravel): members different new names\n$comment") } 54 | return 55 | } 56 | 57 | newEntryPoint = "${newClassName}::${newMemberNames.first()}" 58 | } else { 59 | val mClass = mTree.getClass(entrypoint) ?: return 60 | newEntryPoint = mClass.newPkgPeriodName ?: return 61 | } 62 | 63 | if (newEntryPoint != entrypoint) write { literal.replace(gen.createStringLiteral(newEntryPoint)) } 64 | } 65 | 66 | for (entrypointProp in entrypoints.propertyList) { 67 | val entrypoint = entrypointProp.value as? JsonArray ?: continue 68 | for (entrypointImpl in entrypoint.valueList) when (entrypointImpl) { 69 | is JsonStringLiteral -> remapEntrypoint(entrypointProp, entrypointImpl) 70 | is JsonObject -> { 71 | val valueProp = entrypointImpl.findProperty("value") ?: continue 72 | val value = valueProp.value as? JsonStringLiteral ?: continue 73 | remapEntrypoint(valueProp, value) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/JvmRemapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.openapi.diagnostic.thisLogger 4 | import com.intellij.openapi.vfs.VfsUtil 5 | import com.intellij.psi.* 6 | import lol.bai.ravel.psi.jvmDesc 7 | import lol.bai.ravel.psi.jvmName 8 | 9 | abstract class JvmRemapper( 10 | caster: (PsiFile?) -> F? 11 | ) : PsiRemapper(caster) { 12 | private val logger = thisLogger() 13 | 14 | protected fun remap(pField: PsiField): String? { 15 | val pClass = pField.containingClass ?: return null 16 | val mClass = mTree.get(pClass) ?: return null 17 | 18 | val fieldName = pField.name 19 | val mField = mClass.getField(fieldName) ?: return null 20 | val newFieldName = mField.newName ?: return null 21 | return if (newFieldName == fieldName) null else newFieldName 22 | } 23 | 24 | protected fun remap(pSafeElt: PsiElement, pMethod: PsiMethod): String? { 25 | var pSuperMethods = pMethod.findDeepestSuperMethods() 26 | if (pSuperMethods.isEmpty()) pSuperMethods = arrayOf(pMethod) 27 | 28 | val newMethodNames = linkedMapOf() 29 | for (pMethod in pSuperMethods) { 30 | val pClass = pMethod.containingClass ?: continue 31 | val pClassName = pClass.qualifiedName ?: continue 32 | val pMethodName = pMethod.name 33 | 34 | val key = "$pClassName#$pMethod" 35 | newMethodNames[key] = pMethodName 36 | 37 | val mClass = mTree.get(pClass) ?: continue 38 | val mSignature = pMethod.jvmDesc 39 | val mMethod = mClass.getMethod(pMethodName, mSignature) ?: continue 40 | val newMethodName = mMethod.newName ?: continue 41 | newMethodNames[key] = newMethodName 42 | } 43 | 44 | if (newMethodNames.isEmpty()) return null 45 | if (newMethodNames.size != pSuperMethods.size) { 46 | logger.warn("could not resolve all method origins") 47 | write { comment(pSafeElt, "TODO(Ravel): could not resolve all method origins") } 48 | return null 49 | } 50 | 51 | val uniqueNewMethodNames = newMethodNames.values.toSet() 52 | if (uniqueNewMethodNames.size != 1) { 53 | logger.warn("method origins have different new names") 54 | val comment = newMethodNames.map { (k, v) -> "$k -> $v" }.joinToString(separator = "\n") 55 | write { comment(pSafeElt, "TODO(Ravel): method origins have different new names\n$comment") } 56 | return null 57 | } 58 | 59 | val newMethodName = uniqueNewMethodNames.first() 60 | return if (newMethodName == pMethod.name) null else newMethodName 61 | } 62 | 63 | protected fun renameFile(newPackageName: String?, topLevelClasses: Map) { 64 | if (topLevelClasses.isEmpty()) return 65 | 66 | val fileClassName = file.nameWithoutExtension 67 | val (pClass, newClassJvmName) = topLevelClasses.entries 68 | .firstOrNull { it.key.name == fileClassName } 69 | ?: return 70 | val classJvmName = pClass.jvmName ?: return 71 | 72 | val packageDir = classJvmName.substringBeforeLast('/') 73 | val newPackageDir = newPackageName?.replace('.', '/') 74 | 75 | if (newPackageDir != null && packageDir != newPackageDir) write { 76 | var rootDir = file.parent 77 | repeat(packageDir.split('/').size) { 78 | rootDir = rootDir.parent 79 | } 80 | 81 | file.move(null, VfsUtil.createDirectoryIfMissing(rootDir, newPackageDir)) 82 | } 83 | 84 | if (classJvmName != newClassJvmName) write { 85 | val newClassName = newClassJvmName.substringAfterLast('/') 86 | file.rename(null, "${newClassName}.${file.extension}") 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow is created for testing and preparing the plugin release in the following steps: 2 | # - Validate Gradle Wrapper. 3 | # - Run 'test' and 'verifyPlugin' tasks. 4 | # - Run the 'buildPlugin' task and prepare artifact for further tests. 5 | # - Run the 'runPluginVerifier' task. 6 | # - Create a draft release. 7 | # 8 | # The workflow is triggered on push and pull_request events. 9 | # 10 | # GitHub Actions reference: https://help.github.com/en/actions 11 | # 12 | ## JBIJPPTPL 13 | 14 | name: Build 15 | on: 16 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) 17 | push: 18 | branches: [ master ] 19 | # Trigger the workflow on any pull request 20 | pull_request: 21 | 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | 28 | # Prepare the environment and build the plugin 29 | build: 30 | name: Build 31 | runs-on: ubuntu-latest 32 | steps: 33 | 34 | # Free GitHub Actions Environment Disk Space 35 | - name: Maximize Build Space 36 | uses: jlumbroso/free-disk-space@v1.3.1 37 | with: 38 | tool-cache: false 39 | large-packages: false 40 | 41 | # Check out the current repository 42 | - name: Fetch Sources 43 | uses: actions/checkout@v4 44 | 45 | # Set up the Java environment for the next steps 46 | - name: Setup Java 47 | uses: actions/setup-java@v4 48 | with: 49 | distribution: zulu 50 | java-version: 21 51 | 52 | # Setup Gradle 53 | - name: Setup Gradle 54 | uses: gradle/actions/setup-gradle@v4 55 | 56 | # Build plugin 57 | - name: Build plugin 58 | run: ./gradlew buildPlugin 59 | 60 | # Prepare plugin archive content for creating artifact 61 | - name: Prepare Plugin Artifact 62 | id: artifact 63 | shell: bash 64 | run: | 65 | cd ${{ github.workspace }}/build/distributions 66 | FILENAME=`ls *.zip` 67 | unzip "$FILENAME" -d content 68 | 69 | echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT 70 | 71 | # Store an already-built plugin as an artifact for downloading 72 | - name: Upload artifact 73 | uses: actions/upload-artifact@v4 74 | with: 75 | name: ${{ steps.artifact.outputs.filename }} 76 | path: ./build/distributions/content/*/* 77 | 78 | # Run plugin structure verification along with IntelliJ Plugin Verifier 79 | verify: 80 | name: Verify plugin 81 | needs: [ build ] 82 | runs-on: ubuntu-latest 83 | steps: 84 | 85 | # Free GitHub Actions Environment Disk Space 86 | - name: Maximize Build Space 87 | uses: jlumbroso/free-disk-space@v1.3.1 88 | with: 89 | tool-cache: false 90 | large-packages: false 91 | 92 | # Check out the current repository 93 | - name: Fetch Sources 94 | uses: actions/checkout@v4 95 | 96 | # Set up the Java environment for the next steps 97 | - name: Setup Java 98 | uses: actions/setup-java@v4 99 | with: 100 | distribution: zulu 101 | java-version: 21 102 | 103 | # Setup Gradle 104 | - name: Setup Gradle 105 | uses: gradle/actions/setup-gradle@v4 106 | with: 107 | cache-read-only: true 108 | 109 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 110 | - name: Run Plugin Verification tasks 111 | run: ./gradlew verifyPlugin 112 | 113 | # Collect Plugin Verifier Result 114 | - name: Collect Plugin Verifier Result 115 | if: ${{ always() }} 116 | uses: actions/upload-artifact@v4 117 | with: 118 | name: pluginVerifier-result 119 | path: ${{ github.workspace }}/build/reports/pluginVerifier 120 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/mapping/Mapping.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.mapping 2 | 3 | import com.intellij.psi.PsiClass 4 | import com.intellij.psi.PsiField 5 | import com.intellij.psi.PsiMethod 6 | import lol.bai.ravel.psi.jvmDesc 7 | import lol.bai.ravel.psi.jvmName 8 | import lol.bai.ravel.util.Cache 9 | 10 | val rawClassRegex = Regex("L([A-Za-z_$][A-Za-z0-9_$]*(?:/[A-Za-z_$][A-Za-z0-9_$]*)*);") 11 | val rawQualifierSeparators = Regex("[/$]") 12 | 13 | abstract class MappingTree { 14 | protected val classes = linkedMapOf() 15 | 16 | fun getClass(name: String?): ClassMapping? { 17 | return if (name == null) null else classes[name] 18 | } 19 | 20 | fun get(pClass: PsiClass): ClassMapping? { 21 | val classJvmName = pClass.jvmName ?: return null 22 | return getClass(classJvmName) 23 | } 24 | 25 | fun get(pField: PsiField): FieldMapping? { 26 | val pClass = pField.containingClass ?: return null 27 | val mClass = get(pClass) ?: return null 28 | return mClass.getField(pField.name) 29 | } 30 | 31 | fun get(pMethod: PsiMethod): MethodMapping? { 32 | val pClass = pMethod.containingClass ?: return null 33 | val mClass = get(pClass) ?: return null 34 | return mClass.getMethod(pMethod.name, pMethod.jvmDesc) 35 | } 36 | 37 | fun remapDesc(desc: String): String { 38 | return desc.replace(rawClassRegex) m@{ match -> 39 | val className = match.groupValues[1] 40 | val mClass = getClass(className) ?: return@m match.value 41 | val newClassName = mClass.newName ?: return@m match.value 42 | "L${newClassName};" 43 | } 44 | } 45 | } 46 | 47 | class MutableMappingTree : MappingTree() { 48 | fun getOrPutClass(oldName: String, newName: String?) = classes.getOrPut(oldName) { BasicClassMapping(oldName, newName) } 49 | fun getOrPut(pClass: PsiClass) = getOrPutClass(pClass.jvmName!!, null) 50 | 51 | fun putClass(oldName: String, newName: String?) = putClass(BasicClassMapping(oldName, newName)) 52 | fun putClass(mapping: MutableClassMapping) { 53 | classes[mapping.oldName] = mapping 54 | } 55 | } 56 | 57 | interface Mapping { 58 | val oldName: String 59 | val newName: String? 60 | } 61 | 62 | abstract class ClassMapping : Mapping { 63 | val newFullPeriodName get() = newName?.replace(rawQualifierSeparators, ".") 64 | val newPkgPeriodName get() = newName?.replace('/', '.') 65 | 66 | protected val fieldCache = Cache() 67 | protected val methodCache = Cache() 68 | 69 | protected open fun getAllFieldsImpl(): Collection = emptyList() 70 | val fields: List 71 | get() { 72 | getAllFieldsImpl().forEach { fieldCache.put(it.oldName, it) } 73 | return fieldCache.values 74 | } 75 | 76 | protected open fun getFieldImpl(name: String): FieldMapping? = null 77 | fun getField(name: String) = fieldCache.getOrPut(name) { getFieldImpl(name) } 78 | 79 | protected open fun getAllMethodImpl(): Collection = emptyList() 80 | val methods: List 81 | get() { 82 | getAllMethodImpl().forEach { methodCache.put("${it.oldName}${it.oldDesc}", it) } 83 | return methodCache.values 84 | } 85 | 86 | protected open fun getMethodImpl(name: String, desc: String): MethodMapping? = null 87 | fun getMethod(name: String, desc: String) = methodCache.getOrPut("${name}${desc}") { getMethodImpl(name, desc) } 88 | fun getMethods(name: String) = methods.filter { it.oldName == name } 89 | } 90 | 91 | abstract class MutableClassMapping : ClassMapping() { 92 | fun putField(mapping: FieldMapping) = fieldCache.put(mapping.oldName, mapping) 93 | fun putField(oldName: String, newName: String?) = putField(BasicFieldMapping(oldName, newName)) 94 | 95 | fun putMethod(mapping: MethodMapping) = methodCache.put("${mapping.oldName}${mapping.oldDesc}", mapping) 96 | fun putMethod(oldName: String, oldDesc: String, newName: String?) = putMethod(BasicMethodMapping(oldName, oldDesc, newName)) 97 | } 98 | 99 | abstract class FieldMapping : Mapping 100 | 101 | abstract class MethodMapping : Mapping { 102 | abstract val oldDesc: String 103 | } 104 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/ui/RemapperDialog.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.ui 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.openapi.actionSystem.DataProvider 5 | import com.intellij.openapi.module.Module 6 | import com.intellij.openapi.module.ModuleManager 7 | import com.intellij.openapi.module.ModuleType 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.project.rootManager 10 | import com.intellij.openapi.ui.DialogWrapper 11 | import com.intellij.ui.* 12 | import com.intellij.ui.components.JBList 13 | import com.intellij.ui.dsl.builder.LabelPosition 14 | import com.intellij.util.ui.JBUI 15 | import lol.bai.ravel.mapping.MioMappingConfig 16 | import org.jetbrains.annotations.NonNls 17 | import javax.swing.JLabel 18 | import javax.swing.JList 19 | import javax.swing.event.ListDataEvent 20 | import javax.swing.event.ListDataListener 21 | import com.intellij.ui.dsl.builder.panel as rootPanel 22 | 23 | class RemapperDialog( 24 | val project: Project, 25 | val model: RemapperModel 26 | ) : DialogWrapper(project), DataProvider { 27 | 28 | val fileColor = FileColorManager.getInstance(project)!! 29 | 30 | val mappingsLabel = JLabel(B("dialog.remapper.mappings")).apply { horizontalTextPosition = JLabel.LEFT } 31 | val modulesLabel = JLabel().apply { horizontalTextPosition = JLabel.LEFT } 32 | 33 | lateinit var moduleList: ModuleList 34 | lateinit var mappingsModel: CollectionListModel 35 | 36 | init { 37 | title = B("dialog.remapper.title") 38 | init() 39 | check() 40 | } 41 | 42 | override fun getData(dataId: @NonNls String) = when (dataId) { 43 | K.modelData.name -> model 44 | K.modulesLabel.name -> modulesLabel 45 | K.modulesList.name -> moduleList 46 | K.mappingsModel.name -> mappingsModel 47 | K.check.name -> this::check 48 | else -> null 49 | } 50 | 51 | fun check() { 52 | val hasMappings = model.mappings.isNotEmpty() 53 | val hasModules = model.modules.isNotEmpty() 54 | 55 | mappingsLabel.icon = if (hasMappings) null else AllIcons.General.BalloonError 56 | modulesLabel.icon = if (hasModules) null else AllIcons.General.BalloonError 57 | 58 | okAction.isEnabled = hasMappings && hasModules 59 | } 60 | 61 | override fun createCenterPanel() = rootPanel { 62 | mappingsModel = CollectionListModel(model.mappings, true).apply { 63 | addListDataListener(object : ListDataListener { 64 | override fun intervalAdded(e: ListDataEvent) = check() 65 | override fun intervalRemoved(e: ListDataEvent) = check() 66 | override fun contentsChanged(e: ListDataEvent) = check() 67 | }) 68 | } 69 | val mappingsList = JBList().apply { 70 | model = mappingsModel 71 | setEmptyText(B("dialog.remapper.empty")) 72 | } 73 | val mappings = ToolbarDecorator 74 | .createDecorator(mappingsList) 75 | .setPreferredSize(JBUI.size(300, 500)) 76 | .addExtraAction(A()) 77 | .setButtonComparator( 78 | B("group.lol.bai.ravel.ui.MappingActionGroup.text"), 79 | *CommonActionsPanel.Buttons.entries.map { it.text }.toTypedArray() 80 | ) 81 | .createPanel() 82 | 83 | val moduleModel = CollectionListModel() 84 | ModuleManager.getInstance(project).modules.sortedBy { it.name }.forEach { module -> 85 | if (module.rootManager.sourceRoots.isEmpty()) return@forEach 86 | moduleModel.add(ModuleEntry(module, false)) 87 | } 88 | moduleList = ModuleList(moduleModel) 89 | modulesLabel.text = B("dialog.remapper.modules", model.modules.size, moduleModel.size) 90 | ListSpeedSearch.installOn(moduleList) 91 | val modules = ToolbarDecorator 92 | .createDecorator(moduleList) 93 | .setPreferredSize(JBUI.size(400, 500)) 94 | .disableAddAction() 95 | .disableRemoveAction() 96 | .disableUpDownActions() 97 | .addExtraAction(A()) 98 | .addExtraAction(A()) 99 | .addExtraAction(A()) 100 | .createPanel() 101 | 102 | row { 103 | cell(mappings).label(mappingsLabel, LabelPosition.TOP) 104 | cell(modules).label(modulesLabel, LabelPosition.TOP) 105 | } 106 | } 107 | 108 | inner class ModuleList( 109 | val model: CollectionListModel 110 | ) : JBList() { 111 | init { 112 | super.model = model 113 | cellRenderer = ModuleCellRenderer() 114 | } 115 | } 116 | 117 | data class ModuleEntry( 118 | val module: Module, 119 | var selected: Boolean 120 | ) 121 | 122 | inner class ModuleCellRenderer : ColoredListCellRenderer() { 123 | override fun customizeCellRenderer(list: JList, value: ModuleEntry, index: Int, selected: Boolean, hasFocus: Boolean) { 124 | append(value.module.name) 125 | icon = ModuleType.get(value.module).icon 126 | if (value.selected) background = fileColor.getColor("Green") 127 | if (selected) background = JBUI.CurrentTheme.List.background(true, hasFocus) 128 | } 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/ClassTweakerRemapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.openapi.vfs.writeText 4 | import kotlin.io.path.readLines 5 | import kotlin.io.path.useLines 6 | 7 | private const val accessWidener = "accessWidener" 8 | private const val classTweaker = "classTweaker" 9 | 10 | private object Entry { 11 | val header = Regex("^([A-Za-z]+)\\s+v(\\d+)\\s+(\\w+)\\s*$") 12 | val clazz = Regex("^([\\w-]+\\s+class\\s+)([\\w/$]+)(.*)") 13 | val field = Regex("^([\\w-]+\\s+field\\s+)([\\w/$]+)(\\s+)([\\w$]+)(\\s+)([\\w/$;\\[]+)(.*)") 14 | val method = Regex("^([\\w-]+\\s+method\\s+)([\\w/$]+)(\\s+)([\\w$<>]+)(\\s+)([\\w/$;\\[()]+)(.*)") 15 | val injectInterface = Regex("^([\\w-]*inject-interface\\s+)([\\w/$]+)(\\s+)([\\w/$]+)(.*)") 16 | } 17 | 18 | private val skip = setOf( 19 | "json", "yaml", "yml", "properties", "toml", 20 | "java", "kt", "scala", "groovy", 21 | "png", "jpg", "jpeg", "svg", "ogg", "wav" 22 | ) 23 | 24 | class ClassTweakerRemapperFactory : RemapperFactory(::ClassTweakerRemapper, { !skip.contains(it.extension) }) 25 | class ClassTweakerRemapper : Remapper() { 26 | 27 | private lateinit var lines: List 28 | override fun stages() = listOf(remapClassTweakers) 29 | 30 | override fun init(): Boolean { 31 | val header = try { 32 | file.toNioPath().useLines { it.firstOrNull() } 33 | } catch (_: Exception) { 34 | null 35 | } 36 | 37 | if (header == null) return false 38 | 39 | val headerMatch = Entry.header.matchEntire(header) ?: return false 40 | val (type, versionStr, _) = headerMatch.destructured 41 | val version = versionStr.toIntOrNull() ?: return false 42 | 43 | if (type == accessWidener) { 44 | if (version !in 1..2) return false 45 | } else if (type == classTweaker) { 46 | if (version != 1) return false 47 | } 48 | 49 | lines = file.toNioPath().readLines() 50 | return true 51 | } 52 | 53 | override fun fileComment(comment: String) { 54 | val sb = StringBuilder() 55 | val lineIter = lines.iterator() 56 | 57 | sb.append(lineIter.next()).append('\n') 58 | sb.append(comment).append('\n') 59 | lineIter.forEachRemaining { sb.append(it).append('\n') } 60 | 61 | file.writeText(sb.toString()) 62 | } 63 | 64 | private val remapClassTweakers = Stage { 65 | var modified = false 66 | val sb = StringBuilder() 67 | val lineIter = lines.iterator() 68 | sb.append(lineIter.next()).append('\n') 69 | 70 | lineIter.forEachRemaining l@{ line -> 71 | val classMatch = Entry.clazz.matchEntire(line) 72 | if (classMatch != null) { 73 | val (s1, className, rest) = classMatch.destructured 74 | val newClassName = mTree.getClass(className)?.newName 75 | if (newClassName != null) { 76 | sb.append("${s1}${newClassName}${rest}\n") 77 | modified = true 78 | return@l 79 | } 80 | } 81 | 82 | val fieldMatch = Entry.field.matchEntire(line) 83 | if (fieldMatch != null) { 84 | var (s1, className, s2, fieldName, s3, fieldDesc, rest) = fieldMatch.destructured 85 | 86 | val mClass = mTree.getClass(className) 87 | if (mClass != null) { 88 | val newClassName = mClass.newName 89 | if (newClassName != null) { 90 | className = newClassName 91 | modified = true 92 | } 93 | 94 | val newFieldName = mClass.getField(fieldName)?.newName 95 | if (newFieldName != null) { 96 | fieldName = newFieldName 97 | modified = true 98 | } 99 | } 100 | 101 | val newFieldDesc = mTree.remapDesc(fieldDesc) 102 | if (fieldDesc != newFieldDesc) { 103 | fieldDesc = newFieldDesc 104 | modified = true 105 | } 106 | 107 | sb.append("${s1}${className}${s2}${fieldName}${s3}${fieldDesc}${rest}\n") 108 | return@l 109 | } 110 | 111 | val methodMatch = Entry.method.matchEntire(line) 112 | if (methodMatch != null) { 113 | var (s1, className, s2, methodName, s3, methodDesc, rest) = methodMatch.destructured 114 | 115 | val mClass = mTree.getClass(className) 116 | if (mClass != null) { 117 | val newClassName = mClass.newName 118 | if (newClassName != null) { 119 | className = newClassName 120 | modified = true 121 | } 122 | 123 | val newMethodName = mClass.getMethod(methodName, methodDesc)?.newName 124 | if (newMethodName != null) { 125 | methodName = newMethodName 126 | modified = true 127 | } 128 | } 129 | 130 | val newMethodDesc = mTree.remapDesc(methodDesc) 131 | if (methodDesc != newMethodDesc) { 132 | methodDesc = newMethodDesc 133 | modified = true 134 | } 135 | 136 | sb.append("${s1}${className}${s2}${methodName}${s3}${methodDesc}${rest}\n") 137 | return@l 138 | } 139 | 140 | val injectInterfaceMatch = Entry.injectInterface.matchEntire(line) 141 | if (injectInterfaceMatch != null) { 142 | var (s1, className1, s2, className2, rest) = injectInterfaceMatch.destructured 143 | 144 | val newClassName1 = mTree.getClass(className1)?.newName 145 | if (newClassName1 != null) { 146 | className1 = newClassName1 147 | modified = true 148 | } 149 | 150 | val newClassName2 = mTree.getClass(className2)?.newName 151 | if (newClassName2 != null) { 152 | className2 = newClassName2 153 | modified = true 154 | } 155 | 156 | sb.append("${s1}${className1}${s2}${className2}${rest}\n") 157 | return@l 158 | } 159 | 160 | sb.append(line).append('\n') 161 | } 162 | 163 | if (modified) write { file.writeText(sb.toString()) } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/ui/RemapperAction.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.ui 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.application.runReadAction 7 | import com.intellij.openapi.command.WriteCommandAction 8 | import com.intellij.openapi.diagnostic.thisLogger 9 | import com.intellij.openapi.module.Module 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.openapi.project.rootManager 12 | import com.intellij.openapi.vfs.VfsUtil 13 | import com.intellij.openapi.vfs.VirtualFile 14 | import com.intellij.openapi.vfs.isFile 15 | import com.intellij.platform.ide.progress.ModalTaskOwner 16 | import com.intellij.platform.ide.progress.TaskCancellation 17 | import com.intellij.platform.ide.progress.runWithModalProgressBlocking 18 | import com.intellij.platform.util.progress.RawProgressReporter 19 | import lol.bai.ravel.mapping.MioClassMapping 20 | import lol.bai.ravel.mapping.MioMappingConfig 21 | import lol.bai.ravel.mapping.MutableMappingTree 22 | import lol.bai.ravel.remapper.Remapper 23 | import lol.bai.ravel.remapper.RemapperExtension 24 | import lol.bai.ravel.remapper.RemapperFactory 25 | import lol.bai.ravel.util.NoInline 26 | import lol.bai.ravel.util.listMultiMap 27 | import kotlin.coroutines.resume 28 | import kotlin.coroutines.suspendCoroutine 29 | 30 | data class RemapperModel( 31 | val mappings: MutableList = arrayListOf(), 32 | val modules: MutableSet = linkedSetOf(), 33 | ) 34 | 35 | @Suppress("UnstableApiUsage") 36 | class RemapperAction : AnAction() { 37 | private val logger = thisLogger() 38 | 39 | override fun getActionUpdateThread() = ActionUpdateThread.BGT 40 | 41 | override fun update(e: AnActionEvent) { 42 | e.presentation.isEnabledAndVisible = e.project != null 43 | } 44 | 45 | override fun actionPerformed(e: AnActionEvent) { 46 | val project = e.project ?: return 47 | val model = RemapperModel() 48 | val ok = RemapperDialog(project, model).showAndGet() 49 | 50 | if (!ok) return 51 | if (model.mappings.isEmpty() || model.modules.isEmpty()) return 52 | 53 | runWithModalProgressBlocking(ModalTaskOwner.project(project), B("dialog.remapper.title"), TaskCancellation.nonCancellable()) { 54 | suspendCoroutine { cont -> 55 | NoInline.reportRawProgress(cont) { remap(project, model, it) } 56 | cont.resume(Unit) 57 | } 58 | } 59 | } 60 | 61 | fun remap(project: Project, model: RemapperModel, progress: RawProgressReporter) { 62 | val time = System.currentTimeMillis() 63 | 64 | progress.fraction(null) 65 | progress.text(B("progress.readingMappings")) 66 | val mTree = MutableMappingTree() 67 | model.mappings.first().tree.classes.forEach { 68 | mTree.putClass(MioClassMapping(model.mappings, it)) 69 | } 70 | 71 | data class Target( 72 | val vf: VirtualFile, 73 | val module: Module, 74 | val factories: List, 75 | ) 76 | 77 | val fileWriters = listMultiMap Unit>() 78 | var writersCount = 0 79 | 80 | fun resolve(runCxt: Remapper.Rerun.Context, n: Int) { 81 | val nText = if (n == 1) "" else " ($n)" 82 | 83 | var rerunCxt: Remapper.Rerun.Context? = null 84 | val rerun = Remapper.Rerun { modifier -> 85 | if (rerunCxt == null) rerunCxt = Remapper.Rerun.Context(MutableMappingTree()) 86 | modifier(rerunCxt) 87 | } 88 | 89 | progress.fraction(null) 90 | progress.text(B("progress.fileTraverse")) 91 | val factories = RemapperExtension.createInstances() 92 | val targets = arrayListOf() 93 | for (module in model.modules) { 94 | for (root in module.rootManager.sourceRoots) { 95 | VfsUtil.iterateChildrenRecursively(root, null) v@{ vf -> 96 | if (!vf.isFile) return@v true 97 | val remappers = factories.filter { it.matches(vf) } 98 | if (remappers.isNotEmpty()) targets.add(Target(vf, module, remappers)) 99 | true 100 | } 101 | } 102 | } 103 | 104 | val fileCount = targets.size 105 | var fileIndex = 0 106 | 107 | for ((vf, module, factories) in targets) { 108 | progress.fraction(fileIndex.toDouble() / fileCount.toDouble()) 109 | progress.text(B("progress.resolving", writersCount, fileIndex, fileCount, nText)) 110 | progress.details(vf.path) 111 | fileIndex++ 112 | 113 | if (!vf.isFile) continue 114 | runReadAction { 115 | val scope = module.getModuleWithDependenciesAndLibrariesScope(true) 116 | val write = Remapper.Write { writer -> 117 | fileWriters.put(vf, writer) 118 | writersCount++ 119 | } 120 | 121 | for (factory in factories) { 122 | val remapper = factory.create() 123 | val valid = remapper.init(project, scope, runCxt.mTree, vf, write, rerun) 124 | if (!valid) continue 125 | 126 | try { 127 | remapper.stages().forEach { it.invoke() } 128 | } catch (e: Exception) { 129 | write { remapper.fileComment("TODO(Ravel): Failed to fully resolve file: ${e.message}") } 130 | logger.error("Failed to fully resolve ${vf.path}", e) 131 | } 132 | } 133 | } 134 | } 135 | 136 | if (rerunCxt != null) resolve(rerunCxt, n + 1) 137 | } 138 | resolve(Remapper.Rerun.Context(mTree), 1) 139 | 140 | logger.warn("Mapping resolved in ${System.currentTimeMillis() - time}ms") 141 | progress.fraction(null) 142 | progress.text(null) 143 | progress.details(null) 144 | 145 | val fileCount = fileWriters.size 146 | var fileIndex = 0 147 | var writerIndex = 0 148 | 149 | fileWriters.forEach { (vf, writers) -> 150 | progress.details(vf.path) 151 | WriteCommandAction.runWriteCommandAction(project, "Ravel Remapper", null, { 152 | writers.forEach { writer -> 153 | progress.fraction(writerIndex.toDouble() / writersCount.toDouble()) 154 | progress.text(B("progress.writing", writerIndex, writersCount, fileIndex, fileCount)) 155 | writerIndex++ 156 | 157 | try { 158 | writer.invoke() 159 | } catch (e: Exception) { 160 | logger.error("Failed to write ${vf.path}", e) 161 | } 162 | } 163 | }) 164 | fileIndex++ 165 | } 166 | 167 | logger.warn("Remap finished in ${System.currentTimeMillis() - time}ms") 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/ui/MappingAction.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.ui 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.actionSystem.DefaultActionGroup 7 | import com.intellij.openapi.application.EDT 8 | import com.intellij.openapi.components.Service 9 | import com.intellij.openapi.fileChooser.FileChooser 10 | import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory 11 | import com.intellij.openapi.progress.currentThreadCoroutineScope 12 | import com.intellij.openapi.ui.ComboBox 13 | import com.intellij.openapi.ui.DialogBuilder 14 | import com.intellij.openapi.ui.Messages 15 | import com.intellij.openapi.util.io.toCanonicalPath 16 | import com.intellij.platform.ide.progress.ModalTaskOwner 17 | import com.intellij.platform.ide.progress.TaskCancellation 18 | import com.intellij.platform.ide.progress.withModalProgress 19 | import com.intellij.ui.CollectionComboBoxModel 20 | import com.intellij.ui.CollectionListModel 21 | import com.intellij.ui.ListSpeedSearch 22 | import com.intellij.ui.ToolbarDecorator 23 | import com.intellij.ui.components.JBList 24 | import com.intellij.ui.dsl.builder.LabelPosition 25 | import com.intellij.ui.dsl.builder.bindItem 26 | import com.intellij.ui.dsl.builder.bindText 27 | import com.intellij.ui.dsl.builder.panel 28 | import com.intellij.util.ui.JBUI 29 | import kotlinx.coroutines.CoroutineScope 30 | import kotlinx.coroutines.Dispatchers 31 | import kotlinx.coroutines.launch 32 | import kotlinx.coroutines.withContext 33 | import lol.bai.ravel.mapping.MappingNsVisitor 34 | import lol.bai.ravel.mapping.MioMappingConfig 35 | import lol.bai.ravel.mapping.downloader.MappingDownloader 36 | import lol.bai.ravel.mapping.downloader.MappingDownloaderExtension 37 | import lol.bai.ravel.util.getUserDownloadsDir 38 | import lol.bai.ravel.util.resolveUnique 39 | import net.fabricmc.mappingio.MappingReader 40 | import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch 41 | import net.fabricmc.mappingio.tree.MemoryMappingTree 42 | import java.nio.file.Path 43 | import javax.swing.ListSelectionModel 44 | import javax.swing.SwingUtilities 45 | import kotlin.io.path.Path 46 | 47 | class MappingActionGroup : DefaultActionGroup() 48 | 49 | open class AddMappingAction : AnAction() { 50 | 51 | fun readMapping(e: AnActionEvent, path: Path) { 52 | val mappingsModel = e.getData(K.mappingsModel) ?: return 53 | 54 | val format = MappingReader.detectFormat(path) 55 | if (format == null) { 56 | Messages.showErrorDialog(e.project, B("dialog.mapping.unknownFormat"), B.error) 57 | return 58 | } 59 | 60 | MappingReader.read(path, format, MappingNsVisitor) 61 | val namespaces = arrayListOf(MappingNsVisitor.src) 62 | namespaces.addAll(MappingNsVisitor.dst) 63 | 64 | var srcNs = MappingNsVisitor.src 65 | var dstNs = MappingNsVisitor.dst.first() 66 | 67 | val ok = DialogBuilder(e.project) 68 | .title(B("dialog.mapping.title")) 69 | .centerPanel(panel { 70 | row(B("dialog.mapping.format")) { label(format.name) } 71 | row(B("dialog.mapping.srcNs")) { 72 | comboBox(namespaces).bindItem({ srcNs }, { srcNs = it ?: srcNs }) 73 | } 74 | row(B("dialog.mapping.dstNs")) { 75 | comboBox(namespaces).bindItem({ dstNs }, { dstNs = it ?: dstNs }) 76 | } 77 | }) 78 | .showAndGet() 79 | 80 | if (!ok) return 81 | 82 | val mapping = MemoryMappingTree() 83 | val visitor = 84 | if (srcNs == MappingNsVisitor.src) mapping 85 | else MappingSourceNsSwitch(mapping, srcNs) 86 | 87 | MappingReader.read(path, format, visitor) 88 | mappingsModel.add(MioMappingConfig(mapping, srcNs, dstNs, path)) 89 | } 90 | 91 | override fun actionPerformed(e: AnActionEvent) { 92 | val fileDesc = FileChooserDescriptorFactory.singleFileOrDir() 93 | val path = FileChooser.chooseFile(fileDesc, e.project, null)?.toNioPath() ?: return 94 | readMapping(e, path) 95 | } 96 | 97 | } 98 | 99 | class DownloadMappingAction : AddMappingAction() { 100 | 101 | @Suppress("UnstableApiUsage") 102 | override fun actionPerformed(e: AnActionEvent) { 103 | val project = e.project ?: return 104 | 105 | val downloaders = MappingDownloaderExtension.createInstances() 106 | 107 | var downloadDir = getUserDownloadsDir().toCanonicalPath() 108 | var selectedDownloader: MappingDownloader? = null 109 | 110 | val versionModel = CollectionListModel() 111 | val versionList = JBList(versionModel).apply { 112 | selectionMode = ListSelectionModel.SINGLE_SELECTION 113 | } 114 | 115 | val ok = DialogBuilder(project) 116 | .title(B("dialog.mapping.download.title")) 117 | .centerPanel(panel { 118 | val downloaderModel = CollectionComboBoxModel(downloaders, null) 119 | val downloaderCb = ComboBox(downloaderModel) 120 | 121 | downloaderCb.addActionListener { 122 | val newDownloader = downloaderModel.selected 123 | if (selectedDownloader != newDownloader && newDownloader != null) { 124 | val service = S() 125 | versionList.setSelectedValue(null, true) 126 | versionModel.removeAll() 127 | versionList.setEmptyText(B("dialog.mapping.download.version.pending")) 128 | service.getVersion(newDownloader) { 129 | versionModel.add(it) 130 | versionList.setSelectedValue(it.firstOrNull(), true) 131 | } 132 | } 133 | selectedDownloader = newDownloader ?: selectedDownloader 134 | } 135 | 136 | row(B("dialog.mapping.download.directory")) { 137 | textFieldWithBrowseButton(FileChooserDescriptorFactory.singleDir(), project) { it.toNioPath().toCanonicalPath() } 138 | .bindText({ downloadDir }, { downloadDir = it }) 139 | } 140 | row(B("dialog.mapping.download.type")) { cell(downloaderCb) } 141 | row { 142 | val search = ListSpeedSearch.installOn(versionList) 143 | val pane = ToolbarDecorator.createDecorator(versionList) 144 | .setPreferredSize(JBUI.size(350)) 145 | .setAddIcon(AllIcons.Actions.Search) 146 | .setAddAction { search.showPopup() } 147 | .disableRemoveAction() 148 | .disableUpDownActions() 149 | .createPanel() 150 | cell(pane).label(B("dialog.mapping.download.version"), LabelPosition.TOP) 151 | } 152 | }) 153 | .showAndGet() 154 | 155 | if (!ok) return 156 | val downloader = selectedDownloader ?: return 157 | val version = versionList.selectedValue ?: return 158 | 159 | currentThreadCoroutineScope().launch { 160 | val (name, extension) = downloader.resolveDest(version) 161 | val downloadPath = Path(downloadDir).resolveUnique(name, extension) 162 | 163 | val downloaded = withModalProgress(ModalTaskOwner.project(project), B("dialog.mapping.download.mapping.pending"), TaskCancellation.cancellable()) { 164 | downloader.download(version, downloadPath) 165 | downloader.download(version, downloadPath) 166 | } 167 | 168 | if (downloaded) withContext(Dispatchers.EDT) { 169 | readMapping(e, downloadPath) 170 | } 171 | } 172 | } 173 | 174 | @Service 175 | class GetVersionService(private val cs: CoroutineScope) { 176 | fun getVersion(downloader: MappingDownloader, consumer: (List) -> Unit) = cs.launch { 177 | val versions = downloader.versions() 178 | SwingUtilities.invokeLater { consumer(versions) } 179 | } 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/main/resources/icons/pluginAction.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/JavaRemapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.ide.highlighter.JavaFileType 4 | import com.intellij.openapi.diagnostic.thisLogger 5 | import com.intellij.psi.* 6 | import com.intellij.psi.javadoc.PsiDocTagValue 7 | import com.intellij.psi.util.childrenOfType 8 | import lol.bai.ravel.mapping.rawQualifierSeparators 9 | import lol.bai.ravel.psi.implicitly 10 | import lol.bai.ravel.psi.jvmDesc 11 | import lol.bai.ravel.psi.jvmName 12 | import lol.bai.ravel.psi.jvmRaw 13 | import lol.bai.ravel.util.linkedSetMultiMap 14 | 15 | class JavaRemapperFactory : ExtensionRemapperFactory(::JavaRemapper, "java") 16 | open class JavaRemapper : JvmRemapper({ it as? PsiJavaFile }) { 17 | private val logger = thisLogger() 18 | 19 | protected abstract inner class JavaStage : JavaRecursiveElementWalkingVisitor(), Stage { 20 | override fun invoke() = pFile.accept(this) 21 | } 22 | 23 | override fun stages() = listOf( 24 | collectImports, 25 | remapClassName, 26 | remapPackage, 27 | remapMembers, 28 | remapReferences, 29 | remapStaticImports, 30 | remapDocTagValues, 31 | renameFile, 32 | ) 33 | 34 | protected lateinit var java: JavaPsiFacade 35 | protected lateinit var factory: PsiElementFactory 36 | 37 | override fun init(): Boolean { 38 | if (!super.init()) return false 39 | java = JavaPsiFacade.getInstance(project) 40 | factory = java.elementFactory 41 | return true 42 | } 43 | 44 | override fun comment(pElt: PsiElement, comment: String) { 45 | var pAnchor: PsiElement? = null 46 | comment.split('\n').forEach { line -> 47 | val pComment = factory.createCommentFromText("// $line", pElt) 48 | pAnchor = 49 | if (pAnchor == null) pElt.addBefore(pComment, pElt.firstChild) 50 | else pElt.addAfter(pComment, pAnchor) 51 | } 52 | } 53 | 54 | protected fun findClass(jvmName: String): PsiClass? { 55 | return java.findClass(jvmName.replace(rawQualifierSeparators, "."), scope) 56 | } 57 | 58 | protected fun findMethod(pClass: PsiClass, name: String, signature: String): PsiMethod? { 59 | return pClass.findMethodsByName(name, false).find { it.jvmDesc == signature } 60 | } 61 | 62 | private val importedClasses = linkedSetOf() 63 | private val collectImports = object : JavaStage() { 64 | override fun visitImportStatement(pStatement: PsiImportStatement) { 65 | super.visitImportStatement(pStatement) 66 | if (pStatement.isOnDemand) return 67 | val fqn = pStatement.qualifiedName ?: return 68 | importedClasses.add(fqn) 69 | } 70 | } 71 | private val topLevelClasses = linkedMapOf() 72 | private val nonFqnClassNames = hashMapOf() 73 | private val remapClassName = object : JavaStage() { 74 | override fun visitClass(pClass: PsiClass) { 75 | super.visitClass(pClass) 76 | val className = pClass.name ?: return 77 | val classJvmName = pClass.jvmName ?: return 78 | 79 | val mClass = mTree.get(pClass) 80 | val newClassJvmName = mClass?.newName 81 | val newClassName = mClass?.newFullPeriodName?.substringAfterLast('.') 82 | 83 | nonFqnClassNames[newClassName ?: className] = newClassJvmName ?: classJvmName 84 | if (pClass.containingClass == null) topLevelClasses[pClass] = newClassJvmName ?: classJvmName 85 | 86 | if (newClassName == null) return 87 | val pId = pClass.nameIdentifier ?: return 88 | write { pId.replace(factory.createIdentifier(newClassName)) } 89 | } 90 | } 91 | private var newPackageName: String? = null 92 | private val remapPackage = object : JavaStage() { 93 | override fun visitPackageStatement(pStatement: PsiPackageStatement) { 94 | super.visitPackageStatement(pStatement) 95 | 96 | val newPackageNames = topLevelClasses.values.map { it.substringBeforeLast('/') }.toSet() 97 | if (newPackageNames.size != 1) { 98 | logger.warn("File contains classes with different new packages") 99 | val comment = topLevelClasses.map { (k, v) -> "${k.name} -> $v" }.joinToString(separator = "\n") 100 | write { comment(pStatement, "TODO(Ravel): file contains classes with different new packages\n$comment") } 101 | return 102 | } 103 | 104 | newPackageName = newPackageNames.first().replace('/', '.') 105 | write { pStatement.replace(factory.createPackageStatement(newPackageName!!)) } 106 | } 107 | } 108 | private val remapMembers = object : JavaStage() { 109 | override fun visitField(pField: PsiField) { 110 | super.visitField(pField) 111 | val pId = pField.nameIdentifier 112 | val newFieldName = remap(pField) ?: return 113 | write { pId.replace(factory.createIdentifier(newFieldName)) } 114 | } 115 | 116 | override fun visitMethod(pMethod: PsiMethod) { 117 | super.visitMethod(pMethod) 118 | val pClass = pMethod.containingClass ?: return 119 | val pId = pMethod.nameIdentifier ?: return 120 | 121 | val newMethodName = 122 | if (pMethod.isConstructor) mTree.get(pClass)?.newFullPeriodName?.substringAfterLast('.') 123 | else remap(pMethod, pMethod) 124 | if (newMethodName == null) return 125 | 126 | write { pId.replace(factory.createIdentifier(newMethodName)) } 127 | } 128 | 129 | override fun visitRecordComponent(pRecordComponent: PsiRecordComponent) { 130 | super.visitRecordComponent(pRecordComponent) 131 | val pId = pRecordComponent.nameIdentifier ?: return 132 | val pClass = pRecordComponent.containingClass ?: return 133 | val className = pClass.qualifiedName ?: return 134 | 135 | val recordComponentName = pRecordComponent.name 136 | val recordComponentDesc = pRecordComponent.type.jvmRaw 137 | 138 | val getterDesc = "()${recordComponentDesc}" 139 | val pGetter = pClass.findMethodsByName(recordComponentName, true) 140 | 141 | val newGetterName = linkedMapOf() 142 | for (pGetter in pGetter) { 143 | val pGetterClass = pGetter.containingClass ?: continue 144 | if (pGetterClass == pClass) continue 145 | if (pGetter.jvmDesc != getterDesc) continue 146 | val key = pGetterClass.name + "#" + pGetter.name 147 | newGetterName[key] = remap(pClass, pGetter) ?: pGetter.name 148 | } 149 | 150 | if (newGetterName.isEmpty()) return 151 | 152 | val uniqueNewGetterNames = newGetterName.values.toSet() 153 | if (uniqueNewGetterNames.size != 1) { 154 | logger.warn("$className: record component '$recordComponentName' overrides methods with different new names") 155 | val comment = newGetterName.map { (k, v) -> "$k -> $v" }.joinToString(separator = "\n") 156 | write { comment(pClass, "TODO(Ravel): record component '$recordComponentName' overrides methods with different new names\n$comment") } 157 | return 158 | } 159 | 160 | val uniqueNewGetterName = uniqueNewGetterNames.first() 161 | if (recordComponentName == uniqueNewGetterName) return 162 | 163 | write { pId.replace(factory.createIdentifier(uniqueNewGetterName)) } 164 | rerun { mTree.getOrPut(pClass).putField(recordComponentName, uniqueNewGetterName) } 165 | } 166 | } 167 | private val pStaticImportUsages = linkedSetMultiMap() 168 | private val remapReferences = object : JavaStage() { 169 | override fun visitReferenceElement(pRef: PsiJavaCodeReferenceElement) { 170 | super.visitReferenceElement(pRef) 171 | if (pRef is PsiImportStaticReferenceElement) return 172 | val pRefId = pRef.referenceNameElement as? PsiIdentifier ?: return 173 | 174 | val pTarget = pRef.resolve() ?: return 175 | val pSafeParent = pRef.parent() ?: pFile 176 | 177 | if (pTarget is PsiField) { 178 | if (pTarget.implicitly(PsiModifier.STATIC) && pRef.qualifier == null) { 179 | pStaticImportUsages.put(pTarget.name, pTarget) 180 | } 181 | val newFieldName = remap(pTarget) ?: return 182 | write { pRefId.replace(factory.createIdentifier(newFieldName)) } 183 | return 184 | } 185 | 186 | if (pTarget is PsiMethod) { 187 | if (pTarget.implicitly(PsiModifier.STATIC) && pRef.qualifier == null) { 188 | pStaticImportUsages.put(pTarget.name, pTarget) 189 | } 190 | val newMethodName = remap(pSafeParent, pTarget) ?: return 191 | write { pRefId.replace(factory.createIdentifier(newMethodName)) } 192 | return 193 | } 194 | 195 | if (pTarget is PsiClass) { 196 | val className = pTarget.qualifiedName ?: return 197 | val pClassRefId = pRef.referenceNameElement as? PsiIdentifier ?: return 198 | val mClass = mTree.get(pTarget) ?: return 199 | val newJvmClassName = mClass.newName ?: return 200 | val newClassName = mClass.newFullPeriodName ?: return 201 | val newRefName = newClassName.substringAfterLast('.') 202 | 203 | var isQualified = false 204 | val pRefQual = pRef.qualifier as? PsiJavaCodeReferenceElement 205 | if (pRefQual != null) { 206 | isQualified = true 207 | val pRefQualTarget = pRefQual.resolve() 208 | if (pRefQualTarget is PsiPackage) { 209 | val newQualName = newClassName.substringBeforeLast('.') 210 | write { pRefQual.replace(factory.createPackageReferenceElement(newQualName)) } 211 | } 212 | } 213 | 214 | if (nonFqnClassNames.contains(newRefName) && nonFqnClassNames[newRefName] != newJvmClassName) { 215 | write { pRef.replace(factory.createReferenceFromText(newClassName, pRef)) } 216 | return 217 | } 218 | 219 | nonFqnClassNames[newRefName] = newJvmClassName 220 | write { pClassRefId.replace(factory.createIdentifier(newRefName)) } 221 | 222 | if (!isQualified && pRef.parent !is PsiImportStatement && pTarget.containingFile != pFile && !importedClasses.contains(className)) { 223 | importedClasses.add(className) 224 | 225 | write { 226 | val pDummyClass = pFileFactory.createFileFromText( 227 | "_RavelDummy_.java", JavaFileType.INSTANCE, """ 228 | package ${newClassName.substringBeforeLast('.')}; 229 | public class $newRefName {} 230 | """.trimIndent() 231 | ).childrenOfType().first() 232 | 233 | pFile.importClass(pDummyClass) 234 | } 235 | } 236 | return 237 | } 238 | } 239 | } 240 | private val remapStaticImports = object : JavaStage() { 241 | override fun visitImportStaticReferenceElement(pRef: PsiImportStaticReferenceElement) { 242 | super.visitImportStaticReferenceElement(pRef) 243 | val pRefId = pRef.referenceNameElement as? PsiIdentifier ?: return 244 | val pStatement = pRef.parent() ?: return 245 | val pClass = pRef.classReference.resolve() as? PsiClass ?: return 246 | val memberName = pRefId.text 247 | 248 | val pUsages = pStaticImportUsages[memberName].orEmpty().ifEmpty { 249 | val pMembers = arrayListOf() 250 | pMembers.addAll(pClass.findMethodsByName(memberName, false)) 251 | val pField = pClass.findFieldByName(memberName, false) 252 | if (pField != null) pMembers.add(pField) 253 | pMembers 254 | } 255 | 256 | val newMemberNames = linkedMapOf() 257 | pUsages.forEach { 258 | if (it is PsiMethod) newMemberNames["method " + it.name + it.jvmDesc] = mTree.get(it)?.newName ?: it.name 259 | else if (it is PsiField) newMemberNames["field " + it.name] = mTree.get(it)?.newName ?: it.name 260 | } 261 | 262 | val uniqueNewMemberNames = newMemberNames.values.toSet() 263 | if (uniqueNewMemberNames.size != 1) { 264 | logger.warn("ambiguous static import, members with name $memberName have different new names") 265 | val comment = newMemberNames.map { (k, v) -> "$k -> $v" }.joinToString(separator = "\n") 266 | write { comment(pStatement, "TODO(Ravel): ambiguous static import, members with name $memberName have different new names\n$comment") } 267 | return 268 | } 269 | 270 | write { pRefId.replace(factory.createIdentifier(uniqueNewMemberNames.first())) } 271 | return 272 | } 273 | } 274 | private val remapDocTagValues = object : JavaStage() { 275 | override fun visitDocTagValue(pValue: PsiDocTagValue) { 276 | super.visitDocTagValue(pValue) 277 | val pRef = pValue.reference ?: return 278 | val pRefTarget = pRef.resolve() ?: return 279 | 280 | if (pRefTarget is PsiField) { 281 | val newFieldName = remap(pRefTarget) ?: return 282 | write { pRef.handleElementRename(newFieldName) } 283 | return 284 | } 285 | 286 | if (pRefTarget is PsiMethod) { 287 | val pSafeElt = pValue.parent() ?: pFile 288 | val newMethodName = remap(pSafeElt, pRefTarget) ?: return 289 | write { pRef.handleElementRename(newMethodName) } 290 | return 291 | } 292 | } 293 | } 294 | private val renameFile = Stage s@{ renameFile(newPackageName, topLevelClasses) } 295 | } 296 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/KotlinRemapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.openapi.diagnostic.thisLogger 4 | import com.intellij.psi.* 5 | import lol.bai.ravel.psi.implicitly 6 | import lol.bai.ravel.psi.jvmName 7 | import lol.bai.ravel.util.* 8 | import org.jetbrains.kotlin.asJava.canHaveSyntheticGetter 9 | import org.jetbrains.kotlin.asJava.toLightClass 10 | import org.jetbrains.kotlin.asJava.toLightElements 11 | import org.jetbrains.kotlin.asJava.toLightMethods 12 | import org.jetbrains.kotlin.idea.base.psi.kotlinFqName 13 | import org.jetbrains.kotlin.idea.completion.reference 14 | import org.jetbrains.kotlin.idea.highlighting.analyzers.isCalleeExpression 15 | import org.jetbrains.kotlin.idea.structuralsearch.visitor.KotlinRecursiveElementWalkingVisitor 16 | import org.jetbrains.kotlin.lexer.KtTokens 17 | import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName 18 | import org.jetbrains.kotlin.load.java.propertyNameBySetMethodName 19 | import org.jetbrains.kotlin.name.FqName 20 | import org.jetbrains.kotlin.name.Name 21 | import org.jetbrains.kotlin.psi.* 22 | import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject 23 | import org.jetbrains.kotlin.psi.psiUtil.isImportDirectiveExpression 24 | import org.jetbrains.kotlin.psi.psiUtil.quoteIfNeeded 25 | 26 | // TODO: handle @JvmName 27 | class KotlinRemapperFactory: ExtensionRemapperFactory(::KotlinRemapper, "kt") 28 | class KotlinRemapper : JvmRemapper({ it as? KtFile }) { 29 | 30 | private val logger = thisLogger() 31 | 32 | private abstract inner class KotlinStage : KotlinRecursiveElementWalkingVisitor(), Stage { 33 | override fun invoke() = pFile.accept(this) 34 | } 35 | 36 | override fun stages() = listOf( 37 | remapClassNames, 38 | remapPackage, 39 | remapMembers, 40 | remapReferences, 41 | remapArrayReferences, 42 | remapImports, 43 | renameFile, 44 | ) 45 | 46 | private lateinit var factory: KtPsiFactory 47 | 48 | override fun init(): Boolean { 49 | if (!super.init()) return false 50 | this.factory = KtPsiFactory(project) 51 | return true 52 | } 53 | 54 | override fun comment(pElt: PsiElement, comment: String) { 55 | var pAnchor: PsiElement? = null 56 | comment.split('\n').forEach { line -> 57 | val pComment = factory.createComment("// $line") 58 | pAnchor = 59 | if (pAnchor == null) pElt.addBefore(pComment, pElt.firstChild) 60 | else pElt.addAfter(pComment, pAnchor) 61 | } 62 | } 63 | 64 | private fun remap(pSafeElt: PsiElement, kProperty: T): String? where T : KtNamedDeclaration, T : KtValVarKeywordOwner { 65 | val jElts = kProperty.toLightElements().toSet() 66 | if (jElts.isEmpty()) return null 67 | 68 | val newNames = linkedMapOf>() 69 | for (jElt in jElts) when (jElt) { 70 | is PsiParameter -> newNames[jElt] = null.held 71 | is PsiField -> newNames[jElt] = remap(jElt).held 72 | is PsiMethod -> { 73 | val newMethodName = remap(pSafeElt, jElt) 74 | newNames[jElt] = newMethodName.held 75 | } 76 | } 77 | 78 | if (newNames.size != jElts.size) { 79 | logger.warn("cannot resolve property ${kProperty.name}") 80 | write { comment(pSafeElt, "TODO(Ravel): cannot resolve property ${kProperty.name}") } 81 | return null 82 | } 83 | 84 | val uniqueNewNames = mutableSetOf() 85 | for ((jElt, newNameHolder) in newNames) { 86 | val newName = newNameHolder.value ?: continue 87 | 88 | if (jElt is PsiField) { 89 | uniqueNewNames.add(newName) 90 | continue 91 | } 92 | jElt as PsiMethod 93 | 94 | if (newName.startsWith("get")) { 95 | uniqueNewNames.add(newName.removePrefix("get").decapitalizeFirstChar()) 96 | } else if (newName.startsWith("set")) { 97 | uniqueNewNames.add(newName.removePrefix("set").decapitalizeFirstChar()) 98 | } else if (newName.startsWith("is")) { 99 | uniqueNewNames.add(newName) 100 | } else { 101 | logger.warn("property ${kProperty.name} have get/setter that overrides method which new name is not named get*/set*/is*") 102 | write { comment(pSafeElt, "TODO(Ravel): property ${kProperty.name} have get/setter overrides method which new name is not named get*/set*/is*") } 103 | return null 104 | } 105 | } 106 | 107 | if (uniqueNewNames.isEmpty()) return null 108 | if (uniqueNewNames.size != 1) { 109 | val comment = newNames 110 | .filter { it.key is PsiMethod } 111 | .map { (it.key as PsiMethod).name to it.value.value } 112 | .joinToString { "${it.first} -> ${it.second}" } 113 | logger.warn("property ${kProperty.name} overrides getters/setters with different new names") 114 | write { comment(pSafeElt, "TODO(Ravel): property ${kProperty.name} overrides getters/setters with different new names\n$comment") } 115 | return null 116 | } 117 | 118 | return uniqueNewNames.first() 119 | } 120 | 121 | private fun remap(pSafeElt: PsiElement, kFun: KtNamedFunction): String? { 122 | val jMethods = kFun.toLightMethods().toSet() 123 | if (jMethods.isEmpty()) return null 124 | 125 | val newNames = linkedMapOf>() 126 | for (jMethod in jMethods) newNames[jMethod] = remap(pSafeElt, jMethod).held 127 | 128 | if (newNames.size != jMethods.size) { 129 | logger.warn("cannot resolve function ${kFun.name}") 130 | write { comment(pSafeElt, "TODO(Ravel): cannot resolve function ${kFun.name}") } 131 | return null 132 | } 133 | 134 | val uniqueNewNames = mutableSetOf() 135 | for ((_, newNameHolder) in newNames) { 136 | val newName = newNameHolder.value ?: continue 137 | uniqueNewNames.add(newName) 138 | } 139 | 140 | if (uniqueNewNames.isEmpty()) return null 141 | if (uniqueNewNames.size != 1) { 142 | val comment = newNames 143 | .map { it.key.name to it.value.value } 144 | .joinToString { "${it.first} -> ${it.second}" } 145 | logger.warn("function ${kFun.name} overrides methods with different new names") 146 | write { comment(pSafeElt, "TODO(Ravel): function ${kFun.name} overrides methods with different new names\n$comment") } 147 | return null 148 | } 149 | 150 | return uniqueNewNames.first() 151 | } 152 | 153 | private val topLevelClasses = linkedMapOf() 154 | private val nonFqnClassNames = hashMapOf() 155 | private val remapClassNames = object : KotlinStage() { 156 | override fun visitClassOrObject(kClass: KtClassOrObject) { 157 | super.visitClassOrObject(kClass) 158 | val className = kClass.name ?: return 159 | val classJvmName = kClass.jvmName ?: return 160 | 161 | val jClass = kClass.toLightClass() ?: return 162 | val mClass = mTree.get(jClass) 163 | val newClassJvmName = mClass?.newName 164 | val newClassName = mClass?.newFullPeriodName?.substringAfterLast('.') 165 | 166 | nonFqnClassNames[newClassName ?: className] = newClassJvmName ?: classJvmName 167 | if (kClass.containingClassOrObject == null) topLevelClasses[jClass] = newClassJvmName ?: classJvmName 168 | 169 | if (newClassName == null) return 170 | val pId = kClass.nameIdentifier ?: return 171 | write { pId.replace(factory.createIdentifier(newClassName)) } 172 | } 173 | } 174 | private var newPackageName: String? = null 175 | private val remapPackage = object : KotlinStage() { 176 | override fun visitPackageDirective(kPackage: KtPackageDirective) { 177 | super.visitPackageDirective(kPackage) 178 | if (topLevelClasses.isEmpty()) return 179 | 180 | val newPackageNames = topLevelClasses.values.map { it.substringBeforeLast('/') }.toSet() 181 | if (newPackageNames.size != 1) { 182 | logger.warn("File contains classes with different new packages") 183 | val comment = topLevelClasses.map { (k, v) -> "${k.name} -> $v" }.joinToString(separator = "\n") 184 | write { comment(kPackage, "TODO(Ravel): file contains classes with different new packages\n$comment") } 185 | return 186 | } 187 | 188 | newPackageName = newPackageNames.first().replace('/', '.') 189 | write { kPackage.replace(factory.createPackageDirective(FqName(newPackageName!!))) } 190 | } 191 | } 192 | private val remapMembers = object : KotlinStage() { 193 | override fun visitProperty(kProperty: KtProperty) { 194 | super.visitProperty(kProperty) 195 | val pId = kProperty.nameIdentifier ?: return 196 | val newName = remap(kProperty, kProperty) ?: return 197 | write { pId.replace(factory.createIdentifier(newName.quoteIfNeeded())) } 198 | } 199 | 200 | override fun visitNamedFunction(kFun: KtNamedFunction) { 201 | super.visitNamedFunction(kFun) 202 | val pId = kFun.nameIdentifier ?: return 203 | val newName = remap(kFun, kFun) ?: return 204 | write { pId.replace(factory.createIdentifier(newName.quoteIfNeeded())) } 205 | } 206 | 207 | override fun visitParameter(kParam: KtParameter) { 208 | super.visitParameter(kParam) 209 | if (!kParam.hasValOrVar()) return 210 | val pId = kParam.nameIdentifier ?: return 211 | val kFun = kParam.ownerFunction ?: return 212 | val pSafeElt = if (kFun is KtPrimaryConstructor) kFun.containingClassOrObject else kFun 213 | if (pSafeElt == null) return 214 | 215 | val newName = remap(pSafeElt, kParam) ?: return 216 | write { pId.replace(factory.createIdentifier(newName.quoteIfNeeded())) } 217 | } 218 | } 219 | private val pMemberImportUsages = linkedSetMultiMap() 220 | private val remapReferences = object : KotlinStage() { 221 | private val lateRefWrites = arrayListOf Unit>>() 222 | 223 | override fun invoke() { 224 | super.invoke() 225 | lateRefWrites.sortedByDescending { it.first }.forEach { write(it.second) } 226 | } 227 | 228 | override fun visitSimpleNameExpression(kRef: KtSimpleNameExpression) { 229 | super.visitSimpleNameExpression(kRef) 230 | val kRefParent = kRef.parent 231 | if (kRefParent is KtSuperExpression) return 232 | if (kRefParent is KtThisExpression) return 233 | 234 | val pRef = kRef.reference() ?: return 235 | val pTarget = pRef.resolve() as? PsiNamedElement ?: return 236 | val pSafeParent = kRef.parent() ?: pFile 237 | 238 | val kDot = kRef.parent()?.receiverExpression 239 | 240 | var staticTargetClassName: String? = null 241 | run t@{ 242 | if (pTarget is KtProperty) { 243 | if (kRef.isImportDirectiveExpression()) return@t 244 | if (kDot != null) pTarget.fqName?.let { pMemberImportUsages.put(it, pTarget) } 245 | 246 | if (pTarget.name != kRef.getReferencedName()) return@t 247 | if (pTarget.isTopLevel) staticTargetClassName = pTarget.containingKtFile.jvmName 248 | val newTargetName = remap(pSafeParent, pTarget) ?: return@t 249 | write { kRef.replace(factory.createSimpleName(newTargetName.quoteIfNeeded())) } 250 | return@t 251 | } 252 | 253 | if (pTarget is PsiField) { 254 | if (kRef.isImportDirectiveExpression()) return@t 255 | if (pTarget.implicitly(PsiModifier.STATIC)) { 256 | staticTargetClassName = pTarget.containingClass?.jvmName 257 | if (kDot != null) pTarget.kotlinFqName?.let { pMemberImportUsages.put(it, pTarget) } 258 | if (pTarget.name != kRef.getReferencedName()) return@t 259 | } 260 | 261 | val newTargetName = remap(pTarget) ?: return@t 262 | write { kRef.replace(factory.createSimpleName(newTargetName.quoteIfNeeded())) } 263 | return@t 264 | } 265 | 266 | if (pTarget is KtParameter) { 267 | if (kRef.isImportDirectiveExpression()) return@t 268 | if (!pTarget.hasValOrVar()) return@t 269 | if (kDot != null) pTarget.fqName?.let { pMemberImportUsages.put(it, pTarget) } 270 | 271 | if (pTarget.name != kRef.getReferencedName()) return@t 272 | val newTargetName = remap(pSafeParent, pTarget) ?: return@t 273 | write { kRef.replace(factory.createSimpleName(newTargetName.quoteIfNeeded())) } 274 | return@t 275 | } 276 | 277 | fun remapMethodCall(pMethod: PsiMethod, newTargetName: String) { 278 | val newTargetNameName = Name.guessByFirstCharacter(newTargetName) 279 | 280 | if ((kRef.parent as? KtCallableReferenceExpression)?.callableReference == kRef) { 281 | write { kRef.replace(factory.createSimpleName(newTargetName.quoteIfNeeded())) } 282 | return 283 | } 284 | 285 | val isPropertyAccess = !kRef.isCalleeExpression() 286 | var newTargetSetter = propertyNameBySetMethodName(newTargetNameName, pMethod.returnType == PsiTypes.booleanType())?.asString() 287 | if (newTargetSetter != null) { 288 | if (pMethod.parameterList.parametersCount != 1 289 | || pMethod.returnType != PsiTypes.voidType() 290 | || !newTargetSetter.first().isLetter() 291 | ) newTargetSetter = null 292 | } 293 | if (newTargetSetter != null) run m@{ 294 | val pSuperMethods = pMethod.findDeepestSuperMethods() 295 | val pOriginalMethod = if (pSuperMethods.size == 1) pSuperMethods.first() else pMethod 296 | val pClass = pOriginalMethod.containingClass ?: return@m 297 | val mClass = mTree.get(pClass) ?: return@m 298 | 299 | if (mClass.fields.any { it.newName == newTargetSetter }) { 300 | newTargetSetter = null 301 | return@m 302 | } 303 | 304 | val newTargetGetter = 305 | if (newTargetSetter.startsWith("is")) newTargetSetter 306 | else "get" + newTargetSetter.capitalizeFirstChar() 307 | if (mClass.methods.none { it.newName == newTargetGetter }) { 308 | newTargetSetter = null 309 | } 310 | } 311 | 312 | // TODO: more robust handling 313 | if (newTargetSetter == null && isPropertyAccess) run s@{ 314 | // from setter to function call 315 | var kBinary: KtBinaryExpression = 316 | if (kRefParent is KtBinaryExpression && kRefParent.left == kRef) kRefParent 317 | else { 318 | val kRefGrandParent = kRefParent.parent 319 | if (kRefGrandParent is KtBinaryExpression && kRefGrandParent.left == kRefParent) kRefGrandParent 320 | else return@s 321 | } 322 | 323 | // TODO: +=, -= and co 324 | if (kBinary.operationToken != KtTokens.EQ) return@s 325 | 326 | lateRefWrites.add(kBinary.depth to w@{ 327 | val kLeft = kBinary.left ?: return@w 328 | val kRight = kBinary.right ?: return@w 329 | 330 | val receiverText = 331 | if (kLeft is KtDotQualifiedExpression) kLeft.receiverExpression.text 332 | else null 333 | 334 | val callText = if (receiverText != null) { 335 | "${receiverText}.${newTargetName}(${kRight.text})" 336 | } else { 337 | "${newTargetName}(${kRight.text})" 338 | } 339 | 340 | kBinary.replace(factory.createExpression(callText)) 341 | }) 342 | return 343 | } 344 | if (newTargetSetter != null && !isPropertyAccess && kRefParent is KtCallExpression) { 345 | // from function call to setter 346 | lateRefWrites.add(kRefParent.depth to w@{ 347 | val kArg = kRefParent.valueArguments.firstOrNull() ?: return@w 348 | kRefParent.replace(factory.createExpression("$newTargetSetter = ${kArg.text}")) 349 | }) 350 | return 351 | } 352 | 353 | var newTargetGetter = propertyNameByGetMethodName(newTargetNameName)?.asString() 354 | if (newTargetGetter != null) { 355 | if (pMethod.hasParameters() 356 | || pMethod.returnType == PsiTypes.voidType() 357 | || !newTargetGetter.first().isLetter() 358 | ) newTargetGetter = null 359 | } 360 | 361 | if (newTargetGetter == null && isPropertyAccess && pMethod.canHaveSyntheticGetter) { 362 | // from getter to function call 363 | lateRefWrites.add(kRef.depth to { kRef.replace(factory.createExpression("${newTargetName}()")) }) 364 | return 365 | } 366 | if (newTargetGetter != null && !isPropertyAccess && kRefParent is KtCallExpression) { 367 | // from function call to getter 368 | lateRefWrites.add(kRefParent.depth to { kRefParent.replace(factory.createExpression(newTargetGetter)) }) 369 | return 370 | } 371 | 372 | // from property access to property access or function call to function call 373 | val newTargetAccessor = newTargetSetter ?: newTargetGetter ?: newTargetName 374 | write { kRef.replace(factory.createSimpleName(newTargetAccessor.quoteIfNeeded())) } 375 | } 376 | 377 | if (pTarget is KtNamedFunction) { 378 | if (kRef.isImportDirectiveExpression()) return@t 379 | if (kDot != null) pTarget.fqName?.let { pMemberImportUsages.put(it, pTarget) } 380 | 381 | if (pTarget.name != kRef.getReferencedName()) return@t 382 | if (pTarget.isTopLevel) staticTargetClassName = pTarget.containingKtFile.jvmName 383 | val newTargetName = remap(pSafeParent, pTarget) ?: return@t 384 | 385 | if (pTarget.hasModifier(KtTokens.OVERRIDE_KEYWORD)) run j@{ 386 | val jMethods = pTarget.toLightMethods() 387 | if (jMethods.size != 1) return@j 388 | 389 | return@t remapMethodCall(jMethods.first(), newTargetName) 390 | } 391 | 392 | write { kRef.replace(factory.createSimpleName(newTargetName.quoteIfNeeded())) } 393 | return@t 394 | } 395 | 396 | fun remapKotlinClass(kClass: KtClassOrObject?) { 397 | if (kClass == null) return 398 | if (kClass.name != kRef.getReferencedName()) return 399 | staticTargetClassName = kClass.jvmName 400 | val newTargetName = mTree.getClass(kClass.jvmName)?.newFullPeriodName ?: return 401 | write { kRef.replace(factory.createSimpleName(newTargetName.substringAfterLast('.').quoteIfNeeded())) } 402 | } 403 | 404 | if (pTarget is KtConstructor<*>) { 405 | return@t remapKotlinClass(pTarget.containingClassOrObject) 406 | } 407 | 408 | if (pTarget is KtClassOrObject) { 409 | return@t remapKotlinClass(pTarget) 410 | } 411 | 412 | fun remapJavaClass(jClass: PsiClass?) { 413 | if (jClass == null) return 414 | if (jClass.name != kRef.getReferencedName()) return 415 | staticTargetClassName = jClass.jvmName 416 | val newTargetName = mTree.get(jClass)?.newFullPeriodName ?: return 417 | write { kRef.replace(factory.createSimpleName(newTargetName.substringAfterLast('.').quoteIfNeeded())) } 418 | } 419 | 420 | if (pTarget is PsiMethod) { 421 | if (kRef.isImportDirectiveExpression()) return@t 422 | if (pTarget.isConstructor) return@t remapJavaClass(pTarget.containingClass) 423 | 424 | if (pTarget.implicitly(PsiModifier.STATIC)) { 425 | staticTargetClassName = pTarget.containingClass?.jvmName 426 | if (kDot != null) pTarget.kotlinFqName?.let { pMemberImportUsages.put(it, pTarget) } 427 | if (pTarget.name != kRef.getReferencedName()) return@t 428 | } 429 | 430 | val newTargetName = remap(pSafeParent, pTarget) ?: return@t 431 | return@t remapMethodCall(pTarget, newTargetName) 432 | } 433 | 434 | if (pTarget is PsiClass) { 435 | return@t remapJavaClass(pTarget) 436 | } 437 | } 438 | 439 | if (staticTargetClassName == null) return 440 | if (kDot == null) return 441 | 442 | val kDotRef = 443 | if (kDot is KtDotQualifiedExpression) kDot.selectorExpression as? KtNameReferenceExpression 444 | else kDot as? KtNameReferenceExpression 445 | if (kDotRef?.reference?.resolve() !is PsiPackage) return 446 | 447 | val newClassName = mTree.getClass(staticTargetClassName)?.newPkgPeriodName ?: return 448 | val newPackageName = newClassName.substringBeforeLast('.') 449 | write { kDot.replace(factory.createExpression(newPackageName)) } 450 | } 451 | } 452 | private val remapArrayReferences = object : KotlinStage() { 453 | override fun visitArrayAccessExpression(kRef: KtArrayAccessExpression) { 454 | super.visitArrayAccessExpression(kRef) 455 | val pRef = kRef.reference() ?: return 456 | val pTarget = pRef.resolve() as? PsiNamedElement ?: return 457 | val pSafeParent = kRef.parent() ?: pFile 458 | 459 | if (pTarget is PsiMethod) { 460 | val newTargetName = remap(pSafeParent, pTarget) ?: return 461 | if (newTargetName == "get" || newTargetName == "set") return 462 | 463 | write w@{ 464 | val owner = kRef.arrayExpression?.text ?: return@w 465 | val args = kRef.indexExpressions.joinToString { it.text } 466 | kRef.replace(factory.createExpression("${owner}.${newTargetName}($args)")) 467 | } 468 | return 469 | } 470 | 471 | // TODO: kotlin operator function rename? 472 | } 473 | } 474 | private val remapImports = object : KotlinStage() { 475 | override fun visitImportDirective(kImport: KtImportDirective) { 476 | super.visitImportDirective(kImport) 477 | val kRefExp = kImport.importedReference ?: return 478 | val kRefSelector = 479 | if (kRefExp is KtDotQualifiedExpression) kRefExp.selectorExpression 480 | else kRefExp as? KtNameReferenceExpression 481 | if (kRefSelector == null) return 482 | 483 | val targetName = kImport.importedFqName ?: return 484 | val pUsages = pMemberImportUsages[targetName].orEmpty().ifEmpty { return } 485 | 486 | val newNames = linkedMapOf>() 487 | for (pElt in pUsages) when (pElt) { 488 | is KtProperty -> newNames[pElt] = remap(kImport, pElt).held 489 | is KtParameter -> newNames[pElt] = remap(kImport, pElt).held 490 | is KtNamedFunction -> newNames[pElt] = remap(kImport, pElt).held 491 | is PsiField -> newNames[pElt] = remap(pElt).held 492 | is PsiMethod -> newNames[pElt] = remap(kImport, pElt).held 493 | } 494 | 495 | val uniqueNewNames = mutableSetOf() 496 | for ((_, newNameHolder) in newNames) { 497 | val newName = newNameHolder.value ?: continue 498 | uniqueNewNames.add(newName) 499 | } 500 | if (uniqueNewNames.isEmpty()) return 501 | 502 | if (uniqueNewNames.size != 1) { 503 | val memberName = targetName.shortName().asString() 504 | val comment = newNames 505 | .filter { it.value.value != null } 506 | .map { (k, v) -> "${k.name} -> ${v.value}" } 507 | .joinToString(separator = "\n") 508 | logger.warn("ambiguous import, members with name $memberName have different new names") 509 | write { comment(kImport, "TODO(Ravel): ambiguous import, members with name $memberName have different new names\n$comment") } 510 | return 511 | } 512 | 513 | write { kRefSelector.replace(factory.createExpression(uniqueNewNames.first())) } 514 | } 515 | } 516 | private val renameFile = Stage s@{ renameFile(newPackageName, topLevelClasses) } 517 | } 518 | -------------------------------------------------------------------------------- /src/main/kotlin/lol/bai/ravel/remapper/MixinRemapper.kt: -------------------------------------------------------------------------------- 1 | package lol.bai.ravel.remapper 2 | 3 | import com.intellij.openapi.diagnostic.thisLogger 4 | import com.intellij.psi.* 5 | import lol.bai.ravel.mapping.ClassMapping 6 | import lol.bai.ravel.psi.jvmDesc 7 | import lol.bai.ravel.psi.jvmName 8 | import lol.bai.ravel.util.capitalizeFirstChar 9 | import lol.bai.ravel.util.decapitalizeFirstChar 10 | import lol.bai.ravel.util.setMultiMap 11 | 12 | // @formatter:off 13 | private const val mixin = "org.spongepowered.asm.mixin" 14 | private const val Mixin = "${mixin}.Mixin" 15 | private const val Shadow = "${mixin}.Shadow" 16 | private const val Unique = "${mixin}.Unique" 17 | private const val Final = "${mixin}.Final" 18 | private const val Debug = "${mixin}.Debug" 19 | private const val Intrinsic = "${mixin}.Intrinsic" 20 | private const val Mutable = "${mixin}.Mutable" 21 | private const val Overwrite = "${mixin}.Overwrite" 22 | private const val Dynamic = "${mixin}.Dynamic" 23 | private const val Pseudo = "${mixin}.Pseudo" 24 | private const val Invoker = "${mixin}.gen.Invoker" 25 | private const val Accessor = "${mixin}.gen.Accessor" 26 | private const val At = "${mixin}.injection.At" 27 | private const val Slice = "${mixin}.injection.Slice" 28 | private const val Inject = "${mixin}.injection.Inject" 29 | private const val ModifyArg = "${mixin}.injection.ModifyArg" 30 | private const val ModifyArgs = "${mixin}.injection.ModifyArgs" 31 | private const val ModifyConstant = "${mixin}.injection.ModifyConstant" 32 | private const val ModifyVariable = "${mixin}.injection.ModifyVariable" 33 | private const val Redirect = "${mixin}.injection.Redirect" 34 | private const val Coerce = "${mixin}.injection.Coerce" 35 | private const val Constant = "${mixin}.injection.Constant" 36 | 37 | private const val mixinextras = "com.llamalad7.mixinextras" 38 | private const val ModifyExpressionValue = "${mixinextras}.injector.ModifyExpressionValue" 39 | private const val ModifyReceiver = "${mixinextras}.injector.ModifyReceiver" 40 | private const val ModifyReturnValue = "${mixinextras}.injector.ModifyReturnValue" 41 | private const val WrapWithCondition = "${mixinextras}.injector.WrapWithCondition" 42 | private const val WrapWithCondition2 = "${mixinextras}.injector.v2.WrapWithCondition" 43 | private const val WrapMethod = "${mixinextras}.injector.wrapmethod.WrapMethod" 44 | private const val WrapOperation = "${mixinextras}.injector.wrapoperation.WrapOperation" 45 | private const val Cancellable = "${mixinextras}.sugar.Cancellable" 46 | private const val Local = "${mixinextras}.sugar.Local" 47 | private const val Share = "${mixinextras}.sugar.Share" 48 | private const val Definition = "${mixinextras}.expression.Definition" 49 | // @formatter:on 50 | 51 | private val INJECTS = setOf( 52 | Inject, ModifyArg, ModifyArgs, ModifyConstant, ModifyVariable, Redirect, 53 | ModifyExpressionValue, ModifyReceiver, ModifyReturnValue, WrapWithCondition, WrapWithCondition2, WrapMethod, WrapOperation 54 | ) 55 | 56 | private object Point { 57 | // @formatter:off 58 | const val HEAD = "HEAD" 59 | const val RETURN = "RETURN" 60 | const val TAIL = "TAIL" 61 | const val INVOKE = "INVOKE" 62 | const val INVOKE_ASSIGN = "INVOKE_ASSIGN" 63 | const val FIELD = "FIELD" 64 | const val NEW = "NEW" 65 | const val INVOKE_STRING = "INVOKE_STRING" 66 | const val JUMP = "JUMP" 67 | const val CONSTANT = "CONSTANT" 68 | const val STORE = "STORE" 69 | const val LOAD = "LOAD" 70 | const val EXPRESSION = "MIXINEXTRAS:EXPRESSION" 71 | // @formatter:on 72 | 73 | val INVOKES = setOf(INVOKE, INVOKE_ASSIGN, INVOKE_STRING) 74 | } 75 | 76 | private object AccessorPrefixes { 77 | // @formatter:off 78 | val get = Regex("^((\\w+[_$])?get)(.+)$") 79 | val set = Regex("^((\\w+[_$])?set)(.+)$") 80 | val is_ = Regex("^((\\w+[_$])?is)(.+)$") 81 | 82 | val call = Regex("^((\\w+[_$])?call)(.+)$") 83 | val invoke = Regex("^((\\w+[_$])?invoke)(.+)$") 84 | // @formatter:on 85 | } 86 | 87 | class MixinRemapperFactory : ExtensionRemapperFactory(::MixinRemapper, "java") 88 | class MixinRemapper : JavaRemapper() { 89 | 90 | private val logger = thisLogger() 91 | override fun stages() = listOf(remapMixins) 92 | 93 | private fun splitClassMember(classMember: String): Pair { 94 | if (classMember.startsWith('L')) { 95 | // Lpath/to/Class;method()V 96 | var (className, memberNameAndDesc) = classMember.split(';', limit = 2) 97 | className = className.removePrefix("L") 98 | return className to memberNameAndDesc 99 | } else if (classMember.contains('.')) { 100 | // path/to/Class.method()V 101 | val (className, memberNameAndDesc) = classMember.split('.', limit = 2) 102 | return className to memberNameAndDesc 103 | } else { 104 | return null to classMember 105 | } 106 | } 107 | 108 | private val mixinTargets = setMultiMap() 109 | private val remapMixins = psiStage a@{ pAnnotation: PsiAnnotation -> 110 | val pClass = pAnnotation.parent() ?: return@a 111 | val className = pClass.qualifiedName ?: return@a 112 | val classJvmName = pClass.jvmName ?: return@a 113 | val annotationName = pAnnotation.qualifiedName ?: return@a 114 | 115 | if (!annotationName.startsWith(mixin) && !annotationName.startsWith(mixinextras)) return@a 116 | 117 | if (annotationName == Slice) return@a 118 | if (annotationName == Unique) return@a 119 | if (annotationName == Final) return@a 120 | if (annotationName == Debug) return@a 121 | if (annotationName == Intrinsic) return@a 122 | if (annotationName == Mutable) return@a 123 | if (annotationName == Cancellable) return@a 124 | if (annotationName == Local) return@a 125 | if (annotationName == Share) return@a 126 | if (annotationName == Dynamic) return@a 127 | if (annotationName == Pseudo) return@a 128 | if (annotationName == Coerce) return@a 129 | if (annotationName == Constant) return@a 130 | 131 | fun warnNotLiterals(pElt: PsiElement) { 132 | write { comment(pElt, "TODO(Ravel): target not a literal or array of literals") } 133 | logger.warn("$className: target not a literal or array of literals") 134 | } 135 | 136 | if (annotationName == Mixin) { 137 | val newTargetNames = linkedSetOf>() 138 | 139 | fun remapTarget(pTarget: PsiLiteralExpression) { 140 | var target = pTarget.value as String 141 | target = target.replace('.', '/') 142 | mixinTargets.put(className, target) 143 | 144 | val mTargetClass = mTree.getClass(target) 145 | val newTarget = mTargetClass?.newPkgPeriodName 146 | newTargetNames.add(target.substringAfterLast('/') to newTarget?.substringAfterLast('.')) 147 | if (newTarget != null) write { pTarget.replace(factory.createExpressionFromText("\"${newTarget}\"", pTarget)) } 148 | } 149 | 150 | val pTargets = pAnnotation.findDeclaredAttributeValue("targets") 151 | if (pTargets != null) when (pTargets) { 152 | is PsiLiteralExpression -> remapTarget(pTargets) 153 | is PsiArrayInitializerMemberValue -> pTargets.initializers.forEach { 154 | if (it is PsiLiteralExpression) remapTarget(it) 155 | else warnNotLiterals(pClass) 156 | } 157 | 158 | else -> warnNotLiterals(pClass) 159 | } 160 | 161 | fun putClassTarget(pTarget: PsiClassObjectAccessExpression) { 162 | val type = pTarget.operand.type 163 | fun warnCantResolve() { 164 | write { comment(pClass, "TODO(Ravel): can not resolve target class ${type.canonicalText}") } 165 | logger.warn("$className: can not resolve target class ${type.canonicalText}") 166 | } 167 | 168 | if (type is PsiClassType) { 169 | val pTargetClass = type.resolve() ?: return warnCantResolve() 170 | val targetClassName = pTargetClass.jvmName ?: return warnCantResolve() 171 | mixinTargets.put(className, targetClassName) 172 | 173 | val newTargetClassName = mTree.get(pTargetClass)?.newName?.substringAfterLast('/') 174 | newTargetNames.add(targetClassName.substringAfterLast('/') to newTargetClassName) 175 | } 176 | } 177 | 178 | val pValues = pAnnotation.findDeclaredAttributeValue("value") 179 | if (pValues != null) when (pValues) { 180 | is PsiClassObjectAccessExpression -> putClassTarget(pValues) 181 | is PsiArrayInitializerMemberValue -> pValues.initializers.forEach { 182 | if (it is PsiClassObjectAccessExpression) putClassTarget(it) 183 | else warnNotLiterals(pClass) 184 | } 185 | 186 | else -> warnNotLiterals(pClass) 187 | } 188 | 189 | if (newTargetNames.size == 1) { 190 | fun renameClass(pair: Pair): Boolean { 191 | val (targetClassName, newTargetClassName) = pair 192 | if (newTargetClassName != null && className.contains(targetClassName)) { 193 | rerun { mTree.putClass(classJvmName, classJvmName.replace(targetClassName, newTargetClassName)) } 194 | return true 195 | } 196 | return false 197 | } 198 | 199 | if (!renameClass(newTargetNames.first())) { 200 | renameClass(newTargetNames.first().let { (a, b) -> 201 | a.replace('$', '_') to b?.replace('$', '_') 202 | }) 203 | } 204 | } 205 | 206 | return@a 207 | } 208 | 209 | fun targetClassName(pMember: PsiMember): String? { 210 | val targetClassName = mixinTargets[className].orEmpty() 211 | if (targetClassName.size != 1) { 212 | write { comment(pMember, "TODO(Ravel): Could not determine a single target") } 213 | logger.warn("$className#${pMember.name}: Could not determine a single target") 214 | return null 215 | } 216 | return targetClassName.first() 217 | } 218 | 219 | fun targetClass(pMember: PsiMember): Pair? { 220 | val targetClassName = targetClassName(pMember) ?: return null 221 | val pTargetClass = findClass(targetClassName) 222 | val mTargetClass = mTree.getClass(targetClassName) 223 | if (pTargetClass == null && mTargetClass == null) return null 224 | return pTargetClass to mTargetClass 225 | } 226 | 227 | if (annotationName == Invoker) { 228 | val pMethod = pAnnotation.parent() ?: return@a 229 | val methodName = pMethod.name 230 | val (pTargetClass, mTargetClass) = targetClass(pMethod) ?: return@a 231 | 232 | var targetSignature: String? = null 233 | var invokerPrefix = ( 234 | AccessorPrefixes.call.matchEntire(methodName) 235 | ?: AccessorPrefixes.invoke.matchEntire(methodName) 236 | )?.groups?.get(1)?.value 237 | 238 | var targetMethodName = 239 | if (invokerPrefix != null) methodName.removePrefix(invokerPrefix).decapitalizeFirstChar() 240 | else null 241 | 242 | val pValue = pAnnotation.findDeclaredAttributeValue("value") 243 | if (pValue is PsiLiteralExpression) { 244 | var explicitTargetMethodName: String? = null 245 | val value = pValue.value as String 246 | if (value == "") return@a 247 | if (value.contains('(')) { 248 | explicitTargetMethodName = value.substringBefore('(') 249 | targetSignature = value.removePrefix(explicitTargetMethodName) 250 | } else { 251 | explicitTargetMethodName = value 252 | } 253 | if (explicitTargetMethodName == methodName) invokerPrefix = "" 254 | else if (explicitTargetMethodName != targetMethodName) invokerPrefix = null 255 | targetMethodName = explicitTargetMethodName 256 | } 257 | 258 | if (targetMethodName == null) { 259 | write { comment(pMethod, "TODO(Ravel): No target method") } 260 | logger.warn("$className#$methodName: No target method") 261 | return@a 262 | } 263 | 264 | if (targetSignature == null) targetSignature = pMethod.jvmDesc 265 | 266 | var newMethodName: String 267 | if (pTargetClass != null) { 268 | val pTargetMethod = findMethod(pTargetClass, targetMethodName, targetSignature) ?: return@a 269 | newMethodName = remap(pMethod, pTargetMethod) ?: return@a 270 | } else { 271 | val mTargetMethod = mTargetClass!!.getMethod(targetMethodName, targetSignature) ?: return@a 272 | newMethodName = mTargetMethod.newName ?: return@a 273 | } 274 | 275 | if (pValue != null) { 276 | write { pValue.replace(factory.createExpressionFromText("\"${newMethodName}\"", pValue)) } 277 | } 278 | if (invokerPrefix != null) { 279 | val newInvokerName = invokerPrefix + newMethodName.capitalizeFirstChar() 280 | rerun { mTree.getOrPut(pClass).putMethod(pMethod.name, pMethod.jvmDesc, newInvokerName) } 281 | } 282 | return@a 283 | } 284 | 285 | if (annotationName == Accessor) { 286 | val pMethod = pAnnotation.parent() ?: return@a 287 | val methodName = pMethod.name 288 | val mTargetClass = targetClass(pMethod)?.second ?: return@a 289 | 290 | var accessorPrefix = ( 291 | AccessorPrefixes.get.matchEntire(methodName) 292 | ?: AccessorPrefixes.set.matchEntire(methodName) 293 | ?: AccessorPrefixes.is_.matchEntire(methodName) 294 | )?.groups?.get(1)?.value 295 | 296 | var targetFieldName = 297 | if (accessorPrefix != null) methodName.removePrefix(accessorPrefix).decapitalizeFirstChar() 298 | else null 299 | 300 | val pValue = pAnnotation.findDeclaredAttributeValue("value") 301 | if (pValue is PsiLiteralExpression) { 302 | val explicitTargetFieldName = pValue.value as String 303 | if (explicitTargetFieldName == methodName) accessorPrefix = "" 304 | else if (explicitTargetFieldName != targetFieldName) accessorPrefix = null 305 | targetFieldName = explicitTargetFieldName 306 | } 307 | 308 | if (targetFieldName == null) { 309 | write { comment(pMethod, "TODO(Ravel): No target field") } 310 | logger.warn("$className#$methodName: No target field") 311 | return@a 312 | } 313 | 314 | val mTargetField = mTargetClass.getField(targetFieldName) ?: return@a 315 | val newFieldName = mTargetField.newName ?: return@a 316 | 317 | if (pValue != null) { 318 | write { pValue.replace(factory.createExpressionFromText("\"${newFieldName}\"", pValue)) } 319 | } 320 | if (accessorPrefix != null) { 321 | val newAccessorName = accessorPrefix + newFieldName.capitalizeFirstChar() 322 | rerun { mTree.getOrPut(pClass).putMethod(methodName, pMethod.jvmDesc, newAccessorName) } 323 | } 324 | return@a 325 | } 326 | 327 | fun isWildcardOrRegex(pMethod: PsiMethod, target: String): Boolean { 328 | if (target.contains('*') || target.contains(' ')) { 329 | write { comment(pMethod, "TODO(Ravel): wildcard and regex target are not supported") } 330 | logger.warn("$className#${pMethod.name}: wildcard and regex target are not supported") 331 | return true 332 | } 333 | return false 334 | } 335 | 336 | if (INJECTS.contains(annotationName)) { 337 | val pMethod = pAnnotation.parent() ?: return@a 338 | val methodName = pMethod.name 339 | 340 | val pDesc = pAnnotation.findDeclaredAttributeValue("target") 341 | if (pDesc != null) { 342 | write { comment(pMethod, "TODO(Ravel): target desc is not supported") } 343 | logger.warn("$className#$methodName: target desc is not supported") 344 | return@a 345 | } 346 | 347 | fun remapTargetMethod(pTarget: PsiLiteralExpression) { 348 | val targetClassNames = mixinTargets[className].orEmpty() 349 | if (targetClassNames.isEmpty()) { 350 | write { comment(pMethod, "TODO(Ravel): no target class") } 351 | logger.warn("$className#$methodName: no target class") 352 | return 353 | } 354 | 355 | val targetMethod = pTarget.value as String 356 | if (isWildcardOrRegex(pMethod, targetMethod)) return 357 | 358 | val targetMethodAndDesc = splitClassMember(targetMethod).second 359 | val targetMethodName = targetMethodAndDesc.substringBefore('(') 360 | val targetMethodDesc = targetMethodAndDesc.removePrefix(targetMethodName) 361 | 362 | fun write(newTargetMethodName: String) { 363 | val newTargetMethodDesc = mTree.remapDesc(targetMethodDesc) 364 | val newTarget = "\"${newTargetMethodName}${newTargetMethodDesc}\"" 365 | 366 | write { pTarget.replace(factory.createExpressionFromText(newTarget, pTarget)) } 367 | } 368 | 369 | if (targetMethodName == "" || targetMethodName == "") { 370 | return write(targetMethodName) 371 | } 372 | 373 | fun notFound() { 374 | write { comment(pMethod, "TODO(Ravel): target method $targetMethodName with the signature not found") } 375 | logger.warn("$className#$methodName: target method $targetMethodName not found") 376 | } 377 | 378 | fun ambiguous() { 379 | write { comment(pMethod, "TODO(Ravel): target method $targetMethodName is ambiguous") } 380 | logger.warn("$className#$methodName: target method $targetMethodName is ambiguous") 381 | } 382 | 383 | val newTargetMethodNames = linkedMapOf() 384 | for (targetClassName in targetClassNames) { 385 | val key = "${targetClassName}#${targetMethodName}" 386 | newTargetMethodNames[key] = targetMethodName 387 | 388 | val pTargetClass = findClass(targetClassName) 389 | if (pTargetClass != null) { 390 | var newTargetMethodName: String? = null 391 | if (targetMethodDesc.isNotEmpty()) { 392 | val pTargetMethod = findMethod(pTargetClass, targetMethodName, targetMethodDesc) ?: return notFound() 393 | newTargetMethodName = remap(pMethod, pTargetMethod) ?: targetMethodName 394 | } else { 395 | for (pTargetMethod in pTargetClass.findMethodsByName(targetMethodName, false)) { 396 | val newTargetMethodName0 = 397 | remap(pMethod, pTargetMethod) ?: targetMethodName 398 | if (newTargetMethodName != null && newTargetMethodName != newTargetMethodName0) return ambiguous() 399 | newTargetMethodName = newTargetMethodName0 400 | } 401 | } 402 | newTargetMethodNames[key] = newTargetMethodName ?: targetMethodName 403 | } else { 404 | val mTargetClass = mTree.getClass(targetClassName) ?: continue 405 | var newTargetMethodName: String? = null 406 | if (targetMethodDesc.isNotEmpty()) { 407 | val mTargetMethod = mTargetClass.getMethod(targetMethodName, targetMethodDesc) ?: return notFound() 408 | newTargetMethodName = mTargetMethod.newName ?: targetMethodName 409 | } else { 410 | for (mTargetMethod in mTargetClass.getMethods(targetMethodName)) { 411 | val newTargetMethodName0 = mTargetMethod.newName ?: targetMethodName 412 | if (newTargetMethodName != null && newTargetMethodName != newTargetMethodName0) return ambiguous() 413 | newTargetMethodName = newTargetMethodName0 414 | } 415 | } 416 | newTargetMethodNames[key] = newTargetMethodName ?: targetMethodName 417 | } 418 | } 419 | 420 | val uniqueNewTargetMethodNames = newTargetMethodNames.values.toSet() 421 | if (uniqueNewTargetMethodNames.size != 1) { 422 | logger.warn("method target have different new names") 423 | val comment = newTargetMethodNames.map { (k, v) -> " $k -> $v" }.joinToString(separator = "\n") 424 | write { comment(pMethod, "TODO(Ravel): method target have different new names\n$comment") } 425 | return 426 | } 427 | 428 | return write(uniqueNewTargetMethodNames.first()) 429 | } 430 | 431 | val pTargetMethods = pAnnotation.findDeclaredAttributeValue("method") ?: return@a 432 | when (pTargetMethods) { 433 | is PsiLiteralExpression -> remapTargetMethod(pTargetMethods) 434 | is PsiArrayInitializerMemberValue -> pTargetMethods.initializers.forEach { 435 | if (it is PsiLiteralExpression) remapTargetMethod(it) 436 | else warnNotLiterals(pMethod) 437 | } 438 | 439 | else -> warnNotLiterals(pMethod) 440 | } 441 | return@a 442 | } 443 | 444 | fun remapAtField(pMethod: PsiMethod, pLiteral: PsiLiteralExpression, target: String) { 445 | if (isWildcardOrRegex(pMethod, target)) return 446 | 447 | if (!target.contains(':')) { 448 | write { comment(pMethod, "TODO(Ravel): target field doesn't have a description") } 449 | logger.warn("$className#${pMethod.name}: target field doesn't have a description") 450 | return 451 | } 452 | 453 | var (targetClassName, targetFieldAndDesc) = splitClassMember(target) 454 | if (targetClassName == null) targetClassName = targetClassName(pMethod) 455 | if (targetClassName == null) { 456 | write { comment(pMethod, "TODO(Ravel): Could not determine target field owner") } 457 | logger.warn("$className#${pMethod.name}: Could not determine target field owner") 458 | return 459 | } 460 | 461 | val targetFieldName = targetFieldAndDesc.substringBefore(':') 462 | val targetFieldDesc = targetFieldAndDesc.substringAfter(':') 463 | 464 | var newTargetClassName = targetClassName 465 | var newTargetFieldName = targetFieldName 466 | val mTargetClass = mTree.getClass(targetClassName) 467 | if (mTargetClass != null) { 468 | newTargetClassName = mTargetClass.newName ?: targetClassName 469 | val mField = mTargetClass.getField(targetFieldName) 470 | if (mField != null) { 471 | newTargetFieldName = mField.newName ?: targetFieldName 472 | } 473 | } 474 | 475 | val newTargetFieldDesc = mTree.remapDesc(targetFieldDesc) 476 | val newTarget = "\"L${newTargetClassName};${newTargetFieldName}:${newTargetFieldDesc}\"" 477 | 478 | write { pLiteral.replace(factory.createExpressionFromText(newTarget, pLiteral)) } 479 | } 480 | 481 | fun remapAtInvoke(pMethod: PsiMethod, pLiteral: PsiLiteralExpression, target: String) { 482 | if (isWildcardOrRegex(pMethod, target)) return 483 | 484 | if (!target.contains('(')) { 485 | write { comment(pMethod, "TODO(Ravel): target method doesn't have a description") } 486 | logger.warn("$className#${pMethod.name}: target method doesn't have a description") 487 | return 488 | } 489 | 490 | var (targetClassName, targetMethodAndDesc) = splitClassMember(target) 491 | if (targetClassName == null) targetClassName = targetClassName(pMethod) 492 | if (targetClassName == null) { 493 | write { comment(pMethod, "TODO(Ravel): Could not determine target method owner") } 494 | logger.warn("$className#${pMethod.name}: Could not determine target method owner") 495 | return 496 | } 497 | 498 | val targetMethodName = targetMethodAndDesc.substringBefore('(') 499 | val targetMethodDesc = targetMethodAndDesc.removePrefix(targetMethodName) 500 | 501 | var newTargetClassName = targetClassName 502 | var newTargetMethodName = targetMethodName 503 | val mTargetClass = mTree.getClass(targetClassName) 504 | if (mTargetClass != null) { 505 | newTargetClassName = mTargetClass.newName ?: targetClassName 506 | val mMethod = mTargetClass.getMethod(targetMethodName, targetMethodDesc) 507 | if (mMethod != null) { 508 | newTargetMethodName = mMethod.newName ?: targetMethodName 509 | } 510 | } 511 | 512 | val newTargetMethodDesc = mTree.remapDesc(targetMethodDesc) 513 | val newTarget = "\"L${newTargetClassName};${newTargetMethodName}${newTargetMethodDesc}\"" 514 | 515 | write { pLiteral.replace(factory.createExpressionFromText(newTarget, pLiteral)) } 516 | } 517 | 518 | if (annotationName == Definition) { 519 | val pMethod = pAnnotation.parent() ?: return@a 520 | 521 | fun remapTarget(pTargetElt: PsiElement, remap: (PsiMethod, PsiLiteralExpression, String) -> Unit) = when (pTargetElt) { 522 | is PsiLiteralExpression -> remap(pMethod, pTargetElt, pTargetElt.value as String) 523 | is PsiArrayInitializerMemberValue -> pTargetElt.initializers.forEach { 524 | if (it is PsiLiteralExpression) remap(pMethod, it, it.value as String) 525 | else warnNotLiterals(pMethod) 526 | } 527 | 528 | else -> warnNotLiterals(pMethod) 529 | } 530 | 531 | val pTargetFields = pAnnotation.findDeclaredAttributeValue("field") 532 | if (pTargetFields != null) remapTarget(pTargetFields, ::remapAtField) 533 | 534 | val pTargetMethods = pAnnotation.findDeclaredAttributeValue("method") 535 | if (pTargetMethods != null) remapTarget(pTargetMethods, ::remapAtInvoke) 536 | return@a 537 | } 538 | 539 | if (annotationName == At) { 540 | val pMethod = pAnnotation.parent() ?: return@a 541 | val methodName = pMethod.name 542 | 543 | val pPoint = pAnnotation.findDeclaredAttributeValue("value") ?: return@a 544 | pPoint as PsiLiteralExpression 545 | val point = pPoint.value as String 546 | 547 | if (point == Point.HEAD) return@a 548 | if (point == Point.RETURN) return@a 549 | if (point == Point.TAIL) return@a 550 | if (point == Point.JUMP) return@a 551 | if (point == Point.CONSTANT) return@a 552 | if (point == Point.STORE) return@a 553 | if (point == Point.LOAD) return@a 554 | if (point == Point.EXPRESSION) return@a 555 | 556 | val pDesc = pAnnotation.findDeclaredAttributeValue("desc") 557 | if (pDesc != null) { 558 | write { comment(pMethod, "TODO(Ravel): @At.desc is not supported") } 559 | logger.warn("$className#$methodName: @At.desc is not supported") 560 | } 561 | 562 | val pArgs = pAnnotation.findDeclaredAttributeValue("args") 563 | if (pArgs != null) { 564 | write { comment(pMethod, "TODO(Ravel): @At.args is not supported") } 565 | logger.warn("$className#$methodName: @At.args is not supported") 566 | } 567 | 568 | val pTarget = pAnnotation.findDeclaredAttributeValue("target") ?: return@a 569 | pTarget as PsiLiteralExpression 570 | val target = pTarget.value as String 571 | 572 | if (point == Point.FIELD) { 573 | remapAtField(pMethod, pTarget, target) 574 | return@a 575 | } 576 | 577 | if (Point.INVOKES.contains(point)) { 578 | remapAtInvoke(pMethod, pTarget, target) 579 | return@a 580 | } 581 | 582 | if (isWildcardOrRegex(pMethod, target)) return@a 583 | 584 | if (point == Point.NEW) { 585 | val newTarget = if (target.startsWith('(')) mTree.remapDesc(target) else { 586 | val mClass = mTree.getClass(target) ?: return@a 587 | mClass.newName ?: target 588 | } 589 | 590 | write { pTarget.replace(factory.createExpressionFromText("\"${newTarget}\"", pTarget)) } 591 | return@a 592 | } 593 | 594 | write { comment(pMethod, "TODO(Ravel): Unknown injection point $point") } 595 | logger.warn("$className#$methodName: Unknown injection point $point") 596 | return@a 597 | } 598 | 599 | if (annotationName == Shadow || annotationName == Overwrite) { 600 | val pMember = pAnnotation.parent() ?: return@a 601 | val memberName = pMember.name ?: return@a 602 | 603 | val alias = pAnnotation.findDeclaredAttributeValue("alias") 604 | if (alias != null) { 605 | write { comment(pMember, "TODO(Ravel): @Shadow.alias is not supported") } 606 | logger.warn("$className#$memberName: @Shadow.alias is not supported") 607 | return@a 608 | } 609 | 610 | val (pTargetClass, mTargetClass) = targetClass(pMember) ?: return@a 611 | 612 | val pPrefix = pAnnotation.findDeclaredAttributeValue("prefix") 613 | val prefix = if (pPrefix is PsiLiteralExpression) (pPrefix.value as String) else "shadow$" 614 | val memberNameHasPrefix = annotationName == Shadow && memberName.startsWith(prefix) 615 | val targetName = if (memberNameHasPrefix) memberName.substring(prefix.length) else memberName 616 | 617 | var newMemberName: String? = when (pMember) { 618 | is PsiField -> { 619 | if (mTargetClass == null) return@a 620 | val mTargetField = mTargetClass.getField(targetName) ?: return@a 621 | mTargetField.newName 622 | } 623 | 624 | is PsiMethod -> { 625 | val targetMethodSignature = pMember.jvmDesc 626 | if (pTargetClass != null) { 627 | val pTargetMethod = findMethod(pTargetClass, targetName, targetMethodSignature) ?: return@a 628 | remap(pMember, pTargetMethod) 629 | } else { 630 | val mTargetMethod = mTargetClass!!.getMethod(targetName, targetMethodSignature) ?: return@a 631 | mTargetMethod.newName 632 | } 633 | } 634 | 635 | else -> return@a 636 | } 637 | if (newMemberName == null) return@a 638 | if (memberNameHasPrefix) newMemberName = prefix + newMemberName 639 | 640 | rerun { 641 | val mClass = mTree.getOrPut(pClass) 642 | if (pMember is PsiField) mClass.putField(memberName, newMemberName) 643 | else if (pMember is PsiMethod) mClass.putMethod(memberName, pMember.jvmDesc, newMemberName) 644 | } 645 | return@a 646 | } 647 | 648 | val pMember = pAnnotation.parent() ?: pClass 649 | write { comment(pMember, "TODO(Ravel): remapper for $annotationName is not implemented") } 650 | logger.warn("$className: remapper for $annotationName is not implemented") 651 | } 652 | } 653 | --------------------------------------------------------------------------------