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 Method parameters #1006

Merged
merged 47 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
fb19551
default and nullable params initial changes
akulk022 Jul 12, 2024
d76a3b2
Adding javaparser type solver to use fully qualified names for method…
akulk022 Jul 22, 2024
6ef1afe
Merge branch 'master' into library_model_nullable_params
akulk022 Jul 22, 2024
9b7d5c1
clean up
akulk022 Jul 22, 2024
0182cf9
Merge remote-tracking branch 'origin/library_model_nullable_params' i…
akulk022 Jul 22, 2024
96be113
specify javaparser version in one place
msridhar Jul 22, 2024
9be47fe
Merge branch 'master' into library_model_nullable_params
msridhar Aug 21, 2024
528ff22
Merge branch 'master' into library_model_nullable_params
msridhar Sep 4, 2024
972c977
WIP
msridhar Sep 4, 2024
38f639f
fix more tests
msridhar Sep 5, 2024
4d375d1
more
msridhar Sep 5, 2024
3f25628
resolving conflicts for merge
akulk022 Sep 7, 2024
8abe95e
Merge branch 'master' into library_model_nullable_params
akulk022 Sep 7, 2024
515bb29
updating test with fully qualified name
akulk022 Sep 7, 2024
e1daf7b
fix another
msridhar Sep 7, 2024
0681d1f
latest javaparser
msridhar Sep 7, 2024
5b29341
Merge branch 'master' into library_model_nullable_params
msridhar Sep 7, 2024
81ec20c
adding logic and test for checking null marked classes
akulk022 Sep 9, 2024
f61478d
updating test cases
akulk022 Sep 10, 2024
aaaa00a
adding test case for nullable parameters
akulk022 Sep 10, 2024
b372f53
Merge branch 'master' into library_model_nullable_params
akulk022 Sep 10, 2024
e5dc95f
removing unnecessary suppress warning
akulk022 Sep 10, 2024
e204963
Merge remote-tracking branch 'origin/library_model_nullable_params' i…
akulk022 Sep 10, 2024
3a5946b
clean up
akulk022 Sep 10, 2024
d83e024
adding check for Nullable annotation
akulk022 Sep 11, 2024
8083b57
Merge branch 'master' into library_model_nullable_params
akulk022 Sep 11, 2024
32f6abb
adding test case for explicitly @NonNull parameter
akulk022 Sep 12, 2024
ef50479
Merge remote-tracking branch 'origin/library_model_nullable_params' i…
akulk022 Sep 12, 2024
13083c1
Merge branch 'master' into library_model_nullable_params
akulk022 Sep 16, 2024
b0a25f4
updating logic for using Array type qualified names
akulk022 Sep 17, 2024
254ada4
updating logic for correctly taking the annotation for @Nullable arra…
akulk022 Sep 17, 2024
10ddcd3
updating test case logic
akulk022 Sep 17, 2024
2edafa6
Merge branch 'master' into library_model_nullable_params
akulk022 Sep 18, 2024
c3f669c
handling primitive return type scenario
akulk022 Sep 18, 2024
8600bf9
adding test cases for JarInfer
akulk022 Sep 19, 2024
eb832dc
test case
msridhar Sep 20, 2024
0b898b9
tweak test
msridhar Sep 20, 2024
6c1cea8
another test
msridhar Sep 20, 2024
55dc4e2
fix test?
msridhar Sep 20, 2024
6bd3185
more tests and tweaks
msridhar Sep 21, 2024
1e8ac44
fix tests
msridhar Sep 21, 2024
16ac993
bump astubx version number
msridhar Sep 21, 2024
f8fd564
suppress
msridhar Sep 21, 2024
d24267c
add integration test for nullable array parameter
msridhar Sep 21, 2024
29a902b
adding test case for void return type and updating the NullUnmarked test
akulk022 Sep 22, 2024
5a4e7cd
remove unnecessary locals
msridhar Sep 22, 2024
e1cbcf9
Merge branch 'master' into library_model_nullable_params
akulk022 Sep 22, 2024
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
4 changes: 3 additions & 1 deletion gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def versions = [
commonscli : "1.4",
autoValue : "1.10.2",
autoService : "1.1.1",
javaparser : "3.26.2",
]

