Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
d804e2e
WIP
msridhar Jan 6, 2026
2c9c3f1
tweaks
msridhar Jan 6, 2026
a5ee613
Merge branch 'master' into lambda-poly-expression
msridhar Jan 6, 2026
808fb74
tweak
msridhar Jan 6, 2026
642a623
refactor
msridhar Jan 6, 2026
b6cb584
fix test case
msridhar Jan 6, 2026
cd9a4e3
Merge branch 'master' into lambda-poly-expression
msridhar Jan 7, 2026
3a36420
WIP
msridhar Jan 7, 2026
f528062
bug fix
msridhar Jan 7, 2026
a104dcc
comment and test case for #1307
msridhar Jan 7, 2026
c2c5808
fix nullaway error
msridhar Jan 7, 2026
9cd8239
Merge branch 'master' into method-ref-improve
msridhar Jan 7, 2026
fd64445
improve test
msridhar Jan 7, 2026
cae7447
remove unnecessary code and fix test
msridhar Jan 8, 2026
b84dd93
rename test file and move tests
msridhar Jan 8, 2026
6667ae3
a failing test we will handle in a follow up
msridhar Jan 8, 2026
05ec1ae
tweaks
msridhar Jan 8, 2026
c0031e0
cleanup
msridhar Jan 8, 2026
9a95c56
further cleanup
msridhar Jan 8, 2026
8d47ece
Merge branch 'master' into method-ref-improve
msridhar Jan 11, 2026
1a53741
Merge branch 'master' into method-ref-improve
msridhar Jan 12, 2026
6afc36b
Merge branch 'master' into method-ref-improve
msridhar Jan 13, 2026
468c520
WIP
msridhar Jan 11, 2026
a01a513
Merge branch 'master' into method-ref-generic-inference
msridhar Jan 14, 2026
2d7fa93
failing test
msridhar Jan 14, 2026
d67c661
fix test
msridhar Jan 14, 2026
9091dd1
Merge branch 'master' into method-ref-generic-inference
msridhar Jan 15, 2026
b736fe2
Merge branch 'master' into method-ref-generic-inference
msridhar Jan 15, 2026
fa16569
re-jigger logic
msridhar Jan 18, 2026
741de42
todos
msridhar Jan 18, 2026
fb60aee
WIP
msridhar Jan 19, 2026
ced8cae
WIP
msridhar Jan 19, 2026
029aa72
fix
msridhar Jan 19, 2026
dc1d9aa
complete test
msridhar Jan 19, 2026
659f169
another test
msridhar Jan 19, 2026
2c0cab4
WIP
msridhar Jan 19, 2026
4dfb323
fix test
msridhar Jan 19, 2026
4d50575
more tests
msridhar Jan 20, 2026
93a208d
WIP
msridhar Jan 20, 2026
ac4673b
cleanup and docs
msridhar Jan 20, 2026
ef71582
fix npe
msridhar Jan 20, 2026
6918968
Merge branch 'master' into method-ref-generic-inference
msridhar Jan 23, 2026
c4e25b3
Merge branch 'master' into method-ref-generic-inference
msridhar Jan 25, 2026
a8b1917
Merge branch 'master' into method-ref-generic-inference
msridhar Jan 25, 2026
4abedaf
Merge branch 'master' into method-ref-generic-inference
msridhar Feb 5, 2026
238a3e2
add a test for stream with values mapped to @Nullable; doesn't work yet
msridhar Feb 5, 2026
0e1b7ec
Merge branch 'master' into method-ref-generic-inference
msridhar Feb 5, 2026
1e9ab99
some improvements
msridhar Feb 5, 2026
16942b2
more cleanup
msridhar Feb 5, 2026
28bf098
add issue link
msridhar Feb 5, 2026
c76fbd5
Merge branch 'master' into method-ref-generic-inference
msridhar Feb 9, 2026
b90e54d
Merge branch 'master' into method-ref-generic-inference
msridhar Feb 10, 2026
2f924bb
add comment
msridhar Feb 10, 2026
de6b0ce
cleanup and todos
msridhar Feb 10, 2026
f695f56
test
msridhar Feb 11, 2026
8b4f0d0
another test case
msridhar Feb 12, 2026
3406847
yet another test
msridhar Feb 12, 2026
e2e735c
Add TODO comment
msridhar Feb 12, 2026
23bfde7
another comment
msridhar Feb 12, 2026
9a989da
add issue link
msridhar Feb 12, 2026
839d22c
comment and remove unnecessary check
msridhar Feb 13, 2026
a2d2b5d
test for new bug
msridhar Feb 13, 2026
5ce5c46
initial fix
msridhar Feb 13, 2026
d800190
WIP
msridhar Feb 13, 2026
69914e2
move method
msridhar Feb 14, 2026
5b2b3dd
refactoring and docs
msridhar Feb 14, 2026
93c4820
more cleanup
msridhar Feb 14, 2026
6ecb972
add a contract
msridhar Feb 14, 2026
d7e4772
remove incorrect comment in test
msridhar Feb 14, 2026
724e54c
tweak diagnostic message that is matched on
msridhar Feb 15, 2026
81f844f
Merge branch 'master' into method-ref-generic-inference
msridhar Feb 16, 2026
0fdd94d
review feedback
msridhar Feb 16, 2026
f952e8a
augment tests
msridhar Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 100 additions & 5 deletions nullaway/src/main/java/com/uber/nullaway/NullAway.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.nullaway.javacutil.ElementUtils;
import org.checkerframework.nullaway.javacutil.TreeUtils;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;

