Skip to content

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

Merged
msridhar merged 11 commits intomasterfrom
issue-1455
Mar 25, 2026
Merged

Re-introduce annotations elided by javac for certain cases#1473
msridhar merged 11 commits intomasterfrom
issue-1455

Conversation

@msridhar
Copy link
Copy Markdown
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. I cannot figure out exactly when annotations get dropped; e.g., it does not seem to happen if sup is a formal parameter rather than a field. For the case above, 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 nested in actual parameter types onto the formal parameter types. We try to tailor the fix narrowly for this case and to not introduce unsoundness by re-introducing annotations across parameter positions inconsistently.

Summary by CodeRabbit

  • Bug Fixes

    • Improved null-safety for generic method calls by restoring nested nullability on inferred type arguments, ensuring consistent substitutions and safely falling back when restoration isn’t possible.
  • Tests

    • Added a unit test covering generic method type inference and warning behavior for JSPECIFY-style nullability scenarios.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a private helper method restoreNestedNullabilityForTypeVarArguments to com.uber.nullaway.generics.GenericsChecks and calls it from substituteTypeArgsInGenericMethodType for generic method calls with no explicit type arguments. The helper attempts to reconstruct nested type-use nullability for top-level type-variable arguments using actual invocation argument types, producing a repaired Type.MethodType when consistent substitutions are found; if inconsistent or unrecoverable, the original call-site method type is preserved. A unit test issue1455 exercising JSPECIFY-style inference warnings was added.

Possibly related PRs

Suggested labels

jspecify

Suggested reviewers

  • yuxincs
  • lazaroclapp
🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: re-introducing nullability annotations that javac elides during type inference, which is the core fix for issue #1455.
Linked Issues check ✅ Passed The PR successfully addresses issue #1455 by adding restoreNestedNullabilityForTypeVarArguments to repair method types and restore annotations javac elides, plus a test case validating the fix.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing #1455: a new private helper method in GenericsChecks to restore nullability, its integration into type substitution logic, and a corresponding test case.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown

codecov Bot commented Feb 15, 2026

Codecov Report

❌ Patch coverage is 93.47826% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.43%. Comparing base (d1aa504) to head (a2682e4).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...ava/com/uber/nullaway/generics/GenericsChecks.java 93.47% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1473      +/-   ##
============================================
+ Coverage     88.40%   88.43%   +0.02%     
- Complexity     2799     2808       +9     
============================================
  Files            99       99              
  Lines          9360     9406      +46     
  Branches       1880     1890      +10     
============================================
+ Hits           8275     8318      +43     
  Misses          529      529              
- Partials        556      559       +3     

☔ 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
Copy link
Copy Markdown
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.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 1819-1901: The method
restoreMissingNullabilityFromSingleTopLevelTypeVarArguments updates only
parameter types but leaves return and thrown types unmodified, so occurrences of
the same method type variables in return/throws remain unrepaired; update the
return type and each thrown type using the same repairedTopLevelSubstitutions
before constructing the new Type.MethodType. Specifically, for method
restoreMissingNullabilityFromSingleTopLevelTypeVarArguments compute
substitutedReturn = if returnType contains a method type variable present in
repairedTopLevelSubstitutions then use
TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations or equivalent
substitution (ensuring erasure matches as you do for params) to apply the
repaired substitution; do the same for each element of
methodTypeAtCallSite.getThrownTypes(); then pass updatedArgTypes.toList(),
substitutedReturn, and the updated thrown types into the new Type.MethodType so
return/throws reflect the same repaired top-level substitutions.

Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 1978-1981: The null check on ASTHelpers.getSymbol(invocationTree)
is dead code; update the guard in GenericsChecks.java to only check for varargs
by replacing the condition "methodSymbol == null || methodSymbol.isVarArgs()"
with "methodSymbol.isVarArgs()", and remove the TODO/comment about handling
nulls; keep the early return of methodTypeAtCallSite when the method is varargs
so callers (methodSymbol, ASTHelpers.getSymbol(invocationTree),
methodTypeAtCallSite) remain consistent.

---

Duplicate comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 2044-2049: The returned Type.MethodType still uses the original
un-repaired return and thrown types (methodTypeAtCallSite.getReturnType() and
getThrownTypes()), causing inconsistent views; update this to repair the return
type and each thrown type before constructing the new Type.MethodType (e.g.,
call the same repair logic you used for parameters on
methodTypeAtCallSite.getReturnType() and map/repair each element of
methodTypeAtCallSite.getThrownTypes()), then pass the repairedReturnType and
repairedThrownTypes to the Type.MethodType constructor instead of the originals.

