diff --git a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java b/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java index 6616785dbc..2d5d195765 100644 --- a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java +++ b/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java @@ -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; @@ -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 { diff --git a/library-model/library-model-generator-integration-test/build.gradle b/library-model/library-model-generator-integration-test/build.gradle index 664e2bc4f0..25fbcef159 100644 --- a/library-model/library-model-generator-integration-test/build.gradle +++ b/library-model/library-model-generator-integration-test/build.gradle @@ -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" } diff --git a/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java b/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java index c0149fd78f..3a230d0200 100644 --- a/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java +++ b/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java @@ -8,6 +8,11 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; +/** + * Integration test for library model support. The library models are contained in the jar for the + * test-library-model-generator project, as a stubx file. These tests ensure that NullAway correctly + * loads the stubx file and reports the right errors based on those models. + */ public class LibraryModelIntegrationTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -42,6 +47,8 @@ public void libraryModelNullableReturnsTest() { " test(annotationExample.makeUpperCase(\"nullaway\"));", " }", " static void testNegative() {", + " // no error since nullReturn is annotated with javax.annotation.Nullable,", + " // which is not considered when generating stubx files", " test(annotationExample.nullReturn());", " }", "}") @@ -123,4 +130,53 @@ 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 {", + " 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.getNullable()'", + " test(upperBoundExample.getNullable());", + " }", + "}") + .doTest(); + } + + @Test + public void libraryModelNullableUpperBoundsWithoutJarInferTest() { + compilationHelper + .setArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:JSpecifyMode=true")) + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.jspecify.annotations.Nullable;", + "import com.uber.nullaway.libmodel.AnnotationExample;", + "class Test {", + " // BUG: Diagnostic contains: Generic type parameter cannot be @Nullable", + " static AnnotationExample.UpperBoundExample<@Nullable Object> upperBoundExample = new AnnotationExample.UpperBoundExample<@Nullable Object>();", + "}") + .doTest(); + } } diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 306a36611f..97ef44c7d3 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -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; @@ -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. @@ -59,21 +62,18 @@ */ public class LibraryModelGenerator { - public void generateAstubxForLibraryModels(String inputSourceDirectory, String outputDirectory) { - Map 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 processDirectory(String sourceDirectoryRoot) { + public void generateAstubxForLibraryModels(String inputSourceDirectory, String outputDirectory) { Map methodRecords = new LinkedHashMap<>(); - Path root = dirnameToPath(sourceDirectoryRoot); - AnnotationCollectorCallback ac = new AnnotationCollectorCallback(methodRecords); + Map> nullableUpperBounds = new LinkedHashMap<>(); + Path root = dirnameToPath(inputSourceDirectory); + AnnotationCollectorCallback ac = + new AnnotationCollectorCallback(methodRecords, nullableUpperBounds); CollectionStrategy strategy = new ParserCollectionStrategy(); // Required to include directories that contain a module-info.java, which don't parse by // default. @@ -90,7 +90,7 @@ private Map processDirectory(String sourceDirec throw new RuntimeException(e); } }); - return methodRecords; + writeToAstubx(outputDirectory, methodRecords, nullableUpperBounds); } /** @@ -100,8 +100,10 @@ private Map processDirectory(String sourceDirec * @param methodRecords Map containing the collected Nullability annotation information. */ private void writeToAstubx( - String outputPath, Map methodRecords) { - if (methodRecords.isEmpty()) { + String outputPath, + Map methodRecords, + Map> nullableUpperBounds) { + if (methodRecords.isEmpty() && nullableUpperBounds.isEmpty()) { return; } Map importedAnnotations = @@ -117,7 +119,8 @@ private void writeToAstubx( importedAnnotations, Collections.emptyMap(), Collections.emptyMap(), - methodRecords); + methodRecords, + nullableUpperBounds); } } catch (IOException e) { throw new RuntimeException(e); @@ -137,8 +140,11 @@ private static class AnnotationCollectorCallback implements SourceRoot.Callback private final AnnotationCollectionVisitor annotationCollectionVisitor; - public AnnotationCollectorCallback(Map methodRecords) { - this.annotationCollectionVisitor = new AnnotationCollectionVisitor(methodRecords); + public AnnotationCollectorCallback( + Map methodRecords, + Map> nullableUpperBounds) { + this.annotationCollectionVisitor = + new AnnotationCollectionVisitor(methodRecords, nullableUpperBounds); } @Override @@ -158,14 +164,18 @@ private static class AnnotationCollectionVisitor extends VoidVisitorAdapter methodRecords; + private final Map methodRecords; + private final Map> 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 methodRecords) { + public AnnotationCollectionVisitor( + Map methodRecords, + Map> nullableUpperBounds) { this.methodRecords = methodRecords; + this.nullableUpperBounds = nullableUpperBounds; } @Override @@ -197,6 +207,12 @@ public void visit(ClassOrInterfaceDeclaration cid, Void arg) { this.isNullMarked = true; } }); + if (this.isNullMarked) { + Set nullableUpperBoundParams = getGenericTypeParameterNullableUpperBounds(cid); + if (!nullableUpperBoundParams.isEmpty()) { + nullableUpperBounds.put(parentName, nullableUpperBoundParams); + } + } 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())); @@ -261,5 +277,28 @@ private boolean isAnnotationNullable(AnnotationExpr annotation) { && 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 getGenericTypeParameterNullableUpperBounds( + ClassOrInterfaceDeclaration cid) { + ImmutableSet.Builder setBuilder = ImmutableSet.builder(); + List typeParamList = cid.getTypeParameters(); + for (int i = 0; i < typeParamList.size(); i++) { + TypeParameter param = typeParamList.get(i); + for (ClassOrInterfaceType type : param.getTypeBound()) { + Optional nullableAnnotation = type.getAnnotationByName(NULLABLE); + if (nullableAnnotation.isPresent() && isAnnotationNullable(nullableAnnotation.get())) { + setBuilder.add(i); + } + } + } + return setBuilder.build(); + } } } diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java index 9dc6052341..8bcb99323f 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java @@ -36,7 +36,8 @@ public static void write( Map importedAnnotations, Map> packageAnnotations, Map> typeAnnotations, - Map methodRecords) + Map methodRecords, + Map> nullableUpperBounds) throws IOException { // File format version/magic number out.writeInt(VERSION_0_FILE_MAGIC_NUMBER); @@ -49,7 +50,8 @@ public static void write( importedAnnotations.values(), packageAnnotations.keySet(), typeAnnotations.keySet(), - methodRecords.keySet()); + methodRecords.keySet(), + nullableUpperBounds.keySet()); for (Collection keyset : keysets) { for (String key : keyset) { assert !encodingDictionary.containsKey(key); @@ -118,5 +120,17 @@ public static void write( } } } + // Followed by the number of nullable upper bounds records + out.writeInt(nullableUpperBounds.size()); + for (Map.Entry> entry : nullableUpperBounds.entrySet()) { + // Followed by the number of parameters with nullable upper bound + Set parameters = entry.getValue(); + out.writeInt(parameters.size()); + 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); + } + } } } diff --git a/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java index ce8405fe44..984b969c35 100644 --- a/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java @@ -39,4 +39,14 @@ public String returnNull() { return null; } } + + /** In the library model we add a {@code @Nullable} upper bound for T */ + public static class UpperBoundExample { + + T nullableObject; + + public T getNullable() { + return nullableObject; + } + } } diff --git a/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java index f95c54090a..5fef2e2118 100644 --- a/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -45,4 +45,13 @@ public String returnNull() { return null; } } + + public static class UpperBoundExample { + + T nullableObject; + + public T getNullable() { + return nullableObject; + } + } } diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java index 495ac46334..f171c502da 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java @@ -34,14 +34,9 @@ import com.uber.nullaway.Nullness; import com.uber.nullaway.dataflow.AccessPath; import com.uber.nullaway.dataflow.AccessPathNullnessPropagation; -import com.uber.nullaway.jarinfer.JarInferStubxProvider; -import java.io.DataInputStream; -import java.io.IOException; import java.io.InputStream; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; -import java.util.ServiceLoader; import java.util.Set; import javax.annotation.Nullable; import javax.lang.model.element.Modifier; @@ -59,7 +54,6 @@ private static void LOG(boolean cond, String tag, String msg) { } } - private static final int VERSION_0_FILE_MAGIC_NUMBER = 691458791; private static final String ANDROID_ASTUBX_LOCATION = "jarinfer.astubx"; private static final String ANDROID_MODEL_CLASS = "com.uber.nullaway.jarinfer.AndroidJarInferModels"; @@ -69,12 +63,14 @@ private static void LOG(boolean cond, String tag, String msg) { private final Map>>> argAnnotCache; private final Config config; + private final StubxCacheUtil cacheUtil; public InferredJARModelsHandler(Config config) { super(); this.config = config; - argAnnotCache = new LinkedHashMap<>(); - loadStubxFiles(); + String jarInferLogName = "JI"; + this.cacheUtil = new StubxCacheUtil(jarInferLogName); + argAnnotCache = cacheUtil.getArgAnnotCache(); // Load Android SDK JarInfer models try { InputStream androidStubxIS = @@ -82,7 +78,7 @@ public InferredJARModelsHandler(Config config) { .getClassLoader() .getResourceAsStream(ANDROID_ASTUBX_LOCATION); if (androidStubxIS != null) { - parseStubStream(androidStubxIS, "android.jar: " + ANDROID_ASTUBX_LOCATION); + cacheUtil.parseStubStream(androidStubxIS, "android.jar: " + ANDROID_ASTUBX_LOCATION); LOG(DEBUG, "DEBUG", "Loaded Android RT models."); } } catch (ClassNotFoundException e) { @@ -97,29 +93,6 @@ public InferredJARModelsHandler(Config config) { } } - /** - * Loads all stubx files discovered in the classpath. Stubx files are discovered via - * implementations of {@link JarInferStubxProvider} loaded using a {@link ServiceLoader} - */ - private void loadStubxFiles() { - Iterable astubxProviders = - ServiceLoader.load( - JarInferStubxProvider.class, InferredJARModelsHandler.class.getClassLoader()); - for (JarInferStubxProvider provider : astubxProviders) { - for (String astubxPath : provider.pathsToStubxFiles()) { - Class providerClass = provider.getClass(); - InputStream stubxInputStream = providerClass.getResourceAsStream(astubxPath); - String stubxLocation = providerClass + ":" + astubxPath; - try { - parseStubStream(stubxInputStream, stubxLocation); - LOG(DEBUG, "DEBUG", "loaded stubx file " + stubxLocation); - } catch (IOException e) { - throw new RuntimeException("could not parse stubx file " + stubxLocation, e); - } - } - } - } - @Override public Nullness[] onOverrideMethodInvocationParametersNullability( Context context, @@ -286,76 +259,4 @@ private String getSimpleTypeName(Type typ) { return typ.tsym.getSimpleName().toString(); } } - - private void parseStubStream(InputStream stubxInputStream, String stubxLocation) - throws IOException { - String[] strings; - DataInputStream in = new DataInputStream(stubxInputStream); - // Read and check the magic version number - if (in.readInt() != VERSION_0_FILE_MAGIC_NUMBER) { - throw new Error("Invalid file version/magic number for stubx file!" + stubxLocation); - } - // Read the number of strings in the string dictionary - int numStrings = in.readInt(); - // Populate the string dictionary {idx => value}, where idx is encoded by the string position - // inside this section. - strings = new String[numStrings]; - for (int i = 0; i < numStrings; ++i) { - strings[i] = in.readUTF(); - } - // Read the number of (package, annotation) entries - int numPackages = in.readInt(); - // Read each (package, annotation) entry, where the int values point into the string - // dictionary loaded before. - for (int i = 0; i < numPackages; ++i) { - in.readInt(); // String packageName = strings[in.readInt()]; - in.readInt(); // String annotation = strings[in.readInt()]; - } - // Read the number of (type, annotation) entries - int numTypes = in.readInt(); - // Read each (type, annotation) entry, where the int values point into the string - // dictionary loaded before. - for (int i = 0; i < numTypes; ++i) { - in.readInt(); // String typeName = strings[in.readInt()]; - in.readInt(); // String annotation = strings[in.readInt()]; - } - // Read the number of (method, annotation) entries - int numMethods = in.readInt(); - // Read each (method, annotation) record - for (int i = 0; i < numMethods; ++i) { - String methodSig = strings[in.readInt()]; - String annotation = strings[in.readInt()]; - LOG(DEBUG, "DEBUG", "method: " + methodSig + ", return annotation: " + annotation); - cacheAnnotation(methodSig, RETURN, annotation); - } - // Read the number of (method, argument, annotation) entries - int numArgumentRecords = in.readInt(); - // Read each (method, argument, annotation) record - for (int i = 0; i < numArgumentRecords; ++i) { - String methodSig = strings[in.readInt()]; - if (methodSig.lastIndexOf(':') == -1 || methodSig.split(":")[0].lastIndexOf('.') == -1) { - throw new Error( - "Invalid method signature " + methodSig + " in stubx file " + stubxLocation); - } - int argNum = in.readInt(); - String annotation = strings[in.readInt()]; - LOG( - DEBUG, - "DEBUG", - "method: " + methodSig + ", argNum: " + argNum + ", arg annotation: " + annotation); - cacheAnnotation(methodSig, argNum, annotation); - } - } - - private void cacheAnnotation(String methodSig, Integer argNum, String annotation) { - // TODO: handle inner classes properly - String className = methodSig.split(":")[0].replace('$', '.'); - Map>> cacheForClass = - argAnnotCache.computeIfAbsent(className, s -> new LinkedHashMap<>()); - Map> cacheForMethod = - cacheForClass.computeIfAbsent(methodSig, s -> new LinkedHashMap<>()); - Set cacheForArgument = - cacheForMethod.computeIfAbsent(argNum, s -> new LinkedHashSet<>()); - cacheForArgument.add(annotation); - } } diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java index 7b5676984c..b85b8a560a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java @@ -378,6 +378,9 @@ private static LibraryModels loadLibraryModels(Config config) { ServiceLoader.load(LibraryModels.class, LibraryModels.class.getClassLoader()); ImmutableSet.Builder libModelsBuilder = new ImmutableSet.Builder<>(); libModelsBuilder.add(new DefaultLibraryModels()).addAll(externalLibraryModels); + if (config.isJarInferEnabled()) { + libModelsBuilder.add(new ExternalStubxLibraryModels()); + } return new CombinedLibraryModels(libModelsBuilder.build(), config); } @@ -1281,4 +1284,79 @@ private static Symbol.MethodSymbol lookupHandlingOverrides( return null; } } + + /** Constructs Library Models from stubx files */ + private static class ExternalStubxLibraryModels implements LibraryModels { + + private final Map>>> argAnnotCache; + private final Map upperBoundsCache; + + ExternalStubxLibraryModels() { + String libraryModelLogName = "LM"; + StubxCacheUtil cacheUtil = new StubxCacheUtil(libraryModelLogName); + argAnnotCache = cacheUtil.getArgAnnotCache(); + upperBoundsCache = cacheUtil.getUpperBoundCache(); + } + + @Override + public ImmutableSet nullMarkedClasses() { + Set cachedNullMarkedClasses = argAnnotCache.keySet(); + return new ImmutableSet.Builder().addAll(cachedNullMarkedClasses).build(); + } + + @Override + public ImmutableSetMultimap typeVariablesWithNullableUpperBounds() { + ImmutableSetMultimap.Builder mapBuilder = + new ImmutableSetMultimap.Builder<>(); + for (Map.Entry entry : upperBoundsCache.entrySet()) { + mapBuilder.put(entry.getKey(), entry.getValue()); + } + return mapBuilder.build(); + } + + @Override + public ImmutableSetMultimap failIfNullParameters() { + return ImmutableSetMultimap.of(); + } + + @Override + public ImmutableSetMultimap explicitlyNullableParameters() { + return ImmutableSetMultimap.of(); + } + + @Override + public ImmutableSetMultimap nonNullParameters() { + return ImmutableSetMultimap.of(); + } + + @Override + public ImmutableSetMultimap nullImpliesTrueParameters() { + return ImmutableSetMultimap.of(); + } + + @Override + public ImmutableSetMultimap nullImpliesFalseParameters() { + return ImmutableSetMultimap.of(); + } + + @Override + public ImmutableSetMultimap nullImpliesNullParameters() { + return ImmutableSetMultimap.of(); + } + + @Override + public ImmutableSet nullableReturns() { + return ImmutableSet.of(); + } + + @Override + public ImmutableSet nonNullReturns() { + return ImmutableSet.of(); + } + + @Override + public ImmutableSetMultimap castToNonNullMethods() { + return ImmutableSetMultimap.of(); + } + } } diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/StubxCacheUtil.java b/nullaway/src/main/java/com/uber/nullaway/handlers/StubxCacheUtil.java new file mode 100644 index 0000000000..e2ac9cb991 --- /dev/null +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/StubxCacheUtil.java @@ -0,0 +1,190 @@ +package com.uber.nullaway.handlers; + +/* + * 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. + */ + +import com.uber.nullaway.jarinfer.JarInferStubxProvider; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +/** + * A class responsible for caching annotation information extracted from stubx files. + * + *

This class provides mechanisms to cache annotations and retrieve them efficiently when needed. + * It uses a nested map structure to store annotations, which are indexed by class name, method + * signature, and argument index. It also stores a Map containing the indices for Nullable upper + * bounds for generic type parameters. + */ +public class StubxCacheUtil { + + private static final int VERSION_0_FILE_MAGIC_NUMBER = 691458791; + private boolean DEBUG = false; + private String logCaller = ""; + + private void LOG(boolean cond, String tag, String msg) { + if (cond) { + System.out.println("[" + logCaller + " " + tag + "] " + msg); + } + } + + private static final int RETURN = -1; + + private final Map>>> argAnnotCache; + + private final Map upperBoundCache; + + /** + * Initializes a new {@code StubxCacheUtil} instance. + * + *

This sets up the caches for argument annotations and upper bounds, sets the log caller, and + * loads the stubx files. + * + * @param logCaller Identifier for logging purposes. + */ + public StubxCacheUtil(String logCaller) { + argAnnotCache = new LinkedHashMap<>(); + upperBoundCache = new HashMap<>(); + this.logCaller = logCaller; + loadStubxFiles(); + } + + public Map getUpperBoundCache() { + return upperBoundCache; + } + + public Map>>> getArgAnnotCache() { + return argAnnotCache; + } + + /** + * Loads all stubx files discovered in the classpath. Stubx files are discovered via + * implementations of {@link JarInferStubxProvider} loaded using a {@link ServiceLoader} + */ + private void loadStubxFiles() { + Iterable astubxProviders = + ServiceLoader.load(JarInferStubxProvider.class, StubxCacheUtil.class.getClassLoader()); + for (JarInferStubxProvider provider : astubxProviders) { + for (String astubxPath : provider.pathsToStubxFiles()) { + Class providerClass = provider.getClass(); + InputStream stubxInputStream = providerClass.getResourceAsStream(astubxPath); + String stubxLocation = providerClass + ":" + astubxPath; + try { + parseStubStream(stubxInputStream, stubxLocation); + LOG(DEBUG, "DEBUG", "loaded stubx file " + stubxLocation); + } catch (IOException e) { + throw new RuntimeException("could not parse stubx file " + stubxLocation, e); + } + } + } + } + + public void parseStubStream(InputStream stubxInputStream, String stubxLocation) + throws IOException { + String[] strings; + DataInputStream in = new DataInputStream(stubxInputStream); + // Read and check the magic version number + if (in.readInt() != VERSION_0_FILE_MAGIC_NUMBER) { + throw new Error("Invalid file version/magic number for stubx file!" + stubxLocation); + } + // Read the number of strings in the string dictionary + int numStrings = in.readInt(); + // Populate the string dictionary {idx => value}, where idx is encoded by the string position + // inside this section. + strings = new String[numStrings]; + for (int i = 0; i < numStrings; ++i) { + strings[i] = in.readUTF(); + } + // Read the number of (package, annotation) entries + int numPackages = in.readInt(); + // Read each (package, annotation) entry, where the int values point into the string + // dictionary loaded before. + for (int i = 0; i < numPackages; ++i) { + in.readInt(); // String packageName = strings[in.readInt()]; + in.readInt(); // String annotation = strings[in.readInt()]; + } + // Read the number of (type, annotation) entries + int numTypes = in.readInt(); + // Read each (type, annotation) entry, where the int values point into the string + // dictionary loaded before. + for (int i = 0; i < numTypes; ++i) { + in.readInt(); // String typeName = strings[in.readInt()]; + in.readInt(); // String annotation = strings[in.readInt()]; + } + // Read the number of (method, annotation) entries + int numMethods = in.readInt(); + // Read each (method, annotation) record + for (int i = 0; i < numMethods; ++i) { + String methodSig = strings[in.readInt()]; + String annotation = strings[in.readInt()]; + LOG(DEBUG, "DEBUG", "method: " + methodSig + ", return annotation: " + annotation); + cacheAnnotation(methodSig, RETURN, annotation); + } + // Read the number of (method, argument, annotation) entries + int numArgumentRecords = in.readInt(); + // Read each (method, argument, annotation) record + for (int i = 0; i < numArgumentRecords; ++i) { + String methodSig = strings[in.readInt()]; + if (methodSig.lastIndexOf(':') == -1 || methodSig.split(":")[0].lastIndexOf('.') == -1) { + throw new Error( + "Invalid method signature " + methodSig + " in stubx file " + stubxLocation); + } + int argNum = in.readInt(); + String annotation = strings[in.readInt()]; + LOG( + DEBUG, + "DEBUG", + "method: " + methodSig + ", argNum: " + argNum + ", arg annotation: " + annotation); + cacheAnnotation(methodSig, argNum, annotation); + } + // read the number of nullable upper bound entries + int numClassesWithNullableUpperBounds = in.readInt(); + for (int i = 0; i < numClassesWithNullableUpperBounds; i++) { + int numParams = in.readInt(); + for (int j = 0; j < numParams; j++) { + cacheUpperBounds(strings[in.readInt()], in.readInt()); + } + } + } + + private void cacheAnnotation(String methodSig, Integer argNum, String annotation) { + // TODO: handle inner classes properly + String className = methodSig.split(":")[0].replace('$', '.'); + Map>> cacheForClass = + argAnnotCache.computeIfAbsent(className, s -> new LinkedHashMap<>()); + Map> cacheForMethod = + cacheForClass.computeIfAbsent(methodSig, s -> new LinkedHashMap<>()); + Set cacheForArgument = + cacheForMethod.computeIfAbsent(argNum, s -> new LinkedHashSet<>()); + cacheForArgument.add(annotation); + } + + private void cacheUpperBounds(String className, Integer paramIndex) { + upperBoundCache.put(className, paramIndex); + } +}