Skip to content

Re-introduce annotations elided by javac for certain cases#1473

Open
msridhar wants to merge 4 commits intomasterfrom
issue-1455
Open

Re-introduce annotations elided by javac for certain cases#1473
msridhar wants to merge 4 commits intomasterfrom
issue-1455

Conversation

@msridhar
Copy link
Collaborator

@msridhar msridhar commented Feb 15, 2026

Fixes #1455

For reference here is the test case:

@NullMarked
class Foo<OuterT> {

  interface Supplier<T extends @Nullable Object> {
    T get();
  }

  Supplier<@Nullable OuterT> sup;

  <T extends Supplier<?>> T acceptSup(T supplier) {
    return supplier;
  }

  void test() {
    acceptSup(sup);
  }
}

What goes wrong here is at the call acceptSup(sup), javac infers the type argument to be Supplier<OuterT>, dropping the @Nullable annotation on OuterT seen in the declaration of sup. Our inference correctly infers that T itself should be @NonNull, but then we still report an error since Supplier<@Nullable OuterT> (the type of sup) is incompatible with Supplier<OuterT> (what we think is the formal parameter type).

This started happening in 0.13.0 due to #1348, because we started computing the proper method types at call sites to report better error messages. Unfortunately, this is leading to unforeseen interactions with javac's type inference like the issue above; but I'd like to see if we can work around the corner cases since it's important to have good error messages.

For this case, we add a method restoreMissingNullabilityFromSingleTopLevelTypeVarArguments that repairs the method type by re-introducing explicit nullability annotations from actual parameters onto the formal parameter types.

We also do a drive-by fix in GenericTypePrettyPrintingVisitor to improve how we print type variables (not printing fully-qualified names of annotations).

Summary by CodeRabbit

Release Notes

  • New Features

    • Improved handling of nullability annotations in generic method type arguments.
  • Bug Fixes

    • Enhanced type annotation rendering in generic type signatures.
    • Updated diagnostic messages to provide more precise type incompatibility reporting.
  • Tests

    • Added test coverage for generic method inference scenarios.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 2026

Walkthrough

This pull request adds support for restoring missing type-use nullability annotations on type variables during generic method type inference. The changes include introducing a new visitTypeVar method in GenericTypePrettyPrintingVisitor to print annotated type variables, adding a helper method restoreMissingNullabilityFromSingleTopLevelTypeVarArguments in GenericsChecks to recover missing nested nullability for type-variable arguments, integrating this helper into the type substitution pipeline, and adding corresponding tests with updated diagnostic messages.

Possibly related PRs

Suggested labels

jspecify

Suggested reviewers

  • yuxincs
  • lazaroclapp
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR directly addresses issue #1455 by implementing restoreMissingNullabilityFromSingleTopLevelTypeVarArguments to recover nullability annotations dropped by javac during type inference, which fixes the false-positive warning for the Supplier<@nullable OuterT> case.
Out of Scope Changes check ✅ Passed All changes are within scope: the core fix addresses the nullability restoration logic, test additions verify the fix works for issue #1455, and the diagnostic message updates reflect corrected error reporting.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master
Title check ✅ Passed The pull request title accurately captures the main objective—re-introducing annotations that javac elides in certain type inference scenarios, which is the core fix for issue #1455.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-1455

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 1615-1697: The method
restoreMissingNullabilityFromSingleTopLevelTypeVarArguments currently only
updates parameter types but can leave return and thrown types inconsistent when
a method type variable also appears there; after computing
repairedTopLevelSubstitutions, apply those substitutions to
methodTypeAtCallSite.getReturnType() and each type in
methodTypeAtCallSite.getThrownTypes() using
TypeSubstitutionUtils.subst(state.getTypes(), type, fromMap, toMap, config) (or
equivalent API) to produce repairedReturnType and repairedThrownTypes, set
changed if any differ, and use those repaired types when constructing the new
Type.MethodType so return/thrown nullability stays consistent with the repaired
parameters.