/**
Expand Down Expand Up @@ -808,6 +809,12 @@ private Description checkParamOverriding(
(overridingMethod != null
&& !codeAnnotationInfo.isSymbolUnannotated(overridingMethod, config, handler))
|| lambdaExpressionTree != null;
Type.MethodType jspecifyMemberReferenceMethodType = null;
if (memberReferenceTree != null) {
jspecifyMemberReferenceMethodType =
genericsChecks.getMemberReferenceMethodType(
memberReferenceTree, castToNonNull(overridingMethod), state);
}

// Get argument nullability for the overridden method. If overriddenMethodArgNullnessMap[i] is
// null, parameter i is treated as unannotated.
Expand Down Expand Up @@ -897,6 +904,14 @@ private Description checkParamOverriding(
}
int methodParamInd = i - startParam;
VarSymbol paramSymbol = overridingParamSymbols.get(methodParamInd);
boolean paramIsNonNull =
paramOfOverridingMethodIsNonNull(
paramSymbol,
methodParamInd,
overridingMethod,
isOverridingMethodAnnotated,
memberReferenceTree,
jspecifyMemberReferenceMethodType);
// in the case where we have a parameter of a lambda expression, we do
// *not* force the parameter to be annotated with @Nullable; instead we "inherit"
// nullability from the corresponding functional interface method.
Expand All @@ -906,7 +921,7 @@ private Description checkParamOverriding(
lambdaExpressionTree != null
&& NullabilityUtil.lambdaParamIsImplicitlyTyped(
lambdaExpressionTree.getParameters().get(methodParamInd));
if (!implicitlyTypedLambdaParam && paramIsNonNull(paramSymbol, isOverridingMethodAnnotated)) {
if (!implicitlyTypedLambdaParam && paramIsNonNull) {
String message =
"parameter "
+ paramSymbol.name.toString()
Expand Down Expand Up @@ -945,14 +960,94 @@ private Description checkParamOverriding(
return Description.NO_MATCH;
}

private boolean paramIsNonNull(VarSymbol paramSymbol, boolean isMethodAnnotated) {
/**
* Checks if the parameter of the overriding method is {@code @NonNull}
*
* @param paramSymbol the symbol for the parameter of the overriding method
* @param methodParamInd the index of the parameter in the method signature of the overriding
* method (adjusted for unbound member references)
* @param overridingMethod if available, the symbol for the overriding method
* @param isMethodAnnotated whether the overriding method is annotated
* @param memberReferenceTree if the overriding method is a member reference, the tree for the
* member reference; otherwise {@code null}
* @param memberReferenceMethodType if the overriding method is a member reference, the method
* type of the member reference after handling generics; otherwise {@code null}
* @return true if the parameter of the overriding method is effectively {@code @NonNull}, false
* otherwise
*/
private boolean paramOfOverridingMethodIsNonNull(
VarSymbol paramSymbol,
int methodParamInd,
Symbol.@Nullable MethodSymbol overridingMethod,
boolean isMethodAnnotated,
@Nullable MemberReferenceTree memberReferenceTree,
Type.@Nullable MethodType memberReferenceMethodType) {
boolean result = false;
if (isMethodAnnotated) {
return !Nullness.hasNullableAnnotation(paramSymbol, config);
result = !Nullness.hasNullableAnnotation(paramSymbol, config);
} else if (config.acknowledgeRestrictiveAnnotations()) {
// can still be @NonNull if there is a restrictive annotation
return Nullness.hasNonNullAnnotation(paramSymbol, config);
result = Nullness.hasNonNullAnnotation(paramSymbol, config);
}
if (result && memberReferenceMethodType != null) {
// when the overriding method is a member reference, also check that the parameter is not
// effectively @Nullable due to generics. memberReferenceMethodType should be the method type
// of the member reference after handling generics
com.sun.tools.javac.util.List<Type> parameterTypes =
memberReferenceMethodType.getParameterTypes();
int memberRefParamIndex =
getMemberRefParamIndex(methodParamInd, overridingMethod, memberReferenceTree);
Type paramType = parameterTypes.get(memberRefParamIndex);
// if we have a method reference to a varargs method where varargs are passed individually
// (not as an array), and this parameter is the varargs
// parameter, then we need to check the nullability of the varargs element type
if (memberRefToVarargsPassedIndividually(overridingMethod, memberReferenceTree)
&& methodParamInd >= overridingMethod.getParameters().size() - 1) {
Verify.verify(
paramType.getKind() == TypeKind.ARRAY,
"Expected array type for varargs parameter in %s, got %s",
memberReferenceTree,
paramType);
paramType = ((Type.ArrayType) paramType).getComponentType();
}
result = !Nullness.hasNullableAnnotation(paramType.getAnnotationMirrors().stream(), config);
}
return false;
return result;
}

