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 {
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("org.vineflower:vineflower:1.10.1")
testImplementation(kotlin("test"))

View file

@ -1,14 +1,18 @@
package org.ecorous.esnesnon.gradle
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.RenamingPlugin
import org.gradle.api.Plugin
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.SingleFileSaver
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences
import java.io.PrintStream
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.deleteExisting
import kotlin.io.path.exists
@ -47,25 +51,44 @@ class NonsenseGradlePlugin : Plugin<Project> {
group = "nonsense"
doFirst {
val fileName = remappedGameJarPath.fileName.toString()
val output = remappedGameJarPath.resolveSibling(fileName.substring(0, fileName.length-4)+"-sources.jar")
if (output.exists()){
val output =
remappedGameJarPath.resolveSibling(fileName.substring(0, fileName.length - 4) + "-sources.jar")
if (output.exists()) {
println("Output $output already exists, deleting!")
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...")
val logger = PrintStreamLogger(System.out)
val options = mapOf(
IFabricJavadocProvider.PROPERTY_NAME to ParchmentJavadocProvider(),
val log = project.rootDir.resolve("vineflower.log").toPath() // TODO temporarily log VF output more verbosely to a separate file to have more debug information
Files.deleteIfExists(log)
val logger = PrintStreamLogger(PrintStream(Files.newOutputStream(log)))
options.putAll(
mapOf(
IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES to "1",
IFernflowerPreferences.BYTECODE_SOURCE_MAPPING to "1",
IFernflowerPreferences.REMOVE_SYNTHETIC to "1",
IFernflowerPreferences.LOG_LEVEL to "warn",
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()),
options, logger)
val decomp = Fernflower(
SingleFileSaver(output.toFile()),
options, logger
)
decomp.addSource(remappedGameJarPath.toFile())
decomp.decompileContext()
@ -89,5 +112,7 @@ class NonsenseGradlePlugin : Plugin<Project> {
companion object {
lateinit var minecraftVersion: String
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.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 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)) {
NonsenseGradlePlugin.minecraftVersion = version
println("Valid version! $version")
@ -20,7 +21,7 @@ fun Project.minecraft(version: String) {
NonsenseGradlePlugin.remappedGameJarPath = remappedJar
println("Time to setup Minecraft!")
if (remappedJar.notExists()) {
MojMapPatcher.run(version, clientJar, remappedJar, true)
NonsenseRemapper.run(version, clientJar, remappedJar, true)
}
VersionChecker.getDependencies(version){
dependencies.add("implementation", it)
@ -31,4 +32,10 @@ fun Project.minecraft(version: String) {
error("Invalid minecraft version provided: $version")
}
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
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.StructField
import org.jetbrains.java.decompiler.struct.StructMethod
class ParchmentJavadocProvider : IFabricJavadocProvider {
override fun getClassDoc(structClass: StructClass): String {
return ""
class ParchmentJavadocProvider(val parchment: Parchment) : IFabricJavadocProvider {
override fun getClassDoc(structClass: StructClass): String? {
return parchment.getClass(structClass.qualifiedName)?.javadoc?.joinToString("\n")
}
override fun getFieldDoc(structClass: StructClass, structField: StructField): String {
return ""
override fun getFieldDoc(structClass: StructClass, structField: StructField): String? {
return parchment.getClass(structClass.qualifiedName)
?.getField(structField.name, structField.descriptor)?.javadoc?.joinToString("\n")
}
override fun getMethodDoc(structClass: StructClass, structMethod: StructMethod): String {
return ""
override fun getMethodDoc(structClass: StructClass, structMethod: StructMethod): String? {
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