Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infer generics for assignments #1131

Open
wants to merge 64 commits into
base: master
Choose a base branch
from
Open

Conversation

haewiful
Copy link
Contributor

  • Infer Nullability for assignments of instances with type parameters. ex) Foo<@Nullable Object> f = Foo.make(null);

  • void genericInferenceOnAssignments() in GenericMethodTests.java

@msridhar
Copy link
Collaborator

@haewiful thanks for this! It looks like CI fails on the :nullaway:buildWithNullAway task:

https://github.com/uber/NullAway/actions/runs/12978381939/job/36192901792?pr=1131#step:5:606

I suggest looking at the code it is crashing on and trying to write a unit test that causes the same failure, which will make it easier to debug.

Copy link

codecov bot commented Feb 3, 2025

Codecov Report

Attention: Patch coverage is 92.20779% with 6 lines in your changes missing coverage. Please review.

Project coverage is 88.16%. Comparing base (15c817a) to head (b45bbad).

Files with missing lines Patch % Lines
...ava/com/uber/nullaway/generics/GenericsChecks.java 92.68% 0 Missing and 3 partials ⚠️
...a/com/uber/nullaway/generics/InferTypeVisitor.java 90.32% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1131      +/-   ##
============================================
+ Coverage     88.12%   88.16%   +0.03%     
- Complexity     2268     2286      +18     
============================================
  Files            87       88       +1     
  Lines          7438     7512      +74     
  Branches       1484     1500      +16     
============================================
+ Hits           6555     6623      +68     
- Misses          445      446       +1     
- Partials        438      443       +5     

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

@haewiful haewiful marked this pull request as ready for review February 10, 2025 08:44
Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

Looking good! Some feedback below

@@ -277,6 +277,8 @@ public Config getConfig() {
*/
private final Map<ExpressionTree, Nullness> computedNullnessMap = new LinkedHashMap<>();

private GenericsChecks genericsChecks = new GenericsChecks();
Copy link
Collaborator

Choose a reason for hiding this comment

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

In a follow up (not for this PR), we should probably make a Config field inside GenericsChecks and then stop passing it as a parameter to a whole bunch of methods. Again not for this PR

@@ -46,8 +46,7 @@
/** Methods for performing checks related to generic types and nullability. */
public final class GenericsChecks {

/** Do not instantiate; all methods should be static */
private GenericsChecks() {}
private final Map<Tree, Map<Type, Type>> inferredTypes = new HashMap<>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add Javadoc on what this is. For now, can the key type for the outer map be MethodInvocationTree? And are the keys of the nested maps always TypeVariables?

// update inferredTypes cache for assignments
if (methodSymbol.type instanceof Type.ForAll // generic method call
&& methodInvocationTree.getTypeArguments().isEmpty() // no explicit generic arguments
&& lhsTypeTree instanceof ParameterizedTypeTree) { // lhs type has generic
Copy link
Collaborator

Choose a reason for hiding this comment

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

Rather than checking the tree, can we check if lhsType is generic? That might be more general.

@@ -443,6 +487,58 @@ public static void checkTypeParameterNullnessForAssignability(
}
}

private @Nullable Type inferMethodTypeArgument(
Type typeParam, List<Type> lhsTypeArg, List<Type> typeArg, VisitorState state) {
Type inferType = null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Declare this variable inside the else if case in the loop. Declaring it up here obscures the logic

}
}
// base case: typeArg doesn't contain typeParam
return inferType;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here just do return null

if (getTypeNullness(upperBound, analysis.getConfig()) == Nullness.NULLABLE) {
Type lhsInferredType =
inferMethodTypeArgument(
typeParam.get(i).type,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Store this expression in a local variable with a readable name, like curTypeParam, and re-use it

@@ -443,6 +487,58 @@ public static void checkTypeParameterNullnessForAssignability(
}
}

private @Nullable Type inferMethodTypeArgument(
Copy link
Collaborator

Choose a reason for hiding this comment

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

add javadoc for this method

@@ -443,6 +487,58 @@ public static void checkTypeParameterNullnessForAssignability(
}
}

private @Nullable Type inferMethodTypeArgument(
Copy link
Collaborator

Choose a reason for hiding this comment

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

For both this method and the next one (replaceGenerics), I think you need a proper visitor rather than just a recursive function. E.g., what if you have a generic method with return type Foo<U>[], or Foo<Foo<U>[]>? I don't think the current code will handle it.

return inferType;
}

private Type replaceGenerics(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this method be rewritten as a call to com.uber.nullaway.generics.TypeSubstitutionUtils#subst?

Tree rhsTree;
if (tree instanceof VariableTree) {
VariableTree varTree = (VariableTree) tree;
rhsTree = varTree.getInitializer();

if (rhsTree instanceof MethodInvocationTree) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this logic is not in the right place, as it won't work for regular assignments; it only runs for variable declarations. I added a failing test to illustrate.

Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

Getting there! Few more comments and another failing test

import org.jspecify.annotations.Nullable;

/** Visitor that uses two types to infer the type of type variables. */
public class InferTypeVisitor
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that returning Maps from each visit method can work, but it's a bit convoluted and also inefficient (as many short-lived maps may be allocated). Instead, I suggest making the genericNullness map an instance field of InferTypeVisitor, initialized to an empty map. The visit methods could mutate this map. And then, at the end, the caller could call a method like getGenericNullessMap() to get the result. With this approach, this class could extend Types.DefaultTypeVisitor<Void, Type>, and all the visit methods would just return null.

// rhsTree can be null for a VariableTree. Also, we don't need to do a check
// if rhsTree is the null literal
if (rhsTree == null || rhsTree.getKind().equals(Tree.Kind.NULL_LITERAL)) {
return;
}
Type rhsType = getTreeType(rhsTree, config);

if (lhsType != null && rhsType != null) {
if (rhsTree instanceof MethodInvocationTree) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Couldn't we move the code under the if at line 440 to also be under this if? We'd still need to store genericNullness inside inferredTypes but then we could just use it immediately here to reconstruct the inferred type.

Comment on lines +468 to +474
List<Type> keyTypeList =
genericNullness.keySet().stream()
.map(typeVar -> (Type) typeVar)
.collect(Collectors.toList());
com.sun.tools.javac.util.List<Type> from = com.sun.tools.javac.util.List.from(keyTypeList);
com.sun.tools.javac.util.List<Type> to =
com.sun.tools.javac.util.List.from(genericNullness.values());
Copy link
Collaborator

Choose a reason for hiding this comment

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

From this code, it's unclear that the from and to lists will end up in the appropriate order, where the right keys are lined up with the right values. (It might work with LinkedHashMaps but the code doesn't use those or enforce that right now.) Instead, I suggest looping over the entries of genericNullness, and building the lists one key-value pair at a time. You can build up the right list type using a ListBuffer and calling append on it, like here:

" return new Foo<>(u);",
" }",
" static void test(Foo<@Nullable Object> f1, Foo<Object> f2) {",
" // no error expected",
Copy link
Collaborator

@msridhar msridhar Feb 22, 2025

Choose a reason for hiding this comment

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

This test currently does not pass. I would expect no error for the first call to create, but an error for the second call to create (since the second argument is Foo<Object>). Do you agree? If so we need to fix this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants