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

Extend library models to mark fields as nullable #878

Merged
merged 16 commits into from
Dec 15, 2023
Merged
62 changes: 62 additions & 0 deletions nullaway/src/main/java/com/uber/nullaway/LibraryModels.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@
*/
ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods();

/**
* Get the set of library fields that may be null. This is mostly used to index the impact of
* making fields <code>@Nullable</code> on downstream dependencies.
*
* @return set of library fields that may be <code>null</code>.
*/
ImmutableSet<FieldRef> nullableFields();

/**
* Get a list of custom stream library specifications.
*
Expand Down Expand Up @@ -244,4 +252,58 @@
+ '}';
}
}

/** Representation of a field as a qualified class name + a field name */
final class FieldRef {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we just use AutoValue for this class, since we already rely on it? Would get rid of a lot of boilerplate. See, e.g.:

https://github.com/uber/NullAway/blob/master/nullaway/src/main/java/com/uber/nullaway/dataflow/DataFlow.java#L318-L318

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@msridhar Sure. Used @AutoValue : f15b983


public final String enclosingClass;

public final String fieldName;

private FieldRef(String enclosingClass, String fieldName) {
this.enclosingClass = enclosingClass;
this.fieldName = fieldName;
}

/**
* Construct a field reference.
*
* @param enclosingClass containing flat name class
* @param fieldName field name
* @return corresponding {@link FieldRef}
*/
public static FieldRef fieldRef(String enclosingClass, String fieldName) {
return new FieldRef(enclosingClass, fieldName);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;

Check warning on line 282 in nullaway/src/main/java/com/uber/nullaway/LibraryModels.java

View check run for this annotation

Codecov / codecov/patch

nullaway/src/main/java/com/uber/nullaway/LibraryModels.java#L282

Added line #L282 was not covered by tests
}
if (o == null || getClass() != o.getClass()) {
return false;

Check warning on line 285 in nullaway/src/main/java/com/uber/nullaway/LibraryModels.java

View check run for this annotation

Codecov / codecov/patch

nullaway/src/main/java/com/uber/nullaway/LibraryModels.java#L285

Added line #L285 was not covered by tests
}
FieldRef fieldRef = (FieldRef) o;

Check warning on line 287 in nullaway/src/main/java/com/uber/nullaway/LibraryModels.java

View check run for this annotation

Codecov / codecov/patch

nullaway/src/main/java/com/uber/nullaway/LibraryModels.java#L287

Added line #L287 was not covered by tests
return Objects.equals(enclosingClass, fieldRef.enclosingClass)
&& Objects.equals(fieldName, fieldRef.fieldName);
}

@Override
public int hashCode() {
return Objects.hash(enclosingClass, fieldName);
}

