From 296e56efa4b40b29b08b8c1ff93d46ca87dda76d Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Sat, 14 Sep 2024 17:54:24 +0200 Subject: [PATCH 1/3] Add Type.parse() --- core/src/main/java/org/jboss/jandex/Type.java | 28 +++ .../java/org/jboss/jandex/TypeParser.java | 169 +++++++++++++++++ .../org/jboss/jandex/test/TypeParserTest.java | 173 ++++++++++++++++++ 3 files changed, 370 insertions(+) create mode 100644 core/src/main/java/org/jboss/jandex/TypeParser.java create mode 100644 core/src/test/java/org/jboss/jandex/test/TypeParserTest.java diff --git a/core/src/main/java/org/jboss/jandex/Type.java b/core/src/main/java/org/jboss/jandex/Type.java index 3ccb5c3d..ab632d98 100644 --- a/core/src/main/java/org/jboss/jandex/Type.java +++ b/core/src/main/java/org/jboss/jandex/Type.java @@ -263,6 +263,34 @@ public static Type createWithAnnotations(DotName name, Kind kind, AnnotationInst return annotations == null ? type : type.copyType(annotations); } + /** + * Creates a {@link Type} by parsing the given string according to the following grammar: + * + *
+     * Type -> VoidType | PrimitiveType | ReferenceType
+     * VoidType -> 'void'
+     * PrimitiveType -> 'boolean' | 'byte' | 'short' | 'int'
+     *                | 'long' | 'float' | 'double' | 'char'
+     * ReferenceType -> PrimitiveType ('[' ']')+
+     *                | ClassType ('<' TypeArgument (',' TypeArgument)* '>')? ('[' ']')*
+     * ClassType -> FULLY_QUALIFIED_NAME
+     * TypeArgument -> ReferenceType | WildcardType
+     * WildcardType -> '?' | '?' ('extends' | 'super') ReferenceType
+     * 
+ * + * Notice that the resulting type never contains type variables, only "proper" types. + * Also notice that the grammar above does not support all kinds of nested types; + * it should be possible to add that later, if there's an actual need. + * + * @param type the string to parse; must not be {@code null} + * @return the parsed type + * @throws IllegalArgumentException if the string does not conform to the grammar given above + * @since 3.2.3 + */ + public static Type parse(String type) { + return new TypeParser(type).parse(); + } + /** * Returns the name of this type (or its erasure in case of generic types) as a {@link DotName}, * using the {@link Class#getName()} format. Specifically: diff --git a/core/src/main/java/org/jboss/jandex/TypeParser.java b/core/src/main/java/org/jboss/jandex/TypeParser.java new file mode 100644 index 00000000..edbccc32 --- /dev/null +++ b/core/src/main/java/org/jboss/jandex/TypeParser.java @@ -0,0 +1,169 @@ +package org.jboss.jandex; + +import java.util.Objects; + +// see Type.parse() for the grammar +class TypeParser { + private final String str; + + private int pos = 0; + + TypeParser(String str) { + this.str = Objects.requireNonNull(str); + } + + Type parse() { + Type result; + + String token = nextToken(); + if (token.isEmpty()) { + throw unexpected(token); + } else if (token.equals("void")) { + result = VoidType.VOID; + } else if (isPrimitiveType(token) && peekToken().isEmpty()) { + result = PrimitiveType.decode(token); + } else { + result = parseReferenceType(token); + } + + expect(""); + return result; + } + + private Type parseReferenceType(String token) { + if (isPrimitiveType(token)) { + PrimitiveType primitive = PrimitiveType.decode(token); + return parseArrayType(primitive); + } else if (isClassType(token)) { + Type result = ClassType.create(token); + if (peekToken().equals("<")) { + expect("<"); + ParameterizedType.Builder builder = ParameterizedType.builder(result.name()); + builder.addArgument(parseTypeArgument()); + while (peekToken().equals(",")) { + expect(","); + builder.addArgument(parseTypeArgument()); + } + expect(">"); + result = builder.build(); + } + if (peekToken().equals("[")) { + return parseArrayType(result); + } + return result; + } else { + throw unexpected(token); + } + } + + private Type parseArrayType(Type elementType) { + expect("["); + expect("]"); + int dimensions = 1; + while (peekToken().equals("[")) { + expect("["); + expect("]"); + dimensions++; + } + return ArrayType.create(elementType, dimensions); + } + + private Type parseTypeArgument() { + String token = nextToken(); + if (token.equals("?")) { + if (peekToken().equals("extends")) { + expect("extends"); + Type bound = parseReferenceType(nextToken()); + return WildcardType.createUpperBound(bound); + } else if (peekToken().equals("super")) { + expect("super"); + Type bound = parseReferenceType(nextToken()); + return WildcardType.createLowerBound(bound); + } else { + return WildcardType.UNBOUNDED; + } + } else { + return parseReferenceType(token); + } + } + + private boolean isPrimitiveType(String token) { + return token.equals("boolean") + || token.equals("byte") + || token.equals("short") + || token.equals("int") + || token.equals("long") + || token.equals("float") + || token.equals("double") + || token.equals("char"); + } + + private boolean isClassType(String token) { + return !token.isEmpty() && Character.isJavaIdentifierStart(token.charAt(0)); + } + + // --- + + private void expect(String expected) { + String token = nextToken(); + if (!expected.equals(token)) { + throw unexpected(token); + } + } + + private IllegalArgumentException unexpected(String token) { + if (token.isEmpty()) { + throw new IllegalArgumentException("Unexpected end of input: " + str); + } + return new IllegalArgumentException("Unexpected token '" + token + "' at position " + (pos - token.length()) + + ": " + str); + } + + private String peekToken() { + // skip whitespace + while (pos < str.length() && Character.isWhitespace(str.charAt(pos))) { + pos++; + } + + // end of input + if (pos == str.length()) { + return ""; + } + + int pos = this.pos; + + // current char is a token on its own + if (isSpecial(str.charAt(pos))) { + return str.substring(pos, pos + 1); + } + + // token is a keyword or fully qualified name + int begin = pos; + while (pos < str.length() && Character.isJavaIdentifierStart(str.charAt(pos))) { + do { + pos++; + } while (pos < str.length() && Character.isJavaIdentifierPart(str.charAt(pos))); + + if (pos < str.length() && str.charAt(pos) == '.') { + pos++; + } else { + return str.substring(begin, pos); + } + } + + if (pos == str.length()) { + throw new IllegalArgumentException("Unexpected end of input: " + str); + } + throw new IllegalArgumentException("Unexpected character '" + str.charAt(pos) + "' at position " + pos + ": " + str); + } + + private String nextToken() { + String result = peekToken(); + pos += result.length(); + return result; + } + + private boolean isSpecial(char c) { + return c == ',' || c == '?' || c == '<' || c == '>' || c == '[' || c == ']'; + } +} diff --git a/core/src/test/java/org/jboss/jandex/test/TypeParserTest.java b/core/src/test/java/org/jboss/jandex/test/TypeParserTest.java new file mode 100644 index 00000000..a7c1e8ac --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/TypeParserTest.java @@ -0,0 +1,173 @@ +package org.jboss.jandex.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.ArrayType; +import org.jboss.jandex.ClassType; +import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.Type; +import org.jboss.jandex.VoidType; +import org.jboss.jandex.WildcardType; +import org.junit.jupiter.api.Test; + +public class TypeParserTest { + @Test + public void testVoid() { + assertCorrect("void", VoidType.VOID); + assertCorrect(" void", VoidType.VOID); + assertCorrect("void ", VoidType.VOID); + assertCorrect(" void ", VoidType.VOID); + } + + @Test + public void testPrimitive() { + assertCorrect("boolean", PrimitiveType.BOOLEAN); + assertCorrect(" byte", PrimitiveType.BYTE); + assertCorrect("short ", PrimitiveType.SHORT); + assertCorrect(" int ", PrimitiveType.INT); + assertCorrect("\tlong", PrimitiveType.LONG); + assertCorrect("float\t", PrimitiveType.FLOAT); + assertCorrect("\tdouble\t", PrimitiveType.DOUBLE); + assertCorrect(" \n char \n ", PrimitiveType.CHAR); + } + + @Test + public void testPrimitiveArray() { + assertCorrect("boolean[]", ArrayType.create(PrimitiveType.BOOLEAN, 1)); + assertCorrect("byte [][]", ArrayType.create(PrimitiveType.BYTE, 2)); + assertCorrect("short [] [] []", ArrayType.create(PrimitiveType.SHORT, 3)); + assertCorrect("int [ ] [ ] [ ] [ ]", ArrayType.create(PrimitiveType.INT, 4)); + assertCorrect("long [][][]", ArrayType.create(PrimitiveType.LONG, 3)); + assertCorrect(" float[][]", ArrayType.create(PrimitiveType.FLOAT, 2)); + assertCorrect(" double [] ", ArrayType.create(PrimitiveType.DOUBLE, 1)); + assertCorrect(" char [ ][ ] ", ArrayType.create(PrimitiveType.CHAR, 2)); + } + + @Test + public void testClass() { + assertCorrect("java.lang.Object", ClassType.OBJECT_TYPE); + assertCorrect("java.lang.String", ClassType.create(DotName.STRING_NAME)); + + assertCorrect(" java.lang.Boolean", ClassType.BOOLEAN_CLASS); + assertCorrect("java.lang.Byte ", ClassType.BYTE_CLASS); + assertCorrect(" java.lang.Short ", ClassType.SHORT_CLASS); + assertCorrect("\tjava.lang.Integer", ClassType.INTEGER_CLASS); + assertCorrect("java.lang.Long\t", ClassType.LONG_CLASS); + assertCorrect("\tjava.lang.Float\t", ClassType.FLOAT_CLASS); + assertCorrect(" java.lang.Double", ClassType.DOUBLE_CLASS); + assertCorrect("java.lang.Character ", ClassType.CHARACTER_CLASS); + } + + @Test + public void testClassArray() { + assertCorrect("java.lang.Object[]", ArrayType.create(ClassType.OBJECT_TYPE, 1)); + assertCorrect("java.lang.String[][]", ArrayType.create(ClassType.create(DotName.STRING_NAME), 2)); + + assertCorrect("java.lang.Boolean[][][]", ArrayType.create(ClassType.BOOLEAN_CLASS, 3)); + assertCorrect("java.lang.Byte[][][][]", ArrayType.create(ClassType.BYTE_CLASS, 4)); + assertCorrect("java.lang.Short[][][]", ArrayType.create(ClassType.SHORT_CLASS, 3)); + assertCorrect("java.lang.Integer[][]", ArrayType.create(ClassType.INTEGER_CLASS, 2)); + assertCorrect("java.lang.Long[]", ArrayType.create(ClassType.LONG_CLASS, 1)); + assertCorrect("java.lang.Float[][]", ArrayType.create(ClassType.FLOAT_CLASS, 2)); + assertCorrect("java.lang.Double[][][]", ArrayType.create(ClassType.DOUBLE_CLASS, 3)); + assertCorrect("java.lang.Character[][][][]", ArrayType.create(ClassType.CHARACTER_CLASS, 4)); + } + + @Test + public void testParameterizedType() { + assertCorrect("java.util.List", + ParameterizedType.builder(List.class).addArgument(ClassType.INTEGER_CLASS).build()); + assertCorrect("java.util.Map", + ParameterizedType.builder(Map.class) + .addArgument(ClassType.INTEGER_CLASS) + .addArgument(ArrayType.create(PrimitiveType.INT, 1)) + .build()); + + assertCorrect("java.util.List", + ParameterizedType.builder(List.class) + .addArgument(WildcardType.createUpperBound(ClassType.INTEGER_CLASS)) + .build()); + assertCorrect("java.util.Map>", + ParameterizedType.builder(Map.class) + .addArgument(WildcardType.createLowerBound(ArrayType.create(PrimitiveType.INT, 2))) + .addArgument(ParameterizedType.builder(List.class).addArgument(WildcardType.UNBOUNDED).build()) + .build()); + } + + @Test + public void testParameterizedTypeArray() { + assertCorrect("java.util.List[]", + ArrayType.create(ParameterizedType.builder(List.class).addArgument(ClassType.INTEGER_CLASS).build(), 1)); + assertCorrect("java.util.Map[][]", + ArrayType.create(ParameterizedType.builder(Map.class) + .addArgument(ClassType.INTEGER_CLASS) + .addArgument(ArrayType.create(PrimitiveType.INT, 1)) + .build(), 2)); + } + + @Test + public void testIncorrect() { + assertIncorrect(""); + assertIncorrect(" "); + assertIncorrect("\t"); + assertIncorrect(" "); + assertIncorrect(" \n "); + + assertIncorrect("."); + assertIncorrect(","); + assertIncorrect("["); + assertIncorrect("]"); + assertIncorrect("<"); + assertIncorrect(">"); + + assertIncorrect("int."); + assertIncorrect("int,"); + assertIncorrect("int["); + assertIncorrect("int]"); + assertIncorrect("int[[]"); + assertIncorrect("int[]["); + assertIncorrect("int[]]"); + assertIncorrect("int[0]"); + assertIncorrect("int<"); + assertIncorrect("int>"); + assertIncorrect("int<>"); + + assertIncorrect("java.util.List<"); + assertIncorrect("java.util.List<>"); + assertIncorrect("java.util.List>"); + assertIncorrect("java.util.List"); + assertIncorrect("java.util.List>>"); + + assertIncorrect("java.util.List"); + assertIncorrect("java.util.Map"); + + assertIncorrect("java.lang.Integer."); + assertIncorrect("java .lang.Integer"); + assertIncorrect("java. lang.Integer"); + assertIncorrect("java . lang.Integer"); + assertIncorrect(".java.lang.Integer"); + assertIncorrect(".java.lang.Integer."); + + assertIncorrect("java.lang.Integer["); + assertIncorrect("java.lang.Integer[[]"); + assertIncorrect("java.lang.Integer[]["); + assertIncorrect("java.lang.Integer[]]"); + assertIncorrect("java.lang.Integer[0]"); + } + + private void assertCorrect(String str, Type expectedType) { + assertEquals(expectedType, Type.parse(str)); + } + + private void assertIncorrect(String str) { + assertThrows(IllegalArgumentException.class, () -> Type.parse(str)); + } +} From ca7a4dc4109bc0874d51166f07cbceddf0ae8cae Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Mon, 16 Sep 2024 12:31:31 +0200 Subject: [PATCH 2/3] Add missing @since tag to ClassInfo.canonicalRecordConstructor() --- core/src/main/java/org/jboss/jandex/ClassInfo.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/org/jboss/jandex/ClassInfo.java b/core/src/main/java/org/jboss/jandex/ClassInfo.java index 3a00eadc..7b04da0d 100644 --- a/core/src/main/java/org/jboss/jandex/ClassInfo.java +++ b/core/src/main/java/org/jboss/jandex/ClassInfo.java @@ -762,6 +762,7 @@ public final List constructors() { * order of record components corresponds to the declaration order. * * @return the canonical constructor of this record, or {@code null} if this class is not a record + * @since 3.2.2 */ public MethodInfo canonicalRecordConstructor() { if (!isRecord()) { From 77f0532713e762cf6a06e042f29da1a571fd3ad5 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Mon, 16 Sep 2024 12:31:56 +0200 Subject: [PATCH 3/3] Improve WildcardType javadoc --- .../src/main/java/org/jboss/jandex/WildcardType.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/jboss/jandex/WildcardType.java b/core/src/main/java/org/jboss/jandex/WildcardType.java index 84647143..f3b562e3 100644 --- a/core/src/main/java/org/jboss/jandex/WildcardType.java +++ b/core/src/main/java/org/jboss/jandex/WildcardType.java @@ -38,7 +38,7 @@ public class WildcardType extends Type { * Creates a new wildcard type. * * @param bound the bound (lower or upper) - * @param isExtends true if the bound is an upper (extends) bound, false if lower (super) + * @param isExtends true if the bound is an upper ({@code extends}) bound, false if lower ({@code super}) * @return the new instance * * @since 2.1 @@ -50,7 +50,7 @@ public static WildcardType create(Type bound, boolean isExtends) { } /** - * Create a new wildcard type with an upper bound. + * Create a new wildcard type with an upper ({@code extends}) bound. * * @param upperBound the upper bound * @return the new instance @@ -61,7 +61,7 @@ public static WildcardType createUpperBound(Type upperBound) { } /** - * Create a new wildcard type with an upper bound. + * Create a new wildcard type with an upper ({@code extends}) bound. * * @param upperBound the upper bound * @return the new instance @@ -72,7 +72,7 @@ public static WildcardType createUpperBound(Class upperBound) { } /** - * Create a new wildcard type with a lower bound. + * Create a new wildcard type with a lower ({@code super}) bound. * * @param lowerBound the lower bound * @return the new instance @@ -83,7 +83,7 @@ public static WildcardType createLowerBound(Type lowerBound) { } /** - * Create a new wildcard type with a lower bound. + * Create a new wildcard type with a lower ({@code super}) bound. * * @param lowerBound the lower bound * @return the new instance @@ -146,7 +146,7 @@ public Type extendsBound() { * Returns {@code null} if this wildcard declares an upper bound * ({@code ? extends SomeType}). * - * @return the lower bound, or {@code null} if this wildcard has an uper bound + * @return the lower bound, or {@code null} if this wildcard has an upper bound */ public Type superBound() { return isExtends ? null : bound;