Comment on lines +1615 to +1697
/**
* For some calls, javac drops nested type-use nullability annotations in inferred substitutions
* for method type variables. Recover these annotations from the corresponding actual argument
* types, while preserving one consistent top-level substitution per method type variable.
*/
@SuppressWarnings("ReferenceEquality")
private Type.MethodType restoreMissingNullabilityFromSingleTopLevelTypeVarArguments(
MethodInvocationTree invocationTree,
Type.MethodType origMethodType,
Type.MethodType methodTypeAtCallSite,
VisitorState state) {
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invocationTree);
if (methodSymbol == null || methodSymbol.isVarArgs()) {
// TODO handle varargs methods
return methodTypeAtCallSite;
}
com.sun.tools.javac.util.List<Type> origArgTypes = origMethodType.getParameterTypes();
com.sun.tools.javac.util.List<Type> callSiteArgTypes = methodTypeAtCallSite.getParameterTypes();
List<? extends ExpressionTree> callArgs = invocationTree.getArguments();
if (origArgTypes.size() != callSiteArgTypes.size() || callArgs.size() != origArgTypes.size()) {
return methodTypeAtCallSite;
}

// use this map to store repaired substitutions for method type variables, to ensure we use the
// same repaired
// substitution for all occurrences of the same method type variable
Map<Symbol.TypeVariableSymbol, Type> repairedTopLevelSubstitutions = new HashMap<>();
ListBuffer<Type> updatedArgTypes = new ListBuffer<>();
boolean changed = false;
for (int i = 0; i < origArgTypes.size(); i++) {
Type updatedType = callSiteArgTypes.get(i);
Type origArgType = origArgTypes.get(i);
if (origArgType instanceof Type.TypeVar typeVar
&& typeVar.tsym.owner == methodSymbol
&& !(updatedType instanceof Type.TypeVar)) {
Symbol.TypeVariableSymbol typeVarSymbol = (Symbol.TypeVariableSymbol) typeVar.tsym;
Type repairedSubstitution = repairedTopLevelSubstitutions.get(typeVarSymbol);
if (repairedSubstitution != null) {
if (!state
.getTypes()
.isSameType(
state.getTypes().erasure(repairedSubstitution),
state.getTypes().erasure(updatedType))) {
// Inconsistent substitution for the same top-level type variable; bail out.
return methodTypeAtCallSite;
}
if (repairedSubstitution != updatedType) {
changed = true;
updatedType = repairedSubstitution;
}
} else { // need to compute the substitution
Type actualArgType = getTreeType(callArgs.get(i), state);
if (actualArgType != null
&& !actualArgType.isRaw()
&& state
.getTypes()
.isSameType(
state.getTypes().erasure(actualArgType),
state.getTypes().erasure(updatedType))) {
Type restoredType =
TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations(
actualArgType, updatedType, config, Collections.emptyMap());
repairedTopLevelSubstitutions.put(typeVarSymbol, restoredType);
if (restoredType != updatedType) {
changed = true;
updatedType = restoredType;
}
} else {
repairedTopLevelSubstitutions.put(typeVarSymbol, updatedType);
}
}
}
updatedArgTypes.append(updatedType);
}
if (!changed) {
return methodTypeAtCallSite;
}
return new Type.MethodType(
updatedArgTypes.toList(),
methodTypeAtCallSite.getReturnType(),
methodTypeAtCallSite.getThrownTypes(),
methodTypeAtCallSite.tsym);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate existing usage patterns of Types.subst and TypeSubstitutionUtils.subst
rg -n "Types\.subst\(|TypeSubstitutionUtils\.subst" -S --type java | head -30

Repository: uber/NullAway

Length of output: 171


🏁 Script executed:

#!/bin/bash
# Check the broader context of the GenericsChecks.java file to understand the pattern
rg -n "TypeSubstitutionUtils|Types.*subst" nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java

Repository: uber/NullAway

Length of output: 1390


🏁 Script executed:

#!/bin/bash
# Look for usages of getTypeParameters and method type variable handling in the file
rg -n "getTypeParameters|TypeVariableSymbol" nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java -A 2 -B 2

