diff --git a/README.md b/README.md index 7550448..2a01f1f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The interfaces can have generic parameter declarations. Assuming the target clas we could implement `Supplier` using `java/util/function/Supplier`. > [!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`). +> 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`). As an exception to this rule, inner classes should be separated by `$` (e.g. `java/util/function/Supplier`). ### Custom transformers Third parties can use JST to implement their source file own transformations. diff --git a/interfaceinjection/src/main/java/net/neoforged/jst/interfaceinjection/StubStore.java b/interfaceinjection/src/main/java/net/neoforged/jst/interfaceinjection/StubStore.java index 6283bfa..7f0ddfd 100644 --- a/interfaceinjection/src/main/java/net/neoforged/jst/interfaceinjection/StubStore.java +++ b/interfaceinjection/src/main/java/net/neoforged/jst/interfaceinjection/StubStore.java @@ -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; @@ -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 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); @@ -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 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; @@ -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; + } + } } diff --git a/tests/data/interfaceinjection/nested_generic_stubs/expected/com/MyTarget.java b/tests/data/interfaceinjection/nested_generic_stubs/expected/com/MyTarget.java new file mode 100644 index 0000000..119c1a1 --- /dev/null +++ b/tests/data/interfaceinjection/nested_generic_stubs/expected/com/MyTarget.java @@ -0,0 +1,7 @@ +package com; + +import com.CustomInterface; +import com.InjectedInterface; + +public class MyTarget implements CustomInterface, InjectedInterface, com.example.Classes.Generics>>> { +} diff --git a/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/CustomInterface.java b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/CustomInterface.java new file mode 100644 index 0000000..12898ae --- /dev/null +++ b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/CustomInterface.java @@ -0,0 +1,4 @@ +package com; + +public interface CustomInterface { +} diff --git a/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/InjectedInterface.java b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/InjectedInterface.java new file mode 100644 index 0000000..e554151 --- /dev/null +++ b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/InjectedInterface.java @@ -0,0 +1,4 @@ +package com; + +public interface InjectedInterface { +} diff --git a/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/example/Classes.java b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/example/Classes.java new file mode 100644 index 0000000..b7d2558 --- /dev/null +++ b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/example/Classes.java @@ -0,0 +1,6 @@ +package com.example; + +public interface Classes { + public interface Generics { + } +} diff --git a/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/example/TypeParameter.java b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/example/TypeParameter.java new file mode 100644 index 0000000..afaf9ac --- /dev/null +++ b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/example/TypeParameter.java @@ -0,0 +1,6 @@ +package com.example; + +public interface TypeParameter { + public interface InnerClass { + } +} diff --git a/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/example/WeirdSupplier.java b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/example/WeirdSupplier.java new file mode 100644 index 0000000..d6c1fcf --- /dev/null +++ b/tests/data/interfaceinjection/nested_generic_stubs/expected_stub/com/example/WeirdSupplier.java @@ -0,0 +1,4 @@ +package com.example; + +public interface WeirdSupplier { +} diff --git a/tests/data/interfaceinjection/nested_generic_stubs/injectedinterfaces.json b/tests/data/interfaceinjection/nested_generic_stubs/injectedinterfaces.json new file mode 100644 index 0000000..10507e9 --- /dev/null +++ b/tests/data/interfaceinjection/nested_generic_stubs/injectedinterfaces.json @@ -0,0 +1,6 @@ +{ + "com/MyTarget": [ + "com/InjectedInterface, com.example.Classes$Generics>>>", + "com/CustomInterface" + ] +} diff --git a/tests/data/interfaceinjection/nested_generic_stubs/source/com/MyTarget.java b/tests/data/interfaceinjection/nested_generic_stubs/source/com/MyTarget.java new file mode 100644 index 0000000..05b1f47 --- /dev/null +++ b/tests/data/interfaceinjection/nested_generic_stubs/source/com/MyTarget.java @@ -0,0 +1,4 @@ +package com; + +public class MyTarget { +} diff --git a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java index e7de356..8c1a4ab 100644 --- a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java +++ b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java @@ -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 {