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

External Library Models: Adding support for Nullable upper bounds of Generic Type parameters #949

Merged
merged 21 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -449,7 +450,13 @@ private void writeModel(DataOutputStream out) throws IOException {
nullableReturnMethodSign,
MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of()));
}
StubxWriter.write(out, importedAnnotations, packageAnnotations, typeAnnotations, methodRecords);
StubxWriter.write(
out,
importedAnnotations,
packageAnnotations,
typeAnnotations,
methodRecords,
Collections.emptyMap());
}

private void writeAnnotations(String inPath, String outFile) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
testImplementation project(":nullaway")
testImplementation project(":library-model:test-library-model-generator")
testImplementation deps.test.junit4
testImplementation deps.build.jspecify
testImplementation(deps.build.errorProneTestHelpers) {
exclude group: "junit", module: "junit"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,32 @@ public void libraryModelInnerClassNullableReturnsTest() {
"}")
.doTest();
}

@Test
public void libraryModelInnerClassNullableUpperBoundsTest() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:JSpecifyMode=true",
"-XepOpt:NullAway:JarInferEnabled=true",
"-XepOpt:NullAway:JarInferUseReturnAnnotations=true"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"import com.uber.nullaway.libmodel.AnnotationExample;",
"class Test {",
msridhar marked this conversation as resolved.
Show resolved Hide resolved
" static AnnotationExample.UpperBoundExample<@Nullable Object> upperBoundExample = new AnnotationExample.UpperBoundExample<@Nullable Object>();",
" static void test(Object value){",
" }",
" static void testPositive() {",
" // BUG: Diagnostic contains: passing @Nullable parameter 'upperBoundExample.getNull()'",
" test(upperBoundExample.getNull());",
" }",
"}")
.doTest();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.type.ArrayType;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.TypeParameter;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import com.github.javaparser.utils.CollectionStrategy;
import com.github.javaparser.utils.ParserCollectionStrategy;
Expand All @@ -47,8 +48,10 @@
import java.nio.file.Paths;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
* Utilized for generating an astubx file from a directory containing annotated Java source code.
Expand All @@ -59,21 +62,18 @@
*/
public class LibraryModelGenerator {

public void generateAstubxForLibraryModels(String inputSourceDirectory, String outputDirectory) {
Map<String, MethodAnnotationsRecord> methodRecords = processDirectory(inputSourceDirectory);
writeToAstubx(outputDirectory, methodRecords);
}

/**
* Parses all the source files within the directory using javaparser.
*
* @param sourceDirectoryRoot Directory containing annotated java source files.
* @return a Map containing the Nullability annotation information from the source files.
* @param inputSourceDirectory Directory containing annotated java source files.
* @param outputDirectory Directory to write the astubx file into.
*/
private Map<String, MethodAnnotationsRecord> processDirectory(String sourceDirectoryRoot) {
public void generateAstubxForLibraryModels(String inputSourceDirectory, String outputDirectory) {
Map<String, MethodAnnotationsRecord> methodRecords = new LinkedHashMap<>();
Path root = dirnameToPath(sourceDirectoryRoot);
AnnotationCollectorCallback ac = new AnnotationCollectorCallback(methodRecords);
Map<String, Set<Integer>> nullableUpperBounds = new LinkedHashMap<>();
Path root = dirnameToPath(inputSourceDirectory);
AnnotationCollectorCallback ac =

Check warning on line 75 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L73-L75

Added lines #L73 - L75 were not covered by tests
new AnnotationCollectorCallback(methodRecords, nullableUpperBounds);
CollectionStrategy strategy = new ParserCollectionStrategy();
// Required to include directories that contain a module-info.java, which don't parse by
// default.
Expand All @@ -90,7 +90,7 @@
throw new RuntimeException(e);
}
});
return methodRecords;
writeToAstubx(outputDirectory, methodRecords, nullableUpperBounds);

Check warning on line 93 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L93

Added line #L93 was not covered by tests
}

