Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {

include project(":parchment")
include project(":accesstransformers")
include project(":enumextensions")
include project(':interfaceinjection')
include project(':unpick')

Expand Down
8 changes: 8 additions & 0 deletions enumextensions/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
id 'java-library'
}

dependencies {
implementation project(':api')
implementation "com.google.code.gson:gson:${project.gson_version}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.neoforged.jst.enumextensions;

import net.neoforged.jst.api.SourceTransformer;
import net.neoforged.jst.api.SourceTransformerPlugin;

public class EnumExtensionPlugin implements SourceTransformerPlugin {
@Override
public String getName() {
return "enum-extensions";
}

@Override
public SourceTransformer createTransformer() {
return new EnumExtensionTransformer();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package net.neoforged.jst.enumextensions;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.intellij.psi.PsiFile;
import com.intellij.util.containers.MultiMap;
import net.neoforged.jst.api.Replacements;
import net.neoforged.jst.api.SourceTransformer;
import net.neoforged.jst.api.TransformContext;
import org.jetbrains.annotations.Nullable;
import picocli.CommandLine;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.constant.ClassDesc;
import java.lang.constant.MethodTypeDesc;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class EnumExtensionTransformer implements SourceTransformer {
private static final Gson GSON = new Gson();

@Nullable
@CommandLine.Option(names = "--enum-extensions-stubs", description = "The path to a zip to save reference stubs in")
public Path stubOut;

@CommandLine.Option(names = "--enum-extensions-data", description = "The paths to read enum extension JSON files from")
public List<Path> paths = new ArrayList<>();

@Nullable
@CommandLine.Option(names = "--enum-extensions-marker", description = "The name (binary representation) of an annotation to use as a marker for extended enum entries")
public String annotationMarker;

@Nullable
@CommandLine.Option(names = "--enum-extensions-required-interface", description = "The name (binary representation) of an interface to enforce that extendeable enums implement")
public String requiredInterface;

private MultiMap<String, ExtensionPrototype> extensions;
private StubStore stubs;
private String marker;
private String requiredInterfaceFqn;

@Override
public void beforeRun(TransformContext context) {
extensions = new MultiMap<>();
stubs = new StubStore(context.environment().getPsiFacade());

if (annotationMarker != null) {
marker = annotationMarker.replace('/', '.').replace('$', '.');
}

if (requiredInterface != null) {
requiredInterfaceFqn = requiredInterface.replace('/', '.').replace('$', '.');
}

for (Path path : paths) {
try {
var json = GSON.fromJson(Files.readString(path), JsonObject.class);
JsonArray entries = json.getAsJsonArray("entries");
for (JsonElement entry : entries) {
JsonObject entryObj = entry.getAsJsonObject();

String enumName = entryObj.get("enum").getAsString();
String fieldName = entryObj.get("name").getAsString();
String ctorDesc = entryObj.get("constructor").getAsString();
JsonElement paramElem = entryObj.get("parameters");
ExtensionPrototype.EnumParameters parameters = null;
if (paramElem.isJsonArray()) {
parameters = loadConstantParameters(context, enumName, fieldName, ctorDesc, paramElem.getAsJsonArray());
} else if (paramElem.isJsonObject()) {
JsonObject obj = paramElem.getAsJsonObject();
String className = obj.get("class").getAsString();
if (obj.has("method")) {
String srcMethodName = obj.get("method").getAsString();
parameters = new ExtensionPrototype.EnumParameters.MethodReference(className, srcMethodName);
} else if (obj.has("field")) {
String srcFieldName = obj.get("field").getAsString();
parameters = new ExtensionPrototype.EnumParameters.FieldReference(className, srcFieldName);
}
}
extensions.putValue(enumName, new ExtensionPrototype(
fieldName,
ctorDesc,
parameters
));
if (parameters == null) {
context.logger().error("Failed to read parameters for enum extension entry: %s", entryObj);
throw new IllegalArgumentException("Invalid parameters for enum extension entry");
}
}
} catch (IOException exception) {
context.logger().error("Failed to read interface injection data file: %s", exception.getMessage());
throw new UncheckedIOException(exception);
}
}
}

private static ExtensionPrototype.EnumParameters loadConstantParameters(TransformContext context, String enumName, String fieldName, String ctorDesc, JsonArray obj) {
List<Object> params = new ArrayList<>(obj.size());
var argTypes = MethodTypeDesc.ofDescriptor(ctorDesc).parameterArray();
if (argTypes.length != obj.size()) {
var message = String.format(
"Parameter count %s does not match argument count %s of constructor %s for field %s in enum %s",
obj.size(), argTypes.length, ctorDesc, fieldName, enumName
);
context.logger().error(message);
throw new IllegalArgumentException(message);
}

int idx = 0;
for (JsonElement element : obj) {
ClassDesc argType = argTypes[idx];
switch (argType.descriptorString()) {
case "Z" -> params.add(element.getAsBoolean());
case "C" -> {
String param = element.getAsString();
if (param.length() != 1) {
var message = String.format(
"Invalid character %s at parameter index %s for field %s in enum %s",
param, idx, fieldName, enumName
);
context.logger().error(message);
throw new IllegalArgumentException(message);
}
params.add(param.charAt(0));
}
case "B" -> params.add(element.getAsByte());
case "S" -> params.add(element.getAsShort());
case "I" -> params.add(element.getAsInt());
case "F" -> params.add(element.getAsFloat());
case "J" -> params.add(element.getAsLong());
case "D" -> params.add(element.getAsDouble());
case "Ljava/lang/String;" -> params.add(element.isJsonNull() ? null : element.getAsString());
default -> {
if (!element.isJsonNull()) {
var message = String.format(
"Unsupported immediate argument type %s at parameter index %s for field %s in enum %s",
argType, idx, fieldName, enumName
);
context.logger().error(message);
throw new IllegalArgumentException(message);
}
params.add(null);
}
}
idx++;
}
return new ExtensionPrototype.EnumParameters.Constant(params);
}

@Override
public boolean afterRun(TransformContext context) {
if (stubOut != null) {
try {
stubs.save(stubOut);
} catch (IOException e) {
context.logger().error("Failed to save stubs: %s", e.getMessage());
throw new UncheckedIOException(e);
}
}

return true;
}

@Override
public void visitFile(PsiFile psiFile, Replacements replacements) {
new EnumExtensionVisitor(replacements, extensions, stubs, marker, requiredInterfaceFqn).visitFile(psiFile);
}
}
Loading
Loading