Skip to content

Commit

Permalink
External Library Models Integration (#922)
Browse files Browse the repository at this point in the history
The newly added `library-model` module consists of a CLI process that
takes an input directory with annotated java source files as a command
line parameter and uses `com.github.javaparser` APIS to generate
`libmodels.astubx` file containing method stubs for methods that return
@nullable. This can be run using the existing `JarInferEnabled` and
`JarInferUseReturnAnnotations` flags.

This allows us to be able catch issues as shown in the below example
from externally annotated source code:

```java
@NullMarked
public class AnnotationExample {
    @nullable
    public String makeUpperCase(String inputString) {
        if (inputString == null || inputString.isEmpty()) {
            return null;
        } else {
            return inputString.toUpperCase();
        }
    }
}

```

```java
class Test {
    static AnnotationExample annotationExample = new AnnotationExample();
    static void test(String value){}
    static void testPositive() {
        // BUG: Diagnostic contains: passing @nullable parameter 'annotationExample.makeUpperCase(\"nullaway\")'
        test(annotationExample.makeUpperCase(\"nullaway\"));
    }    
}
```

---------

Co-authored-by: Manu Sridharan <msridhar@gmail.com>
Co-authored-by: Lázaro Clapp <lazaro.clapp@gmail.com>
  • Loading branch information
3 people committed Apr 9, 2024
1 parent 5acd394 commit 09db47a
Show file tree
Hide file tree
Showing 21 changed files with 743 additions and 39 deletions.
6 changes: 3 additions & 3 deletions build.gradle
Expand Up @@ -95,9 +95,9 @@ subprojects { project ->
google()
}

// For some reason, spotless complains when applied to the jar-infer folder itself, even
// though there is no top-level :jar-infer project
if (project.name != "jar-infer") {
// Spotless complains when applied to the folders containing projects
// when they do not have a build.gradle file
if (project.name != "jar-infer" && project.name != "library-model") {
project.apply plugin: "com.diffplug.spotless"
spotless {
java {
Expand Down
2 changes: 2 additions & 0 deletions code-coverage-report/build.gradle
Expand Up @@ -80,4 +80,6 @@ dependencies {
implementation project(':jar-infer:nullaway-integration-test')
implementation project(':guava-recent-unit-tests')
implementation project(':jdk-recent-unit-tests')
implementation project(':library-model:library-model-generator')
implementation project(':library-model:library-model-generator-integration-test')
}
1 change: 1 addition & 0 deletions gradle/dependencies.gradle
Expand Up @@ -75,6 +75,7 @@ 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.25.8",
javaxValidation : "javax.validation:validation-api:2.0.1.Final",
jspecify : "org.jspecify:jspecify:0.3.0",
jsr305Annotations : "com.google.code.findbugs:jsr305:3.0.2",
Expand Down
1 change: 1 addition & 0 deletions jar-infer/jar-infer-cli/build.gradle
Expand Up @@ -18,6 +18,7 @@ dependencies {
implementation deps.build.commonscli
implementation deps.build.guava
implementation project(":jar-infer:jar-infer-lib")
implementation project(":library-model:library-model-generator")

testImplementation deps.test.junit4
testImplementation(deps.build.errorProneTestHelpers) {
Expand Down
1 change: 1 addition & 0 deletions jar-infer/jar-infer-lib/build.gradle
Expand Up @@ -37,6 +37,7 @@ dependencies {
api deps.build.guava
api deps.build.commonsIO
compileOnly deps.build.errorProneCheckApi
implementation project(":library-model:library-model-generator")

testImplementation deps.test.junit4
testImplementation(deps.build.errorProneTestHelpers) {
Expand Down
Expand Up @@ -43,6 +43,8 @@
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.collections.Iterator2Iterable;
import com.ibm.wala.util.config.FileOfClasses;
import com.uber.nullaway.libmodel.MethodAnnotationsRecord;
import com.uber.nullaway.libmodel.StubxWriter;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.File;
Expand Down Expand Up @@ -437,15 +439,15 @@ private void writeModel(DataOutputStream out) throws IOException {
}
methodRecords.put(
sign,
new MethodAnnotationsRecord(
MethodAnnotationsRecord.create(
nullableReturns.contains(sign) ? ImmutableSet.of("Nullable") : ImmutableSet.of(),
ImmutableMap.copyOf(argAnnotation)));
nullableReturns.remove(sign);
}
for (String nullableReturnMethodSign : Iterator2Iterable.make(nullableReturns.iterator())) {
methodRecords.put(
nullableReturnMethodSign,
new MethodAnnotationsRecord(ImmutableSet.of("Nullable"), ImmutableMap.of()));
MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of()));
}
StubxWriter.write(out, importedAnnotations, packageAnnotations, typeAnnotations, methodRecords);
}
Expand Down

This file was deleted.

43 changes: 43 additions & 0 deletions library-model/library-model-generator-cli/build.gradle
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2024. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "java-library"
id "com.github.johnrengelman.shadow"
}

jar{
manifest {
attributes('Main-Class':'com.uber.nullaway.libmodel.LibraryModelGeneratorCLI')
}
// add this classifier so that the output file for the jar task differs from
// the output file for the shadowJar task (otherwise they overwrite each other's
// outputs, forcing the tasks to always re-run)
archiveClassifier = "nonshadow"
}

shadowJar {
mergeServiceFiles()
configurations = [
project.configurations.runtimeClasspath
]
archiveClassifier = ""
}
shadowJar.dependsOn jar
assemble.dependsOn shadowJar

dependencies {
implementation project(":library-model:library-model-generator")
}
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2024 Uber Technologies, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package com.uber.nullaway.libmodel;

/**
* A CLI tool for invoking the process for {@link LibraryModelGenerator} which generates astubx
* file(s) from a directory containing annotated source code to be used as external library models.
*/
public class LibraryModelGeneratorCLI {
/**
* This is the main method of the cli tool. It parses the source files within a specified
* directory, obtains meaningful Nullability annotation information and writes it into an astubx
* file.
*
* @param args Command line arguments for the directory containing source files and the output
* directory.
*/
public static void main(String[] args) {
if (args.length != 2) {
System.out.println(
"Incorrect number of command line arguments. Required arguments: <inputSourceDirectory> <outputDirectory>");
return;
}
LibraryModelGenerator libraryModelGenerator = new LibraryModelGenerator();
libraryModelGenerator.generateAstubxForLibraryModels(args[0], args[1]);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2024. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "java-library"
id "nullaway.java-test-conventions"
}

dependencies {
testImplementation project(":nullaway")
testImplementation project(":library-model:test-library-model-generator")
testImplementation deps.test.junit4
testImplementation(deps.build.errorProneTestHelpers) {
exclude group: "junit", module: "junit"
}
implementation deps.build.guava
implementation deps.build.javaparser
compileOnly deps.apt.autoValueAnnot
annotationProcessor deps.apt.autoValue
}
@@ -0,0 +1,126 @@
package com.uber.nullaway.libmodel;

import com.google.errorprone.CompilationTestHelper;
import com.uber.nullaway.NullAway;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

public class LibraryModelIntegrationTest {

@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();

private CompilationTestHelper compilationHelper;

@Before
public void setup() {
compilationHelper = CompilationTestHelper.newInstance(NullAway.class, getClass());
}

@Test
public void libraryModelNullableReturnsTest() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:JarInferEnabled=true",
"-XepOpt:NullAway:JarInferUseReturnAnnotations=true"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.nullaway.libmodel.AnnotationExample;",
"class Test {",
" static AnnotationExample annotationExample = new AnnotationExample();",
" static void test(String value){",
" }",
" static void testPositive() {",
" // BUG: Diagnostic contains: passing @Nullable parameter 'annotationExample.makeUpperCase(\"nullaway\")'",
" test(annotationExample.makeUpperCase(\"nullaway\"));",
" }",
" static void testNegative() {",
" test(annotationExample.nullReturn());",
" }",
"}")
.doTest();
}

@Test
public void libraryModelNullableReturnsArrayTest() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:JarInferEnabled=true",
"-XepOpt:NullAway:JarInferUseReturnAnnotations=true"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.nullaway.libmodel.AnnotationExample;",
"class Test {",
" static AnnotationExample annotationExample = new AnnotationExample();",
" static void test(Integer[] value){",
" }",
" static void testPositive() {",
" // BUG: Diagnostic contains: passing @Nullable parameter 'annotationExample.generateIntArray(7)'",
" test(annotationExample.generateIntArray(7));",
" }",
"}")
.doTest();
}

@Test
public void libraryModelWithoutJarInferEnabledTest() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.nullaway.libmodel.AnnotationExample;",
"class Test {",
" static AnnotationExample annotationExample = new AnnotationExample();",
" static void test(String value){",
" }",
" static void testNegative() {",
" // Since the JarInferEnabled and JarInferUseReturnAnnotations flags are not set, we don't get an error here",
" test(annotationExample.makeUpperCase(\"nullaway\"));",
" }",
"}")
.doTest();
}

@Test
public void libraryModelInnerClassNullableReturnsTest() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:JarInferEnabled=true",
"-XepOpt:NullAway:JarInferUseReturnAnnotations=true"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.nullaway.libmodel.AnnotationExample;",
"class Test {",
" static AnnotationExample.InnerExample innerExample = new AnnotationExample.InnerExample();",
" static void test(String value){",
" }",
" static void testPositive() {",
" // BUG: Diagnostic contains: passing @Nullable parameter 'innerExample.returnNull()'",
" test(innerExample.returnNull());",
" }",
"}")
.doTest();
}
}
26 changes: 26 additions & 0 deletions library-model/library-model-generator/build.gradle
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2024. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id 'java-library'
id 'nullaway.java-test-conventions'
}

dependencies {
implementation deps.build.guava
implementation deps.build.javaparser
compileOnly deps.apt.autoValueAnnot
annotationProcessor deps.apt.autoValue
}

0 comments on commit 09db47a

Please sign in to comment.