/**
* Checks if we have a member reference to a varargs method where the varargs are passed
* individually rather than as an array
*
* @param referencedMethod if available, the symbol for the referenced method
* @param memberReferenceTree the tree for the member reference
* @return true if we have a member reference to a varargs method where the varargs are passed
* individually
*/
@Contract("null, _ -> false; _, null -> false")
private static boolean memberRefToVarargsPassedIndividually(
Symbol.@Nullable MethodSymbol referencedMethod,
@Nullable MemberReferenceTree memberReferenceTree) {
return memberReferenceTree != null
&& referencedMethod != null
&& referencedMethod.isVarArgs()
&& ((JCTree.JCMemberReference) memberReferenceTree).varargsElement != null;
}

private static int getMemberRefParamIndex(
int methodParamInd,
Symbol.@Nullable MethodSymbol overridingMethod,
@Nullable MemberReferenceTree memberReferenceTree) {
int memberRefParamIndex = methodParamInd;
if (memberRefToVarargsPassedIndividually(overridingMethod, memberReferenceTree)) {
// With varargs adaptation, one or more functional-interface parameters can map to the
// varargs element type of the referenced method.
int varargsParamIndex = overridingMethod.getParameters().size() - 1;
if (methodParamInd >= varargsParamIndex) {
memberRefParamIndex = varargsParamIndex;
}
}
return memberRefParamIndex;
}

