-
Notifications
You must be signed in to change notification settings - Fork 301
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
base: master
Are you sure you want to change the base?
Conversation
@haewiful thanks for this! It looks like CI fails on the 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. |
# Conflicts: # nullaway/src/main/java/com/uber/nullaway/NullAway.java
Codecov ReportAttention: Patch coverage is
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. |
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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<>(); |
There was a problem hiding this comment.
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 TypeVariable
s?
// 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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that returning Map
s 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) { |
There was a problem hiding this comment.
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.
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()); |
There was a problem hiding this comment.
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 LinkedHashMap
s 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:
NullAway/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java
Line 181 in 15c817a
ListBuffer<Type> buf = new ListBuffer<>(); |
" return new Foo<>(u);", | ||
" }", | ||
" static void test(Foo<@Nullable Object> f1, Foo<Object> f2) {", | ||
" // no error expected", |
There was a problem hiding this comment.
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.
Infer Nullability for assignments of instances with type parameters. ex)
Foo<@Nullable Object> f = Foo.make(null);
void genericInferenceOnAssignments()
in GenericMethodTests.java