Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The interfaces can have generic parameter declarations. Assuming the target clas
we could implement `Supplier<T>` using `java/util/function/Supplier<T>`.

> [!NOTE]
> Generics are *copied verbatim*. If you need the generics to reference a class, please use its fully qualified name (e.g. `java/util/function/Supplier<java.util.concurrent.atomic.AtomicInteger>`).
> Generics are *copied verbatim*. If you need the generics to reference a class, please use its fully qualified name (e.g. `java/util/function/Supplier<java.util.concurrent.atomic.AtomicInteger>`). As an exception to this rule, inner classes should be separated by `$` (e.g. `java/util/function/Supplier<java.util.Map$Entry>`).

### Custom transformers
Third parties can use JST to implement their source file own transformations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.ZipEntry;
Expand Down Expand Up @@ -50,8 +52,18 @@ public InterfaceInformation createStub(String jvm) {
if (generics.isBlank()) {
logger.error("Interface injection %s has blank type parameters", jvm);
} else {
// Ignore any nested generics when counting the amount of parameters the interface has
typeParameterCount = generics.replaceAll("<[^>]*>", "").split(",").length;
var reader = new StringReader(generics);
List<String> typeArgs = new ArrayList<>();
while (reader.hasNext()) {
typeArgs.add(stubGenericArguments(reader));
reader.skipWhitespace();
if (reader.hasNext() && reader.next() != ',') {
logger.error("Interface injection generics declaration %s is invalid", generics);
}
}

generics = String.join(", ", typeArgs);
typeParameterCount = typeArgs.size();
}
}
jvm = jvm.substring(0, genericsStart);
Expand All @@ -60,6 +72,53 @@ public InterfaceInformation createStub(String jvm) {
return new InterfaceInformation(createStub(jvm, typeParameterCount), generics);
}

private static final Pattern BOUNDED_WILDCARD_PATTERN = Pattern.compile("\\?\\s+(extends|super)\\s+(.+)");

private String stubGenericArguments(StringReader generics) {
StringBuilder typeName = new StringBuilder();
List<String> genericArgs = new ArrayList<>();
while (generics.hasNext() && generics.peek() != ',' && generics.peek() != '>') {
var ch = generics.next();
if (ch == '<') {
do {
genericArgs.add(stubGenericArguments(generics));
generics.skipWhitespace();
} while (generics.next() != '>'); // The next character can either be a comma or a >. If it's a > we exit the generic declaration, otherwise we consume the comma and stub the next argument
break; // No point in continuing to parse if we found and parsed the nested generic arguments
} else {
typeName.append(ch);
}
}

String base;

var type = typeName.toString().trim();
// Within bounded wildcards (? extends X) or (? super X) we need to make sure we stub the type
var boundedMatcher = BOUNDED_WILDCARD_PATTERN.matcher(type);
if (boundedMatcher.matches()) {
var name = boundedMatcher.group(2);
base = "? " + boundedMatcher.group(1) + " " + possiblyStubTypeName(name, genericArgs.size());
} else {
base = possiblyStubTypeName(type, genericArgs.size());
}

if (genericArgs.isEmpty()) {
return base;
} else {
return base + "<" + String.join(", ", genericArgs) + ">";
}
}

private String possiblyStubTypeName(String name, int genericCount) {
// If the type argument contains a dot we assume it is a class, so we have to stub it
if (name.contains(".")) {
return createStub(name.replace('.', '/'), genericCount);
} else {
// Otherwise, it could be a wildcard or it could be another type parameter
return name;
}
}

private synchronized String createStub(String jvm, int typeParameterCount) {
var fqn = jvmToFqn.get(jvm);
if (fqn != null) return fqn;
Expand Down Expand Up @@ -142,4 +201,34 @@ public String toString() {
return generics.isBlank() ? interfaceDeclaration : interfaceDeclaration + "<" + generics + ">";
}
}

private static class StringReader {
private final String string;
private int i = -1;

private StringReader(String string) {
this.string = string;
}

public boolean hasNext() {
return i < string.length() - 1;
}

public char peek() {
return string.charAt(i + 1);
}

public char next() {
return string.charAt(++i);
}

public void skipWhitespace() {
while (hasNext() && peek() == ' ') next();
}

@Override
public String toString() {
return string;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com;

import com.CustomInterface;
import com.InjectedInterface;

public class MyTarget<T> implements CustomInterface<com.example.TypeParameter.InnerClass>, InjectedInterface<java.util.Map.Entry<? extends com.example.WeirdSupplier<T>, com.example.Classes.Generics<T, java.lang.Integer, com.example.WeirdSupplier<?>>>> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com;

public interface CustomInterface<A> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com;

public interface InjectedInterface<A> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example;

public interface Classes {
public interface Generics<A, B, C> {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example;

public interface TypeParameter {
public interface InnerClass {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.example;

public interface WeirdSupplier<A> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"com/MyTarget": [
"com/InjectedInterface<java.util.Map$Entry<? extends com.example.WeirdSupplier<T>, com.example.Classes$Generics<T, java.lang.Integer, com.example.WeirdSupplier<?>>>>",
"com/CustomInterface<com.example.TypeParameter$InnerClass>"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com;

public class MyTarget<T> {
}
5 changes: 5 additions & 0 deletions tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ void testInjectedMarker() throws Exception {
void testGenerics() throws Exception {
runInterfaceInjectionTest("generics", tempDir);
}

@Test
void testNestedGenericStubs() throws Exception {
runInterfaceInjectionTest("nested_generic_stubs", tempDir);
}
}

protected final void runInterfaceInjectionTest(String testDirName, Path tempDir, String... additionalArgs) throws Exception {
Expand Down
Loading