-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement interface injections (#33)
- Loading branch information
1 parent
eb609a9
commit 78b8c0a
Showing
48 changed files
with
597 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}" | ||
} |
103 changes: 103 additions & 0 deletions
103
...injection/src/main/java/net/neoforged/jst/interfaceinjection/InjectInterfacesVisitor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package net.neoforged.jst.interfaceinjection; | ||
|
||
import com.intellij.psi.PsiClass; | ||
import com.intellij.psi.PsiClassType; | ||
import com.intellij.psi.PsiElement; | ||
import com.intellij.psi.PsiFile; | ||
import com.intellij.psi.PsiRecursiveElementVisitor; | ||
import com.intellij.psi.PsiWhiteSpace; | ||
import com.intellij.psi.util.ClassUtil; | ||
import com.intellij.util.containers.MultiMap; | ||
import net.neoforged.jst.api.Replacements; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collection; | ||
import java.util.Comparator; | ||
import java.util.Objects; | ||
import java.util.stream.Collectors; | ||
|
||
class InjectInterfacesVisitor extends PsiRecursiveElementVisitor { | ||
private final Replacements replacements; | ||
private final MultiMap<String, String> interfaces; | ||
private final StubStore stubs; | ||
|
||
@Nullable | ||
private final String marker; | ||
|
||
InjectInterfacesVisitor(Replacements replacements, MultiMap<String, String> interfaces, StubStore stubs, @Nullable String marker) { | ||
this.replacements = replacements; | ||
this.interfaces = interfaces; | ||
this.stubs = stubs; | ||
this.marker = marker; | ||
} | ||
|
||
@Override | ||
public void visitElement(@NotNull PsiElement element) { | ||
if (element instanceof PsiClass psiClass) { | ||
if (psiClass.getQualifiedName() == null) { | ||
return; | ||
} | ||
|
||
String className = ClassUtil.getJVMClassName(psiClass); | ||
inject(psiClass, interfaces.get(className.replace('.', '/'))); | ||
|
||
for (PsiClass innerClass : psiClass.getInnerClasses()) { | ||
visitElement(innerClass); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public void visitFile(@NotNull PsiFile file) { | ||
file.acceptChildren(this); | ||
} | ||
|
||
private void inject(PsiClass psiClass, Collection<String> targets) { | ||
// We cannot add implements clauses to anonymous or unnamed classes | ||
if (targets.isEmpty() || psiClass.getImplementsList() == null) { | ||
return; | ||
} | ||
|
||
var implementsList = psiClass.isInterface() ? psiClass.getExtendsList() : psiClass.getImplementsList(); | ||
var implementedInterfaces = Arrays.stream(implementsList.getReferencedTypes()) | ||
.map(PsiClassType::resolve) | ||
.filter(Objects::nonNull) | ||
.map(PsiClass::getQualifiedName) | ||
.collect(Collectors.toSet()); | ||
|
||
var interfaceImplementation = targets.stream() | ||
.distinct() | ||
.map(stubs::createStub) | ||
.filter(iface -> !implementedInterfaces.contains(iface.interfaceDeclaration())) | ||
.map(StubStore.InterfaceInformation::toString) | ||
.map(this::decorate) | ||
.sorted(Comparator.naturalOrder()) | ||
.collect(Collectors.joining(", ")); | ||
|
||
if (implementsList.getChildren().length == 0) { | ||
StringBuilder text = new StringBuilder(); | ||
|
||
// `public class Cls{}` is valid, but we cannot inject the implements exactly next to the class name, so we need | ||
// to make sure that we have spacing | ||
if (!(psiClass.getLBrace().getPrevSibling() instanceof PsiWhiteSpace)) { | ||
text.append(' '); | ||
} | ||
text.append(psiClass.isInterface() ? "extends" : "implements").append(' '); | ||
text.append(interfaceImplementation); | ||
text.append(' '); | ||
|
||
replacements.insertBefore(psiClass.getLBrace(), text.toString()); | ||
} else { | ||
replacements.insertAfter(implementsList.getLastChild(), ", " + interfaceImplementation); | ||
} | ||
} | ||
|
||
private String decorate(String iface) { | ||
if (marker == null) { | ||
return iface; | ||
} | ||
return "@" + marker + " " + iface; | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
...njection/src/main/java/net/neoforged/jst/interfaceinjection/InterfaceInjectionPlugin.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package net.neoforged.jst.interfaceinjection; | ||
|
||
import net.neoforged.jst.api.SourceTransformer; | ||
import net.neoforged.jst.api.SourceTransformerPlugin; | ||
|
||
/** | ||
* Plugin that adds implements/extends clauses for interfaces to classes in a data-driven fashion, and creates stubs for these interfaces | ||
* to be able to still compile the modified code without access to the actual interface definitions. | ||
* <p> | ||
* Mods can use interface injection to have compile-time access to the interfaces they add to classes via Mixins. | ||
*/ | ||
public class InterfaceInjectionPlugin implements SourceTransformerPlugin { | ||
@Override | ||
public String getName() { | ||
return "interface-injection"; | ||
} | ||
|
||
@Override | ||
public SourceTransformer createTransformer() { | ||
return new InterfaceInjectionTransformer(); | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
...ion/src/main/java/net/neoforged/jst/interfaceinjection/InterfaceInjectionTransformer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package net.neoforged.jst.interfaceinjection; | ||
|
||
import com.google.gson.Gson; | ||
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.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
public class InterfaceInjectionTransformer implements SourceTransformer { | ||
private static final Gson GSON = new Gson(); | ||
|
||
@Nullable | ||
@CommandLine.Option(names = "--interface-injection-stubs", description = "The path to a zip to save interface stubs in") | ||
public Path stubOut; | ||
|
||
@Nullable | ||
@CommandLine.Option(names = "--interface-injection-marker", description = "The name (binary representation) of an annotation to use as a marker for injected interfaces") | ||
public String annotationMarker; | ||
|
||
@CommandLine.Option(names = "--interface-injection-data", description = "The paths to read interface injection JSON files from") | ||
public List<Path> paths = new ArrayList<>(); | ||
|
||
private MultiMap<String, String> interfaces; | ||
private StubStore stubs; | ||
private String marker; | ||
|
||
@Override | ||
public void beforeRun(TransformContext context) { | ||
interfaces = new MultiMap<>(); | ||
stubs = new StubStore(context.logger(), context.environment().getPsiFacade()); | ||
|
||
if (annotationMarker != null) { | ||
marker = annotationMarker.replace('/', '.').replace('$', '.'); | ||
} | ||
|
||
for (Path path : paths) { | ||
try { | ||
var json = GSON.fromJson(Files.readString(path), JsonObject.class); | ||
for (String clazz : json.keySet()) { | ||
var entry = json.get(clazz); | ||
if (entry.isJsonArray()) { | ||
entry.getAsJsonArray().forEach(el -> interfaces.putValue(clazz, el.getAsString())); | ||
} else { | ||
interfaces.putValue(clazz, entry.getAsString()); | ||
} | ||
} | ||
} catch (IOException exception) { | ||
context.logger().error("Failed to read interface injection data file: %s", exception.getMessage()); | ||
throw new UncheckedIOException(exception); | ||
} | ||
} | ||
} | ||
|
||
@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 InjectInterfacesVisitor(replacements, interfaces, stubs, marker).visitFile(psiFile); | ||
} | ||
} |
Oops, something went wrong.