From fe39f043e1725c3ed885386037491abf19613228 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:07:13 +0700 Subject: [PATCH 1/4] Fix JSpecify false negative when override narrows method type variable bound In JSpecify mode, compare upper-bound nullability of corresponding method type variables between an overriding method and the method it overrides. Narrowing `` to `` (or the reverse) is unsound because callers can still instantiate the type variable via the overridden signature. Fixes #1512 --- .../nullaway/generics/GenericsChecks.java | 74 ++++++++++ .../nullaway/jspecify/GenericMethodTests.java | 139 ++++++++++++++++++ 2 files changed, 213 insertions(+) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index 49c5c1293d..8bc7ed700c 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -2341,6 +2341,80 @@ public void checkTypeParameterNullnessForMethodOverriding( checkTypeParameterNullnessForOverridingMethodReturnType(tree, methodWithTypeParams, state); checkTypeParameterNullnessForOverridingMethodParameterType(tree, methodWithTypeParams, state); + checkMethodTypeVariableUpperBoundNullnessForOverriding( + tree, overridingMethod, overriddenMethod, state); + } + + /** + * Checks that corresponding method type variables have the same upper-bound nullability on an + * overriding method and the method it overrides. + * + *

Narrowing a {@code @Nullable} upper bound to a non-null upper bound (or the reverse) is + * unsound: callers can still instantiate the type variable via the overridden signature. See issue 1512. + * + * @param tree tree for the overriding method + * @param overridingMethod symbol of the overriding method + * @param overriddenMethod symbol of the overridden method + * @param state the visitor state + */ + private void checkMethodTypeVariableUpperBoundNullnessForOverriding( + MethodTree tree, + Symbol.MethodSymbol overridingMethod, + Symbol.MethodSymbol overriddenMethod, + VisitorState state) { + List overridingTypeParams = overridingMethod.getTypeParameters(); + List overriddenTypeParams = overriddenMethod.getTypeParameters(); + // If counts differ, javac would not treat this as a valid override; leave that to the + // compiler. + if (overridingTypeParams.size() != overriddenTypeParams.size()) { + return; + } + List typeParameterTrees = tree.getTypeParameters(); + for (int i = 0; i < overridingTypeParams.size(); i++) { + Symbol.TypeVariableSymbol overridingTv = overridingTypeParams.get(i); + Symbol.TypeVariableSymbol overriddenTv = overriddenTypeParams.get(i); + boolean overridingNullable = + GenericsUtils.upperBoundIsNullable(overridingTv, config, handler, state); + boolean overriddenNullable = + GenericsUtils.upperBoundIsNullable(overriddenTv, config, handler, state); + if (overridingNullable != overriddenNullable) { + Tree errorTree = i < typeParameterTrees.size() ? typeParameterTrees.get(i) : tree; + reportMismatchedMethodTypeVariableBoundError( + errorTree, + overridingTv, + overridingNullable, + overriddenMethod, + overriddenNullable, + state); + } + } + } + + private void reportMismatchedMethodTypeVariableBoundError( + Tree errorTree, + Symbol.TypeVariableSymbol overridingTv, + boolean overridingNullable, + Symbol.MethodSymbol overriddenMethod, + boolean overriddenNullable, + VisitorState state) { + ErrorBuilder errorBuilder = analysis.getErrorBuilder(); + String overridingBound = overridingNullable ? "@Nullable" : "non-null"; + String overriddenBound = overriddenNullable ? "@Nullable" : "non-null"; + ErrorMessage errorMessage = + new ErrorMessage( + ErrorMessage.MessageTypes.WRONG_OVERRIDE_PARAM_GENERIC, + String.format( + "Method type variable %s has a %s upper bound, but corresponding type variable of" + + " overridden method %s.%s has a %s upper bound", + overridingTv.name, + overridingBound, + ASTHelpers.enclosingClass(overriddenMethod), + overriddenMethod.name, + overriddenBound)); + state.reportMatch( + errorBuilder.createErrorDescription( + errorMessage, analysis.buildDescription(errorTree), state, null)); } /** diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java index d8e838d705..b707aa3a31 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java @@ -1773,6 +1773,145 @@ void test(Box> box) { .doTest(); } + /** + * Regression test for issue 1512: + * overriding a method must not narrow a {@code @Nullable} method type-variable upper bound to a + * non-null bound (and must not widen a non-null bound to {@code @Nullable}). + */ + @Test + public void overrideNarrowsNullableMethodTypeVariableBound() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + interface Foo { + void bar(T arg); + } + static class Baz implements Foo { + @Override + // BUG: Diagnostic contains: Method type variable T has a non-null upper bound + public void bar(T arg) { + arg.hashCode(); + } + } + static void use(Foo f) { + f.<@Nullable String>bar(null); + } + } + """) + .doTest(); + } + + @Test + public void overrideWidensNonNullMethodTypeVariableBound() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + interface Foo { + void bar(T arg); + } + static class Baz implements Foo { + @Override + // BUG: Diagnostic contains: Method type variable T has a @Nullable upper bound + public void bar(T arg) {} + } + } + """) + .doTest(); + } + + @Test + public void overridePreservesNullableMethodTypeVariableBound() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + interface Foo { + void bar(T arg); + } + static class Baz implements Foo { + @Override + public void bar(T arg) { + // legal override; arg may be null when T is instantiated as @Nullable + if (arg != null) { + arg.hashCode(); + } + } + } + static void use(Foo f) { + f.<@Nullable String>bar(null); + } + } + """) + .doTest(); + } + + @Test + public void overridePreservesNonNullMethodTypeVariableBound() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + @NullMarked + class Test { + interface Foo { + void bar(T arg); + } + static class Baz implements Foo { + @Override + public void bar(T arg) { + arg.hashCode(); + } + } + } + """) + .doTest(); + } + + @Test + public void overrideNarrowsNullableMethodTypeVariableBoundOnReturn() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + interface Foo { + T bar(); + } + static class Baz implements Foo { + @Override + // BUG: Diagnostic contains: Method type variable T has a non-null upper bound + public T bar() { + throw new RuntimeException(); + } + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From d4861b382aeaeee1161ca98643c41c4285389b18 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 7 Aug 2026 16:43:46 -0700 Subject: [PATCH 2/4] add failing test --- .../nullaway/jspecify/GenericMethodTests.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java index b707aa3a31..6c388e31b0 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java @@ -1912,6 +1912,29 @@ public T bar() { .doTest(); } + @Test + public void overridePreservesSubstitutedNullableMethodTypeVariableBound() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + interface Foo { + void bar(T arg); + } + static class Baz implements Foo<@Nullable Object> { + @Override + public void bar(T arg) {} + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From baebf51d5cc442ec3d8257c7a18a0fc7a848a495 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:18:56 +0700 Subject: [PATCH 3/4] Fix override type-var bound check for substituted and NullUnmarked methods Read overridden method type-variable upper bounds from the method type after member-type substitution in the overriding class, so bounds that reference enclosing-class type variables compare correctly after instantiation (e.g. on Foo<@Nullable Object>). Skip the check when the overridden method is from @NullUnmarked / unannotated code to avoid false positives from unmarked bounds. Add regressions for nullable and non-null enclosing-class instantiations, narrowing after substitution, and NullUnmarked overrides. --- .../nullaway/generics/GenericsChecks.java | 107 +++++++++++++++++- .../nullaway/jspecify/GenericMethodTests.java | 84 ++++++++++++++ 2 files changed, 186 insertions(+), 5 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index 8bc7ed700c..ce97c3420a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -2342,7 +2342,7 @@ public void checkTypeParameterNullnessForMethodOverriding( checkTypeParameterNullnessForOverridingMethodReturnType(tree, methodWithTypeParams, state); checkTypeParameterNullnessForOverridingMethodParameterType(tree, methodWithTypeParams, state); checkMethodTypeVariableUpperBoundNullnessForOverriding( - tree, overridingMethod, overriddenMethod, state); + tree, overridingMethod, overriddenMethod, methodWithTypeParams, state); } /** @@ -2353,31 +2353,50 @@ public void checkTypeParameterNullnessForMethodOverriding( * unsound: callers can still instantiate the type variable via the overridden signature. See issue 1512. * + *

Overridden method type-variable bounds are read from {@code overriddenMethodType}, the + * overridden method type after member-type substitution in the overriding class context. That + * ensures bounds that reference enclosing-class type variables are compared after those variables + * have been instantiated (e.g. {@code } on {@code Foo} becomes {@code } when overriding in a {@code Foo<@Nullable Object>} subtype). + * + *

Overrides of methods from {@code @NullUnmarked} / unannotated code are skipped: method + * type-variable bound nullness is not specified there, and treating unmarked bounds as nullable + * would false-positive against typical {@code } overrides in marked code. + * * @param tree tree for the overriding method * @param overridingMethod symbol of the overriding method * @param overriddenMethod symbol of the overridden method + * @param overriddenMethodType type of the overridden method after member-type substitution in the + * overriding class context * @param state the visitor state */ private void checkMethodTypeVariableUpperBoundNullnessForOverriding( MethodTree tree, Symbol.MethodSymbol overridingMethod, Symbol.MethodSymbol overriddenMethod, + Type overriddenMethodType, VisitorState state) { + if (CodeAnnotationInfo.instance(state.context) + .isSymbolUnannotated(overriddenMethod, config, handler)) { + return; + } List overridingTypeParams = overridingMethod.getTypeParameters(); - List overriddenTypeParams = overriddenMethod.getTypeParameters(); + com.sun.tools.javac.util.List overriddenTypeVars = + getMethodTypeVariables(overriddenMethodType); // If counts differ, javac would not treat this as a valid override; leave that to the // compiler. - if (overridingTypeParams.size() != overriddenTypeParams.size()) { + if (overridingTypeParams.size() != overriddenTypeVars.size()) { return; } List typeParameterTrees = tree.getTypeParameters(); for (int i = 0; i < overridingTypeParams.size(); i++) { Symbol.TypeVariableSymbol overridingTv = overridingTypeParams.get(i); - Symbol.TypeVariableSymbol overriddenTv = overriddenTypeParams.get(i); + Type overriddenTypeVar = overriddenTypeVars.get(i); boolean overridingNullable = GenericsUtils.upperBoundIsNullable(overridingTv, config, handler, state); boolean overriddenNullable = - GenericsUtils.upperBoundIsNullable(overriddenTv, config, handler, state); + substitutedMethodTypeVarUpperBoundIsNullable( + overriddenTypeVar, overriddenMethod, i, state); if (overridingNullable != overriddenNullable) { Tree errorTree = i < typeParameterTrees.size() ? typeParameterTrees.get(i) : tree; reportMismatchedMethodTypeVariableBoundError( @@ -2391,6 +2410,84 @@ private void checkMethodTypeVariableUpperBoundNullnessForOverriding( } } + /** + * Returns the method type variables of {@code methodType}, or an empty list if the method has + * none. + * + *

Generic methods are represented as {@link Type.ForAll}; non-generic methods have no type + * variables to compare for this check. + */ + private static com.sun.tools.javac.util.List getMethodTypeVariables(Type methodType) { + if (methodType instanceof Type.ForAll forAll) { + return forAll.tvars; + } + return com.sun.tools.javac.util.List.nil(); + } + + /** + * Returns whether the upper bound of a method type variable, viewed after member-type + * substitution in the overriding class, should be treated as nullable. + * + *

Prefers annotations / nullability of the substituted bound so enclosing-class type variables + * are accounted for. Falls back to library models for the original type variable index when + * present. + * + * @param substitutedTypeVar type variable from the overridden method type after substitution + * @param overriddenMethod symbol of the overridden method (for library models) + * @param typeVarIndex index of the type variable on the overridden method + * @param state the visitor state + */ + private boolean substitutedMethodTypeVarUpperBoundIsNullable( + Type substitutedTypeVar, + Symbol.MethodSymbol overriddenMethod, + int typeVarIndex, + VisitorState state) { + if (handler.onOverrideMethodTypeVariableUpperBound(overriddenMethod, typeVarIndex, state)) { + return true; + } + if (!(substitutedTypeVar instanceof Type.TypeVar typeVar)) { + // Unexpected representation; treat conservatively as non-null so we do not emit a + // mismatched-bound warning based on incomplete information. + return false; + } + Type upperBound = typeVar.getUpperBound(); + if (Nullness.hasNullableAnnotation(upperBound.getAnnotationMirrors().stream(), config)) { + return true; + } + // Bound may still be a free type variable (e.g. subclass keeps the enclosing type parameter). + // In that case, use the declaration-site nullability of that type variable's upper bound. + if (upperBound.getKind() == TypeKind.TYPEVAR) { + return GenericsUtils.upperBoundIsNullable(upperBound.asElement(), config, handler, state); + } + // javac member-type substitution can drop type-use annotations on method type-variable + // bounds. If the original bound was a concrete type with an explicit @Nullable, honor that + // declaration. Do not consult original bounds that are still type variables — those must be + // resolved via substitution (or the free type-var path above). + List originalTypeParams = overriddenMethod.getTypeParameters(); + if (typeVarIndex >= 0 && typeVarIndex < originalTypeParams.size()) { + Type originalBound = + (Type) ((TypeVariable) originalTypeParams.get(typeVarIndex).asType()).getUpperBound(); + if (originalBound.getKind() != TypeKind.TYPEVAR + && Nullness.hasNullableAnnotation( + originalBound.getAnnotationMirrors().stream(), config)) { + return true; + } + } + return false; + } + + /** + * Reports an error when an overriding method's type variable has a different upper-bound + * nullability than the corresponding type variable of the overridden method. + * + * @param errorTree tree to attach the diagnostic to (usually the overriding type parameter) + * @param overridingTv type variable of the overriding method + * @param overridingNullable whether the overriding type variable's upper bound is nullable + * @param overriddenMethod symbol of the overridden method + * @param overriddenNullable whether the overridden type variable's upper bound is nullable (in + * the overriding class context) + * @param state the visitor state + */ private void reportMismatchedMethodTypeVariableBoundError( Tree errorTree, Symbol.TypeVariableSymbol overridingTv, diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java index 6c388e31b0..a1aa806f72 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java @@ -1935,6 +1935,90 @@ static class Baz implements Foo<@Nullable Object> { .doTest(); } + @Test + public void overridePreservesSubstitutedNonNullMethodTypeVariableBound() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + interface Foo { + void bar(T arg); + } + // Instantiating X as non-null String means the method type-var bound is non-null; + // overriding with must not be treated as a mismatch. + static class Baz implements Foo { + @Override + public void bar(T arg) { + arg.hashCode(); + } + } + } + """) + .doTest(); + } + + @Test + public void overrideNarrowsSubstitutedNullableMethodTypeVariableBound() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + interface Foo { + void bar(T arg); + } + static class Baz implements Foo<@Nullable Object> { + @Override + // BUG: Diagnostic contains: Method type variable T has a non-null upper bound + public void bar(T arg) { + arg.hashCode(); + } + } + } + """) + .doTest(); + } + + @Test + public void overrideOfNullUnmarkedMethodTypeVariableBoundSkipped() { + makeHelper() + .addSourceLines( + "Test.java", + """ + package com.uber; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.NullUnmarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + @NullUnmarked + interface Foo { + void bar(T arg); + } + // Overriding an unmarked generic method must not warn: unmarked type-variable + // bounds are not part of the nullness contract. + static class Baz implements Foo { + @Override + public void bar(T arg) { + if (arg != null) { + arg.hashCode(); + } + } + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From 4d6bc7e4885ed9a951e17c6a198aa672ea8ab5c6 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:05:09 +0700 Subject: [PATCH 4/4] Address review: simplify method type-var override bound check - Bail out in the caller when overridden type is not ForAll - Drop defensive typeParameterTrees size guard - Take Type.TypeVar in substituted bound helper - Document when original-declaration @Nullable fallback applies --- .../nullaway/generics/GenericsChecks.java | 49 +++++++------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index 56f4d7c769..712d8539dd 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -2450,9 +2450,12 @@ private void checkMethodTypeVariableUpperBoundNullnessForOverriding( .isSymbolUnannotated(overriddenMethod, config, handler)) { return; } + // Generic methods are Type.ForAll; non-generic overridden methods have no method type vars. + if (!(overriddenMethodType instanceof Type.ForAll forAll)) { + return; + } + com.sun.tools.javac.util.List overriddenTypeVars = forAll.tvars; List overridingTypeParams = overridingMethod.getTypeParameters(); - com.sun.tools.javac.util.List overriddenTypeVars = - getMethodTypeVariables(overriddenMethodType); // If counts differ, javac would not treat this as a valid override; leave that to the // compiler. if (overridingTypeParams.size() != overriddenTypeVars.size()) { @@ -2461,16 +2464,16 @@ private void checkMethodTypeVariableUpperBoundNullnessForOverriding( List typeParameterTrees = tree.getTypeParameters(); for (int i = 0; i < overridingTypeParams.size(); i++) { Symbol.TypeVariableSymbol overridingTv = overridingTypeParams.get(i); - Type overriddenTypeVar = overriddenTypeVars.get(i); + // ForAll.tvars are method type variables after member-type substitution. + Type.TypeVar overriddenTypeVar = (Type.TypeVar) overriddenTypeVars.get(i); boolean overridingNullable = GenericsUtils.upperBoundIsNullable(overridingTv, config, handler, state); boolean overriddenNullable = substitutedMethodTypeVarUpperBoundIsNullable( overriddenTypeVar, overriddenMethod, i, state); if (overridingNullable != overriddenNullable) { - Tree errorTree = i < typeParameterTrees.size() ? typeParameterTrees.get(i) : tree; reportMismatchedMethodTypeVariableBoundError( - errorTree, + typeParameterTrees.get(i), overridingTv, overridingNullable, overriddenMethod, @@ -2480,20 +2483,6 @@ private void checkMethodTypeVariableUpperBoundNullnessForOverriding( } } - /** - * Returns the method type variables of {@code methodType}, or an empty list if the method has - * none. - * - *

Generic methods are represented as {@link Type.ForAll}; non-generic methods have no type - * variables to compare for this check. - */ - private static com.sun.tools.javac.util.List getMethodTypeVariables(Type methodType) { - if (methodType instanceof Type.ForAll forAll) { - return forAll.tvars; - } - return com.sun.tools.javac.util.List.nil(); - } - /** * Returns whether the upper bound of a method type variable, viewed after member-type * substitution in the overriding class, should be treated as nullable. @@ -2508,19 +2497,14 @@ private static com.sun.tools.javac.util.List getMethodTypeVariables(Type m * @param state the visitor state */ private boolean substitutedMethodTypeVarUpperBoundIsNullable( - Type substitutedTypeVar, + Type.TypeVar substitutedTypeVar, Symbol.MethodSymbol overriddenMethod, int typeVarIndex, VisitorState state) { if (handler.onOverrideMethodTypeVariableUpperBound(overriddenMethod, typeVarIndex, state)) { return true; } - if (!(substitutedTypeVar instanceof Type.TypeVar typeVar)) { - // Unexpected representation; treat conservatively as non-null so we do not emit a - // mismatched-bound warning based on incomplete information. - return false; - } - Type upperBound = typeVar.getUpperBound(); + Type upperBound = substitutedTypeVar.getUpperBound(); if (Nullness.hasNullableAnnotation(upperBound.getAnnotationMirrors().stream(), config)) { return true; } @@ -2529,10 +2513,15 @@ private boolean substitutedMethodTypeVarUpperBoundIsNullable( if (upperBound.getKind() == TypeKind.TYPEVAR) { return GenericsUtils.upperBoundIsNullable(upperBound.asElement(), config, handler, state); } - // javac member-type substitution can drop type-use annotations on method type-variable - // bounds. If the original bound was a concrete type with an explicit @Nullable, honor that - // declaration. Do not consult original bounds that are still type variables — those must be - // resolved via substitution (or the free type-var path above). + // Member-type substitution (asMemberOf) can strip type-use @Nullable from a concrete method + // type-variable bound while leaving the bound type itself (e.g. Object). Example that needs + // this fallback: + // interface Foo { void bar(T arg); } + // class Baz implements Foo { public void bar(T arg) {} } + // After substitution the bound may look like plain Object with no annotation mirrors; without + // consulting the original declaration we would treat the overridden bound as non-null and + // false-positive on a matching @Nullable override. Skip original bounds that are still type + // variables — those must be resolved via substitution (or the free type-var path above). List originalTypeParams = overriddenMethod.getTypeParameters(); if (typeVarIndex >= 0 && typeVarIndex < originalTypeParams.size()) { Type originalBound =