Skip to content

Commit

Permalink
Fix Kotlin decompilation, support plugin options, support K2 Mode
Browse files Browse the repository at this point in the history
  • Loading branch information
sschr15 committed Aug 24, 2024
1 parent d17adb9 commit c02d7ae
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 221 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package org.vineflower.ijplugin

import com.intellij.application.options.CodeStyle
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.JBIntSpinner
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import java.util.*
import javax.swing.event.DocumentEvent

object ClassicVineflowerPreferences : VineflowerPreferences() {
val ignoredPreferences = setOf(
"ban", // banner
"bsm", // bytecode source mapping
"nls", // newline separator
"__unit_test_mode__",
"log", // log level
"urc", // use renamer class
"thr", // threads
"mpm", // max processing method
"\r\n", // irrelevant constant
"\n", // irrelevant constant
)

val defaultOverrides = mapOf(
"hdc" to "0", // hide default constructor
"dgs" to "1", // decompile generic signatures
"rsy" to "1", // remove synthetic
"rbr" to "1", // remove bridge
"nls" to "1", // newline separator
"ban" to "//\n// Source code recreated from a .class file by Vineflower\n//\n\n", // banner
"mpm" to 0, // max processing method
"iib" to "1", // ignore invalid bytecode
"vac" to "1", // verify anonymous classes
"ind" to CodeStyle.getDefaultSettings().indentOptions.INDENT_SIZE, // indent size
"__unit_test_mode__" to if (ApplicationManager.getApplication().isUnitTestMode) "1" else "0",
)

override fun setupSettings(entries: MutableList<SettingsEntry>, settingsMap: MutableMap<String, String>) {
val classLoader = VineflowerState.getInstance().getVineflowerClassLoader().getNow(null) ?: return
val preferencesClass = classLoader.loadClass("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences")

@Suppress("UNCHECKED_CAST")
val defaults = (preferencesClass.getField("DEFAULTS").get(null) as Map<String, *>).toMutableMap()
defaults.putAll(defaultOverrides)


val fieldAnnotations = FieldAnnotations(classLoader)

for (field in preferencesClass.fields) {
if (!Modifier.isStatic(field.modifiers) || field.type != String::class.java) {
continue
}
val key = inferShortKey(field, fieldAnnotations) ?: continue
if (key in ignoredPreferences) {
continue
}
val longKey = inferLongKey(field) ?: continue

val type = inferType(key, defaults, field, fieldAnnotations) ?: continue
val name = inferName(key, field, fieldAnnotations)
val description = inferDescription(field, fieldAnnotations)
val currentValue = settingsMap[key] ?: (defaults[key] ?: defaults[longKey]!!).toString()
val component = when (type) {
Type.BOOLEAN -> JBCheckBox().also { checkBox ->
checkBox.isSelected = currentValue == "1"
checkBox.addActionListener {
val newValue = if (checkBox.isSelected) "1" else "0"
if (newValue != defaults[key]) {
settingsMap[key] = newValue
} else {
settingsMap.remove(key)
}
}
}
Type.STRING -> JBTextField(currentValue).also { textField ->
textField.columns = 20
textField.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
val newValue = textField.text
if (newValue != defaults[key]) {
settingsMap[key] = newValue
} else {
settingsMap.remove(key)
}
}
})
}
Type.INTEGER -> JBIntSpinner(currentValue.toInt(), 0, Int.MAX_VALUE).also { spinner ->
spinner.addChangeListener {
val newValue = spinner.value.toString()
if (newValue != defaults[key]) {
settingsMap[key] = newValue
} else {
settingsMap.remove(key)
}
}
}
}
entries += SettingsEntry(name, component, description)
}
}

private val nameOverrides = mapOf(
"dc4" to "Decompile Class 1.4",
"ind" to "Indent Size",
"lit" to "Literals As-Is",
)

private fun inferLongKey(field: Field): String? {
return field.get(null) as String?
}