static Trees getTreesInstance(VisitorState state) {
Expand Down
211 changes: 204 additions & 7 deletions nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
Original file line number Diff line number Diff line change
Expand Up @@ -852,10 +852,6 @@ private void generateConstraintsForPseudoAssignment(
ExpressionTree rhsExpr,
Type lhsType) {
rhsExpr = ASTHelpers.stripParentheses(rhsExpr);
if (rhsExpr instanceof MemberReferenceTree) {
// TODO generate constraints from method reference argument types
return;
}
// if the parameter is itself a generic call requiring inference, generate constraints for
// that call
if (isGenericCallNeedingInference(rhsExpr)) {
Expand All @@ -864,16 +860,18 @@ private void generateConstraintsForPseudoAssignment(
allInvocations.add(invTree);
generateConstraintsForCall(
state, path, lhsType, false, solver, symbol, invTree, allInvocations);
} else if (!(rhsExpr instanceof LambdaExpressionTree lambda)) {
} else if (rhsExpr instanceof LambdaExpressionTree lambda) {
handleLambdaInGenericMethodInference(state, path, solver, allInvocations, lhsType, lambda);
} else if (rhsExpr instanceof MemberReferenceTree memberReferenceTree) {
handleMethodRefInGenericMethodInference(state, solver, lhsType, memberReferenceTree);
} else { // all other cases
Type argumentType = getTreeType(rhsExpr, state);
if (argumentType == null) {
// bail out of any checking involving raw types for now
return;
}
argumentType = refineArgumentTypeWithDataflow(argumentType, rhsExpr, state, path);
solver.addSubtypeConstraint(argumentType, lhsType, false);
} else {
handleLambdaInGenericMethodInference(state, path, solver, allInvocations, lhsType, lambda);
}
}

Expand Down Expand Up @@ -928,6 +926,175 @@ private void handleLambdaInGenericMethodInference(
}
}

/**
* Generate constraints for a method reference argument by comparing functional interface method
* parameter and return types against the referenced method.
*
* @param state the visitor state
* @param solver the constraint solver
* @param lhsType the type to which the method reference is being assigned
* @param memberReferenceTree the method reference argument
*/
private void handleMethodRefInGenericMethodInference(
VisitorState state,
ConstraintSolver solver,
Type lhsType,
MemberReferenceTree memberReferenceTree) {
if (lhsType.isRaw()) {
return;
}
Types types = state.getTypes();

// first, figure out the proper method type to use for the member reference
Symbol.MethodSymbol referencedMethod = ASTHelpers.getSymbol(memberReferenceTree);
if (referencedMethod == null || referencedMethod.isConstructor()) {
Comment thread
msridhar marked this conversation as resolved.
// TODO handle constructor references like Foo::new;
// https://github.com/uber/NullAway/issues/1468
return;
Comment thread
msridhar marked this conversation as resolved.
}
Type.MethodType referencedMethodType = referencedMethod.asType().asMethodType();
Type qualifierType = null;
if (!referencedMethod.isStatic()) {
// get the referenced method type as a member of the type of the qualifier expression
qualifierType = getTreeType(memberReferenceTree.getQualifierExpression(), state);
if (qualifierType != null) {
referencedMethodType =
TypeSubstitutionUtils.memberType(types, qualifierType, referencedMethod, config)
.asMethodType();
}
}
// substitute any explicit type arguments from the member reference
List<? extends ExpressionTree> typeArgumentTrees = memberReferenceTree.getTypeArguments();
if (typeArgumentTrees != null && !typeArgumentTrees.isEmpty()) {
referencedMethodType =
TypeSubstitutionUtils.subst(
state.getTypes(),
referencedMethodType,
((Type.ForAll) referencedMethod.asType()).tvars,
convertTreesToTypes(typeArgumentTrees),
config)
.asMethodType();
}
Comment thread
msridhar marked this conversation as resolved.
// allow for handler overrides
referencedMethodType =
handler.onOverrideMethodType(referencedMethod, referencedMethodType, state);

// now, get the type of the corresponding functional interface method, as a member of lhsType
Symbol.MethodSymbol fiMethod =
NullabilityUtil.getFunctionalInterfaceMethod(memberReferenceTree, types);
Type.MethodType fiMethodTypeAsMember =
TypeSubstitutionUtils.memberType(types, lhsType, fiMethod, config).asMethodType();

// constrain returns: method reference return type <: functional interface return type
Type fiReturnType = fiMethodTypeAsMember.getReturnType();
Type referencedReturnType = referencedMethodType.getReturnType();
if (fiReturnType.getKind() != TypeKind.VOID
&& referencedReturnType.getKind() != TypeKind.VOID) {
solver.addSubtypeConstraint(referencedReturnType, fiReturnType, false);
}

// constrain parameters:
// i^{th} functional interface parameter type <: i^{th} method reference parameter type,
// aligned appropriately in the case of unbound method references
com.sun.tools.javac.util.List<Type> fiParamTypes = fiMethodTypeAsMember.getParameterTypes();
com.sun.tools.javac.util.List<Type> referencedParamTypes =
referencedMethodType.getParameterTypes();
int fiStartIndex = 0;
if (((JCTree.JCMemberReference) memberReferenceTree).kind.isUnbound()) {
// an unbound method reference like String::length has an implicit receiver parameter
// that needs to be aligned with the first functional interface parameter
Verify.verify(
!fiParamTypes.isEmpty(),
"Expected receiver parameter for unbound method ref %s",
memberReferenceTree);
if (qualifierType != null) {
Type receiverType = fiParamTypes.get(0);
solver.addSubtypeConstraint(receiverType, qualifierType, false);
}
fiStartIndex = 1;
}
// first, handle the non-varargs case
int fiParamCount = fiParamTypes.size() - fiStartIndex;
int nonVarargsParamCount =
referencedMethod.isVarArgs()
? Math.min(fiParamCount, referencedParamTypes.size() - 1)
: referencedParamTypes.size();
for (int i = 0; i < nonVarargsParamCount; i++) {
solver.addSubtypeConstraint(
fiParamTypes.get(fiStartIndex + i), referencedParamTypes.get(i), false);
}
if (!referencedMethod.isVarArgs()) {
return;
}

// For varargs references, the functional interface can map to fixed-arity form (single array
// argument at the varargs position) or variable-arity form (zero or more element arguments).
int varargsParamPosition = referencedParamTypes.size() - 1;
if (fiParamCount == varargsParamPosition) {
// No varargs arguments; this is the variable-arity case, passing zero arguments
return;
}
Type varargsArrayType = referencedParamTypes.get(varargsParamPosition);
Verify.verify(
varargsArrayType.getKind() == TypeKind.ARRAY,
"Expected array type for varargs parameter in %s, got %s",
memberReferenceTree,
varargsArrayType);
JCTree.JCMemberReference javacMemberRef = (JCTree.JCMemberReference) memberReferenceTree;
int firstVarargsFiParamIndex = fiStartIndex + varargsParamPosition;
if (javacMemberRef.varargsElement == null) {
// javac resolved this member reference using non-varargs (fixed-arity) adaptation.
solver.addSubtypeConstraint(
fiParamTypes.get(firstVarargsFiParamIndex), varargsArrayType, false);
return;
}
// javac resolved this member reference using varargs (variable-arity) adaptation.
// Use the element type from the referenced varargs array type
Type varargsElementType = types.elemtype(varargsArrayType);
for (int i = varargsParamPosition; i < fiParamCount; i++) {
solver.addSubtypeConstraint(fiParamTypes.get(fiStartIndex + i), varargsElementType, false);
}
}

/**
* Gets the method type for a member reference handling generics, in JSpecify mode
*
* @param memberReferenceTree the member reference tree
* @param overridingMethod the method symbol for the method referenced by {@code
* memberReferenceTree}
* @param state the visitor state
* @return the method type for the member reference, with generics handled, or null if not in
* JSpecify mode
*/
public Type.@Nullable MethodType getMemberReferenceMethodType(
MemberReferenceTree memberReferenceTree,
Symbol.MethodSymbol overridingMethod,
VisitorState state) {
if (!config.isJSpecifyMode()) {
return null;
}
Type.MethodType result = overridingMethod.asType().asMethodType();
if (!overridingMethod.isStatic()) {
// This handles any generic type parameters of the qualifier of the member reference, e.g. for
// x::m, where x is of type Foo<Integer>, it handles the type parameter Integer whereever it
// appears in the signature of m.
Type qualifierType = ASTHelpers.getType(memberReferenceTree.getQualifierExpression());
if (qualifierType != null && !qualifierType.isRaw()) {
result =
TypeSubstitutionUtils.memberType(
state.getTypes(), qualifierType, overridingMethod, config)
.asMethodType();
}
}
if (overridingMethod.asType() instanceof Type.ForAll) {
// the referenced method is a generic method; we need to substitute inferred nullability for
// type arguments if it was inferred
result = getInferredMethodTypeForGenericMethodReference(result, state);
}
// finally, run any handlers
return handler.onOverrideMethodType(overridingMethod, result, state);
}

/**
* A visitor that scans a {@link Tree} (typically a lambda or method body) to find all {@code
* return} statements and collect their expressions.
Expand Down Expand Up @@ -1365,6 +1532,36 @@ public void compareGenericTypeParameterNullabilityForCall(
});
}

/**
* For a generic method reference, if it is being called in a context that requires type argument
* nullability inference, return the method type with inferred nullability for type parameters.
* Otherwise, return the original method type.
*
* @param methodType the original method type
* @param state the visitor state (generic method reference should be leaf of {@code
* state.getPath()})
* @return the method type with inferred nullability for type parameters if inference was
* performed, or the original method type otherwise
*/
private Type.MethodType getInferredMethodTypeForGenericMethodReference(
Type.MethodType methodType, VisitorState state) {
TreePath parentPath = state.getPath().getParentPath();
while (parentPath != null && parentPath.getLeaf() instanceof ParenthesizedTree) {
parentPath = parentPath.getParentPath();
}
Tree parentTree = parentPath != null ? parentPath.getLeaf() : null;
if (parentTree instanceof MethodInvocationTree methodInvocationTree
&& isGenericCallNeedingInference(methodInvocationTree)) {
MethodInferenceResult inferenceResult =
inferredTypeVarNullabilityForGenericCalls.get(methodInvocationTree);
if (inferenceResult instanceof InferenceSuccess successResult) {
return TypeSubstitutionUtils.updateMethodTypeWithInferredNullability(
methodType, methodType, successResult.typeVarNullability, state, config);
}
}
return methodType;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Checks that type parameter nullability is consistent between an overriding method and the
* corresponding overridden method.
Expand Down
Loading