@Override
public String toString() {
return "FieldRef{"

Check warning on line 299 in nullaway/src/main/java/com/uber/nullaway/LibraryModels.java

View check run for this annotation

Codecov / codecov/patch

nullaway/src/main/java/com/uber/nullaway/LibraryModels.java#L299

Added line #L299 was not covered by tests
+ "enclosingClass='"
+ enclosingClass
+ '\''
+ ", field name='"
+ fieldName
+ '\''
+ '}';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import java.util.Set;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.checkerframework.nullaway.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.nullaway.dataflow.cfg.node.Node;

Expand All @@ -80,6 +81,20 @@ public LibraryModelsHandler(Config config) {
libraryModels = loadLibraryModels(config);
}

@Override
public NullnessHint onDataflowVisitFieldAccess(
FieldAccessNode node,
Symbol symbol,
Types types,
Context context,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates updates) {
return isNullableFieldInLibraryModels(symbol)
? NullnessHint.HINT_NULLABLE
: NullnessHint.UNKNOWN;
}

@Override
public Nullness[] onOverrideMethodInvocationParametersNullability(
VisitorState state,
Expand Down Expand Up @@ -132,6 +147,9 @@ public boolean onOverrideMayBeNullExpr(
@Nullable Symbol exprSymbol,
VisitorState state,
boolean exprMayBeNull) {
if (isNullableFieldInLibraryModels(exprSymbol)) {
return true;
}
if (!(expr.getKind() == Tree.Kind.METHOD_INVOCATION
&& exprSymbol instanceof Symbol.MethodSymbol)) {
return exprMayBeNull;
Expand Down Expand Up @@ -233,6 +251,34 @@ public NullnessHint onDataflowVisitMethodInvocation(
}
}

/**
* Check if the given symbol is a field that is marked as nullable in any of our library models.
*
* @param symbol The symbol to check.
* @return True if the symbol is a field that is marked as nullable in any of our library models.
*/
private boolean isNullableFieldInLibraryModels(@Nullable Symbol symbol) {
if (symbol instanceof Symbol.VarSymbol && symbol.getKind().isField()) {
msridhar marked this conversation as resolved.
Show resolved Hide resolved
Symbol.VarSymbol varSymbol = (Symbol.VarSymbol) symbol;
Symbol.ClassSymbol classSymbol = varSymbol.enclClass();
if (classSymbol == null) {
// e.g. .class expressions
return false;
}
String fieldName = varSymbol.getSimpleName().toString();
String enclosingClassName = classSymbol.flatName().toString();
// check presence. This is a bit inefficient, but it's only used while indexing impact of
// making fields @Nullable on downstream dependencies. Also, the set of nullable fields is not
// expected to be large. We can optimize in future if needed.
return libraryModels.nullableFields().stream()
.anyMatch(
fieldRef ->
fieldRef.fieldName.equals(fieldName)
&& fieldRef.enclosingClass.equals(enclosingClassName));
}
return false;
}

private void setConditionalArgumentNullness(
AccessPathNullnessPropagation.Updates thenUpdates,
AccessPathNullnessPropagation.Updates elseUpdates,
Expand Down Expand Up @@ -824,6 +870,12 @@ public ImmutableSet<MethodRef> nonNullReturns() {
public ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods() {
return CAST_TO_NONNULL_METHODS;
}

@Override
public ImmutableSet<FieldRef> nullableFields() {
// No nullable field by default.
return ImmutableSet.of();
}
}

private static class CombinedLibraryModels implements LibraryModels {
Expand All @@ -846,6 +898,8 @@ private static class CombinedLibraryModels implements LibraryModels {

private final ImmutableSet<MethodRef> nonNullReturns;

private final ImmutableSet<FieldRef> nullableFields;

private final ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods;

private final ImmutableList<StreamTypeRecord> customStreamNullabilitySpecs;
Expand All @@ -870,6 +924,7 @@ public CombinedLibraryModels(Iterable<LibraryModels> models, Config config) {
new ImmutableSetMultimap.Builder<>();
ImmutableList.Builder<StreamTypeRecord> customStreamNullabilitySpecsBuilder =
new ImmutableList.Builder<>();
ImmutableSet.Builder<FieldRef> nullableFieldsBuilder = new ImmutableSet.Builder<>();
for (LibraryModels libraryModels : models) {
for (Map.Entry<MethodRef, Integer> entry : libraryModels.failIfNullParameters().entries()) {
if (shouldSkipModel(entry.getKey())) {
Expand Down Expand Up @@ -932,6 +987,9 @@ public CombinedLibraryModels(Iterable<LibraryModels> models, Config config) {
for (StreamTypeRecord streamTypeRecord : libraryModels.customStreamNullabilitySpecs()) {
customStreamNullabilitySpecsBuilder.add(streamTypeRecord);
}
for (FieldRef fieldRef : libraryModels.nullableFields()) {
nullableFieldsBuilder.add(fieldRef);
}
}
failIfNullParameters = failIfNullParametersBuilder.build();
explicitlyNullableParameters = explicitlyNullableParametersBuilder.build();
Expand All @@ -943,6 +1001,7 @@ public CombinedLibraryModels(Iterable<LibraryModels> models, Config config) {
nonNullReturns = nonNullReturnsBuilder.build();
castToNonNullMethods = castToNonNullMethodsBuilder.build();
customStreamNullabilitySpecs = customStreamNullabilitySpecsBuilder.build();
nullableFields = nullableFieldsBuilder.build();
}

private boolean shouldSkipModel(MethodRef key) {
Expand Down Expand Up @@ -989,6 +1048,11 @@ public ImmutableSet<MethodRef> nonNullReturns() {
return nonNullReturns;
}

@Override
public ImmutableSet<FieldRef> nullableFields() {
return nullableFields;
}

@Override
public ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods() {
return castToNonNullMethods;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,34 @@ public void libraryModelsAndSelectiveSkippingViaCommandLineOptions2() {
"}")
.doTest();
}

@Test
public void libraryModelsAndOverridingFieldNullability() {
msridhar marked this conversation as resolved.
Show resolved Hide resolved
makeLibraryModelsTestHelperWithArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.lib.unannotated.UnannotatedWithModels;",
"public class Test {",
" UnannotatedWithModels uwm = new UnannotatedWithModels();",
" Object nonnullField = new Object();",
" void assignNullableFromLibraryModelField() {",
" // BUG: Diagnostic contains: assigning @Nullable",
" this.nonnullField = uwm.nullableFieldUnannotated1;",
" // BUG: Diagnostic contains: assigning @Nullable",
" this.nonnullField = uwm.nullableFieldUnannotated2;",
" }",
" void flowTest() {",
msridhar marked this conversation as resolved.
Show resolved Hide resolved
" if(uwm.nullableFieldUnannotated1 != null){",
" // no error here, to check that library models only initialize flow store",
" this.nonnullField = uwm.nullableFieldUnannotated1;",
" }",
" }",
"}")
.doTest();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,9 @@ public ImmutableSet<MethodRef> nonNullReturns() {
public ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods() {
return ImmutableSetMultimap.of();
}

@Override
public ImmutableSet<FieldRef> nullableFields() {
return ImmutableSet.of();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

public class UnannotatedWithModels {

public Object nullableFieldUnannotated1;

public Object nullableFieldUnannotated2;
msridhar marked this conversation as resolved.
Show resolved Hide resolved

public Object returnsNullUnannotated() {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.uber.nullaway.testlibrarymodels;

import static com.uber.nullaway.LibraryModels.FieldRef.fieldRef;
import static com.uber.nullaway.LibraryModels.MethodRef.methodRef;

import com.google.auto.service.AutoService;
Expand Down Expand Up @@ -122,4 +123,13 @@ public ImmutableList<StreamTypeRecord> customStreamNullabilitySpecs() {
.withPassthroughMethodFromSignature("distinct()")
.end();
}

@Override
public ImmutableSet<FieldRef> nullableFields() {
return ImmutableSet.<FieldRef>builder()
.add(
fieldRef("com.uber.lib.unannotated.UnannotatedWithModels", "nullableFieldUnannotated1"),
fieldRef("com.uber.lib.unannotated.UnannotatedWithModels", "nullableFieldUnannotated2"))
.build();
}
}