Repository: uber/NullAway

Length of output: 1302


🏁 Script executed:

#!/bin/bash
# Check if there are existing tests for the restoreMissingNullabilityFromSingleTopLevelTypeVarArguments method
fd -t f "Test.*\.java" nullaway | xargs grep -l "restoreMissingNullabilityFromSingleTopLevelTypeVarArguments" 2>/dev/null

Repository: uber/NullAway

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check the context around line 1611 to see how TypeSubstitutionUtils.subst is used
sed -n '1605,1620p' nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java

Repository: uber/NullAway

Length of output: 784


🏁 Script executed:

#!/bin/bash
# Look at the TypeSubstitutionUtils class to understand available subst methods
grep -n "\.subst\|public.*subst" nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java | head -20

Repository: uber/NullAway

Length of output: 210


🏁 Script executed:

#!/bin/bash
# Examine the entire method being reviewed to check if return types are mentioned
sed -n '1615,1697p' nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java

Repository: uber/NullAway

Length of output: 3886


🏁 Script executed:

#!/bin/bash
# Find where restoreMissingNullabilityFromSingleTopLevelTypeVarArguments is called
rg -n "restoreMissingNullabilityFromSingleTopLevelTypeVarArguments" nullaway/

Repository: uber/NullAway

Length of output: 364


🏁 Script executed:

#!/bin/bash
# Look for tests related to this method or similar return type scenarios
fd -t f ".*Test.*\.java" nullaway | xargs grep -l "restoreMissing\|return.*type.*var" 2>/dev/null | head -5

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check if there are any TODOs or comments about return type handling near this method
sed -n '1597,1700p' nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java | grep -i "todo\|fixme\|return\|thrown"

Repository: uber/NullAway

Length of output: 615


Consider return/thrown type repair as a follow-up enhancement.
The method currently repairs parameter substitutions but leaves return and thrown types untouched. While technically consistent within this method's narrow scope, if a method type variable appears in the return type (e.g., <T extends Supplier<?>> T acceptSup(T)), parameter repairs to Supplier<@Nullable OuterT> paired with an unrepaired return Supplier<OuterT> could create inconsistent nullability in inferred types.

The suggested approach—applying repaired substitutions to return/thrown types using TypeSubstitutionUtils.subst(state.getTypes(), type, from, to, config)—is sound. However, this extends the method's scope beyond single top-level type var arguments. Consider addressing this in a follow-up PR to keep the current change focused.

🤖 Prompt for AI Agents
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java` around
lines 1615 - 1697, The method
restoreMissingNullabilityFromSingleTopLevelTypeVarArguments currently only
updates parameter types but can leave return and thrown types inconsistent when
a method type variable also appears there; after computing
repairedTopLevelSubstitutions, apply those substitutions to
methodTypeAtCallSite.getReturnType() and each type in
methodTypeAtCallSite.getThrownTypes() using
TypeSubstitutionUtils.subst(state.getTypes(), type, fromMap, toMap, config) (or
equivalent API) to produce repairedReturnType and repairedThrownTypes, set
changed if any differ, and use those repaired types when constructing the new
Type.MethodType so return/thrown nullability stays consistent with the repaired
parameters.

@codecov
Copy link

codecov bot commented Feb 15, 2026

Codecov Report

❌ Patch coverage is 81.66667% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.43%. Comparing base (cc6df93) to head (be220e7).

Files with missing lines Patch % Lines
...ava/com/uber/nullaway/generics/GenericsChecks.java 78.84% 4 Missing and 7 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1473      +/-   ##
============================================
- Coverage     88.48%   88.43%   -0.05%     
- Complexity     2723     2734      +11     
============================================
  Files            99       99              
  Lines          9073     9133      +60     
  Branches       1812     1825      +13     
============================================
+ Hits           8028     8077      +49     
- Misses          518      522       +4     
- Partials        527      534       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@msridhar msridhar changed the title Re-introduce annotations missed by javac for certain cases Re-introduce annotations elided by javac for certain cases Feb 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False-positive for wildcard generics

1 participant

Comments