private fun inferShortKey(field: Field, fieldAnnotations: FieldAnnotations): String? {
if (fieldAnnotations.shortName != null) {
val shortName = field.getAnnotation(fieldAnnotations.shortName)?.value
if (shortName != null) {
return shortName
}
}

return field.get(null) as String?
}

private fun inferType(key: String, defaults: Map<String, *>, field: Field, fieldAnnotations: FieldAnnotations): Type? {
if (fieldAnnotations.type != null) {
when (field.getAnnotation(fieldAnnotations.type)?.value) {
"bool" -> return Type.BOOLEAN
"int" -> return Type.INTEGER
"string" -> return Type.STRING
}
}

val dflt = defaults[key]?.toString() ?: return null
if (dflt == "0" || dflt == "1") {
return Type.BOOLEAN
}
if (dflt.toIntOrNull() != null) {
return Type.INTEGER
}
return Type.STRING
}

private fun inferName(key: String, field: Field, fieldAnnotations: FieldAnnotations): String {
if (fieldAnnotations.name != null) {
val name = field.getAnnotation(fieldAnnotations.name)?.value
if (name != null) {
return name
}
}

val nameOverride = nameOverrides[key]
if (nameOverride != null) {
return nameOverride
}

return StringUtil.toTitleCase(field.name.replace("_", " ").toLowerCase(Locale.ROOT))
}

private fun inferDescription(field: Field, fieldAnnotations: FieldAnnotations): String? {
if (fieldAnnotations.description != null) {
val description = field.getAnnotation(fieldAnnotations.description)?.value
if (description != null) {
return description
}
}

return null
}

private val Annotation.value get() = javaClass.getMethod("value").invoke(this) as? String

class FieldAnnotations(classLoader: ClassLoader) {
val name = classLoader.tryLoadAnnotation("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences\$Name")
val description = classLoader.tryLoadAnnotation("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences\$Description")
val shortName = classLoader.tryLoadAnnotation("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences\$ShortName")
val type = classLoader.tryLoadAnnotation("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences\$Type")

companion object {
private fun ClassLoader.tryLoadAnnotation(name: String): Class<out Annotation>? {
return try {
loadClass(name).asSubclass(Annotation::class.java)
} catch (e: ClassNotFoundException) {
null
} catch (e: ClassCastException) {
null
}
}
}
}
}
144 changes: 144 additions & 0 deletions src/main/kotlin/org/vineflower/ijplugin/NewVineflowerPreferences.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package org.vineflower.ijplugin

import com.intellij.application.options.CodeStyle
import com.intellij.openapi.application.ApplicationManager
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.JBIntSpinner
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import javax.swing.event.DocumentEvent