Comment thread nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java Outdated
Copy link
Copy Markdown
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.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 1978-1982: The null check against
ASTHelpers.getSymbol(invocationTree) is unreachable; simplify the guard in
GenericsChecks by removing the methodSymbol == null branch and only checking
methodSymbol.isVarArgs(), i.e., replace the current if-condition that tests both
null and varargs with a single methodSymbol.isVarArgs() check (you can also drop
the accompanying "TODO handle varargs methods" only-if you prefer to keep it but
ensure logic only depends on methodSymbol.isVarArgs()); reference symbols:
ASTHelpers.getSymbol(invocationTree), methodSymbol, invocationTree, and
methodTypeAtCallSite.
- Around line 2044-2048: The returned MethodType is not applying the
repairedTopLevelSubstitutions to the return and thrown types, so update the code
that constructs the Type.MethodType in GenericsChecks (where new Type.MethodType
is created from methodTypeAtCallSite) to mirror how argument types are repaired:
apply repairedTopLevelSubstitutions to methodTypeAtCallSite.getReturnType() and
to each type in methodTypeAtCallSite.getThrownTypes() (using the same
substitution utility used for updatedArgTypes) before passing them into the
Type.MethodType constructor so repaired/nullability annotations are preserved.

Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 1996-2040: The loop in GenericsChecks.java uses indexed get(i) on
com.sun.tools.javac.util.List (origArgTypes, callSiteArgTypes, callArgs) which
is O(n^2); replace it with parallel head/tail traversal: create three list
cursors (e.g., orig = origArgTypes, callSite = callSiteArgTypes, callArgTrees =
callArgs) and iterate while orig != null, using orig.head / orig.tail and
callSite.head / callSite.tail and callArgTrees.head / callArgTrees.tail to
obtain origArgType, updatedType, and call arg tree respectively, preserving the
existing logic that references repairedTopLevelSubstitutions,
getTreeType(callArgs.get(i), state),
TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations, and returning
methodTypeAtCallSite when needed; append updatedType to updatedArgTypes as
before.

---

Duplicate comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 2044-2049: The new Type.MethodType currently uses
methodTypeAtCallSite.getReturnType() and getThrownTypes() unchanged, so
repairedTopLevelSubstitutions aren't applied to return/thrown types; update
updateMethodTypeWithInferredNullability to call
TypeSubstitutionUtils.subst(repairedTopLevelSubstitutions,
methodTypeAtCallSite.getReturnType()) and similarly subst each entry of
methodTypeAtCallSite.getThrownTypes() before constructing the Type.MethodType
(while still using updatedArgTypes.toList() for params and
methodTypeAtCallSite.tsym) so the repaired nullability is reflected in return
and thrown types.

Comment on lines +1996 to +2040
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);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Optional: Replace indexed access with iterator-based traversal for com.sun.tools.javac.util.List.

origArgTypes.get(i) and callSiteArgTypes.get(i) are each O(i) on the linked-list com.sun.tools.javac.util.List, making the loop O(n²). In practice this is negligible, but the idiomatic javac pattern (used in visitTypeLists in RestoreNullnessAnnotationsVisitor) uses parallel l.tail/l.head traversal:

♻️ Proposed refactor (O(n) traversal)
-    for (int i = 0; i < origArgTypes.size(); i++) {
-      Type updatedType = callSiteArgTypes.get(i);
-      Type origArgType = origArgTypes.get(i);
+    int i = 0;
+    for (
+        com.sun.tools.javac.util.List<Type> origL = origArgTypes,
+            callSiteL = callSiteArgTypes;
+        origL.nonEmpty();
+        origL = origL.tail, callSiteL = callSiteL.tail, i++) {
+      Type updatedType = callSiteL.head;
+      Type origArgType = origL.head;
       ...
       Type actualArgType = getTreeType(callArgs.get(i), state);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java` around
lines 1996 - 2040, The loop in GenericsChecks.java uses indexed get(i) on
com.sun.tools.javac.util.List (origArgTypes, callSiteArgTypes, callArgs) which
is O(n^2); replace it with parallel head/tail traversal: create three list
cursors (e.g., orig = origArgTypes, callSite = callSiteArgTypes, callArgTrees =
callArgs) and iterate while orig != null, using orig.head / orig.tail and
callSite.head / callSite.tail and callArgTrees.head / callArgTrees.tail to
obtain origArgType, updatedType, and call arg tree respectively, preserving the
existing logic that references repairedTopLevelSubstitutions,
getTreeType(callArgs.get(i), state),
TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations, and returning
methodTypeAtCallSite when needed; append updatedType to updatedArgTypes as
before.

@msridhar msridhar requested a review from lazaroclapp March 24, 2026 14:50
Copy link
Copy Markdown
Collaborator

@lazaroclapp lazaroclapp left a comment

Choose a reason for hiding this comment

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

This LGTM as far as it goes, not sure what the full scope of the javac bug is, but it does seem to fix the issue without breaking existing tests. Not sure in which cases it could diverge from "expected" javac inference 🤔

@msridhar msridhar merged commit e78dc89 into master Mar 25, 2026
12 checks passed
@msridhar msridhar deleted the issue-1455 branch March 25, 2026 16:30
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

3 participants