Skip to content

Commit

Permalink
Decompiler option access API (#418)
Browse files Browse the repository at this point in the history
* Add a programmatic way of getting all options, including plugin options

* Missed one Type call

* Fix case when `null` is in the defaults

* Add documentation on the option getters
  • Loading branch information
sschr15 authored Aug 18, 2024
1 parent 6930e47 commit 6333850
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 159 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package org.vineflower.ideanotnull;

import org.jetbrains.java.decompiler.api.DecompilerOption;
import org.jetbrains.java.decompiler.api.plugin.PluginOptions;
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences;

public interface IdeaNotNullOptions {
@IFernflowerPreferences.Name("Resugar Intellij IDEA @NotNull")
@IFernflowerPreferences.Description("Resugar Intellij IDEA's code generated by @NotNull annotations.")
@IFernflowerPreferences.ShortName("inn")
@IFernflowerPreferences.Type(IFernflowerPreferences.Type.BOOLEAN)
@IFernflowerPreferences.Type(DecompilerOption.Type.BOOLEAN)
String IDEA_NOT_NULL_ANNOTATION = "resugar-idea-notnull";

static void addDefaults(PluginOptions.AddDefaults cons) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
package org.vineflower.kotlin;

import org.jetbrains.java.decompiler.api.DecompilerOption;
import org.jetbrains.java.decompiler.api.plugin.PluginOptions;
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences.*;

public interface KotlinOptions {
@Name("Show public visibility")
@Description("If a construct is public, show the public keyword")
@Type(Type.BOOLEAN)
@Type(DecompilerOption.Type.BOOLEAN)
String SHOW_PUBLIC_VISIBILITY = "kt-show-public";

@Name("Enable Kotlin plugin")
@Description("Decompile Kotlin classes as Kotlin instead of Java")
@Type(Type.BOOLEAN)
@Type(DecompilerOption.Type.BOOLEAN)
String DECOMPILE_KOTLIN = "kt-enable";

@Name("Unknown default arg string")
@Description("String to use for unknown default arguments, or empty to not indicate unknown defaults")
@Type(Type.STRING)
@Type(DecompilerOption.Type.STRING)
String UNKNOWN_DEFAULT_ARG_STRING = "kt-unknown-defaults";

@Name("Collapse string concatenation")
@Description("Convert string concatenations to Kotlin string templates.")
@Type(Type.BOOLEAN)
@Type(DecompilerOption.Type.BOOLEAN)
String COLLAPSE_STRING_CONCATENATION = "kt-collapse-string-concat";

static void addDefaults(PluginOptions.AddDefaults cons) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
package org.vineflower.variablerenaming;

import org.jetbrains.java.decompiler.api.DecompilerOption;
import org.jetbrains.java.decompiler.api.plugin.PluginOptions;
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences;
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences.*;

public interface VariableRenamingOptions {
@Name("Variable Renaming")
@Description("Use a custom renamer for variable names. Built-in options include \"jad\" and \"tiny\".")
@Type(Type.STRING)
@Type(DecompilerOption.Type.STRING)
String VARIABLE_RENAMER = "variable-renaming";

@Name("Rename Parameters")
@Description("Use the custom renamer for parameters in addition to locals.")
@Type(Type.BOOLEAN)
@Type(DecompilerOption.Type.BOOLEAN)
String RENAME_PARAMETERS = "rename-parameters";

@Name("[Deprecated] JAD-Style Variable Naming")
@Description("Use JAD-style variable naming. Deprecated, set \"variable-renamer=jad\" instead.")
@Type(Type.BOOLEAN)
@Type(DecompilerOption.Type.BOOLEAN)
String USE_JAD_VARNAMING = "jad-style-variable-naming";

@Name("[Deprecated] JAD-Style Parameter Naming")
@Description("Alias for \"rename-parameters\". Deprecated, use that option instead.")
@Type(Type.BOOLEAN)
@Type(DecompilerOption.Type.BOOLEAN)
String USE_JAD_PARAMETER_NAMING = "jad-style-parameter-naming";

static void addDefaults(PluginOptions.AddDefaults cons) {
Expand Down
194 changes: 194 additions & 0 deletions src/org/jetbrains/java/decompiler/api/DecompilerOption.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package org.jetbrains.java.decompiler.api;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.java.decompiler.api.plugin.Plugin;
import org.jetbrains.java.decompiler.api.plugin.PluginOptions;
import org.jetbrains.java.decompiler.main.Fernflower;
import org.jetbrains.java.decompiler.main.decompiler.BaseDecompiler;
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences;
import org.jetbrains.java.decompiler.main.plugins.PluginContext;

import java.lang.reflect.Field;
import java.util.*;

/**
* Represents a decompiler option. These can be passed from command line or as
* a map into a {@link BaseDecompiler} or {@link Fernflower} constructor.
* <p>
* As plugins might not provide all information, some fields are nullable.
*
* @param id The unique identifier of the option. This is what is passed to the decompiler.
* @param name A human-readable name for the option.
* @param description A human-readable description of the option.
* @param type The type of the option.
* @param plugin The plugin that provides this option. If {@code null}, it comes from the core decompiler.
* @param defaultValue The default value of the option.
*/
public record DecompilerOption(
@NotNull String id,
@NotNull String name,
@NotNull String description,
@NotNull Type type,
@Nullable String plugin,
@Nullable String defaultValue
) implements Comparable<DecompilerOption> {
public enum Type {
BOOLEAN("bool"),
STRING("string"),
INTEGER("int"),

;

private final String friendlyName;

Type(String friendlyName) {
this.friendlyName = friendlyName;
}

public String toString() {
return friendlyName;
}
}

/**
* Compares this option to another option. The order is set first by the plugin, then by the id.
* Core decompiler options come first.
*/
@Override
public int compareTo(@NotNull DecompilerOption decompilerOption) {
if (!Objects.equals(decompilerOption.plugin, plugin)) {
if (plugin == null) {
return -1;
}
if (decompilerOption.plugin == null) {
return 1;
}
return plugin.compareTo(decompilerOption.plugin);
}
return id.compareTo(decompilerOption.id);
}

/**
* Get all decompiler options from all plugins and the core decompiler.
* @return A list of all decompiler options, sorted by plugin and id, with core decompiler options first.
*/
public static List<DecompilerOption> getAll() {
List<DecompilerOption> options = new ArrayList<>();

List<Field> fields = Arrays.stream(IFernflowerPreferences.class.getDeclaredFields())
.filter(field -> field.getType() == String.class)
.toList();

Map<String, Object> defaults = IFernflowerPreferences.DEFAULTS;
addOptions(fields, options, defaults, null);

PluginContext ctx = new PluginContext();
ctx.findPlugins();

for (Plugin plugin : ctx.getPlugins()) {
PluginOptions opt = plugin.getPluginOptions();

if (opt != null) {
var opts = opt.provideOptions();

List<Field> pluginFields = Arrays.stream(opts.a.getDeclaredFields())
.filter(field -> field.getType() == String.class)
.toList();

Map<String, Object> pluginDefaults = new HashMap<>();
opts.b.accept(pluginDefaults::put);

addOptions(pluginFields, options, pluginDefaults, plugin);
}
}

options.sort(DecompilerOption::compareTo);

return options;
}

/**
* Get all decompiler options from all plugins and the core decompiler, grouped by plugin.
* Calling {@link Map#get} with {@code null} will return the core decompiler options.
* @return A map of plugins to their decompiler options, sorted by id within each plugin.
*/
public static Map<Plugin, List<DecompilerOption>> getAllByPlugin() {
Map<Plugin, List<DecompilerOption>> options = new HashMap<>();

List<Field> fields = Arrays.stream(IFernflowerPreferences.class.getDeclaredFields())
.filter(field -> field.getType() == String.class)
.toList();

Map<String, Object> defaults = IFernflowerPreferences.DEFAULTS;
List<DecompilerOption> coreOptions = new ArrayList<>();
addOptions(fields, coreOptions, defaults, null);
coreOptions.sort(DecompilerOption::compareTo);
options.put(null, coreOptions);

PluginContext ctx = new PluginContext();
ctx.findPlugins();

for (Plugin plugin : ctx.getPlugins()) {
PluginOptions opt = plugin.getPluginOptions();

if (opt != null) {
var opts = opt.provideOptions();

List<Field> pluginFields = Arrays.stream(opts.a.getDeclaredFields())
.filter(field -> field.getType() == String.class)
.toList();

Map<String, Object> pluginDefaults = new HashMap<>();
opts.b.accept(pluginDefaults::put);

List<DecompilerOption> pluginOptions = new ArrayList<>();
addOptions(pluginFields, pluginOptions, pluginDefaults, plugin);

pluginOptions.sort(DecompilerOption::compareTo);

options.put(plugin, pluginOptions);
}
}

return options;
}

private static void addOptions(List<Field> fields, List<DecompilerOption> options, Map<String, Object> defaults, Plugin plugin) {
for (Field field : fields) {
IFernflowerPreferences.Name name = field.getAnnotation(IFernflowerPreferences.Name.class);
IFernflowerPreferences.Description description = field.getAnnotation(IFernflowerPreferences.Description.class);
IFernflowerPreferences.Type type = field.getAnnotation(IFernflowerPreferences.Type.class);

String paramName;
try {
paramName = (String) field.get(null);
} catch (IllegalAccessException e) {
IFernflowerPreferences.ShortName shortName = field.getAnnotation(IFernflowerPreferences.ShortName.class);
if (shortName == null) {
continue;
}
paramName = shortName.value();
}

if (name == null || description == null || type == null) {
continue;
}

String friendlyName = name.value();
String friendlyDescription = description.value();
Type friendlyType = type.value();
Object defaultValue = defaults.get(paramName);
String defaultValueString = defaultValue != null ? defaultValue.toString() : null;

options.add(new DecompilerOption(
paramName,
friendlyName,
friendlyDescription,
friendlyType,
plugin != null ? plugin.id() : null,
defaultValueString
));
}
}
}
Loading

0 comments on commit 6333850

Please sign in to comment.