From aaf2c8eed72eee672adb55bcdbfddbae9fc02e8c Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 7 Sep 2025 10:29:14 +0100 Subject: [PATCH 1/5] Add support for annotations in `TypeVariable` handling. Enhance `Types` class with methods to create artificial type variables that preserve annotations and annotated bounds. Update `TypeVariableImpl` to implement `AnnotatedElement` interface, allowing retrieval of annotations. This change improves compatibility and functionality for type variable management. RELNOTES=n/a --- .../google/common/reflect/TypeResolver.java | 2 +- .../src/com/google/common/reflect/Types.java | 167 +++++++++++++++++- 2 files changed, 166 insertions(+), 3 deletions(-) diff --git a/guava/src/com/google/common/reflect/TypeResolver.java b/guava/src/com/google/common/reflect/TypeResolver.java index b28ffbb7228c..dddf75b3c6b0 100644 --- a/guava/src/com/google/common/reflect/TypeResolver.java +++ b/guava/src/com/google/common/reflect/TypeResolver.java @@ -378,7 +378,7 @@ Type resolveInternal(TypeVariable var, TypeTable forDependants) { return var; } return Types.newArtificialTypeVariable( - var.getGenericDeclaration(), var.getName(), resolvedBounds); + var.getGenericDeclaration(), var.getName(), resolvedBounds, var); } // in case the type is yet another type variable. return new TypeResolver(forDependants).resolveType(type); diff --git a/guava/src/com/google/common/reflect/Types.java b/guava/src/com/google/common/reflect/Types.java index 209369d017a4..615bd63feeb4 100644 --- a/guava/src/com/google/common/reflect/Types.java +++ b/guava/src/com/google/common/reflect/Types.java @@ -27,6 +27,7 @@ import com.google.common.collect.Iterables; import com.google.errorprone.annotations.Keep; import java.io.Serializable; +import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; @@ -143,6 +144,17 @@ static TypeVariable newArtificialTypeVariable( declaration, name, (bounds.length == 0) ? new Type[] {Object.class} : bounds); } + /** + * Returns a new {@link TypeVariable} that belongs to {@code declaration} with {@code name} and + * {@code bounds}, preserving annotations from the original TypeVariable. + */ + static TypeVariable newArtificialTypeVariable( + D declaration, String name, Type[] bounds, TypeVariable original) { + Annotation[] annotations = extractAnnotations(original); + Object annotatedBounds = extractAnnotatedBounds(original); + return newTypeVariableImpl(declaration, name, bounds, annotations, annotatedBounds); + } + /** Returns a new {@link WildcardType} with {@code upperBound}. */ @VisibleForTesting static WildcardType subtypeOf(Type upperBound) { @@ -315,7 +327,18 @@ public boolean equals(@Nullable Object other) { private static TypeVariable newTypeVariableImpl( D genericDeclaration, String name, Type[] bounds) { - TypeVariableImpl typeVariableImpl = new TypeVariableImpl<>(genericDeclaration, name, bounds); + // Use overloaded method with empty annotations for backward compatibility + return newTypeVariableImpl(genericDeclaration, name, bounds, new Annotation[0], null); + } + + /** + * Overloaded method that supports annotations and annotated bounds. + */ + private static TypeVariable newTypeVariableImpl( + D genericDeclaration, String name, Type[] bounds, Annotation[] annotations, + @Nullable Object annotatedBounds) { + TypeVariableImpl typeVariableImpl = + new TypeVariableImpl<>(genericDeclaration, name, bounds, annotations, annotatedBounds); @SuppressWarnings("unchecked") TypeVariable typeVariable = Reflection.newProxy( @@ -323,6 +346,58 @@ private static TypeVariable newTypeVariableImp return typeVariable; } + /** + * Extracts annotations from a TypeVariable using AnnotatedElement interface or reflection fallback. + * Works on both JDK 8+ (AnnotatedElement) and Android (reflection) platforms. + */ + private static Annotation[] extractAnnotations(TypeVariable typeVariable) { + // First try the standard AnnotatedElement interface (JDK 8+) + if (typeVariable instanceof AnnotatedElement) { + try { + return ((AnnotatedElement) typeVariable).getDeclaredAnnotations(); + } catch (Exception ignored) { + // Continue to reflection fallback + } + } + + // Fallback: Use reflection to find annotation methods (Android compatibility) + try { + Method getDeclaredAnnotations = typeVariable.getClass().getMethod("getDeclaredAnnotations"); + Object result = getDeclaredAnnotations.invoke(typeVariable); + if (result instanceof Annotation[]) { + return (Annotation[]) result; + } + } catch (Exception ignored) { + // Method doesn't exist or failed - try getAnnotations() + } + + try { + Method getAnnotations = typeVariable.getClass().getMethod("getAnnotations"); + Object result = getAnnotations.invoke(typeVariable); + if (result instanceof Annotation[]) { + return (Annotation[]) result; + } + } catch (Exception ignored) { + // No annotation methods available + } + + // Final fallback: empty annotations + return new Annotation[0]; + } + + /** + * Extracts annotated bounds from a TypeVariable if available. + */ + private static @Nullable Object extractAnnotatedBounds(TypeVariable typeVariable) { + try { + Method getAnnotatedBounds = typeVariable.getClass().getMethod("getAnnotatedBounds"); + return getAnnotatedBounds.invoke(typeVariable); + } catch (Exception ignored) { + // Platform doesn't support getAnnotatedBounds() or method failed + return null; + } + } + /** * Invocation handler to work around a compatibility problem between Android and Java. * @@ -393,17 +468,27 @@ private static final class TypeVariableInvocationHandler implements InvocationHa } } - private static final class TypeVariableImpl { + private static final class TypeVariableImpl implements AnnotatedElement { private final D genericDeclaration; private final String name; private final ImmutableList bounds; + private final ImmutableList annotations; + private final @Nullable Object annotatedBounds; + // Backward compatibility constructor TypeVariableImpl(D genericDeclaration, String name, Type[] bounds) { + this(genericDeclaration, name, bounds, new Annotation[0], null); + } + + TypeVariableImpl(D genericDeclaration, String name, Type[] bounds, + Annotation[] annotations, @Nullable Object annotatedBounds) { disallowPrimitiveType(bounds, "bound for type variable"); this.genericDeclaration = checkNotNull(genericDeclaration); this.name = checkNotNull(name); this.bounds = ImmutableList.copyOf(bounds); + this.annotations = ImmutableList.copyOf(annotations); + this.annotatedBounds = annotatedBounds; } @Keep @@ -426,6 +511,84 @@ public String getTypeName() { return name; } + @Keep + public Object getAnnotatedBounds() { + if (annotatedBounds != null) { + return annotatedBounds; + } + throw new UnsupportedOperationException( + "getAnnotatedBounds() not supported on this platform"); + } + + // AnnotatedElement implementation + @Keep + @Override + public @Nullable A getAnnotation(Class annotationType) { + checkNotNull(annotationType); + for (Annotation annotation : annotations) { + if (annotationType.isInstance(annotation)) { + return annotationType.cast(annotation); + } + } + return null; + } + + @Keep + @Override + public Annotation[] getAnnotations() { + return getDeclaredAnnotations(); + } + + @Keep + @Override + public A[] getAnnotationsByType(Class annotationType) { + return getDeclaredAnnotationsByType(annotationType); + } + + @Keep + @Override + public Annotation[] getDeclaredAnnotations() { + return annotations.toArray(new Annotation[0]); + } + + @Keep + @Override + public @Nullable A getDeclaredAnnotation(Class annotationType) { + checkNotNull(annotationType); + for (Annotation annotation : annotations) { + if (annotationType.isInstance(annotation)) { + return annotationType.cast(annotation); + } + } + return null; + } + + @Keep + @Override + public A[] getDeclaredAnnotationsByType(Class annotationType) { + int count = 0; + for (Annotation annotation : annotations) { + if (annotationType.isInstance(annotation)) { + count++; + } + } + @SuppressWarnings("unchecked") + A[] result = (A[]) Array.newInstance(annotationType, count); + int index = 0; + for (Annotation annotation : annotations) { + if (annotationType.isInstance(annotation)) { + result[index++] = annotationType.cast(annotation); + } + } + return result; + } + + @Keep + @Override + public boolean isAnnotationPresent(Class annotationType) { + return getAnnotation(annotationType) != null; + } + @Override public String toString() { return name; From 6c1fe9ba245d463ac7d30a17e1c07b9bbfb482c6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 14 Sep 2025 14:25:26 +0100 Subject: [PATCH 2/5] Clarify annotation handling in `TypeResolver` and `Types` classes. Update comments to reflect that custom TypeVariables created by Guava do not preserve annotations intentionally due to unclear semantics. Enhance exception messages for unsupported annotation-related methods to guide users towards using original TypeVariables for annotation access. See b/147144588 for more details. --- .../src/com/google/common/reflect/TypeResolver.java | 10 ++++++---- guava/src/com/google/common/reflect/Types.java | 12 +++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/guava/src/com/google/common/reflect/TypeResolver.java b/guava/src/com/google/common/reflect/TypeResolver.java index dddf75b3c6b0..c8b7068cde41 100644 --- a/guava/src/com/google/common/reflect/TypeResolver.java +++ b/guava/src/com/google/common/reflect/TypeResolver.java @@ -368,10 +368,12 @@ Type resolveInternal(TypeVariable var, TypeTable forDependants) { * by us. And that equality is guaranteed to hold because it doesn't involve the JDK * TypeVariable implementation at all. * - * TODO: b/147144588 - But what about when the TypeVariable has annotations? Our - * implementation currently doesn't support annotations _at all_. It could at least be made - * to respond to queries about annotations by returning null/empty, but are there situations - * in which it should return something else? + * NOTE: b/147144588 - Custom TypeVariables created by Guava do not preserve annotations. + * This is intentional. The semantics of annotation handling during type resolution are + * unclear and have changed across Java versions. Until there's a clear specification for + * what annotations should mean on resolved TypeVariables with modified bounds, annotation + * methods will throw UnsupportedOperationException. Frameworks requiring annotation + * preservation should use the original TypeVariable when bounds haven't changed. */ if (Types.NativeTypeVariableEquals.NATIVE_TYPE_VARIABLE_ONLY && Arrays.equals(bounds, resolvedBounds)) { diff --git a/guava/src/com/google/common/reflect/Types.java b/guava/src/com/google/common/reflect/Types.java index 615bd63feeb4..16c6d1361fb2 100644 --- a/guava/src/com/google/common/reflect/Types.java +++ b/guava/src/com/google/common/reflect/Types.java @@ -457,7 +457,17 @@ private static final class TypeVariableInvocationHandler implements InvocationHa String methodName = method.getName(); Method typeVariableMethod = typeVariableMethods.get(methodName); if (typeVariableMethod == null) { - throw new UnsupportedOperationException(methodName); + // Provide helpful error message for annotation-related methods + if (methodName.equals("getAnnotatedBounds") + || methodName.startsWith("getAnnotation") + || methodName.startsWith("getDeclaredAnnotation") + || methodName.equals("isAnnotationPresent")) { + throw new UnsupportedOperationException( + "Annotation methods are not supported on synthetic TypeVariables created during type " + + "resolution. The semantics of annotations on resolved types with modified bounds are " + + "undefined. Use the original TypeVariable for annotation access. See b/147144588."); + } + throw new UnsupportedOperationException(methodName); // Keep original behavior for other methods } else { try { return typeVariableMethod.invoke(typeVariableImpl, args); From 28e379376506302970bd29cd3ab5c87f21503ebd Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 14 Sep 2025 14:30:25 +0100 Subject: [PATCH 3/5] Refactor `TypeResolver` to remove unnecessary parameter from `newArtificialTypeVariable` method call. This change simplifies the method signature by omitting the original type variable, aligning with the intent to create a new type variable without preserving the original's context. --- guava/src/com/google/common/reflect/TypeResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guava/src/com/google/common/reflect/TypeResolver.java b/guava/src/com/google/common/reflect/TypeResolver.java index c8b7068cde41..a69ddb80c23c 100644 --- a/guava/src/com/google/common/reflect/TypeResolver.java +++ b/guava/src/com/google/common/reflect/TypeResolver.java @@ -380,7 +380,7 @@ Type resolveInternal(TypeVariable var, TypeTable forDependants) { return var; } return Types.newArtificialTypeVariable( - var.getGenericDeclaration(), var.getName(), resolvedBounds, var); + var.getGenericDeclaration(), var.getName(), resolvedBounds); } // in case the type is yet another type variable. return new TypeResolver(forDependants).resolveType(type); From 0c0d483eeb2e2edff3e2e61bb529a6e8f0798fe0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 14 Sep 2025 14:32:14 +0100 Subject: [PATCH 4/5] Remove unused `newArtificialTypeVariable` method in `Types` class to streamline type variable creation. This change aligns with recent refactoring efforts to simplify method signatures and clarify the handling of annotations in custom TypeVariables. --- .../src/com/google/common/reflect/Types.java | 167 +----------------- 1 file changed, 2 insertions(+), 165 deletions(-) diff --git a/guava/src/com/google/common/reflect/Types.java b/guava/src/com/google/common/reflect/Types.java index 16c6d1361fb2..51bba3f7d1d0 100644 --- a/guava/src/com/google/common/reflect/Types.java +++ b/guava/src/com/google/common/reflect/Types.java @@ -27,7 +27,6 @@ import com.google.common.collect.Iterables; import com.google.errorprone.annotations.Keep; import java.io.Serializable; -import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; @@ -144,17 +143,6 @@ static TypeVariable newArtificialTypeVariable( declaration, name, (bounds.length == 0) ? new Type[] {Object.class} : bounds); } - /** - * Returns a new {@link TypeVariable} that belongs to {@code declaration} with {@code name} and - * {@code bounds}, preserving annotations from the original TypeVariable. - */ - static TypeVariable newArtificialTypeVariable( - D declaration, String name, Type[] bounds, TypeVariable original) { - Annotation[] annotations = extractAnnotations(original); - Object annotatedBounds = extractAnnotatedBounds(original); - return newTypeVariableImpl(declaration, name, bounds, annotations, annotatedBounds); - } - /** Returns a new {@link WildcardType} with {@code upperBound}. */ @VisibleForTesting static WildcardType subtypeOf(Type upperBound) { @@ -327,18 +315,7 @@ public boolean equals(@Nullable Object other) { private static TypeVariable newTypeVariableImpl( D genericDeclaration, String name, Type[] bounds) { - // Use overloaded method with empty annotations for backward compatibility - return newTypeVariableImpl(genericDeclaration, name, bounds, new Annotation[0], null); - } - - /** - * Overloaded method that supports annotations and annotated bounds. - */ - private static TypeVariable newTypeVariableImpl( - D genericDeclaration, String name, Type[] bounds, Annotation[] annotations, - @Nullable Object annotatedBounds) { - TypeVariableImpl typeVariableImpl = - new TypeVariableImpl<>(genericDeclaration, name, bounds, annotations, annotatedBounds); + TypeVariableImpl typeVariableImpl = new TypeVariableImpl<>(genericDeclaration, name, bounds); @SuppressWarnings("unchecked") TypeVariable typeVariable = Reflection.newProxy( @@ -346,58 +323,6 @@ private static TypeVariable newTypeVariableImp return typeVariable; } - /** - * Extracts annotations from a TypeVariable using AnnotatedElement interface or reflection fallback. - * Works on both JDK 8+ (AnnotatedElement) and Android (reflection) platforms. - */ - private static Annotation[] extractAnnotations(TypeVariable typeVariable) { - // First try the standard AnnotatedElement interface (JDK 8+) - if (typeVariable instanceof AnnotatedElement) { - try { - return ((AnnotatedElement) typeVariable).getDeclaredAnnotations(); - } catch (Exception ignored) { - // Continue to reflection fallback - } - } - - // Fallback: Use reflection to find annotation methods (Android compatibility) - try { - Method getDeclaredAnnotations = typeVariable.getClass().getMethod("getDeclaredAnnotations"); - Object result = getDeclaredAnnotations.invoke(typeVariable); - if (result instanceof Annotation[]) { - return (Annotation[]) result; - } - } catch (Exception ignored) { - // Method doesn't exist or failed - try getAnnotations() - } - - try { - Method getAnnotations = typeVariable.getClass().getMethod("getAnnotations"); - Object result = getAnnotations.invoke(typeVariable); - if (result instanceof Annotation[]) { - return (Annotation[]) result; - } - } catch (Exception ignored) { - // No annotation methods available - } - - // Final fallback: empty annotations - return new Annotation[0]; - } - - /** - * Extracts annotated bounds from a TypeVariable if available. - */ - private static @Nullable Object extractAnnotatedBounds(TypeVariable typeVariable) { - try { - Method getAnnotatedBounds = typeVariable.getClass().getMethod("getAnnotatedBounds"); - return getAnnotatedBounds.invoke(typeVariable); - } catch (Exception ignored) { - // Platform doesn't support getAnnotatedBounds() or method failed - return null; - } - } - /** * Invocation handler to work around a compatibility problem between Android and Java. * @@ -478,27 +403,17 @@ private static final class TypeVariableInvocationHandler implements InvocationHa } } - private static final class TypeVariableImpl implements AnnotatedElement { + private static final class TypeVariableImpl { private final D genericDeclaration; private final String name; private final ImmutableList bounds; - private final ImmutableList annotations; - private final @Nullable Object annotatedBounds; - // Backward compatibility constructor TypeVariableImpl(D genericDeclaration, String name, Type[] bounds) { - this(genericDeclaration, name, bounds, new Annotation[0], null); - } - - TypeVariableImpl(D genericDeclaration, String name, Type[] bounds, - Annotation[] annotations, @Nullable Object annotatedBounds) { disallowPrimitiveType(bounds, "bound for type variable"); this.genericDeclaration = checkNotNull(genericDeclaration); this.name = checkNotNull(name); this.bounds = ImmutableList.copyOf(bounds); - this.annotations = ImmutableList.copyOf(annotations); - this.annotatedBounds = annotatedBounds; } @Keep @@ -521,84 +436,6 @@ public String getTypeName() { return name; } - @Keep - public Object getAnnotatedBounds() { - if (annotatedBounds != null) { - return annotatedBounds; - } - throw new UnsupportedOperationException( - "getAnnotatedBounds() not supported on this platform"); - } - - // AnnotatedElement implementation - @Keep - @Override - public @Nullable A getAnnotation(Class annotationType) { - checkNotNull(annotationType); - for (Annotation annotation : annotations) { - if (annotationType.isInstance(annotation)) { - return annotationType.cast(annotation); - } - } - return null; - } - - @Keep - @Override - public Annotation[] getAnnotations() { - return getDeclaredAnnotations(); - } - - @Keep - @Override - public A[] getAnnotationsByType(Class annotationType) { - return getDeclaredAnnotationsByType(annotationType); - } - - @Keep - @Override - public Annotation[] getDeclaredAnnotations() { - return annotations.toArray(new Annotation[0]); - } - - @Keep - @Override - public @Nullable A getDeclaredAnnotation(Class annotationType) { - checkNotNull(annotationType); - for (Annotation annotation : annotations) { - if (annotationType.isInstance(annotation)) { - return annotationType.cast(annotation); - } - } - return null; - } - - @Keep - @Override - public A[] getDeclaredAnnotationsByType(Class annotationType) { - int count = 0; - for (Annotation annotation : annotations) { - if (annotationType.isInstance(annotation)) { - count++; - } - } - @SuppressWarnings("unchecked") - A[] result = (A[]) Array.newInstance(annotationType, count); - int index = 0; - for (Annotation annotation : annotations) { - if (annotationType.isInstance(annotation)) { - result[index++] = annotationType.cast(annotation); - } - } - return result; - } - - @Keep - @Override - public boolean isAnnotationPresent(Class annotationType) { - return getAnnotation(annotationType) != null; - } - @Override public String toString() { return name; From 3c4ea15e4b1d28a9329865cbf7dd84e784a51b35 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Sep 2025 17:27:47 +0100 Subject: [PATCH 5/5] Enhance exception handling for annotation methods in `Types` class. Added support for `getAnnotations` and `getDeclaredAnnotations` to throw `UnsupportedOperationException`, clarifying that these methods are not applicable to synthetic TypeVariables. This change improves user guidance regarding annotation access on resolved types. --- guava/src/com/google/common/reflect/Types.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/guava/src/com/google/common/reflect/Types.java b/guava/src/com/google/common/reflect/Types.java index 51bba3f7d1d0..57c3a3d51862 100644 --- a/guava/src/com/google/common/reflect/Types.java +++ b/guava/src/com/google/common/reflect/Types.java @@ -386,7 +386,9 @@ private static final class TypeVariableInvocationHandler implements InvocationHa if (methodName.equals("getAnnotatedBounds") || methodName.startsWith("getAnnotation") || methodName.startsWith("getDeclaredAnnotation") - || methodName.equals("isAnnotationPresent")) { + || methodName.equals("isAnnotationPresent") + || methodName.equals("getAnnotations") + || methodName.equals("getDeclaredAnnotations")) { throw new UnsupportedOperationException( "Annotation methods are not supported on synthetic TypeVariables created during type " + "resolution. The semantics of annotations on resolved types with modified bounds are "