Skip to content

Commit c02d7ae

Browse files
committed
Fix Kotlin decompilation, support plugin options, support K2 Mode
1 parent d17adb9 commit c02d7ae

File tree

9 files changed

+403
-221
lines changed

9 files changed

+403
-221
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package org.vineflower.ijplugin
2+
3+
import com.intellij.application.options.CodeStyle
4+
import com.intellij.openapi.application.ApplicationManager
5+
import com.intellij.openapi.util.text.StringUtil
6+
import com.intellij.ui.DocumentAdapter
7+
import com.intellij.ui.JBIntSpinner
8+
import com.intellij.ui.components.JBCheckBox
9+
import com.intellij.ui.components.JBTextField
10+
import java.lang.reflect.Field
11+
import java.lang.reflect.Modifier
12+
import java.util.*
13+
import javax.swing.event.DocumentEvent
14+
15+
object ClassicVineflowerPreferences : VineflowerPreferences() {
16+
val ignoredPreferences = setOf(
17+
"ban", // banner
18+
"bsm", // bytecode source mapping
19+
"nls", // newline separator
20+
"__unit_test_mode__",
21+
"log", // log level
22+
"urc", // use renamer class
23+
"thr", // threads
24+
"mpm", // max processing method
25+
"\r\n", // irrelevant constant
26+
"\n", // irrelevant constant
27+
)
28+
29+
val defaultOverrides = mapOf(
30+
"hdc" to "0", // hide default constructor
31+
"dgs" to "1", // decompile generic signatures
32+
"rsy" to "1", // remove synthetic
33+
"rbr" to "1", // remove bridge
34+
"nls" to "1", // newline separator
35+
"ban" to "//\n// Source code recreated from a .class file by Vineflower\n//\n\n", // banner
36+
"mpm" to 0, // max processing method
37+
"iib" to "1", // ignore invalid bytecode
38+
"vac" to "1", // verify anonymous classes
39+
"ind" to CodeStyle.getDefaultSettings().indentOptions.INDENT_SIZE, // indent size
40+
"__unit_test_mode__" to if (ApplicationManager.getApplication().isUnitTestMode) "1" else "0",
41+
)
42+
43+
override fun setupSettings(entries: MutableList<SettingsEntry>, settingsMap: MutableMap<String, String>) {
44+
val classLoader = VineflowerState.getInstance().getVineflowerClassLoader().getNow(null) ?: return
45+
val preferencesClass = classLoader.loadClass("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences")
46+
47+
@Suppress("UNCHECKED_CAST")
48+
val defaults = (preferencesClass.getField("DEFAULTS").get(null) as Map<String, *>).toMutableMap()
49+
defaults.putAll(defaultOverrides)
50+
51+
52+
val fieldAnnotations = FieldAnnotations(classLoader)
53+
54+
for (field in preferencesClass.fields) {
55+
if (!Modifier.isStatic(field.modifiers) || field.type != String::class.java) {
56+
continue
57+
}
58+
val key = inferShortKey(field, fieldAnnotations) ?: continue
59+
if (key in ignoredPreferences) {
60+
continue
61+
}
62+
val longKey = inferLongKey(field) ?: continue
63+
64+
val type = inferType(key, defaults, field, fieldAnnotations) ?: continue
65+
val name = inferName(key, field, fieldAnnotations)
66+
val description = inferDescription(field, fieldAnnotations)
67+
val currentValue = settingsMap[key] ?: (defaults[key] ?: defaults[longKey]!!).toString()
68+
val component = when (type) {
69+
Type.BOOLEAN -> JBCheckBox().also { checkBox ->
70+
checkBox.isSelected = currentValue == "1"
71+
checkBox.addActionListener {
72+
val newValue = if (checkBox.isSelected) "1" else "0"
73+
if (newValue != defaults[key]) {
74+
settingsMap[key] = newValue
75+
} else {
76+
settingsMap.remove(key)
77+
}
78+
}
79+
}
80+
Type.STRING -> JBTextField(currentValue).also { textField ->
81+
textField.columns = 20
82+
textField.document.addDocumentListener(object : DocumentAdapter() {
83+
override fun textChanged(e: DocumentEvent) {
84+
val newValue = textField.text
85+
if (newValue != defaults[key]) {
86+
settingsMap[key] = newValue
87+
} else {
88+
settingsMap.remove(key)
89+
}
90+
}
91+
})
92+
}
93+
Type.INTEGER -> JBIntSpinner(currentValue.toInt(), 0, Int.MAX_VALUE).also { spinner ->
94+
spinner.addChangeListener {
95+
val newValue = spinner.value.toString()
96+
if (newValue != defaults[key]) {
97+
settingsMap[key] = newValue
98+
} else {
99+
settingsMap.remove(key)
100+
}
101+
}
102+
}
103+
}
104+
entries += SettingsEntry(name, component, description)
105+
}
106+
}
107+
108+
private val nameOverrides = mapOf(
109+
"dc4" to "Decompile Class 1.4",
110+
"ind" to "Indent Size",
111+
"lit" to "Literals As-Is",
112+
)
113+
114+
private fun inferLongKey(field: Field): String? {
115+
return field.get(null) as String?
116+
}
117+
118+
private fun inferShortKey(field: Field, fieldAnnotations: FieldAnnotations): String? {
119+
if (fieldAnnotations.shortName != null) {
120+
val shortName = field.getAnnotation(fieldAnnotations.shortName)?.value
121+
if (shortName != null) {
122+
return shortName
123+
}
124+
}
125+
126+
return field.get(null) as String?
127+
}
128+
129+
private fun inferType(key: String, defaults: Map<String, *>, field: Field, fieldAnnotations: FieldAnnotations): Type? {
130+
if (fieldAnnotations.type != null) {
131+
when (field.getAnnotation(fieldAnnotations.type)?.value) {
132+
"bool" -> return Type.BOOLEAN
133+
"int" -> return Type.INTEGER
134+
"string" -> return Type.STRING
135+
}
136+
}
137+
138+
val dflt = defaults[key]?.toString() ?: return null
139+
if (dflt == "0" || dflt == "1") {
140+
return Type.BOOLEAN
141+
}
142+
if (dflt.toIntOrNull() != null) {
143+
return Type.INTEGER
144+
}
145+
return Type.STRING
146+
}
147+
148+
private fun inferName(key: String, field: Field, fieldAnnotations: FieldAnnotations): String {
149+
if (fieldAnnotations.name != null) {
150+
val name = field.getAnnotation(fieldAnnotations.name)?.value
151+
if (name != null) {
152+
return name
153+
}
154+
}
155+
156+
val nameOverride = nameOverrides[key]
157+
if (nameOverride != null) {
158+
return nameOverride
159+
}
160+
161+
return StringUtil.toTitleCase(field.name.replace("_", " ").toLowerCase(Locale.ROOT))
162+
}
163+
164+
private fun inferDescription(field: Field, fieldAnnotations: FieldAnnotations): String? {
165+
if (fieldAnnotations.description != null) {
166+
val description = field.getAnnotation(fieldAnnotations.description)?.value
167+
if (description != null) {
168+
return description
169+
}
170+
}
171+
172+
return null
173+
}
174+
175+
private val Annotation.value get() = javaClass.getMethod("value").invoke(this) as? String
176+
177+
class FieldAnnotations(classLoader: ClassLoader) {
178+
val name = classLoader.tryLoadAnnotation("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences\$Name")
179+
val description = classLoader.tryLoadAnnotation("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences\$Description")
180+
val shortName = classLoader.tryLoadAnnotation("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences\$ShortName")
181+
val type = classLoader.tryLoadAnnotation("org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences\$Type")
182+
183+
companion object {
184+
private fun ClassLoader.tryLoadAnnotation(name: String): Class<out Annotation>? {
185+
return try {
186+
loadClass(name).asSubclass(Annotation::class.java)
187+
} catch (e: ClassNotFoundException) {
188+
null
189+
} catch (e: ClassCastException) {
190+
null
191+
}
192+
}
193+
}
194+
}
195+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package org.vineflower.ijplugin
2+
3+
import com.intellij.application.options.CodeStyle
4+
import com.intellij.openapi.application.ApplicationManager
5+
import com.intellij.ui.DocumentAdapter
6+
import com.intellij.ui.JBIntSpinner
7+
import com.intellij.ui.components.JBCheckBox
8+
import com.intellij.ui.components.JBTextField
9+
import javax.swing.event.DocumentEvent
10+
11+
class NewVineflowerPreferences : VineflowerPreferences() {
12+
val ignoredPreferences = setOf(
13+
"banner",
14+
"bytecode-source-mapping",
15+
"new-line-separator",
16+
"log-level",
17+
"user-renamer-class",
18+
"thread-count",
19+
"max-time-per-method",
20+
"indent-string", // Custom implementation
21+
)
22+
23+
val defaultOverrides = mapOf(
24+
"hide-default-constructor" to "0",
25+
"decompile-generics" to "1",
26+
"remove-synthetic" to "1",
27+
"remove-bridge" to "1",
28+
"new-line-separator" to "1",
29+
"banner" to "//\n// Source code recreated from a .class file by Vineflower\n//\n\n",
30+
"max-time-per-method" to "0",
31+
"ignore-invalid-bytecode" to "1",
32+
"verify-anonymous-classes" to "1",
33+
"indent-string" to " ".repeat(CodeStyle.getDefaultSettings().indentOptions.INDENT_SIZE),
34+
"__unit_test_mode__" to if (ApplicationManager.getApplication().isUnitTestMode) "1" else "0",
35+
)
36+
37+
val nameOverrides = mapOf(
38+
"keep-literals" to "Literals As-Is",
39+
"decompiler-java4" to "Resugar 1-4 Class Refs",
40+
)
41+
42+
data class Option(
43+
val key: String,
44+
val type: Type,
45+
val name: String,
46+
val description: String,
47+
val plugin: String?,
48+
val defaultValue: String?,
49+
) {
50+
constructor(vfOption: Any, vfClass: Class<*>) : this(
51+
key = vfClass.getMethod("id").invoke(vfOption) as String,
52+
type = when (vfClass.getMethod("type").invoke(vfOption).toString()) {
53+
"bool" -> Type.BOOLEAN
54+
"int" -> Type.INTEGER
55+
else -> Type.STRING
56+
},
57+
name = vfClass.getMethod("name").invoke(vfOption) as String,
58+
description = vfClass.getMethod("description").invoke(vfOption) as String,
59+
plugin = vfClass.getMethod("plugin").invoke(vfOption) as String?,
60+
defaultValue = vfClass.getMethod("defaultValue").invoke(vfOption) as String?,
61+
)
62+
}
63+
64+
private val classLoader = VineflowerState.getInstance().getVineflowerClassLoader().getNow(null)
65+
private val optionClass = classLoader.loadClass("org.jetbrains.java.decompiler.api.DecompilerOption")
66+
private val getAllOptions = optionClass.getMethod("getAll")
67+
private val initVineflower = classLoader.loadClass("org.jetbrains.java.decompiler.main.Init")
68+
.getMethod("init")
69+
70+
override fun setupSettings(entries: MutableList<SettingsEntry>, settingsMap: MutableMap<String, String>) {
71+
initVineflower(null)
72+
73+
val options = (getAllOptions(null) as List<*>)
74+
.map { Option(it!!, optionClass) }
75+
.filter { it.key !in ignoredPreferences }
76+
77+
for (option in options) {
78+
if (option.key in ignoredPreferences) continue
79+
80+
val defaultValue = if (option.key in defaultOverrides) defaultOverrides[option.key] else option.defaultValue
81+
val currentValue = settingsMap[option.key] ?: defaultValue
82+
83+
val component = when (option.type) {
84+
Type.BOOLEAN -> JBCheckBox().apply {
85+
isSelected = currentValue == "1"
86+
addActionListener {
87+
val newValue = if (isSelected) "1" else "0"
88+
if (newValue != defaultValue) {
89+
settingsMap[option.key] = newValue
90+
} else {
91+
settingsMap.remove(option.key)
92+
}
93+
}
94+
}
95+
Type.STRING -> JBTextField(currentValue).apply {
96+
columns = 20
97+
document.addDocumentListener(object : DocumentAdapter() {
98+
override fun textChanged(e: DocumentEvent) {
99+
val newValue = text
100+
if (newValue != defaultValue) {
101+
settingsMap[option.key] = newValue
102+
} else {
103+
settingsMap.remove(option.key)
104+
}
105+
}
106+
})
107+
}
108+
Type.INTEGER -> JBIntSpinner(currentValue?.toInt() ?: 0, 0, Int.MAX_VALUE).apply {
109+
addChangeListener {
110+
val newValue = value.toString()
111+
if (newValue != defaultValue) {
112+
settingsMap[option.key] = newValue
113+
} else {
114+
settingsMap.remove(option.key)
115+
}
116+
}
117+
}
118+
}
119+
120+
val name = nameOverrides[option.key] ?: option.name
121+
val desc = option.description
122+
123+
entries.add(SettingsEntry(name, component, desc, option.plugin))
124+
}
125+
126+
run {
127+
val currentIndentString = settingsMap["indent-string"] ?: defaultOverrides["indent-string"]!!
128+
val component = JBIntSpinner(currentIndentString.length, 0, Int.MAX_VALUE).apply {
129+
addChangeListener {
130+
val newValue = " ".repeat(value.toString().toInt())
131+
if (newValue != defaultOverrides["indent-string"]) {
132+
settingsMap["indent-string"] = newValue
133+
} else {
134+
settingsMap.remove("indent-string")
135+
}
136+
}
137+
}
138+
139+
entries.add(SettingsEntry("Indent Size", component, "Number of spaces to use for each indentation level."))
140+
}
141+
142+
entries.sortBy { it.name }
143+
}
144+
}

src/main/kotlin/org/vineflower/ijplugin/VineflowerDecompilerBase.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ abstract class VineflowerDecompilerBase : ClassFileDecompilers.Full() {
6363
if (!state.enabled || state.hadError) {
6464
return false
6565
}
66-
val language = getLanguage(file) ?: return false
66+
val language = getLanguage(file)
67+
?: return false
6768
return acceptsLanguage(language)
6869
}
6970

src/main/kotlin/org/vineflower/ijplugin/VineflowerDecompilerKotlin.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,7 @@ class VineflowerDecompilerKotlin : VineflowerDecompilerBase() {
7171

7272
private class MyDecompiledFile(viewProvider: KotlinDecompiledFileViewProvider, contents: (VirtualFile) -> DecompiledText) : KtDecompiledFile(viewProvider, contents) {
7373
override fun getStub() = stubTree?.root as KotlinFileStub?
74+
75+
override fun toString(): String = toString().replace("KtFile", "VfDecompiledFile")
7476
}
7577
}

0 commit comments

Comments
 (0)