using parchment: parameter names & javadoc; use vineflower for source generation #1

Merged
owlsys merged 4 commits from parchment into mistress 2024-05-18 07:05:46 -04:00
8 changed files with 388 additions and 25 deletions
Showing only changes of commit 83aa861eb9 - Show all commits

View file

@ -21,7 +21,7 @@ repositories {
} }
dependencies { dependencies {
implementation("org.ecorous.esnesnon:mojmap-patcher:1.0.0-SNAPSHOT") implementation("org.ecorous.esnesnon:nonsense-remapper:1.0.0-SNAPSHOT")
implementation("com.google.code.gson:gson:2.10.1") implementation("com.google.code.gson:gson:2.10.1")
implementation("org.vineflower:vineflower:1.10.1") implementation("org.vineflower:vineflower:1.10.1")
testImplementation(kotlin("test")) testImplementation(kotlin("test"))

View file

@ -1,14 +1,18 @@
package org.ecorous.esnesnon.gradle package org.ecorous.esnesnon.gradle
import net.fabricmc.fernflower.api.IFabricJavadocProvider import net.fabricmc.fernflower.api.IFabricJavadocProvider
import org.ecorous.esnesnon.gradle.parchment.ParchmentProvider
import org.ecorous.esnesnon.gradle.vineflower.ParchmentJavadocProvider import org.ecorous.esnesnon.gradle.vineflower.ParchmentJavadocProvider
import org.ecorous.esnesnon.gradle.vineflower.RenamingPlugin
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.jetbrains.java.decompiler.main.decompiler.BaseDecompiler import org.jetbrains.java.decompiler.main.Fernflower
import org.jetbrains.java.decompiler.main.decompiler.PrintStreamLogger import org.jetbrains.java.decompiler.main.decompiler.PrintStreamLogger
import org.jetbrains.java.decompiler.main.decompiler.SingleFileSaver import org.jetbrains.java.decompiler.main.decompiler.SingleFileSaver
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences
import java.io.PrintStream
import java.net.URI import java.net.URI
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.deleteExisting import kotlin.io.path.deleteExisting
import kotlin.io.path.exists import kotlin.io.path.exists
@ -47,25 +51,44 @@ class NonsenseGradlePlugin : Plugin<Project> {
group = "nonsense" group = "nonsense"
doFirst { doFirst {
val fileName = remappedGameJarPath.fileName.toString() val fileName = remappedGameJarPath.fileName.toString()
val output = remappedGameJarPath.resolveSibling(fileName.substring(0, fileName.length-4)+"-sources.jar") val output =
if (output.exists()){ remappedGameJarPath.resolveSibling(fileName.substring(0, fileName.length - 4) + "-sources.jar")
if (output.exists()) {
println("Output $output already exists, deleting!") println("Output $output already exists, deleting!")
output.deleteExisting() output.deleteExisting()
} }
val options = mutableMapOf<String, Any>()
if (useParchment) {
println("Preparing Parchment...")
val parchment = ParchmentProvider.getParchment(
minecraftVersion,
parchmentVersion,
project.gradle.gradleUserHomeDir.resolve("caches/nonsense-gradle/").toPath()
)
RenamingPlugin.parchment = parchment
/*options["variable-renaming"] = "parchment"
options["rename-parameters"] = "1"*/
options[IFabricJavadocProvider.PROPERTY_NAME] = ParchmentJavadocProvider(parchment)
}
println("Decompiling...") println("Decompiling...")
val logger = PrintStreamLogger(System.out) val log = project.rootDir.resolve("vineflower.log").toPath() // TODO temporarily log VF output more verbosely to a separate file to have more debug information
val options = mapOf( Files.deleteIfExists(log)
IFabricJavadocProvider.PROPERTY_NAME to ParchmentJavadocProvider(), val logger = PrintStreamLogger(PrintStream(Files.newOutputStream(log)))
IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES to "1", options.putAll(
IFernflowerPreferences.BYTECODE_SOURCE_MAPPING to "1", mapOf(
IFernflowerPreferences.REMOVE_SYNTHETIC to "1", IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES to "1",
IFernflowerPreferences.LOG_LEVEL to "warn", IFernflowerPreferences.BYTECODE_SOURCE_MAPPING to "1",
IFernflowerPreferences.THREADS to Runtime.getRuntime().availableProcessors().toString(), IFernflowerPreferences.REMOVE_SYNTHETIC to "1",
IFernflowerPreferences.INDENT_STRING to "\t" IFernflowerPreferences.LOG_LEVEL to "info", // TODO set to 'warn' before commit/push! (related to the comment above)
IFernflowerPreferences.THREADS to Runtime.getRuntime().availableProcessors().toString(),
IFernflowerPreferences.INDENT_STRING to "\t"
)
) )
val decomp = BaseDecompiler(SingleFileSaver(output.toFile()), val decomp = Fernflower(
options, logger) SingleFileSaver(output.toFile()),
options, logger
)
decomp.addSource(remappedGameJarPath.toFile()) decomp.addSource(remappedGameJarPath.toFile())
decomp.decompileContext() decomp.decompileContext()
@ -89,5 +112,7 @@ class NonsenseGradlePlugin : Plugin<Project> {
companion object { companion object {
lateinit var minecraftVersion: String lateinit var minecraftVersion: String
lateinit var remappedGameJarPath: Path lateinit var remappedGameJarPath: Path
var useParchment = false
lateinit var parchmentVersion: String
} }
} }

View file

@ -2,11 +2,12 @@ package org.ecorous.esnesnon.gradle.ext
import org.ecorous.esnesnon.gradle.NonsenseGradlePlugin import org.ecorous.esnesnon.gradle.NonsenseGradlePlugin
import org.ecorous.esnesnon.gradle.VersionChecker import org.ecorous.esnesnon.gradle.VersionChecker
import org.ecorous.esnesnon.mojmap_patcher.MojMapPatcher import org.ecorous.esnesnon.gradle.parchment.ParchmentProvider
import org.ecorous.esnesnon.nonsense_remapper.NonsenseRemapper
import org.gradle.api.Project import org.gradle.api.Project
import kotlin.io.path.notExists import kotlin.io.path.notExists
fun Project.minecraft(version: String) { fun Project.minecraft(version: String): Project { // return self to allow for chaining
if (VersionChecker.validateVersion(version)) { if (VersionChecker.validateVersion(version)) {
NonsenseGradlePlugin.minecraftVersion = version NonsenseGradlePlugin.minecraftVersion = version
println("Valid version! $version") println("Valid version! $version")
@ -20,7 +21,7 @@ fun Project.minecraft(version: String) {
NonsenseGradlePlugin.remappedGameJarPath = remappedJar NonsenseGradlePlugin.remappedGameJarPath = remappedJar
println("Time to setup Minecraft!") println("Time to setup Minecraft!")
if (remappedJar.notExists()) { if (remappedJar.notExists()) {
MojMapPatcher.run(version, clientJar, remappedJar, true) NonsenseRemapper.run(version, clientJar, remappedJar, true)
} }
VersionChecker.getDependencies(version){ VersionChecker.getDependencies(version){
dependencies.add("implementation", it) dependencies.add("implementation", it)
@ -31,4 +32,10 @@ fun Project.minecraft(version: String) {
error("Invalid minecraft version provided: $version") error("Invalid minecraft version provided: $version")
} }
println("Minecraft!") println("Minecraft!")
return this
}
fun Project.parchment(version: String = ParchmentProvider.findLatestValidVersion()) {
NonsenseGradlePlugin.useParchment = true
NonsenseGradlePlugin.parchmentVersion = version
} }

View file

@ -0,0 +1,101 @@
package org.ecorous.esnesnon.gradle.parchment
import com.google.gson.Gson
import org.ecorous.esnesnon.gradle.NonsenseGradlePlugin
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.io.path.createParentDirectories
import kotlin.io.path.notExists
object ParchmentProvider {
const val PARCHMENT_URL: String = "https://maven.parchmentmc.org/org/parchmentmc/data"
private fun getParchmentUrl(gameVersion: String): String {
return "$PARCHMENT_URL/parchment-$gameVersion"
}
private fun findParchmentVersion(gameVersion: String): String {
val url = getParchmentUrl(gameVersion) + "/maven-metadata.xml"
val stream = URI.create(url).toURL().openStream()
val document = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().parse(stream)
stream?.close()
return document.getElementsByTagName("release").item(0).textContent
}
fun getParchment(gameVersion: String, cacheDir: Path): Parchment {
return getParchment(gameVersion, findParchmentVersion(gameVersion), cacheDir)
}
fun getParchment(gameVersion: String, parchmentVer: String, cacheDir: Path): Parchment {
val cachePath = cacheDir.resolve("parchment/$gameVersion/$parchmentVer/parchment-$gameVersion-$parchmentVer.zip")
cachePath.createParentDirectories()
if (cachePath.notExists()){
val url = "${getParchmentUrl(gameVersion)}/$parchmentVer/parchment-$gameVersion-$parchmentVer.zip"
Files.copy(URI.create(url).toURL().openStream(), cachePath)
}
// TODO hash verification
FileSystems.newFileSystem(cachePath).use {fs ->
val mappings = Files.readString(fs.getPath("parchment.json"))
return Gson().fromJson(mappings, Parchment::class.java)
}
}
fun findLatestValidVersion(): String {
return findForMinecraftVersion(NonsenseGradlePlugin.minecraftVersion)
}
fun findForMinecraftVersion(minecraftVersion: String): String {
return findParchmentVersion(minecraftVersion)
}
}
class Parchment(val version: String, val packages: List<Package>, val classes: List<Class>?){
fun getClass(name: String): Class? {
classes?.forEach {
if(it.name == name){
return it
}
}
return null
}
}
class Package(val name: String, val javadoc: List<String>?)
class Class(val name: String, val javadoc: List<String>?, val fields: List<Field>?, val methods: List<Method>?){
fun getField(name: String, descriptor: String): Field? {
fields?.forEach {
if(it.name == name && it.descriptor == descriptor){
return it
}
}
return null
}
fun getMethod(name: String, descriptor: String): Method? {
methods?.forEach {
if(it.name == name && it.descriptor == descriptor){
return it
}
}
return null
}
}
class Field(val name: String, val descriptor: String, val javadoc: List<String>?)
class Method(val name: String, val descriptor: String, val javadoc: List<String>?, val parameters: List<Parameter>?){
fun getParameter(index: Int): Parameter? {
parameters?.forEach {
if (it.index == index){
return it
}
}
return null
}
}
class Parameter(val index: Int, val name: String)

View file

@ -1,20 +1,23 @@
package org.ecorous.esnesnon.gradle.vineflower package org.ecorous.esnesnon.gradle.vineflower
import net.fabricmc.fernflower.api.IFabricJavadocProvider import net.fabricmc.fernflower.api.IFabricJavadocProvider
import org.ecorous.esnesnon.gradle.parchment.Parchment
import org.jetbrains.java.decompiler.struct.StructClass import org.jetbrains.java.decompiler.struct.StructClass
import org.jetbrains.java.decompiler.struct.StructField import org.jetbrains.java.decompiler.struct.StructField
import org.jetbrains.java.decompiler.struct.StructMethod import org.jetbrains.java.decompiler.struct.StructMethod
class ParchmentJavadocProvider : IFabricJavadocProvider { class ParchmentJavadocProvider(val parchment: Parchment) : IFabricJavadocProvider {
override fun getClassDoc(structClass: StructClass): String { override fun getClassDoc(structClass: StructClass): String? {
return "" return parchment.getClass(structClass.qualifiedName)?.javadoc?.joinToString("\n")
Outdated
Review

might be nicer in kotlin to do .orElse(null) and use ?.
or at least combine the map calls into one as it seems unnecessary to split them

might be nicer in kotlin to do .orElse(null) and use ?. or at least combine the map calls into one as it seems unnecessary to split them
} }
override fun getFieldDoc(structClass: StructClass, structField: StructField): String { override fun getFieldDoc(structClass: StructClass, structField: StructField): String? {
return "" return parchment.getClass(structClass.qualifiedName)
?.getField(structField.name, structField.descriptor)?.javadoc?.joinToString("\n")
} }
override fun getMethodDoc(structClass: StructClass, structMethod: StructMethod): String { override fun getMethodDoc(structClass: StructClass, structMethod: StructMethod): String? {
return "" return parchment.getClass(structClass.qualifiedName)
?.getMethod(structMethod.name, structMethod.descriptor)?.javadoc?.joinToString("\n")
} }
} }

View file

@ -0,0 +1,203 @@
package org.ecorous.esnesnon.gradle.vineflower
import org.ecorous.esnesnon.gradle.parchment.Parchment
import org.jetbrains.java.decompiler.code.CodeConstants
import org.jetbrains.java.decompiler.main.DecompilerContext
import org.jetbrains.java.decompiler.main.extern.IVariableNameProvider
import org.jetbrains.java.decompiler.main.extern.IVariableNamingFactory
import org.jetbrains.java.decompiler.modules.decompiler.ExprProcessor
import org.jetbrains.java.decompiler.modules.decompiler.vars.VarVersionPair
import org.jetbrains.java.decompiler.struct.StructMethod
import org.jetbrains.java.decompiler.struct.gen.CodeType
import org.jetbrains.java.decompiler.struct.gen.MethodDescriptor
import org.jetbrains.java.decompiler.struct.gen.VarType
import org.jetbrains.java.decompiler.util.Pair
import org.jetbrains.java.decompiler.util.TextUtil
// Adapted from https://github.com/Vineflower/vineflower/blob/ddf84710f8521ce62d1e3e55a39d300432d6b729/plugins/variable-renaming/src/main/java/org/vineflower/variablerenaming/TinyNameProvider.java
class ParchmentNameProvider(private val parchment: Parchment, val method: StructMethod) : IVariableNameProvider {
private val parameters: MutableMap<Int, String?> = mutableMapOf()
private val renameParameters = true
private val usedNames: MutableSet<String> = mutableSetOf()
override fun rename(entries: Map<VarVersionPair, Pair<VarType, String>>): Map<VarVersionPair, String?> {
var params = 0
if ((this.method.accessFlags and CodeConstants.ACC_STATIC) != CodeConstants.ACC_STATIC) {
params++
}
val md = MethodDescriptor.parseDescriptor(this.method.descriptor)
for (param in md.params) {
params += param.stackSize
}
val keys: MutableList<VarVersionPair> = ArrayList(entries.keys)
keys.sortWith { o1: VarVersionPair, o2: VarVersionPair -> if ((o1.`var` != o2.`var`)) o1.`var` - o2.`var` else o1.version - o2.version }
val result: MutableMap<VarVersionPair, String?> = LinkedHashMap()
for (ver in keys) {
// note: entries[ver]!!.a is currently null for references that are not parameter definitions. Figure this out. It currently causes errors further down the line
// and prevents VF from working correctly.
val type = cleanType(entries[ver]!!.b)
if (ver.`var` >= params) {
result[ver] = getNewName(
Pair.of(
entries[ver]!!.a, type
)
)
} else if (renameParameters) {
result[ver] = parameters.computeIfAbsent(
ver.`var`
) {
parchment.getClass(method.classQualifiedName)?.getMethod(method.name, method.descriptor)
?.getParameter(ver.`var`)?.name ?: getNewName(
Pair.of(
entries[ver]!!.a, type
)
)
}
}
}
return result
}
private fun getNewName(pair: Pair<VarType, String>): String? {
val type = pair.a
usedNames.add(pair.b)
var increment = true
var name: String
when (type.type) {
CodeType.BYTECHAR, CodeType.SHORTCHAR, CodeType.INT -> name = "i"
CodeType.LONG -> name = "l"
CodeType.BYTE -> name = "b"
CodeType.SHORT -> name = "s"
CodeType.CHAR -> name = "c"
CodeType.FLOAT -> name = "f"
CodeType.DOUBLE -> name = "d"
CodeType.BOOLEAN -> {
name = "bl"
increment = false
}
CodeType.OBJECT, CodeType.GENVAR -> {
name = pair.b
// Lowercase first letter
if (Character.isUpperCase(name[0])) {
name = name[0].lowercaseChar().toString() + name.substring(1)
}
if (type.arrayDim > 0 && !name.endsWith("s")) {
name += "s"
}
increment = false
}
else -> return null
}
if (increment) {
// Must be of length 1
var idxStart = name[0].code - 'a'.code
while (usedNames.contains(name)) {
name = convertToName(idxStart++)
}
} else {
// Increment via numbers
val oname = name
var idx = 0
while (usedNames.contains(name)) {
name = oname + (++idx)
}
}
if (TextUtil.isKeyword(name, method.bytecodeVersion, method)) {
name += "_"
}
usedNames.add(name)
return name
}
override fun renameParameter(flags: Int, type: VarType, name: String?, index: Int): String? {
var typeName: String
DecompilerContext.getImportCollector().lock().use {
typeName = ExprProcessor.getCastTypeName(type)
}
if (!this.renameParameters) {
return super.renameParameter(flags, type, name, index)
}
return parameters.computeIfAbsent(
index
) {
parchment.getClass(method.classQualifiedName)?.getMethod(method.name, method.descriptor)
?.getParameter(index)?.name ?: getNewName(
Pair.of(
type,
cleanType(typeName)
)
)
}
}
private fun convertToName(idx: Int): String {
// Convert to base 26
val str = idx.toString(26)
// Remap start of name from '0' to 'a'
val res = StringBuilder()
for (i in str.length - 1 downTo 0) {
var c = str[i]
// If we're numerical, remap to lowercase ascii range
if (c <= '9') {
c = ('a'.code + (c.code - '0'.code)).toChar()
} else {
// If we're not, simply shift up 10 ascii characters
c += 10
}
// TODO: idx 26 starts at 'ba', when it should start at 'aa'
res.insert(0, c)
}
return res.toString()
}
private fun cleanType(type: String): String {
var cleaned = type
if (cleaned.indexOf('<') != -1) {
cleaned = cleaned.substring(0, cleaned.indexOf('<'))
}
if (cleaned.indexOf('.') != -1) {
cleaned = cleaned.substring(cleaned.lastIndexOf('.') + 1)
}
if (cleaned.indexOf('$') != -1) {
cleaned = cleaned.substring(cleaned.lastIndexOf('$') + 1)
}
cleaned = cleaned.replace("\\[]".toRegex(), "")
return cleaned
}
override fun addParentContext(renamer: IVariableNameProvider) {
val prov = renamer as ParchmentNameProvider
usedNames.addAll(prov.usedNames)
}
class ParchmentNameProviderFactory(private val parchment: Parchment) : IVariableNamingFactory {
override fun createFactory(structMethod: StructMethod?): IVariableNameProvider {
return ParchmentNameProvider(parchment, structMethod!!)
}
}
}

View file

@ -0,0 +1,23 @@
package org.ecorous.esnesnon.gradle.vineflower
import org.ecorous.esnesnon.gradle.parchment.Parchment
import org.jetbrains.java.decompiler.api.plugin.Plugin
import org.jetbrains.java.decompiler.main.extern.IVariableNamingFactory
class RenamingPlugin : Plugin {
override fun id(): String {
return "NonsenseGradle"
}
override fun description(): String {
return "Renaming of parameters using the parchment mappings set"
}
override fun getRenamingFactory(): IVariableNamingFactory {
return ParchmentNameProvider.ParchmentNameProviderFactory(parchment)
}
companion object {
lateinit var parchment: Parchment
}
}

View file

@ -0,0 +1 @@
org.ecorous.esnesnon.gradle.vineflower.RenamingPlugin