From a1e4bb8d72aa89a6c8e369c0002f58c0445cd154 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 28 Jan 2026 18:53:22 -0800 Subject: [PATCH 01/34] WIP --- .../nullaway/jspecify/GenericDiamondTests.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java new file mode 100644 index 0000000000..935993c78b --- /dev/null +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -0,0 +1,15 @@ +package com.uber.nullaway.jspecify; + +import com.google.errorprone.CompilationTestHelper; +import com.uber.nullaway.NullAwayTestsBase; +import com.uber.nullaway.generics.JSpecifyJavacConfig; +import java.util.Arrays; + +public class GenericDiamondTests extends NullAwayTestsBase { + + private CompilationTestHelper makeHelper() { + return makeTestHelperWithArgs( + JSpecifyJavacConfig.withJSpecifyModeArgs( + Arrays.asList("-XepOpt:NullAway:AnnotatedPackages=com.uber"))); + } +} From 22fb655208a575d7cadbfabbed872e4ec2340e31 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 5 Feb 2026 17:27:11 -0800 Subject: [PATCH 02/34] test case --- .../jspecify/GenericDiamondTests.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index 935993c78b..1eff6468e2 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -4,9 +4,36 @@ import com.uber.nullaway.NullAwayTestsBase; import com.uber.nullaway.generics.JSpecifyJavacConfig; import java.util.Arrays; +import org.junit.Test; public class GenericDiamondTests extends NullAwayTestsBase { + @Test + public void issue1451() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + static class Foo { + static Foo<@Nullable Void> make() { + throw new RuntimeException(); + } + } + static class Bar { + Bar(Foo foo) { + } + } + void test() { + Bar<@Nullable Void> b = new Bar<>(Foo.make()); + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From e86b344a7f418a822a896fc742cf9e2e17f706f9 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 5 Feb 2026 17:31:07 -0800 Subject: [PATCH 03/34] tweak test --- .../uber/nullaway/jspecify/GenericDiamondTests.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index 1eff6468e2..62ff42ca67 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -21,14 +21,22 @@ static class Foo { static Foo<@Nullable Void> make() { throw new RuntimeException(); } + static Foo<@Nullable String> makeNullableStr() { + throw new RuntimeException(); + } } static class Bar { Bar(Foo foo) { } } - void test() { + void testNegative() { + // should be legal Bar<@Nullable Void> b = new Bar<>(Foo.make()); } + void testPositive() { + // BUG: Diagnostic contains: + Bar b = new Bar<>(Foo.makeNullableStr()); + } } """) .doTest(); From 81f952c6f75b000fa6a75263aeca0131d09ce4a7 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 5 Feb 2026 22:46:59 -0800 Subject: [PATCH 04/34] better tests --- .../jspecify/GenericDiamondTests.java | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index 62ff42ca67..80b01b3b8a 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -9,7 +9,7 @@ public class GenericDiamondTests extends NullAwayTestsBase { @Test - public void issue1451() { + public void assignToLocal() { makeHelper() .addSourceLines( "Test.java", @@ -42,6 +42,76 @@ void testPositive() { .doTest(); } + @Test + public void returnDiamond() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + static class Foo { + static Foo<@Nullable Void> make() { + throw new RuntimeException(); + } + static Foo<@Nullable String> makeNullableStr() { + throw new RuntimeException(); + } + } + static class Bar { + Bar(Foo foo) { + } + } + Bar<@Nullable Void> testNegative() { + // should be legal + return new Bar<>(Foo.make()); + } + Bar testPositive() { + // BUG: Diagnostic contains: + return new Bar<>(Foo.makeNullableStr()); + } + } + """) + .doTest(); + } + + @Test + public void paramPassing() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + static class Foo { + static Foo<@Nullable Void> make() { + throw new RuntimeException(); + } + static Foo<@Nullable String> makeNullableStr() { + throw new RuntimeException(); + } + } + static class Bar { + Bar(Foo foo) { + } + } + static void takeNullableVoid(Bar<@Nullable Void> b) {} + static void takeStr(Bar b) {} + void testNegative() { + // should be legal + takeNullableVoid(new Bar<>(Foo.make())); + } + void testPositive() { + // BUG: Diagnostic contains: + takeStr(new Bar<>(Foo.makeNullableStr())); + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From 03ce93165d762451cc43fee76a7610ac4e86cf89 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 5 Feb 2026 22:54:15 -0800 Subject: [PATCH 05/34] WIP --- .../nullaway/generics/GenericsChecks.java | 203 ++++++++++++++++-- .../jspecify/GenericDiamondTests.java | 6 +- 2 files changed, 194 insertions(+), 15 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 db63aec22f..8e356c0da0 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -28,6 +28,7 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; import com.sun.source.util.TreeScanner; import com.sun.tools.javac.code.Attribute; import com.sun.tools.javac.code.Symbol; @@ -36,6 +37,7 @@ import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeInfo; import com.sun.tools.javac.util.Name; import com.sun.tools.javac.util.Names; import com.uber.nullaway.CodeAnnotationInfo; @@ -434,14 +436,27 @@ private void reportInvalidOverridingMethodParamTypeError( } return result; } - if (tree instanceof NewClassTree - && ((NewClassTree) tree).getIdentifier() instanceof ParameterizedTypeTree paramTypedTree) { - if (paramTypedTree.getTypeArguments().isEmpty()) { - // diamond operator, which we do not yet support; for now, return null - // TODO: support diamond operators + if (tree instanceof NewClassTree newClassTree) { + if (newClassTree.getClassBody() != null + && newClassTree.getIdentifier() instanceof JCTree idTree + && TreeInfo.isDiamond(idTree)) { + // Keep existing behavior for diamond anonymous classes, which are not yet fully supported. return null; } - return typeWithPreservedAnnotations(paramTypedTree); + if (newClassTree.getIdentifier() instanceof ParameterizedTypeTree paramTypedTree + && !(newClassTree.getIdentifier() instanceof JCTree idTree && TreeInfo.isDiamond(idTree)) + && !paramTypedTree.getTypeArguments().isEmpty()) { + return typeWithPreservedAnnotations(paramTypedTree); + } + if (hasInferredClassTypeArguments(newClassTree)) { + // For constructor calls with inferred class type arguments, infer from assignment context + // when possible so we preserve explicit nullability annotations from the target type. + Type fromAssignmentContext = getDiamondTypeFromContext(newClassTree, state); + if (fromAssignmentContext != null) { + return fromAssignmentContext; + } + } + return ASTHelpers.getType(tree); } else if (tree instanceof NewArrayTree && ((NewArrayTree) tree).getType() instanceof AnnotatedTypeTree) { return typeWithPreservedAnnotations(tree); @@ -522,6 +537,144 @@ private void reportInvalidOverridingMethodParamTypeError( } } + /** + * Gets the type of a constructor call using a diamond operator from its assignment context, if + * available. + */ + private @Nullable Type getDiamondTypeFromContext(NewClassTree tree, VisitorState state) { + Type fromCurrentPathContext = getDiamondTypeFromCurrentPath(tree, state); + if (fromCurrentPathContext != null) { + return fromCurrentPathContext; + } + TreePath treePath = findPathToSubtree(state.getPath(), tree); + if (treePath == null) { + return null; + } + TreePath parentPath = treePath.getParentPath(); + if (parentPath == null) { + return null; + } + return getDiamondTypeFromParentContext(tree, state, parentPath); + } + + private @Nullable Type getDiamondTypeFromCurrentPath(NewClassTree tree, VisitorState state) { + TreePath currentPath = state.getPath(); + if (currentPath == null) { + return null; + } + if (currentPath.getLeaf() == tree) { + return null; + } + return getDiamondTypeFromParentContext(tree, state, currentPath); + } + + private @Nullable Type getDiamondTypeFromParentContext( + NewClassTree tree, VisitorState state, TreePath parentPath) { + Tree parent = parentPath.getLeaf(); + while (parent instanceof ParenthesizedTree) { + parentPath = parentPath.getParentPath(); + if (parentPath == null) { + return null; + } + parent = parentPath.getLeaf(); + } + if (parent instanceof VariableTree variableTree) { + Tree declaredTypeTree = variableTree.getType(); + return declaredTypeTree == null + ? getTreeType(parent, state) + : typeWithPreservedAnnotations(declaredTypeTree); + } + if (parent instanceof AssignmentTree assignmentTree) { + return getTreeType(assignmentTree.getVariable(), state); + } + if (parent instanceof ReturnTree) { + TreePath enclosingMethodOrLambda = + NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(parentPath); + if (enclosingMethodOrLambda != null + && enclosingMethodOrLambda.getLeaf() instanceof MethodTree methodTree) { + Tree returnTypeTree = methodTree.getReturnType(); + if (returnTypeTree != null) { + return typeWithPreservedAnnotations(returnTypeTree); + } + } + return null; + } + if (parent instanceof MethodInvocationTree parentInvocation) { + Type methodType = ASTHelpers.getType(parentInvocation.getMethodSelect()); + if (methodType == null) { + return null; + } + AtomicReference<@Nullable Type> formalParamTypeRef = new AtomicReference<>(); + new InvocationArguments(parentInvocation, methodType.asMethodType()) + .forEach( + (arg, pos, formalParamType, unused) -> { + if (ASTHelpers.stripParentheses(arg) == tree) { + formalParamTypeRef.set(formalParamType); + } + }); + return formalParamTypeRef.get(); + } + if (parent instanceof NewClassTree parentConstructorCall) { + Type parentCtorType = ASTHelpers.getType(parentConstructorCall.getIdentifier()); + if (parentCtorType == null) { + return getTargetTypeForDiamond(state, parentPath); + } + AtomicReference<@Nullable Type> formalParamTypeRef = new AtomicReference<>(); + new InvocationArguments(parentConstructorCall, parentCtorType.asMethodType()) + .forEach( + (arg, pos, formalParamType, unused) -> { + if (ASTHelpers.stripParentheses(arg) == tree) { + formalParamTypeRef.set(formalParamType); + } + }); + return formalParamTypeRef.get(); + } + return getTargetTypeForDiamond(state, parentPath); + } + + private static @Nullable Type getTargetTypeForDiamond(VisitorState state, TreePath treePath) { + com.google.errorprone.util.TargetType targetType = + com.google.errorprone.util.TargetType.targetType(state.withPath(treePath)); + return targetType == null ? null : targetType.type(); + } + + private static @Nullable TreePath findPathToSubtree(@Nullable TreePath rootPath, Tree target) { + if (rootPath == null) { + return null; + } + return new TreePathScanner<@Nullable TreePath, Object>() { + @Override + public @Nullable TreePath scan(Tree tree, @Nullable Object unused) { + if (tree == target) { + return getCurrentPath(); + } + return super.scan(tree, null); + } + }.scan(rootPath, null); + } + + private static VisitorState withPathToSubtree(VisitorState state, Tree subtree) { + TreePath subtreePath = findPathToSubtree(state.getPath(), subtree); + return subtreePath == null ? state : state.withPath(subtreePath); + } + + private static boolean hasInferredClassTypeArguments(NewClassTree newClassTree) { + if (newClassTree.getClassBody() != null) { + // For anonymous classes, javac does not preserve all nullability details for the inferred + // type arguments. Keep legacy behavior for now. + return false; + } + if (newClassTree.getIdentifier() instanceof ParameterizedTypeTree paramTypedTree + && newClassTree.getIdentifier() instanceof JCTree idTree + && !TreeInfo.isDiamond(idTree) + && !paramTypedTree.getTypeArguments().isEmpty()) { + // explicit class type arguments in source + return false; + } + Type newClassType = ASTHelpers.getType(newClassTree); + return newClassType != null && !newClassType.getTypeArguments().isEmpty(); + } + /** * Gets the inferred type of lambda parameter, if the lambda was passed to a generic method and * its type was inferred previously @@ -606,7 +759,7 @@ public void checkTypeParameterNullnessForAssignability(Tree tree, VisitorState s && isAssignmentToField(tree)) { maybeStoreLambdaTypeFromTarget(lambdaExpressionTree, lhsType); } - Type rhsType = getTreeType(rhsTree, state); + Type rhsType = getTreeType(rhsTree, withPathToSubtree(state, rhsTree)); if (rhsType != null) { if (isGenericCallNeedingInference(rhsTree)) { rhsType = @@ -1154,7 +1307,7 @@ public void checkTypeParameterNullnessForFunctionReturnType( // bail out of any checking involving raw types for now return; } - Type returnExpressionType = getTreeType(retExpr, state); + Type returnExpressionType = getTreeType(retExpr, withPathToSubtree(state, retExpr)); if (returnExpressionType != null) { if (isGenericCallNeedingInference(retExpr)) { returnExpressionType = @@ -1304,7 +1457,21 @@ public void compareGenericTypeParameterNullabilityForCall( return; } Type invokedMethodType = methodSymbol.type; - Type enclosingType = getEnclosingTypeForCallExpression(methodSymbol, tree, null, state, false); + Type enclosingType = null; + if (tree instanceof NewClassTree newClassTree) { + if (hasInferredClassTypeArguments(newClassTree)) { + TreePath currentPath = state.getPath(); + if (currentPath != null && ASTHelpers.stripParentheses(currentPath.getLeaf()) == tree) { + TreePath parentPath = currentPath.getParentPath(); + if (parentPath != null) { + enclosingType = getDiamondTypeFromParentContext(newClassTree, state, parentPath); + } + } + } + } + if (enclosingType == null) { + enclosingType = getEnclosingTypeForCallExpression(methodSymbol, tree, null, state, false); + } if (enclosingType != null) { invokedMethodType = TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config); @@ -1335,7 +1502,8 @@ public void compareGenericTypeParameterNullabilityForCall( if (inferredPolyType != null) { actualParameterType = inferredPolyType; } else { - actualParameterType = getTreeType(currentActualParam, state); + actualParameterType = + getTreeType(currentActualParam, withPathToSubtree(state, currentActualParam)); } if (actualParameterType != null) { if (isGenericCallNeedingInference(currentActualParam)) { @@ -1840,14 +2008,25 @@ public Nullness getGenericParameterNullnessAtInvocation( false, calledFromDataflow); } else { - enclosingType = getTreeType(receiver, state); + enclosingType = getTreeType(receiver, withPathToSubtree(state, receiver)); } } } else { Verify.verify(tree instanceof NewClassTree); + NewClassTree newClassTree = (NewClassTree) tree; + if (hasInferredClassTypeArguments(newClassTree)) { + Type typeFromAssignmentContext = + getDiamondTypeFromContext(newClassTree, withPathToSubtree(state, tree)); + if (typeFromAssignmentContext != null) { + return typeFromAssignmentContext; + } + } // for a constructor invocation, the type from the invocation itself is the "enclosing type" // for the purposes of determining type arguments - enclosingType = getTreeType(tree, state); + enclosingType = ASTHelpers.getType(newClassTree.getIdentifier()); + if (enclosingType == null) { + enclosingType = getTreeType(tree, withPathToSubtree(state, tree)); + } } return enclosingType; } diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index 80b01b3b8a..313715e555 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -34,7 +34,7 @@ void testNegative() { Bar<@Nullable Void> b = new Bar<>(Foo.make()); } void testPositive() { - // BUG: Diagnostic contains: + // BUG: Diagnostic contains: incompatible types: Foo<@Nullable String> cannot be converted to Foo Bar b = new Bar<>(Foo.makeNullableStr()); } } @@ -68,7 +68,7 @@ static class Bar { return new Bar<>(Foo.make()); } Bar testPositive() { - // BUG: Diagnostic contains: + // BUG: Diagnostic contains: incompatible types: Foo<@Nullable String> cannot be converted to Foo return new Bar<>(Foo.makeNullableStr()); } } @@ -104,7 +104,7 @@ void testNegative() { takeNullableVoid(new Bar<>(Foo.make())); } void testPositive() { - // BUG: Diagnostic contains: + // BUG: Diagnostic contains: incompatible types: Foo<@Nullable String> cannot be converted to Foo takeStr(new Bar<>(Foo.makeNullableStr())); } } From dfba3e5aa2fd8481b48f8d16146b95ede32653c6 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 07:50:49 -0800 Subject: [PATCH 06/34] docs --- .../nullaway/generics/GenericsChecks.java | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 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 8e356c0da0..0ac2277a34 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -437,9 +437,7 @@ private void reportInvalidOverridingMethodParamTypeError( return result; } if (tree instanceof NewClassTree newClassTree) { - if (newClassTree.getClassBody() != null - && newClassTree.getIdentifier() instanceof JCTree idTree - && TreeInfo.isDiamond(idTree)) { + if (isDiamondAnonymousClass(newClassTree)) { // Keep existing behavior for diamond anonymous classes, which are not yet fully supported. return null; } @@ -557,6 +555,10 @@ private void reportInvalidOverridingMethodParamTypeError( return getDiamondTypeFromParentContext(tree, state, parentPath); } + /** + * Same as {@link #getDiamondTypeFromContext(NewClassTree, VisitorState)} but starts from the + * current path when that path already refers to an enclosing context. + */ private @Nullable Type getDiamondTypeFromCurrentPath(NewClassTree tree, VisitorState state) { TreePath currentPath = state.getPath(); if (currentPath == null) { @@ -568,6 +570,10 @@ private void reportInvalidOverridingMethodParamTypeError( return getDiamondTypeFromParentContext(tree, state, currentPath); } + /** + * Computes the assignment-context type for an inferred constructor call, given a path to its + * parent context. + */ private @Nullable Type getDiamondTypeFromParentContext( NewClassTree tree, VisitorState state, TreePath parentPath) { Tree parent = parentPath.getLeaf(); @@ -604,40 +610,41 @@ private void reportInvalidOverridingMethodParamTypeError( if (methodType == null) { return null; } - AtomicReference<@Nullable Type> formalParamTypeRef = new AtomicReference<>(); - new InvocationArguments(parentInvocation, methodType.asMethodType()) - .forEach( - (arg, pos, formalParamType, unused) -> { - if (ASTHelpers.stripParentheses(arg) == tree) { - formalParamTypeRef.set(formalParamType); - } - }); - return formalParamTypeRef.get(); + return getFormalParameterTypeForArgument(parentInvocation, methodType.asMethodType(), tree); } if (parent instanceof NewClassTree parentConstructorCall) { Type parentCtorType = ASTHelpers.getType(parentConstructorCall.getIdentifier()); if (parentCtorType == null) { return getTargetTypeForDiamond(state, parentPath); } - AtomicReference<@Nullable Type> formalParamTypeRef = new AtomicReference<>(); - new InvocationArguments(parentConstructorCall, parentCtorType.asMethodType()) - .forEach( - (arg, pos, formalParamType, unused) -> { - if (ASTHelpers.stripParentheses(arg) == tree) { - formalParamTypeRef.set(formalParamType); - } - }); - return formalParamTypeRef.get(); + return getFormalParameterTypeForArgument( + parentConstructorCall, parentCtorType.asMethodType(), tree); } return getTargetTypeForDiamond(state, parentPath); } + /** Returns the inferred/declared formal parameter type corresponding to {@code argumentTree}. */ + private @Nullable Type getFormalParameterTypeForArgument( + Tree invocationTree, Type.MethodType invocationType, Tree argumentTree) { + AtomicReference<@Nullable Type> formalParamTypeRef = new AtomicReference<>(); + new InvocationArguments(invocationTree, invocationType) + .forEach( + (arg, pos, formalParamType, unused) -> { + if (ASTHelpers.stripParentheses(arg) == argumentTree) { + formalParamTypeRef.set(formalParamType); + } + }); + return formalParamTypeRef.get(); + } + + /** Reads javac's target type for the constructor expression at the given path, if available. */ private static @Nullable Type getTargetTypeForDiamond(VisitorState state, TreePath treePath) { com.google.errorprone.util.TargetType targetType = com.google.errorprone.util.TargetType.targetType(state.withPath(treePath)); return targetType == null ? null : targetType.type(); } + /** Finds the path to {@code target} within {@code rootPath}, or null when not found. */ private static @Nullable TreePath findPathToSubtree(@Nullable TreePath rootPath, Tree target) { if (rootPath == null) { return null; @@ -653,11 +660,16 @@ private void reportInvalidOverridingMethodParamTypeError( }.scan(rootPath, null); } + /** Creates a state whose path points to {@code subtree}, when that subtree is reachable. */ private static VisitorState withPathToSubtree(VisitorState state, Tree subtree) { TreePath subtreePath = findPathToSubtree(state.getPath(), subtree); return subtreePath == null ? state : state.withPath(subtreePath); } + /** + * Returns true when javac inferred class type arguments for a constructor call, i.e. there are + * instantiated type arguments at the type level, but no explicit non-diamond source type args. + */ private static boolean hasInferredClassTypeArguments(NewClassTree newClassTree) { if (newClassTree.getClassBody() != null) { // For anonymous classes, javac does not preserve all nullability details for the inferred @@ -675,6 +687,12 @@ private static boolean hasInferredClassTypeArguments(NewClassTree newClassTree) return newClassType != null && !newClassType.getTypeArguments().isEmpty(); } + private static boolean isDiamondAnonymousClass(NewClassTree newClassTree) { + return newClassTree.getClassBody() != null + && newClassTree.getIdentifier() instanceof JCTree idTree + && TreeInfo.isDiamond(idTree); + } + /** * Gets the inferred type of lambda parameter, if the lambda was passed to a generic method and * its type was inferred previously From fdf01ee826b1926fa9e37be7f43af101aa89bfb3 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 09:13:01 -0800 Subject: [PATCH 07/34] WIP --- .../nullaway/generics/GenericsChecks.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 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 0ac2277a34..e7b9ff11ec 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -442,7 +442,6 @@ private void reportInvalidOverridingMethodParamTypeError( return null; } if (newClassTree.getIdentifier() instanceof ParameterizedTypeTree paramTypedTree - && !(newClassTree.getIdentifier() instanceof JCTree idTree && TreeInfo.isDiamond(idTree)) && !paramTypedTree.getTypeArguments().isEmpty()) { return typeWithPreservedAnnotations(paramTypedTree); } @@ -649,11 +648,19 @@ private void reportInvalidOverridingMethodParamTypeError( if (rootPath == null) { return null; } + if (rootPath.getLeaf() == target) { + return rootPath; + } return new TreePathScanner<@Nullable TreePath, Object>() { @Override public @Nullable TreePath scan(Tree tree, @Nullable Object unused) { if (tree == target) { - return getCurrentPath(); + TreePath currentPath = getCurrentPath(); + if (currentPath != null && currentPath.getLeaf() == tree) { + return currentPath; + } + // When overriding scan(), getCurrentPath() can still point at the parent. + return new TreePath(currentPath, tree); } return super.scan(tree, null); } @@ -672,14 +679,10 @@ private static VisitorState withPathToSubtree(VisitorState state, Tree subtree) */ private static boolean hasInferredClassTypeArguments(NewClassTree newClassTree) { if (newClassTree.getClassBody() != null) { - // For anonymous classes, javac does not preserve all nullability details for the inferred - // type arguments. Keep legacy behavior for now. + // we still need to properly handle anonymous classes return false; } - if (newClassTree.getIdentifier() instanceof ParameterizedTypeTree paramTypedTree - && newClassTree.getIdentifier() instanceof JCTree idTree - && !TreeInfo.isDiamond(idTree) - && !paramTypedTree.getTypeArguments().isEmpty()) { + if (!TreeInfo.isDiamond((JCTree) newClassTree)) { // explicit class type arguments in source return false; } @@ -688,9 +691,7 @@ private static boolean hasInferredClassTypeArguments(NewClassTree newClassTree) } private static boolean isDiamondAnonymousClass(NewClassTree newClassTree) { - return newClassTree.getClassBody() != null - && newClassTree.getIdentifier() instanceof JCTree idTree - && TreeInfo.isDiamond(idTree); + return newClassTree.getClassBody() != null && TreeInfo.isDiamond((JCTree) newClassTree); } /** From 444651671c3d3f92df74cf9751aa7241aa77c6d0 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 09:14:54 -0800 Subject: [PATCH 08/34] simplify --- .../nullaway/generics/GenericsChecks.java | 19 ------------------- 1 file changed, 19 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 e7b9ff11ec..014145256d 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -539,10 +539,6 @@ private void reportInvalidOverridingMethodParamTypeError( * available. */ private @Nullable Type getDiamondTypeFromContext(NewClassTree tree, VisitorState state) { - Type fromCurrentPathContext = getDiamondTypeFromCurrentPath(tree, state); - if (fromCurrentPathContext != null) { - return fromCurrentPathContext; - } TreePath treePath = findPathToSubtree(state.getPath(), tree); if (treePath == null) { return null; @@ -554,21 +550,6 @@ private void reportInvalidOverridingMethodParamTypeError( return getDiamondTypeFromParentContext(tree, state, parentPath); } - /** - * Same as {@link #getDiamondTypeFromContext(NewClassTree, VisitorState)} but starts from the - * current path when that path already refers to an enclosing context. - */ - private @Nullable Type getDiamondTypeFromCurrentPath(NewClassTree tree, VisitorState state) { - TreePath currentPath = state.getPath(); - if (currentPath == null) { - return null; - } - if (currentPath.getLeaf() == tree) { - return null; - } - return getDiamondTypeFromParentContext(tree, state, currentPath); - } - /** * Computes the assignment-context type for an inferred constructor call, given a path to its * parent context. From dc3a22716dbbe09c5254eb0ab7177b6c88233ec4 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 09:19:59 -0800 Subject: [PATCH 09/34] more --- .../nullaway/generics/GenericsChecks.java | 14 ++-- .../jspecify/GenericDiamondTests.java | 72 +++++++++++++++++++ 2 files changed, 82 insertions(+), 4 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 014145256d..16f7d2742a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -593,12 +593,18 @@ private void reportInvalidOverridingMethodParamTypeError( return getFormalParameterTypeForArgument(parentInvocation, methodType.asMethodType(), tree); } if (parent instanceof NewClassTree parentConstructorCall) { - Type parentCtorType = ASTHelpers.getType(parentConstructorCall.getIdentifier()); - if (parentCtorType == null) { + Symbol parentCtorSymbol = ASTHelpers.getSymbol(parentConstructorCall); + if (parentCtorSymbol == null) { return getTargetTypeForDiamond(state, parentPath); } - return getFormalParameterTypeForArgument( - parentConstructorCall, parentCtorType.asMethodType(), tree); + Type parentCtorType = parentCtorSymbol.type; + if (parentCtorType instanceof Type.ForAll) { + parentCtorType = ((Type.ForAll) parentCtorType).qtype; + } + if (!(parentCtorType instanceof Type.MethodType methodType)) { + return getTargetTypeForDiamond(state, parentPath); + } + return getFormalParameterTypeForArgument(parentConstructorCall, methodType, tree); } return getTargetTypeForDiamond(state, parentPath); } diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index 313715e555..ebe5f6c389 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -112,6 +112,78 @@ void testPositive() { .doTest(); } + @Test + public void parenthesizedDiamond() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + static class Foo { + static Foo<@Nullable Void> make() { + throw new RuntimeException(); + } + static Foo<@Nullable String> makeNullableStr() { + throw new RuntimeException(); + } + } + static class Bar { + Bar(Foo foo) { + } + } + Bar<@Nullable Void> testNegative() { + return (new Bar<>(Foo.make())); + } + Bar testPositive() { + // BUG: Diagnostic contains: incompatible types: Foo<@Nullable String> cannot be converted to Foo + return (new Bar<>(Foo.makeNullableStr())); + } + } + """) + .doTest(); + } + + @Test + public void nestedDiamondConstructors() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + @NullMarked + public class Test { + static class Foo { + static Foo<@Nullable Void> make() { + throw new RuntimeException(); + } + static Foo<@Nullable String> makeNullableStr() { + throw new RuntimeException(); + } + } + static class Bar { + Bar(Foo foo) { + } + } + static class Baz { + Baz(Bar bar) { + } + } + Baz<@Nullable Void> testNegative() { + // TODO: support nested constructor inference for the inner diamond call. + // BUG: Diagnostic contains: incompatible types: + return new Baz<>(new Bar<>(Foo.make())); + } + Baz testPositive() { + // BUG: Diagnostic contains: incompatible types: + return new Baz<>(new Bar<>(Foo.makeNullableStr())); + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From 471701de24547188692bc5e7ccf238bf15ccbbb0 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 10:40:56 -0800 Subject: [PATCH 10/34] simplify --- .../java/com/uber/nullaway/generics/GenericsChecks.java | 7 ++----- 1 file changed, 2 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 16f7d2742a..00af987bd4 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -564,11 +564,8 @@ private void reportInvalidOverridingMethodParamTypeError( } parent = parentPath.getLeaf(); } - if (parent instanceof VariableTree variableTree) { - Tree declaredTypeTree = variableTree.getType(); - return declaredTypeTree == null - ? getTreeType(parent, state) - : typeWithPreservedAnnotations(declaredTypeTree); + if (parent instanceof VariableTree) { + return getTreeType(parent, state); } if (parent instanceof AssignmentTree assignmentTree) { return getTreeType(assignmentTree.getVariable(), state); From aea98aecf7807450a4e951f6ab5f11a1e42c6f6c Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 10:45:17 -0800 Subject: [PATCH 11/34] simplify --- .../com/uber/nullaway/generics/GenericsChecks.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 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 00af987bd4..ead8a0dabf 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -564,20 +564,17 @@ private void reportInvalidOverridingMethodParamTypeError( } parent = parentPath.getLeaf(); } - if (parent instanceof VariableTree) { + if (parent instanceof VariableTree || parent instanceof AssignmentTree) { return getTreeType(parent, state); } - if (parent instanceof AssignmentTree assignmentTree) { - return getTreeType(assignmentTree.getVariable(), state); - } if (parent instanceof ReturnTree) { TreePath enclosingMethodOrLambda = NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(parentPath); if (enclosingMethodOrLambda != null - && enclosingMethodOrLambda.getLeaf() instanceof MethodTree methodTree) { - Tree returnTypeTree = methodTree.getReturnType(); - if (returnTypeTree != null) { - return typeWithPreservedAnnotations(returnTypeTree); + && enclosingMethodOrLambda.getLeaf() instanceof MethodTree enclosingMethod) { + Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(enclosingMethod); + if (methodSymbol != null) { + return methodSymbol.getReturnType(); } } return null; From 0e2df186207dc17c9027e42f28a67674f4ea85a5 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 10:48:59 -0800 Subject: [PATCH 12/34] reuse helper method --- .../uber/nullaway/generics/GenericsChecks.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 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 ead8a0dabf..7341acf441 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -1848,16 +1848,11 @@ private InvocationAndContext getInvocationAndContextForInference( } // the generic invocation is either a regular parameter to the parent call, or the // receiver expression - AtomicReference<@Nullable Type> formalParamTypeRef = new AtomicReference<>(); - Type type = ASTHelpers.getSymbol(parentInvocation).type; - new InvocationArguments(parentInvocation, type.asMethodType()) - .forEach( - (arg, pos, formalParamType, unused) -> { - if (ASTHelpers.stripParentheses(arg) == invocation) { - formalParamTypeRef.set(formalParamType); - } - }); - Type formalParamType = formalParamTypeRef.get(); + Type formalParamType = + getFormalParameterTypeForArgument( + parentInvocation, + ASTHelpers.getSymbol(parentInvocation).type.asMethodType(), + invocation); if (formalParamType == null) { // this can happen if the invocation is the receiver expression of the call, e.g., // id(x).foo() (note that foo() need not be generic) From e6078504e55d38871e7f567dad066caf7e84e571 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 15:45:40 -0800 Subject: [PATCH 13/34] bug fix --- .../java/com/uber/nullaway/generics/GenericsChecks.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 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 7341acf441..b2b0e11ea5 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -632,9 +632,9 @@ private void reportInvalidOverridingMethodParamTypeError( if (rootPath.getLeaf() == target) { return rootPath; } - return new TreePathScanner<@Nullable TreePath, Object>() { + return new TreePathScanner<@Nullable TreePath, @Nullable TreePath>() { @Override - public @Nullable TreePath scan(Tree tree, @Nullable Object unused) { + public @Nullable TreePath scan(Tree tree, @Nullable TreePath prevPath) { if (tree == target) { TreePath currentPath = getCurrentPath(); if (currentPath != null && currentPath.getLeaf() == tree) { @@ -645,6 +645,11 @@ private void reportInvalidOverridingMethodParamTypeError( } return super.scan(tree, null); } + + @Override + public @Nullable TreePath reduce(@Nullable TreePath r1, @Nullable TreePath r2) { + return r1 != null ? r1 : r2; + } }.scan(rootPath, null); } From d1636f78542714b292868612e5d19b58224e6bf0 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 19:16:28 -0800 Subject: [PATCH 14/34] fix nested constructors --- .../nullaway/generics/GenericsChecks.java | 31 +++++++------------ .../jspecify/GenericDiamondTests.java | 2 -- 2 files changed, 12 insertions(+), 21 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 b2b0e11ea5..06f9ab174c 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -587,20 +587,20 @@ private void reportInvalidOverridingMethodParamTypeError( return getFormalParameterTypeForArgument(parentInvocation, methodType.asMethodType(), tree); } if (parent instanceof NewClassTree parentConstructorCall) { - Symbol parentCtorSymbol = ASTHelpers.getSymbol(parentConstructorCall); - if (parentCtorSymbol == null) { - return getTargetTypeForDiamond(state, parentPath); + // get the type returned by the parent constructor call + Type parentClassType = getTreeType(parentConstructorCall, state.withPath(parentPath)); + if (parentClassType != null) { + Symbol parentCtorSymbol = ASTHelpers.getSymbol(parentConstructorCall); + // get the proper type for the constructor, as a member of the type returned by the + // constructor + Type parentCtorType = + TypeSubstitutionUtils.memberType( + state.getTypes(), parentClassType, parentCtorSymbol, config); + return getFormalParameterTypeForArgument( + parentConstructorCall, parentCtorType.asMethodType(), tree); } - Type parentCtorType = parentCtorSymbol.type; - if (parentCtorType instanceof Type.ForAll) { - parentCtorType = ((Type.ForAll) parentCtorType).qtype; - } - if (!(parentCtorType instanceof Type.MethodType methodType)) { - return getTargetTypeForDiamond(state, parentPath); - } - return getFormalParameterTypeForArgument(parentConstructorCall, methodType, tree); } - return getTargetTypeForDiamond(state, parentPath); + return null; } /** Returns the inferred/declared formal parameter type corresponding to {@code argumentTree}. */ @@ -617,13 +617,6 @@ private void reportInvalidOverridingMethodParamTypeError( return formalParamTypeRef.get(); } - /** Reads javac's target type for the constructor expression at the given path, if available. */ - private static @Nullable Type getTargetTypeForDiamond(VisitorState state, TreePath treePath) { - com.google.errorprone.util.TargetType targetType = - com.google.errorprone.util.TargetType.targetType(state.withPath(treePath)); - return targetType == null ? null : targetType.type(); - } - /** Finds the path to {@code target} within {@code rootPath}, or null when not found. */ private static @Nullable TreePath findPathToSubtree(@Nullable TreePath rootPath, Tree target) { if (rootPath == null) { diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index ebe5f6c389..f793686c76 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -171,8 +171,6 @@ static class Baz { } } Baz<@Nullable Void> testNegative() { - // TODO: support nested constructor inference for the inner diamond call. - // BUG: Diagnostic contains: incompatible types: return new Baz<>(new Bar<>(Foo.make())); } Baz testPositive() { From c6c1e9a6a34ca895a6467cbf5921198ce2affe23 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 19:17:32 -0800 Subject: [PATCH 15/34] fix diagnostic message --- .../java/com/uber/nullaway/jspecify/GenericDiamondTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index f793686c76..fb1a950cce 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -174,7 +174,7 @@ static class Baz { return new Baz<>(new Bar<>(Foo.make())); } Baz testPositive() { - // BUG: Diagnostic contains: incompatible types: + // BUG: Diagnostic contains: incompatible types: Foo<@Nullable String> cannot be converted to Foo return new Baz<>(new Bar<>(Foo.makeNullableStr())); } } From 68ad56da49ecc53fe32f9298e28bebb289fa07e3 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 6 Feb 2026 19:22:30 -0800 Subject: [PATCH 16/34] simplify --- .../com/uber/nullaway/generics/GenericsChecks.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 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 06f9ab174c..d2aa5aa67a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -445,13 +445,10 @@ private void reportInvalidOverridingMethodParamTypeError( && !paramTypedTree.getTypeArguments().isEmpty()) { return typeWithPreservedAnnotations(paramTypedTree); } - if (hasInferredClassTypeArguments(newClassTree)) { - // For constructor calls with inferred class type arguments, infer from assignment context - // when possible so we preserve explicit nullability annotations from the target type. - Type fromAssignmentContext = getDiamondTypeFromContext(newClassTree, state); - if (fromAssignmentContext != null) { - return fromAssignmentContext; - } + // For constructor calls using diamond operator, infer from assignment context + Type fromAssignmentContext = getDiamondTypeFromContext(newClassTree, state); + if (fromAssignmentContext != null) { + return fromAssignmentContext; } return ASTHelpers.getType(tree); } else if (tree instanceof NewArrayTree From 02d32b6f15901ff8eb0af5e0b8215528d3ebe0e0 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 7 Feb 2026 18:12:13 -0800 Subject: [PATCH 17/34] small fix --- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 5 +---- 1 file changed, 1 insertion(+), 4 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 d2aa5aa67a..6e6e7fba8d 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -2013,10 +2013,7 @@ public Nullness getGenericParameterNullnessAtInvocation( } // for a constructor invocation, the type from the invocation itself is the "enclosing type" // for the purposes of determining type arguments - enclosingType = ASTHelpers.getType(newClassTree.getIdentifier()); - if (enclosingType == null) { - enclosingType = getTreeType(tree, withPathToSubtree(state, tree)); - } + enclosingType = getTreeType(tree, withPathToSubtree(state, tree)); } return enclosingType; } From ec32f8921a50e2401cae23d1fe7a4b129b02a8df Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 7 Feb 2026 18:25:05 -0800 Subject: [PATCH 18/34] another fix --- .../nullaway/generics/GenericsChecks.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 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 6e6e7fba8d..f3ff792a30 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -437,19 +437,23 @@ private void reportInvalidOverridingMethodParamTypeError( return result; } if (tree instanceof NewClassTree newClassTree) { - if (isDiamondAnonymousClass(newClassTree)) { - // Keep existing behavior for diamond anonymous classes, which are not yet fully supported. - return null; + if (TreeInfo.isDiamond((JCTree) newClassTree)) { + if (newClassTree.getClassBody() != null) { + // Keep existing behavior for diamond anonymous classes, which are not yet fully + // supported. + return null; + } + // For constructor calls using diamond operator, infer from assignment context. + // TODO handle diamond constructor calls passed to generic methods + Type fromAssignmentContext = getDiamondTypeFromContext(newClassTree, state); + if (fromAssignmentContext != null) { + return fromAssignmentContext; + } } if (newClassTree.getIdentifier() instanceof ParameterizedTypeTree paramTypedTree && !paramTypedTree.getTypeArguments().isEmpty()) { return typeWithPreservedAnnotations(paramTypedTree); } - // For constructor calls using diamond operator, infer from assignment context - Type fromAssignmentContext = getDiamondTypeFromContext(newClassTree, state); - if (fromAssignmentContext != null) { - return fromAssignmentContext; - } return ASTHelpers.getType(tree); } else if (tree instanceof NewArrayTree && ((NewArrayTree) tree).getType() instanceof AnnotatedTypeTree) { @@ -666,10 +670,6 @@ private static boolean hasInferredClassTypeArguments(NewClassTree newClassTree) return newClassType != null && !newClassType.getTypeArguments().isEmpty(); } - private static boolean isDiamondAnonymousClass(NewClassTree newClassTree) { - return newClassTree.getClassBody() != null && TreeInfo.isDiamond((JCTree) newClassTree); - } - /** * Gets the inferred type of lambda parameter, if the lambda was passed to a generic method and * its type was inferred previously From 89bb268543d791816f409aafd109c678713ce5e2 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 14:29:01 -0800 Subject: [PATCH 19/34] failing test --- .../jspecify/GenericDiamondTests.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index fb1a950cce..407bea12f2 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -182,6 +182,33 @@ Baz testPositive() { .doTest(); } + @Test + public void diamondSubclassPassedToGenericMethod() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.*; + import java.util.List; + @NullMarked + public class Test { + interface Foo { + } + static class FooImpl implements Foo<@Nullable T> { + FooImpl(Class cls) { + } + } + static List make(Foo foo) { + throw new RuntimeException(); + } + static List<@Nullable V> test(Class cls) { + return make(new FooImpl<>(cls)); + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From 36ac60cd28f8d65165fbbf2cdbe3961bd0497e93 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 15:05:44 -0800 Subject: [PATCH 20/34] test fix --- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 5 +++++ .../java/com/uber/nullaway/jspecify/GenericDiamondTests.java | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) 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 c2fbff86df..6d3811c752 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -581,6 +581,11 @@ private void reportInvalidOverridingMethodParamTypeError( return null; } if (parent instanceof MethodInvocationTree parentInvocation) { + if (isGenericCallNeedingInference(parentInvocation)) { + // TODO support full integration of diamond constructor calls with generic method inference + // for now, just give up and return null + return null; + } Type methodType = ASTHelpers.getType(parentInvocation.getMethodSelect()); if (methodType == null) { return null; diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index 407bea12f2..9889dc6f0d 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -212,6 +212,8 @@ static class FooImpl implements Foo<@Nullable T> { private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( - Arrays.asList("-XepOpt:NullAway:AnnotatedPackages=com.uber"))); + Arrays.asList( + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:WarnOnGenericInferenceFailure=true"))); } } From 65fc519647e38c3cd0d357b71bb5038425f1d996 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 15:57:54 -0800 Subject: [PATCH 21/34] tweaks --- .../com/uber/nullaway/generics/GenericsChecks.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 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 6d3811c752..cb9c82a22b 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -544,11 +544,7 @@ private void reportInvalidOverridingMethodParamTypeError( if (treePath == null) { return null; } - TreePath parentPath = treePath.getParentPath(); - if (parentPath == null) { - return null; - } - return getDiamondTypeFromParentContext(tree, state, parentPath); + return getDiamondTypeFromParentContext(tree, state, castToNonNull(treePath.getParentPath())); } /** @@ -624,10 +620,7 @@ private void reportInvalidOverridingMethodParamTypeError( } /** Finds the path to {@code target} within {@code rootPath}, or null when not found. */ - private static @Nullable TreePath findPathToSubtree(@Nullable TreePath rootPath, Tree target) { - if (rootPath == null) { - return null; - } + private static @Nullable TreePath findPathToSubtree(TreePath rootPath, Tree target) { if (rootPath.getLeaf() == target) { return rootPath; } From 2454efdc625dd3e7c29a81669db6c6884d5ee9e5 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 16:03:51 -0800 Subject: [PATCH 22/34] docs --- .../com/uber/nullaway/generics/GenericsChecks.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 cb9c82a22b..e119f7b765 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -640,12 +640,22 @@ private void reportInvalidOverridingMethodParamTypeError( @Override public @Nullable TreePath reduce(@Nullable TreePath r1, @Nullable TreePath r2) { + // we should only find the target once, so at most one of r1 and r2 should be non-null + Verify.verify(r1 == null || r2 == null); return r1 != null ? r1 : r2; } }.scan(rootPath, null); } - /** Creates a state whose path points to {@code subtree}, when that subtree is reachable. */ + /** + * Creates a state whose path points to {@code subtree}, when that subtree is reachable. + * + * @param state the original state with a path to some subtree of the AST + * @param subtree the subtree to find within the original state's path + * @return a state with the same information as the original, but with a path to {@code subtree} + * if it is reachable from the original state's path, or the original state if {@code subtree} + * is not reachable + */ private static VisitorState withPathToSubtree(VisitorState state, Tree subtree) { TreePath subtreePath = findPathToSubtree(state.getPath(), subtree); return subtreePath == null ? state : state.withPath(subtreePath); From df8f89ee8c9528c3428e82c024c6625677bbdcd7 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 16:18:51 -0800 Subject: [PATCH 23/34] add issue links --- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 2 ++ 1 file changed, 2 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 e119f7b765..5e9b4aee8a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -445,6 +445,7 @@ private void reportInvalidOverridingMethodParamTypeError( } // For constructor calls using diamond operator, infer from assignment context. // TODO handle diamond constructor calls passed to generic methods + // https://github.com/uber/NullAway/issues/1470 Type fromAssignmentContext = getDiamondTypeFromContext(newClassTree, state); if (fromAssignmentContext != null) { return fromAssignmentContext; @@ -1280,6 +1281,7 @@ private Type updateTypeWithNullness( private static boolean isGenericCallNeedingInference(ExpressionTree argument) { // For now, we only support calls to generic methods. // TODO also support calls to generic constructors that use the diamond operator + // https://github.com/uber/NullAway/issues/1470 if (argument instanceof MethodInvocationTree methodInvocation) { Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodInvocation); // true for generic method calls with no explicit type arguments From 82e633ee680cf5c6afed5f8b8680b2f6556f76ed Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 16:19:29 -0800 Subject: [PATCH 24/34] remove unnecessary code --- .../java/com/uber/nullaway/generics/GenericsChecks.java | 8 -------- 1 file changed, 8 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 5e9b4aee8a..9b268cf8fe 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -2003,14 +2003,6 @@ public Nullness getGenericParameterNullnessAtInvocation( } } else { Verify.verify(tree instanceof NewClassTree); - NewClassTree newClassTree = (NewClassTree) tree; - if (hasInferredClassTypeArguments(newClassTree)) { - Type typeFromAssignmentContext = - getDiamondTypeFromContext(newClassTree, withPathToSubtree(state, tree)); - if (typeFromAssignmentContext != null) { - return typeFromAssignmentContext; - } - } // for a constructor invocation, the type from the invocation itself is the "enclosing type" // for the purposes of determining type arguments enclosingType = getTreeType(tree, withPathToSubtree(state, tree)); From 319f453042dd873145070cbd231459c6ac64de29 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 14 Feb 2026 16:28:22 -0800 Subject: [PATCH 25/34] more coderabbit comments --- .../com/uber/nullaway/generics/GenericsChecks.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 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 9b268cf8fe..a5cf0bc705 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -629,12 +629,8 @@ private void reportInvalidOverridingMethodParamTypeError( @Override public @Nullable TreePath scan(Tree tree, @Nullable TreePath prevPath) { if (tree == target) { - TreePath currentPath = getCurrentPath(); - if (currentPath != null && currentPath.getLeaf() == tree) { - return currentPath; - } - // When overriding scan(), getCurrentPath() can still point at the parent. - return new TreePath(currentPath, tree); + // When overriding scan(), getCurrentPath() still points at the parent. + return new TreePath(getCurrentPath(), tree); } return super.scan(tree, null); } @@ -1846,7 +1842,8 @@ private InvocationAndContext getInvocationAndContextForInference( Type formalParamType = getFormalParameterTypeForArgument( parentInvocation, - ASTHelpers.getSymbol(parentInvocation).type.asMethodType(), + castToNonNull(ASTHelpers.getType(parentInvocation.getMethodSelect())) + .asMethodType(), invocation); if (formalParamType == null) { // this can happen if the invocation is the receiver expression of the call, e.g., From 4babffe726de74582160edbe2b1bc7a64a57340e Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Tue, 17 Feb 2026 18:36:58 -0800 Subject: [PATCH 26/34] test of Void without @Nullable --- .../java/com/uber/nullaway/jspecify/GenericDiamondTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java index 9889dc6f0d..07c4ac7ed5 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/GenericDiamondTests.java @@ -36,6 +36,8 @@ void testNegative() { void testPositive() { // BUG: Diagnostic contains: incompatible types: Foo<@Nullable String> cannot be converted to Foo Bar b = new Bar<>(Foo.makeNullableStr()); + // BUG: Diagnostic contains: incompatible types: Foo<@Nullable Void> cannot be converted to Foo + Bar b2 = new Bar<>(Foo.make()); } } """) From 78501f2022a75123223755924d3e40da386b91e5 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Tue, 17 Feb 2026 18:45:22 -0800 Subject: [PATCH 27/34] extract helper to check for raw types and use for NewClassTrees --- .../nullaway/generics/GenericsChecks.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 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 1cbb0b2632..95f176de40 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -431,10 +431,7 @@ private void reportInvalidOverridingMethodParamTypeError( if (result == null) { result = ASTHelpers.getType(tree); } - if (result != null && result.isRaw()) { - return null; - } - return result; + return typeOrNullIfRaw(result); } if (tree instanceof NewClassTree newClassTree) { if (TreeInfo.isDiamond((JCTree) newClassTree)) { @@ -455,7 +452,7 @@ private void reportInvalidOverridingMethodParamTypeError( && !paramTypedTree.getTypeArguments().isEmpty()) { return typeWithPreservedAnnotations(paramTypedTree); } - return ASTHelpers.getType(tree); + return typeOrNullIfRaw(ASTHelpers.getType(tree)); } else if (tree instanceof NewArrayTree && ((NewArrayTree) tree).getType() instanceof AnnotatedTypeTree) { return typeWithPreservedAnnotations(tree); @@ -528,12 +525,19 @@ private void reportInvalidOverridingMethodParamTypeError( } } } - if (result != null && result.isRaw()) { - // bail out of any checking involving raw types for now - return null; - } - return result; + return typeOrNullIfRaw(result); + } + } + + /** + * @param type a type to check + * @return the given type, or null if the type is a raw type + */ + private static @Nullable Type typeOrNullIfRaw(@Nullable Type type) { + if (type != null && type.isRaw()) { + return null; } + return type; } /** From 2c50426c9404ca78bb0b26213a26eb719e05d4fc Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Tue, 17 Feb 2026 18:49:51 -0800 Subject: [PATCH 28/34] add tracking issue --- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 95f176de40..19606d0f19 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -437,7 +437,7 @@ private void reportInvalidOverridingMethodParamTypeError( if (TreeInfo.isDiamond((JCTree) newClassTree)) { if (newClassTree.getClassBody() != null) { // Keep existing behavior for diamond anonymous classes, which are not yet fully - // supported. + // supported. Tracked in https://github.com/uber/NullAway/issues/1475 return null; } // For constructor calls using diamond operator, infer from assignment context. From 95ee81dc66e733f1d6cf9d356973210384459bec Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Tue, 17 Feb 2026 18:50:55 -0800 Subject: [PATCH 29/34] add another link to issue --- .../src/main/java/com/uber/nullaway/generics/GenericsChecks.java | 1 + 1 file changed, 1 insertion(+) 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 19606d0f19..836ae486ae 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -584,6 +584,7 @@ private void reportInvalidOverridingMethodParamTypeError( if (parent instanceof MethodInvocationTree parentInvocation) { if (isGenericCallNeedingInference(parentInvocation)) { // TODO support full integration of diamond constructor calls with generic method inference + // https://github.com/uber/NullAway/issues/1470 // for now, just give up and return null return null; } From 39898d7dee8aeba3a97c3bec3ca9c1b5ea33275e Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Tue, 17 Feb 2026 18:53:23 -0800 Subject: [PATCH 30/34] tweak comment --- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 836ae486ae..ce2c23aed2 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -611,7 +611,10 @@ private void reportInvalidOverridingMethodParamTypeError( return null; } - /** Returns the inferred/declared formal parameter type corresponding to {@code argumentTree}. */ + /** + * Returns the inferred/declared formal parameter type corresponding to actual parameter {@code + * argumentTree}. + */ private @Nullable Type getFormalParameterTypeForArgument( Tree invocationTree, Type.MethodType invocationType, Tree argumentTree) { AtomicReference<@Nullable Type> formalParamTypeRef = new AtomicReference<>(); From bfd95f72191f15dbe7908a0912198b2273607526 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 19 Feb 2026 11:51:57 -0800 Subject: [PATCH 31/34] get rid of withPathToSubtree --- .../nullaway/generics/GenericsChecks.java | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 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 ce2c23aed2..b3ee37f720 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -652,20 +652,6 @@ private void reportInvalidOverridingMethodParamTypeError( }.scan(rootPath, null); } - /** - * Creates a state whose path points to {@code subtree}, when that subtree is reachable. - * - * @param state the original state with a path to some subtree of the AST - * @param subtree the subtree to find within the original state's path - * @return a state with the same information as the original, but with a path to {@code subtree} - * if it is reachable from the original state's path, or the original state if {@code subtree} - * is not reachable - */ - private static VisitorState withPathToSubtree(VisitorState state, Tree subtree) { - TreePath subtreePath = findPathToSubtree(state.getPath(), subtree); - return subtreePath == null ? state : state.withPath(subtreePath); - } - /** * Returns true when javac inferred class type arguments for a constructor call, i.e. there are * instantiated type arguments at the type level, but no explicit non-diamond source type args. @@ -767,7 +753,7 @@ public void checkTypeParameterNullnessForAssignability(Tree tree, VisitorState s && isAssignmentToField(tree)) { maybeStoreLambdaTypeFromTarget(lambdaExpressionTree, lhsType); } - Type rhsType = getTreeType(rhsTree, withPathToSubtree(state, rhsTree)); + Type rhsType = getTreeType(rhsTree, state); if (rhsType != null) { if (isGenericCallNeedingInference(rhsTree)) { rhsType = @@ -1490,7 +1476,7 @@ public void checkTypeParameterNullnessForFunctionReturnType( // bail out of any checking involving raw types for now return; } - Type returnExpressionType = getTreeType(retExpr, withPathToSubtree(state, retExpr)); + Type returnExpressionType = getTreeType(retExpr, state); if (returnExpressionType != null) { if (isGenericCallNeedingInference(retExpr)) { returnExpressionType = @@ -1685,8 +1671,7 @@ public void compareGenericTypeParameterNullabilityForCall( if (inferredPolyType != null) { actualParameterType = inferredPolyType; } else { - actualParameterType = - getTreeType(currentActualParam, withPathToSubtree(state, currentActualParam)); + actualParameterType = getTreeType(currentActualParam, state); } if (actualParameterType != null) { if (isGenericCallNeedingInference(currentActualParam)) { @@ -2207,14 +2192,14 @@ public Nullness getGenericParameterNullnessAtInvocation( false, calledFromDataflow); } else { - enclosingType = getTreeType(receiver, withPathToSubtree(state, receiver)); + enclosingType = getTreeType(receiver, state); } } } else { Verify.verify(tree instanceof NewClassTree); // for a constructor invocation, the type from the invocation itself is the "enclosing type" // for the purposes of determining type arguments - enclosingType = getTreeType(tree, withPathToSubtree(state, tree)); + enclosingType = getTreeType(tree, state); } return enclosingType; } From d50bf7b838bbfc547f4f5ed2de5e3f102e16ad85 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 19 Feb 2026 15:21:37 -0800 Subject: [PATCH 32/34] WIP --- .../com/uber/nullaway/NullabilityUtil.java | 27 ++++++++ .../nullaway/generics/GenericsChecks.java | 63 ++++++++++--------- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java b/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java index 42b52f92d7..9a10e9a6c7 100644 --- a/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java +++ b/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java @@ -730,4 +730,31 @@ public static ExpressionTree stripParensAndCasts(ExpressionTree expr) { } return expr; } + + public record ExprTreeAndState(ExpressionTree expr, VisitorState state) {} + + /** + * strip out enclosing parentheses, and update the tree path in the VisitorState to point to the + * stripped expression if the original expression was the leaf of the path + * + * @param expr a potentially parenthesised expression. + * @param state the VisitorState + * @return the same expression without parentheses, and the updated VisitorState + */ + public static ExprTreeAndState stripParensAndUpdateTreePath( + ExpressionTree expr, VisitorState state) { + TreePath path = state.getPath(); + if (path.getLeaf() != expr) { + // if the expression is not the leaf of the path, we can't update the path to point to the + // stripped expression, so we just return the original expression and state + return new ExprTreeAndState(expr, state); + } + ExpressionTree resultExpr = expr; + while (resultExpr instanceof ParenthesizedTree) { + resultExpr = ((ParenthesizedTree) resultExpr).getExpression(); + path = new TreePath(path, resultExpr); + } + VisitorState resultState = path == state.getPath() ? state : state.withPath(path); + return new ExprTreeAndState(resultExpr, resultState); + } } 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 b3ee37f720..ee0efadb8d 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -28,7 +28,6 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; -import com.sun.source.util.TreePathScanner; import com.sun.source.util.TreeScanner; import com.sun.tools.javac.code.Attribute; import com.sun.tools.javac.code.Symbol; @@ -425,7 +424,12 @@ private void reportInvalidOverridingMethodParamTypeError( * @return Type of the tree with preserved annotations. */ private @Nullable Type getTreeType(Tree tree, VisitorState state) { - tree = ASTHelpers.stripParentheses(tree); + if (tree instanceof ExpressionTree exprTree) { + NullabilityUtil.ExprTreeAndState exprTreeAndState = + NullabilityUtil.stripParensAndUpdateTreePath(exprTree, state); + tree = exprTreeAndState.expr(); + state = exprTreeAndState.state(); + } if (tree instanceof LambdaExpressionTree || tree instanceof MemberReferenceTree) { Type result = inferredPolyExpressionTypes.get(tree); if (result == null) { @@ -545,10 +549,10 @@ private void reportInvalidOverridingMethodParamTypeError( * available. */ private @Nullable Type getDiamondTypeFromContext(NewClassTree tree, VisitorState state) { - TreePath treePath = findPathToSubtree(state.getPath(), tree); - if (treePath == null) { - return null; - } + TreePath treePath = state.getPath(); + // if (treePath == null) { + // return null; + // } return getDiamondTypeFromParentContext(tree, state, castToNonNull(treePath.getParentPath())); } @@ -629,28 +633,28 @@ private void reportInvalidOverridingMethodParamTypeError( } /** Finds the path to {@code target} within {@code rootPath}, or null when not found. */ - private static @Nullable TreePath findPathToSubtree(TreePath rootPath, Tree target) { - if (rootPath.getLeaf() == target) { - return rootPath; - } - return new TreePathScanner<@Nullable TreePath, @Nullable TreePath>() { - @Override - public @Nullable TreePath scan(Tree tree, @Nullable TreePath prevPath) { - if (tree == target) { - // When overriding scan(), getCurrentPath() still points at the parent. - return new TreePath(getCurrentPath(), tree); - } - return super.scan(tree, null); - } - - @Override - public @Nullable TreePath reduce(@Nullable TreePath r1, @Nullable TreePath r2) { - // we should only find the target once, so at most one of r1 and r2 should be non-null - Verify.verify(r1 == null || r2 == null); - return r1 != null ? r1 : r2; - } - }.scan(rootPath, null); - } + // private static @Nullable TreePath findPathToSubtree(TreePath rootPath, Tree target) { + // if (rootPath.getLeaf() == target) { + // return rootPath; + // } + // return new TreePathScanner<@Nullable TreePath, @Nullable TreePath>() { + // @Override + // public @Nullable TreePath scan(Tree tree, @Nullable TreePath prevPath) { + // if (tree == target) { + // // When overriding scan(), getCurrentPath() still points at the parent. + // return new TreePath(getCurrentPath(), tree); + // } + // return super.scan(tree, null); + // } + // + // @Override + // public @Nullable TreePath reduce(@Nullable TreePath r1, @Nullable TreePath r2) { + // // we should only find the target once, so at most one of r1 and r2 should be non-null + // Verify.verify(r1 == null || r2 == null); + // return r1 != null ? r1 : r2; + // } + // }.scan(rootPath, null); + // } /** * Returns true when javac inferred class type arguments for a constructor call, i.e. there are @@ -753,7 +757,8 @@ public void checkTypeParameterNullnessForAssignability(Tree tree, VisitorState s && isAssignmentToField(tree)) { maybeStoreLambdaTypeFromTarget(lambdaExpressionTree, lhsType); } - Type rhsType = getTreeType(rhsTree, state); + TreePath pathToRhs = new TreePath(state.getPath(), rhsTree); + Type rhsType = getTreeType(rhsTree, state.withPath(pathToRhs)); if (rhsType != null) { if (isGenericCallNeedingInference(rhsTree)) { rhsType = From bc760124afadf9d60a2d0efcecb01596b363995f Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 19 Feb 2026 15:47:55 -0800 Subject: [PATCH 33/34] fixes --- .../nullaway/generics/GenericsChecks.java | 38 ++++--------------- 1 file changed, 7 insertions(+), 31 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 ee0efadb8d..6827769679 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -549,11 +549,8 @@ private void reportInvalidOverridingMethodParamTypeError( * available. */ private @Nullable Type getDiamondTypeFromContext(NewClassTree tree, VisitorState state) { - TreePath treePath = state.getPath(); - // if (treePath == null) { - // return null; - // } - return getDiamondTypeFromParentContext(tree, state, castToNonNull(treePath.getParentPath())); + return getDiamondTypeFromParentContext( + tree, state, castToNonNull(state.getPath().getParentPath())); } /** @@ -632,30 +629,6 @@ private void reportInvalidOverridingMethodParamTypeError( return formalParamTypeRef.get(); } - /** Finds the path to {@code target} within {@code rootPath}, or null when not found. */ - // private static @Nullable TreePath findPathToSubtree(TreePath rootPath, Tree target) { - // if (rootPath.getLeaf() == target) { - // return rootPath; - // } - // return new TreePathScanner<@Nullable TreePath, @Nullable TreePath>() { - // @Override - // public @Nullable TreePath scan(Tree tree, @Nullable TreePath prevPath) { - // if (tree == target) { - // // When overriding scan(), getCurrentPath() still points at the parent. - // return new TreePath(getCurrentPath(), tree); - // } - // return super.scan(tree, null); - // } - // - // @Override - // public @Nullable TreePath reduce(@Nullable TreePath r1, @Nullable TreePath r2) { - // // we should only find the target once, so at most one of r1 and r2 should be non-null - // Verify.verify(r1 == null || r2 == null); - // return r1 != null ? r1 : r2; - // } - // }.scan(rootPath, null); - // } - /** * Returns true when javac inferred class type arguments for a constructor call, i.e. there are * instantiated type arguments at the type level, but no explicit non-diamond source type args. @@ -1481,7 +1454,8 @@ public void checkTypeParameterNullnessForFunctionReturnType( // bail out of any checking involving raw types for now return; } - Type returnExpressionType = getTreeType(retExpr, state); + TreePath pathToRetExpr = new TreePath(state.getPath(), retExpr); + Type returnExpressionType = getTreeType(retExpr, state.withPath(pathToRetExpr)); if (returnExpressionType != null) { if (isGenericCallNeedingInference(retExpr)) { returnExpressionType = @@ -1676,7 +1650,9 @@ public void compareGenericTypeParameterNullabilityForCall( if (inferredPolyType != null) { actualParameterType = inferredPolyType; } else { - actualParameterType = getTreeType(currentActualParam, state); + TreePath pathToActualParam = new TreePath(state.getPath(), currentActualParam); + actualParameterType = + getTreeType(currentActualParam, state.withPath(pathToActualParam)); } if (actualParameterType != null) { if (isGenericCallNeedingInference(currentActualParam)) { From 2883e5a157f5ffeeb13bf9171f0a3f08f4e2f5e9 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 19 Feb 2026 15:57:36 -0800 Subject: [PATCH 34/34] add case and TODO for ConditionalExpressionTree --- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 5 +++++ 1 file changed, 5 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 6827769679..95f30d1a61 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -609,6 +609,11 @@ private void reportInvalidOverridingMethodParamTypeError( parentConstructorCall, parentCtorType.asMethodType(), tree); } } + if (parent instanceof ConditionalExpressionTree) { + // TODO infer diamond type from the overall conditional expression type + // tracked in https://github.com/uber/NullAway/issues/1477 + return null; + } return null; }