def apt = [
Expand All @@ -75,7 +76,8 @@ def build = [
errorProneTestHelpersOld: "com.google.errorprone:error_prone_test_helpers:${oldestErrorProneVersion}",
checkerDataflow : "org.checkerframework:dataflow-nullaway:${versions.checkerFramework}",
guava : "com.google.guava:guava:30.1-jre",
javaparser : "com.github.javaparser:javaparser-core:3.26.0",
javaparser : "com.github.javaparser:javaparser-core:${versions.javaparser}",
javaparserSymbolSolver : "com.github.javaparser:javaparser-symbol-solver-core:${versions.javaparser}",
javaxValidation : "javax.validation:validation-api:2.0.1.Final",
jspecify : "org.jspecify:jspecify:1.0.0",
commonsIO : "commons-io:commons-io:2.11.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.ibm.wala.classLoader.PhantomClass;
import com.ibm.wala.classLoader.ShrikeCTMethod;
import com.ibm.wala.core.util.config.AnalysisScopeReader;
import com.ibm.wala.core.util.strings.StringStuff;
import com.ibm.wala.core.util.warnings.Warnings;
import com.ibm.wala.ipa.callgraph.AnalysisCache;
import com.ibm.wala.ipa.callgraph.AnalysisCacheImpl;
Expand Down Expand Up @@ -456,6 +457,7 @@ private void writeModel(DataOutputStream out) throws IOException {
packageAnnotations,
typeAnnotations,
methodRecords,
Collections.emptySet(),
Collections.emptyMap());
}

Expand Down Expand Up @@ -527,28 +529,6 @@ private static String getAstubxSignature(IMethod mtd) {
* @return String Unqualified type name.
*/
private static String getSimpleTypeName(TypeReference typ) {
ImmutableMap<String, String> mapFullTypeName =
ImmutableMap.<String, String>builder()
.put("B", "byte")
.put("C", "char")
.put("D", "double")
.put("F", "float")
.put("I", "int")
.put("J", "long")
.put("S", "short")
.put("Z", "boolean")
.build();
if (typ.isArrayType()) {
return "Array";
}
String typName = typ.getName().toString();
if (typName.startsWith("L")) {
typName = typName.split("<")[0].substring(1); // handle generics
typName = typName.substring(typName.lastIndexOf('/') + 1); // get unqualified name
typName = typName.substring(typName.lastIndexOf('$') + 1); // handle inner classes
} else {
typName = mapFullTypeName.get(typName);
}
return typName;
return StringStuff.jvmToBinaryName(typ.getName().toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@

/** Unit tests for {@link com.uber.nullaway.jarinfer}. */
@RunWith(JUnit4.class)
@SuppressWarnings("CheckTestExtendsBaseClass")
public class JarInferTest {
msridhar marked this conversation as resolved.
Show resolved Hide resolved

@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
Expand Down Expand Up @@ -206,8 +205,8 @@ public void toyStatic() throws Exception {
"toys",
"Test",
ImmutableMap.of(
"toys.Test:void test(String, Foo, Bar)", Sets.newHashSet(0, 2),
"toys.Foo:boolean run(String)", Sets.newHashSet(1)),
"toys.Test:void test(java.lang.String, toys.Foo, toys.Bar)", Sets.newHashSet(0, 2),
"toys.Foo:boolean run(java.lang.String)", Sets.newHashSet(1)),
"class Foo {",
" private String foo;",
" public Foo(String str) {",
Expand Down Expand Up @@ -267,7 +266,8 @@ public void toyNonStatic() throws Exception {
"toyNonStatic",
"toys",
"Foo",
ImmutableMap.of("toys.Foo:void test(String, String)", Sets.newHashSet(1)),
ImmutableMap.of(
"toys.Foo:void test(java.lang.String, java.lang.String)", Sets.newHashSet(1)),
"class Foo {",
" private String foo;",
" public Foo(String str) {",
Expand Down Expand Up @@ -303,7 +303,9 @@ public void toyNullTestAPI() throws Exception {
"toyNullTestAPI",
"toys",
"Foo",
ImmutableMap.of("toys.Foo:void test(String, String, String)", Sets.newHashSet(1, 3)),
ImmutableMap.of(
"toys.Foo:void test(java.lang.String, java.lang.String, java.lang.String)",
Sets.newHashSet(1, 3)),
"import com.google.common.base.Preconditions;",
"import java.util.Objects;",
"import org.junit.Assert;",
Expand Down Expand Up @@ -332,7 +334,9 @@ public void toyConditionalFlow() throws Exception {
"toyNullTestAPI",
"toys",
"Foo",
ImmutableMap.of("toys.Foo:void test(String, String, String)", Sets.newHashSet(1, 2)),
ImmutableMap.of(
"toys.Foo:void test(java.lang.String, java.lang.String, java.lang.String)",
Sets.newHashSet(1, 2)),
"import com.google.common.base.Preconditions;",
"import java.util.Objects;",
"import org.junit.Assert;",
Expand Down Expand Up @@ -362,7 +366,8 @@ public void toyConditionalFlow2() throws Exception {
"toys",
"Foo",
ImmutableMap.of(
"toys.Foo:void test(Object, Object, Object, Object)", Sets.newHashSet(1, 4)),
"toys.Foo:void test(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object)",
Sets.newHashSet(1, 4)),
"import com.google.common.base.Preconditions;",
"import java.util.Objects;",
"import org.junit.Assert;",
Expand Down Expand Up @@ -401,7 +406,8 @@ public void toyReassigningTest() throws Exception {
"toyNullTestAPI",
"toys",
"Foo",
ImmutableMap.of("toys.Foo:void test(String, String)", Sets.newHashSet(1)),
ImmutableMap.of(
"toys.Foo:void test(java.lang.String, java.lang.String)", Sets.newHashSet(1)),
"import com.google.common.base.Preconditions;",
"import java.util.Objects;",
"import org.junit.Assert;",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,53 @@ public void libraryModelNullableUpperBoundsWithoutJarInferTest() {
"}")
.doTest();
}

@Test
public void libraryModelDefaultParameterNullabilityTest() {
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 com.test.ParameterAnnotationExample;",
"class Test {",
" static ParameterAnnotationExample annotationExample = new ParameterAnnotationExample();",
" static void testPositive() {",
" // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required",
" annotationExample.add(5,null);",
" }",
"}")
.doTest();
}

@Test
public void libraryModelParameterNullabilityTest() {
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 com.test.ParameterAnnotationExample;",
"class Test {",
" static ParameterAnnotationExample annotationExample = new ParameterAnnotationExample();",
" static void testNegative() {",
" annotationExample.getNewObjectIfNull(null);",
" }",
"}")
.doTest();
}
}
1 change: 1 addition & 0 deletions library-model/library-model-generator/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ plugins {
dependencies {
implementation deps.build.guava
implementation deps.build.javaparser
implementation deps.build.javaparserSymbolSolver
compileOnly deps.apt.autoValueAnnot
annotationProcessor deps.apt.autoValue

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@
import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
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.resolution.TypeSolver;
import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration;
import com.github.javaparser.resolution.types.ResolvedType;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
import com.github.javaparser.utils.CollectionStrategy;
import com.github.javaparser.utils.ParserCollectionStrategy;
import com.github.javaparser.utils.ProjectRoot;
Expand All @@ -47,6 +55,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -69,12 +78,15 @@
public static class LibraryModelData {
public final Map<String, MethodAnnotationsRecord> methodRecords;
public final Map<String, Set<Integer>> nullableUpperBounds;
public final Set<String> nullMarkedClasses;

public LibraryModelData(
Map<String, MethodAnnotationsRecord> methodRecords,
Map<String, Set<Integer>> nullableUpperBounds) {
Map<String, Set<Integer>> nullableUpperBounds,
Set<String> nullMarkedClasses) {
this.methodRecords = methodRecords;
this.nullableUpperBounds = nullableUpperBounds;
this.nullMarkedClasses = nullMarkedClasses;
}

@Override
Expand All @@ -84,6 +96,8 @@
+ methodRecords
+ ", nullableUpperBounds="
+ nullableUpperBounds
+ ", nullMarkedClasses="
+ nullMarkedClasses
+ '}';
}
}
Expand All @@ -97,14 +111,21 @@
public static LibraryModelData generateAstubxForLibraryModels(
String inputSourceDirectory, String outputFile) {
Map<String, MethodAnnotationsRecord> methodRecords = new LinkedHashMap<>();
Set<String> nullMarkedClasses = new HashSet<>();
Map<String, Set<Integer>> nullableUpperBounds = new LinkedHashMap<>();
Path root = dirnameToPath(inputSourceDirectory);
LibraryModelData modelData = new LibraryModelData(methodRecords, nullableUpperBounds);
LibraryModelData modelData =
new LibraryModelData(methodRecords, nullableUpperBounds, nullMarkedClasses);
AnnotationCollectorCallback ac = new AnnotationCollectorCallback(modelData);
CollectionStrategy strategy = new ParserCollectionStrategy();
// Required to include directories that contain a module-info.java, which don't parse by
// default.
TypeSolver typeSolver =
new CombinedTypeSolver(
new ReflectionTypeSolver(), new JavaParserTypeSolver(Paths.get(inputSourceDirectory)));
JavaSymbolSolver symbolSolver = new JavaSymbolSolver(typeSolver);
strategy.getParserConfiguration().setLanguageLevel(LanguageLevel.JAVA_17);
strategy.getParserConfiguration().setSymbolResolver(symbolSolver);
ProjectRoot projectRoot = strategy.collect(root);

projectRoot
Expand All @@ -130,7 +151,8 @@
private static void writeToAstubx(String outputPath, LibraryModelData modelData) {
Map<String, MethodAnnotationsRecord> methodRecords = modelData.methodRecords;
Map<String, Set<Integer>> nullableUpperBounds = modelData.nullableUpperBounds;
if (methodRecords.isEmpty() && nullableUpperBounds.isEmpty()) {
Set<String> nullMarkedClasses = modelData.nullMarkedClasses;
if (methodRecords.isEmpty() && nullableUpperBounds.isEmpty() && nullMarkedClasses.isEmpty()) {
return;
}
ImmutableMap<String, String> importedAnnotations =
Expand All @@ -147,6 +169,7 @@
Collections.emptyMap(),
Collections.emptyMap(),
methodRecords,
nullMarkedClasses,
nullableUpperBounds);
}
} catch (IOException e) {
Expand Down Expand Up @@ -189,7 +212,9 @@
private boolean isJspecifyNullableImportPresent = false;
private boolean isNullMarked = false;
private final Map<String, MethodAnnotationsRecord> methodRecords;
private final Set<String> nullMarkedClasses;
private final Map<String, Set<Integer>> nullableUpperBounds;
// TODO this is sketchy!
Copy link
Collaborator

Choose a reason for hiding this comment

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

I added this comment 🙂 I think we need to do for arrays whatever JarInfer is now doing. We should also add integration tests.

private static final String ARRAY_RETURN_TYPE_STRING = "Array";
private static final String NULL_MARKED = "NullMarked";
private static final String NULLABLE = "Nullable";
Expand All @@ -198,6 +223,7 @@
public AnnotationCollectionVisitor(LibraryModelData modelData) {
this.methodRecords = modelData.methodRecords;
this.nullableUpperBounds = modelData.nullableUpperBounds;
this.nullMarkedClasses = modelData.nullMarkedClasses;
}

@Override
Expand Down Expand Up @@ -231,6 +257,7 @@
a -> {
if (a.getNameAsString().equalsIgnoreCase(NULL_MARKED)) {
this.isNullMarked = true;
this.nullMarkedClasses.add(parentName);
}
});
if (this.isNullMarked) {
Expand All @@ -247,10 +274,24 @@

@Override
public void visit(MethodDeclaration md, Void arg) {
if (this.isNullMarked && hasNullableReturn(md)) {
ImmutableMap<Integer, ImmutableSet<String>> nullableParameterMap = getNullableParameters(md);
boolean isReturnNullable = hasNullableReturn(md);
ImmutableSet<String> nullableReturn =
isReturnNullable ? ImmutableSet.of(NULLABLE) : ImmutableSet.of();
// We write the method record into the astubx if it contains a Nullable return or any Nullable
// parameter.
ResolvedMethodDeclaration resolved = md.resolve();
String qualifiedSignature = resolved.getQualifiedSignature();
String methodSignatureWithQualifiedParameters =
qualifiedSignature.substring(qualifiedSignature.lastIndexOf(md.getNameAsString()));
if (this.isNullMarked && (isReturnNullable || !nullableParameterMap.isEmpty())) {
methodRecords.put(
parentName + ":" + getMethodReturnTypeString(md) + " " + md.getSignature().toString(),
MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of()));
parentName
+ ":"
+ getMethodReturnTypeString(resolved)
+ " "
+ methodSignatureWithQualifiedParameters,
MethodAnnotationsRecord.create(nullableReturn, nullableParameterMap));
}
super.visit(md, null);
}
Expand Down Expand Up @@ -288,13 +329,15 @@
* @param md The MethodDeclaration instance.
* @return The return type string value to be written into the astubx file.
*/
private String getMethodReturnTypeString(MethodDeclaration md) {
if (md.getType() instanceof ClassOrInterfaceType) {
return md.getType().getChildNodes().get(0).toString();
} else if (md.getType() instanceof ArrayType) {
private String getMethodReturnTypeString(ResolvedMethodDeclaration md) {
ResolvedType returnType = md.getReturnType();
if (returnType.isReferenceType()) {
return returnType.asReferenceType().getQualifiedName().toString();
} else if (returnType.isArray()) {
return ARRAY_RETURN_TYPE_STRING;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably this needs to be updated

} else {
return md.getType().toString();
throw new RuntimeException("Unexpected return type: " + returnType);

Check warning on line 339 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#L339

Added line #L339 was not covered by tests
msridhar marked this conversation as resolved.
Show resolved Hide resolved
// return returnType.asPrimitive().toString();
}
}

Expand Down Expand Up @@ -327,5 +370,26 @@
}
return setBuilder.build();
}

/**
* Takes a MethodDeclaration instance and provides a map containing the Integer indices for the
* parameters with their Nullable annotation to be written in the astubx format.
*
* @param md MethodDeclaration instance.
* @return Map of Nullable parameters for the method.
*/
private ImmutableMap<Integer, ImmutableSet<String>> getNullableParameters(
MethodDeclaration md) {
ImmutableMap.Builder<Integer, ImmutableSet<String>> mapBuilder = ImmutableMap.builder();
List<Parameter> parameterList = md.getParameters();
for (int i = 0; i < parameterList.size(); i++) {
Parameter parameter = parameterList.get(i);
Optional<AnnotationExpr> nullableAnnotation = parameter.getAnnotationByName(NULLABLE);
if (nullableAnnotation.isPresent() && isAnnotationNullable(nullableAnnotation.get())) {
mapBuilder.put(i, ImmutableSet.of("Nullable"));
}
}
return mapBuilder.build();
}
}
}
Loading