/**
Expand All @@ -100,8 +100,10 @@
* @param methodRecords Map containing the collected Nullability annotation information.
*/
private void writeToAstubx(
String outputPath, Map<String, MethodAnnotationsRecord> methodRecords) {
if (methodRecords.isEmpty()) {
String outputPath,
Map<String, MethodAnnotationsRecord> methodRecords,
Map<String, Set<Integer>> nullableUpperBounds) {
if (methodRecords.isEmpty() && nullableUpperBounds.isEmpty()) {
return;
}
Map<String, String> importedAnnotations =
Expand All @@ -117,7 +119,8 @@
importedAnnotations,
Collections.emptyMap(),
Collections.emptyMap(),
methodRecords);
methodRecords,
nullableUpperBounds);
}
} catch (IOException e) {
throw new RuntimeException(e);
Expand All @@ -137,8 +140,11 @@

private final AnnotationCollectionVisitor annotationCollectionVisitor;

public AnnotationCollectorCallback(Map<String, MethodAnnotationsRecord> methodRecords) {
this.annotationCollectionVisitor = new AnnotationCollectionVisitor(methodRecords);
public AnnotationCollectorCallback(
Map<String, MethodAnnotationsRecord> methodRecords,
Map<String, Set<Integer>> nullableUpperBounds) {
this.annotationCollectionVisitor =

Check warning on line 146 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L145-L146

Added lines #L145 - L146 were not covered by tests
new AnnotationCollectionVisitor(methodRecords, nullableUpperBounds);
}

@Override
Expand All @@ -158,14 +164,18 @@
private String parentName = "";
private boolean isJspecifyNullableImportPresent = false;
private boolean isNullMarked = false;
private Map<String, MethodAnnotationsRecord> methodRecords;
private final Map<String, MethodAnnotationsRecord> methodRecords;
private final Map<String, Set<Integer>> nullableUpperBounds;
private static final String ARRAY_RETURN_TYPE_STRING = "Array";
private static final String NULL_MARKED = "NullMarked";
private static final String NULLABLE = "Nullable";
private static final String JSPECIFY_NULLABLE_IMPORT = "org.jspecify.annotations.Nullable";

public AnnotationCollectionVisitor(Map<String, MethodAnnotationsRecord> methodRecords) {
public AnnotationCollectionVisitor(
Map<String, MethodAnnotationsRecord> methodRecords,
Map<String, Set<Integer>> nullableUpperBounds) {

Check warning on line 176 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L176

Added line #L176 was not covered by tests
this.methodRecords = methodRecords;
this.nullableUpperBounds = nullableUpperBounds;

Check warning on line 178 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L178

Added line #L178 was not covered by tests
}

@Override
Expand Down Expand Up @@ -197,6 +207,12 @@
this.isNullMarked = true;
}
});
if (this.isNullMarked) {
Set<Integer> nullableUpperBoundParams = getGenericTypeParameterNullableUpperBounds(cid);

Check warning on line 211 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L211

Added line #L211 was not covered by tests
if (!nullableUpperBoundParams.isEmpty()) {
nullableUpperBounds.put(parentName, nullableUpperBoundParams);

Check warning on line 213 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L213

Added line #L213 was not covered by tests
}
}
super.visit(cid, null);
// We reset the variable that constructs the parent name after visiting all the children.
parentName = parentName.substring(0, parentName.lastIndexOf("." + cid.getNameAsString()));
Expand Down Expand Up @@ -261,5 +277,27 @@
&& this.isJspecifyNullableImportPresent)
|| annotation.getNameAsString().equalsIgnoreCase(JSPECIFY_NULLABLE_IMPORT);
}

/**
* Takes a ClassOrInterfaceDeclaration instance and returns a Map of the indexes and a set of
* annotations for generic type parameters with Nullable upper bounds.
*
* @param cid ClassOrInterfaceDeclaration instance.
* @return Set of indices for generic type parameters with Nullable upper bounds.
*/
private ImmutableSet<Integer> getGenericTypeParameterNullableUpperBounds(
ClassOrInterfaceDeclaration cid) {
ImmutableSet.Builder<Integer> setBuilder = ImmutableSet.builder();
List<TypeParameter> typeParamList = cid.getTypeParameters();

Check warning on line 291 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L290-L291

Added lines #L290 - L291 were not covered by tests
for (int i = 0; i < typeParamList.size(); i++) {
TypeParameter param = typeParamList.get(i);

Check warning on line 293 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L293

Added line #L293 was not covered by tests
for (ClassOrInterfaceType type : param.getTypeBound()) {
if (type.isAnnotationPresent(NULLABLE)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't we need logic here like we have in the isAnnotationNullable method, to handle fully-qualified annotations?

setBuilder.add(i);

Check warning on line 296 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L296

Added line #L296 was not covered by tests
}
}

Check warning on line 298 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L298

Added line #L298 was not covered by tests
}
return setBuilder.build();

Check warning on line 300 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java#L300

Added line #L300 was not covered by tests
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
Map<String, String> importedAnnotations,
Map<String, Set<String>> packageAnnotations,
Map<String, Set<String>> typeAnnotations,
Map<String, MethodAnnotationsRecord> methodRecords)
Map<String, MethodAnnotationsRecord> methodRecords,
Map<String, Set<Integer>> nullableUpperBounds)
throws IOException {
// File format version/magic number
out.writeInt(VERSION_0_FILE_MAGIC_NUMBER);
Expand All @@ -49,7 +50,8 @@
importedAnnotations.values(),
packageAnnotations.keySet(),
typeAnnotations.keySet(),
methodRecords.keySet());
methodRecords.keySet(),
nullableUpperBounds.keySet());
for (Collection<String> keyset : keysets) {
for (String key : keyset) {
assert !encodingDictionary.containsKey(key);
Expand Down Expand Up @@ -118,5 +120,17 @@
}
}
}
// Followed by the number of nullable upper bounds records
out.writeInt(nullableUpperBounds.size());
for (Map.Entry<String, Set<Integer>> entry : nullableUpperBounds.entrySet()) {
// Followed by the number of parameters with nullable upper bound
Set<Integer> parameters = entry.getValue();
out.writeInt(parameters.size());

Check warning on line 128 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java#L127-L128

Added lines #L127 - L128 were not covered by tests
for (Integer parameter : parameters) {
// Followed by the nullable upper bound record as a par of integers
out.writeInt(encodingDictionary.get(entry.getKey()));
out.writeInt(parameter);
}
}

Check warning on line 134 in library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java

View check run for this annotation

Codecov / codecov/patch

library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java#L131-L134

Added lines #L131 - L134 were not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ public String returnNull() {
return null;
}
}

/** In the library model we add a {@code @Nullable} upper bound for T */
public static class UpperBoundExample<T> {

public T getNull() {
return null;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a bit weird since the method always returns null, so the return type should really be @Nullable T. Maybe, for readability, rewrite this class to have a public @Nullable field and return that field from getNull()? And maybe rename to getNullable()? Same changes in the annotated version

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should the return type of the method be @Nullable T in the unannotated version as well or only in the annotated version?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not suggesting to change the return type to @Nullable T. I'm suggesting to have a field of type T and to return the value of that field, rather than just returning null. Then the return type T would be appropriate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh that makes sense, thanks!

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,11 @@ public String returnNull() {
return null;
}
}

public static class UpperBoundExample<T extends @Nullable Object> {

public T getNull() {
return null;
}
}
}
Loading
Loading