class NewVineflowerPreferences : VineflowerPreferences() {
val ignoredPreferences = setOf(
"banner",
"bytecode-source-mapping",
"new-line-separator",
"log-level",
"user-renamer-class",
"thread-count",
"max-time-per-method",
"indent-string", // Custom implementation
)

val defaultOverrides = mapOf(
"hide-default-constructor" to "0",
"decompile-generics" to "1",
"remove-synthetic" to "1",
"remove-bridge" to "1",
"new-line-separator" to "1",
"banner" to "//\n// Source code recreated from a .class file by Vineflower\n//\n\n",
"max-time-per-method" to "0",
"ignore-invalid-bytecode" to "1",
"verify-anonymous-classes" to "1",
"indent-string" to " ".repeat(CodeStyle.getDefaultSettings().indentOptions.INDENT_SIZE),
"__unit_test_mode__" to if (ApplicationManager.getApplication().isUnitTestMode) "1" else "0",
)

val nameOverrides = mapOf(
"keep-literals" to "Literals As-Is",
"decompiler-java4" to "Resugar 1-4 Class Refs",
)

data class Option(
val key: String,
val type: Type,
val name: String,
val description: String,
val plugin: String?,
val defaultValue: String?,
) {
constructor(vfOption: Any, vfClass: Class<*>) : this(
key = vfClass.getMethod("id").invoke(vfOption) as String,
type = when (vfClass.getMethod("type").invoke(vfOption).toString()) {
"bool" -> Type.BOOLEAN
"int" -> Type.INTEGER
else -> Type.STRING
},
name = vfClass.getMethod("name").invoke(vfOption) as String,
description = vfClass.getMethod("description").invoke(vfOption) as String,
plugin = vfClass.getMethod("plugin").invoke(vfOption) as String?,
defaultValue = vfClass.getMethod("defaultValue").invoke(vfOption) as String?,
)
}

private val classLoader = VineflowerState.getInstance().getVineflowerClassLoader().getNow(null)
private val optionClass = classLoader.loadClass("org.jetbrains.java.decompiler.api.DecompilerOption")
private val getAllOptions = optionClass.getMethod("getAll")
private val initVineflower = classLoader.loadClass("org.jetbrains.java.decompiler.main.Init")
.getMethod("init")

override fun setupSettings(entries: MutableList<SettingsEntry>, settingsMap: MutableMap<String, String>) {
initVineflower(null)

val options = (getAllOptions(null) as List<*>)
.map { Option(it!!, optionClass) }
.filter { it.key !in ignoredPreferences }

for (option in options) {
if (option.key in ignoredPreferences) continue

val defaultValue = if (option.key in defaultOverrides) defaultOverrides[option.key] else option.defaultValue
val currentValue = settingsMap[option.key] ?: defaultValue

val component = when (option.type) {
Type.BOOLEAN -> JBCheckBox().apply {
isSelected = currentValue == "1"
addActionListener {
val newValue = if (isSelected) "1" else "0"
if (newValue != defaultValue) {
settingsMap[option.key] = newValue
} else {
settingsMap.remove(option.key)
}
}
}
Type.STRING -> JBTextField(currentValue).apply {
columns = 20
document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
val newValue = text
if (newValue != defaultValue) {
settingsMap[option.key] = newValue
} else {
settingsMap.remove(option.key)
}
}
})
}
Type.INTEGER -> JBIntSpinner(currentValue?.toInt() ?: 0, 0, Int.MAX_VALUE).apply {
addChangeListener {
val newValue = value.toString()
if (newValue != defaultValue) {
settingsMap[option.key] = newValue
} else {
settingsMap.remove(option.key)
}
}
}
}

val name = nameOverrides[option.key] ?: option.name
val desc = option.description

entries.add(SettingsEntry(name, component, desc, option.plugin))
}

run {
val currentIndentString = settingsMap["indent-string"] ?: defaultOverrides["indent-string"]!!
val component = JBIntSpinner(currentIndentString.length, 0, Int.MAX_VALUE).apply {
addChangeListener {
val newValue = " ".repeat(value.toString().toInt())
if (newValue != defaultOverrides["indent-string"]) {
settingsMap["indent-string"] = newValue
} else {
settingsMap.remove("indent-string")
}
}
}

entries.add(SettingsEntry("Indent Size", component, "Number of spaces to use for each indentation level."))
}

entries.sortBy { it.name }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ abstract class VineflowerDecompilerBase : ClassFileDecompilers.Full() {
if (!state.enabled || state.hadError) {
return false
}
val language = getLanguage(file) ?: return false
val language = getLanguage(file)
?: return false
return acceptsLanguage(language)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,7 @@ class VineflowerDecompilerKotlin : VineflowerDecompilerBase() {

private class MyDecompiledFile(viewProvider: KotlinDecompiledFileViewProvider, contents: (VirtualFile) -> DecompiledText) : KtDecompiledFile(viewProvider, contents) {
override fun getStub() = stubTree?.root as KotlinFileStub?

override fun toString(): String = toString().replace("KtFile", "VfDecompiledFile")
}
}
Loading

0 comments on commit c02d7ae

Please sign in to comment.