Skip to content

Commit 5609af0

Browse files
authored
Generate stubs for generic arguments (#53)
Until now, generic arguments wouldn't have stubs generated, meaning that you could not inject an interface with a type parameter that references your own classes. This fixes that. For this to be fixed, an exception from the "verbatim copy" rule had to be added: inner classes must use $. This change does not however break any existing *working* builds.
1 parent b58cda4 commit 5609af0

File tree

11 files changed

+138
-3
lines changed

11 files changed

+138
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The interfaces can have generic parameter declarations. Assuming the target clas
3838
we could implement `Supplier<T>` using `java/util/function/Supplier<T>`.
3939

4040
> [!NOTE]
41-
> 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>`).
41+
> 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>`).
4242
4343
### Custom transformers
4444
Third parties can use JST to implement their source file own transformations.

interfaceinjection/src/main/java/net/neoforged/jst/interfaceinjection/StubStore.java

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import java.util.ArrayList;
1313
import java.util.Arrays;
1414
import java.util.HashMap;
15+
import java.util.List;
1516
import java.util.Map;
1617
import java.util.concurrent.atomic.AtomicInteger;
1718
import java.util.function.Consumer;
19+
import java.util.regex.Pattern;
1820
import java.util.stream.Collectors;
1921
import java.util.stream.IntStream;
2022
import java.util.zip.ZipEntry;
@@ -50,8 +52,18 @@ public InterfaceInformation createStub(String jvm) {
5052
if (generics.isBlank()) {
5153
logger.error("Interface injection %s has blank type parameters", jvm);
5254
} else {
53-
// Ignore any nested generics when counting the amount of parameters the interface has
54-
typeParameterCount = generics.replaceAll("<[^>]*>", "").split(",").length;
55+
var reader = new StringReader(generics);
56+
List<String> typeArgs = new ArrayList<>();
57+
while (reader.hasNext()) {
58+
typeArgs.add(stubGenericArguments(reader));
59+
reader.skipWhitespace();
60+
if (reader.hasNext() && reader.next() != ',') {
61+
logger.error("Interface injection generics declaration %s is invalid", generics);
62+
}
63+
}
64+
65+
generics = String.join(", ", typeArgs);
66+
typeParameterCount = typeArgs.size();
5567
}
5668
}
5769
jvm = jvm.substring(0, genericsStart);
@@ -60,6 +72,53 @@ public InterfaceInformation createStub(String jvm) {
6072
return new InterfaceInformation(createStub(jvm, typeParameterCount), generics);
6173
}
6274

75+
private static final Pattern BOUNDED_WILDCARD_PATTERN = Pattern.compile("\\?\\s+(extends|super)\\s+(.+)");
76+
77+
private String stubGenericArguments(StringReader generics) {
78+
StringBuilder typeName = new StringBuilder();
79+
List<String> genericArgs = new ArrayList<>();
80+
while (generics.hasNext() && generics.peek() != ',' && generics.peek() != '>') {
81+
var ch = generics.next();
82+
if (ch == '<') {
83+
do {
84+
genericArgs.add(stubGenericArguments(generics));
85+
generics.skipWhitespace();
86+
} 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
87+
break; // No point in continuing to parse if we found and parsed the nested generic arguments
88+
} else {
89+
typeName.append(ch);
90+
}
91+
}
92+
93+
String base;
94+
95+
var type = typeName.toString().trim();
96+
// Within bounded wildcards (? extends X) or (? super X) we need to make sure we stub the type
97+
var boundedMatcher = BOUNDED_WILDCARD_PATTERN.matcher(type);
98+
if (boundedMatcher.matches()) {
99+
var name = boundedMatcher.group(2);
100+
base = "? " + boundedMatcher.group(1) + " " + possiblyStubTypeName(name, genericArgs.size());
101+
} else {
102+
base = possiblyStubTypeName(type, genericArgs.size());
103+
}
104+
105+
if (genericArgs.isEmpty()) {
106+
return base;
107+
} else {
108+
return base + "<" + String.join(", ", genericArgs) + ">";
109+
}
110+
}
111+
112+
private String possiblyStubTypeName(String name, int genericCount) {
113+
// If the type argument contains a dot we assume it is a class, so we have to stub it
114+
if (name.contains(".")) {
115+
return createStub(name.replace('.', '/'), genericCount);
116+
} else {
117+
// Otherwise, it could be a wildcard or it could be another type parameter
118+
return name;
119+
}
120+
}
121+
63122
private synchronized String createStub(String jvm, int typeParameterCount) {
64123
var fqn = jvmToFqn.get(jvm);
65124
if (fqn != null) return fqn;
@@ -142,4 +201,34 @@ public String toString() {
142201
return generics.isBlank() ? interfaceDeclaration : interfaceDeclaration + "<" + generics + ">";
143202
}
144203
}
204+
205+
private static class StringReader {
206+
private final String string;
207+
private int i = -1;
208+
209+
private StringReader(String string) {
210+
this.string = string;
211+
}
212+
213+
public boolean hasNext() {
214+
return i < string.length() - 1;
215+
}
216+
217+
public char peek() {
218+
return string.charAt(i + 1);
219+
}
220+
221+
public char next() {
222+
return string.charAt(++i);
223+
}
224+
225+
public void skipWhitespace() {
226+
while (hasNext() && peek() == ' ') next();
227+
}
228+
229+
@Override
230+
public String toString() {
231+
return string;
232+
}
233+
}
145234
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com;
2+
3+
import com.CustomInterface;
4+
import com.InjectedInterface;
5+
6+
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<?>>>> {
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com;
2+
3+
public interface CustomInterface<A> {
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com;
2+
3+
public interface InjectedInterface<A> {
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.example;
2+
3+
public interface Classes {
4+
public interface Generics<A, B, C> {
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.example;
2+
3+
public interface TypeParameter {
4+
public interface InnerClass {
5+
}
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.example;
2+
3+
public interface WeirdSupplier<A> {
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"com/MyTarget": [
3+
"com/InjectedInterface<java.util.Map$Entry<? extends com.example.WeirdSupplier<T>, com.example.Classes$Generics<T, java.lang.Integer, com.example.WeirdSupplier<?>>>>",
4+
"com/CustomInterface<com.example.TypeParameter$InnerClass>"
5+
]
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com;
2+
3+
public class MyTarget<T> {
4+
}

0 commit comments

Comments
 (0)