Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
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
37 changes: 37 additions & 0 deletions jdk-javac-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Javac plugin for generating JSpecify JDK astubx file

This module and others contain logic to generate an astubx file from the
[annotated JSpecify JDK](https://github.com/jspecify/jdk). The generation works
in two stages:

1. This module provides a javac plugin that gets injected into the build of the
JSpecify JDK. It generates `.json` files capturing the nullability annotations
in the JDK.
2. We have a separate astubx generator (main entrypoint:
`com.uber.nullaway.jdkannotations.AstubxGeneratorCLI`) that turns the `.json`
files into an `.astubx` file.

Here are the current steps to (re-)generate the file (admittedly janky, we will
work to improve them).

1. Build this module and the `astubx-generator-cli` module:
`./gradlew :jdk-javac-plugin:build :jdk-annotations:astubx-generator-cli:build`
2. Clone [this fork](https://github.com/msridhar/jdk) of the JSpecify JDK, and
check out the `test` branch.
3. In the jdk repo, edit these lines of `make/common/JavaCompilation.gmk`:
```
$1_API_DIGEST_FLAGS += -Xplugin:"NullnessAnnotationSerializer /tmp"
$1_AUGMENTED_CLASSPATH += /Users/msridhar/git-repos/NullAway/jdk-javac-plugin/build/libs/jdk-javac-plugin-all.jar
```
On the first line, you can change `/tmp` to whatever directory should be used to
store the `.json` files generated by the javac plugin. On the second line,
change the absolute path to point to the `jdk-javac-plugin-all.jar` file under
your NullAway repo.
4. In the jdk repo, run: `make clean && make jdk`. (You may need to run
`configure` first.) This should exit successfully and generate the json files.
5. Run the `AstubxGeneratorCLI` main method (e.g., you can run it from within
IntelliJ). It takes two arguments. The first is the directory containing the
json files, and the second is where the output astubx file should be placed.
The output file will be named `output.astubx`.
6. Copy the `output.astubx` file from the previous step to
`nullaway/src/main/resources/jspecify-jdk.astubx` under the NullAway repo.
Comment thread
msridhar marked this conversation as resolved.
Outdated
1 change: 1 addition & 0 deletions nullaway/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ tasks.register('buildWithNullAway', JavaCompile) {
option("NullAway:CheckContracts")
option("NullAway:JSpecifyMode")
option("NullAway:HandleWildcardGenerics")
option("NullAway:JSpecifyJDKModels")
}
// Make sure the jar has already been built
dependsOn 'jar'
Expand Down
7 changes: 7 additions & 0 deletions nullaway/src/main/java/com/uber/nullaway/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
package com.uber.nullaway;

import com.google.common.collect.ImmutableSet;
import com.sun.tools.javac.code.Symbol;

Check failure on line 26 in nullaway/src/main/java/com/uber/nullaway/Config.java

View workflow job for this annotation

GitHub Actions / Build spring-framework with snapshot

package com.sun.tools.javac.code does not exist
import com.uber.nullaway.fixserialization.FixSerializationConfig;
import java.util.Set;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -285,6 +285,13 @@
*/
boolean isJarInferEnabled();

/**
* Checks if <a href="https://github.com/jspecify/jdk">JSpecify JDK models</a> should be enabled.
*
* @return true if JSpecify JDK models should be enabled, false otherwise
*/
boolean isJSpecifyJDKModels();

/**
* Gets the URL to show with NullAway error messages.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ public boolean isJarInferEnabled() {
throw new IllegalStateException(ERROR_MESSAGE);
}

@Override
public boolean isJSpecifyJDKModels() {
throw new IllegalStateException(ERROR_MESSAGE);
}

@Override
public String getErrorURL() {
throw new IllegalStateException(ERROR_MESSAGE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import com.google.common.collect.Iterables;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.util.ASTHelpers;
import com.sun.tools.javac.code.Symbol;

Check failure on line 31 in nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java

View workflow job for this annotation

GitHub Actions / Build spring-framework with snapshot

package com.sun.tools.javac.code does not exist

Check failure on line 31 in nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java

View workflow job for this annotation

GitHub Actions / Build spring-framework with snapshot

package com.sun.tools.javac.code is not visible
import com.uber.nullaway.fixserialization.FixSerializationConfig;
import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
import java.util.Collections;
Expand Down Expand Up @@ -87,6 +87,8 @@
/** --- JarInfer configs --- */
static final String FL_JI_ENABLED = EP_FL_NAMESPACE + ":JarInferEnabled";

static final String FL_JSPECIFY_JDK_ENABLED = EP_FL_NAMESPACE + ":JSpecifyJDKModels";

static final String FL_ERROR_URL = EP_FL_NAMESPACE + ":ErrorURL";

/** --- Serialization configs --- */
Expand Down Expand Up @@ -247,6 +249,8 @@
/** --- JarInfer configs --- */
private final boolean jarInferEnabled;

private final boolean jspecifyJDKModelsEnabled;

private final String errorURL;

/** --- Fully qualified names of custom nonnull/nullable annotation --- */
Expand Down Expand Up @@ -325,6 +329,11 @@

/* --- JarInfer configs --- */
jarInferEnabled = flags.getBoolean(FL_JI_ENABLED).orElse(false);
jspecifyJDKModelsEnabled = flags.getBoolean(FL_JSPECIFY_JDK_ENABLED).orElse(false);
if (jspecifyJDKModelsEnabled && !jspecifyMode) {
throw new IllegalStateException(
"-XepOpt:%s should only be set in JSpecify mode".formatted(FL_JSPECIFY_JDK_ENABLED));
}
errorURL = flags.get(FL_ERROR_URL).orElse(DEFAULT_URL);
if (acknowledgeAndroidRecent && !isAcknowledgeRestrictive) {
throw new IllegalStateException(
Expand Down Expand Up @@ -434,7 +443,7 @@
}

@Override
public boolean isUnannotatedClass(Symbol.ClassSymbol symbol) {

Check failure on line 446 in nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java

View workflow job for this annotation

GitHub Actions / Build spring-framework with snapshot

package Symbol does not exist
if (unannotatedClasses == null) {
return false;
}
Expand Down Expand Up @@ -478,7 +487,7 @@
}

@Override
public boolean isKnownInitializerMethod(Symbol.MethodSymbol methodSymbol) {

Check failure on line 490 in nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java

View workflow job for this annotation

GitHub Actions / Build spring-framework with snapshot

package Symbol does not exist
Symbol.ClassSymbol enclosingClass = ASTHelpers.enclosingClass(methodSymbol);
if (enclosingClass == null) {
return false;
Expand Down Expand Up @@ -581,6 +590,11 @@
return jarInferEnabled;
}

@Override
public boolean isJSpecifyJDKModels() {
return jspecifyJDKModelsEnabled;
}

@Override
public String getErrorURL() {
return errorURL;
Expand Down
4 changes: 3 additions & 1 deletion nullaway/src/main/java/com/uber/nullaway/LibraryModels.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

package com.uber.nullaway;

import static com.uber.nullaway.NullabilityUtil.castToNonNull;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
Expand Down Expand Up @@ -255,7 +257,7 @@ private MethodRef(String enclosingClass, String methodName, String fullMethodSig
public static MethodRef methodRef(String enclosingClass, String methodSignature) {
Matcher matcher = METHOD_SIG_PATTERN.matcher(methodSignature);
if (matcher.find()) {
String methodName = matcher.group(2);
String methodName = castToNonNull(matcher.group(2));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This change is due to the new JDK models (which model the return of Matcher.group as @Nullable)

if (methodName.equals(enclosingClass.substring(enclosingClass.lastIndexOf('.') + 1))) {
// constructor
methodName = "<init>";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import static com.uber.nullaway.LibraryModels.FieldRef.fieldRef;
import static com.uber.nullaway.LibraryModels.MethodRef.methodRef;
import static com.uber.nullaway.NullabilityUtil.castToNonNull;
import static com.uber.nullaway.Nullness.NONNULL;
import static com.uber.nullaway.Nullness.NULLABLE;
import static com.uber.nullaway.librarymodel.NestedAnnotationInfo.TypePathEntry.Kind.ARRAY_ELEMENT;
Expand Down Expand Up @@ -55,7 +56,6 @@
import com.uber.nullaway.LibraryModels.MethodRef;
import com.uber.nullaway.MethodParameterNullness;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.NullabilityUtil;
import com.uber.nullaway.Nullness;
import com.uber.nullaway.annotations.Initializer;
import com.uber.nullaway.dataflow.AccessPath;
Expand All @@ -65,7 +65,9 @@
import com.uber.nullaway.librarymodel.AddAnnotationToNestedTypeVisitor;
import com.uber.nullaway.librarymodel.NestedAnnotationInfo;
import com.uber.nullaway.librarymodel.NestedAnnotationInfo.Annotation;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -532,8 +534,9 @@ private static LibraryModels loadLibraryModels(Config config) {
ServiceLoader.load(LibraryModels.class, LibraryModels.class.getClassLoader());
ImmutableSet.Builder<LibraryModels> libModelsBuilder = new ImmutableSet.Builder<>();
libModelsBuilder.add(new DefaultLibraryModels(config)).addAll(externalLibraryModels);
if (config.isJarInferEnabled()) {
libModelsBuilder.add(new ExternalStubxLibraryModels());
if (config.isJarInferEnabled() || config.isJSpecifyJDKModels()) {
libModelsBuilder.add(
new ExternalStubxLibraryModels(config.isJarInferEnabled(), config.isJSpecifyJDKModels()));
}
return new CombinedLibraryModels(libModelsBuilder.build(), config);
}
Expand Down Expand Up @@ -1587,8 +1590,7 @@ private NameIndexedMap<Boolean> makeOptimizedBoolLookup(
makeOptimizedNestedAnnotationLookup(
Names names,
ImmutableMap<MethodRef, ImmutableSetMultimap<Integer, NestedAnnotationInfo>> refs) {
return makeOptimizedLookup(
names, refs.keySet(), ref -> NullabilityUtil.castToNonNull(refs.get(ref)));
return makeOptimizedLookup(names, refs.keySet(), ref -> castToNonNull(refs.get(ref)));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This change is due to the new static import

}

private <T> NameIndexedMap<T> makeOptimizedLookup(
Expand Down Expand Up @@ -1640,6 +1642,9 @@ private static class ExternalStubxLibraryModels implements LibraryModels {
/** astubx file name used in our Android SDK JarInfer models */
private static final String ANDROID_ASTUBX_LOCATION = "jarinfer.astubx";

/** astubx file name used for the JSpecify JDK models */
private static final String JSPECIFY_JDK_ASTUBX_FILENAME = "jspecify-jdk.astubx";

/** Class we expect to be present in a jar containing Android SDK JarInfer models */
private static final String ANDROID_MODEL_CLASS =
"com.uber.nullaway.jarinfer.AndroidJarInferModels";
Expand All @@ -1650,26 +1655,44 @@ private static class ExternalStubxLibraryModels implements LibraryModels {
private final Multimap<String, Integer> methodTypeParamNullableUpperBoundCache;
private final Map<String, SetMultimap<Integer, NestedAnnotationInfo>> nestedAnnotationInfo;

ExternalStubxLibraryModels() {
ExternalStubxLibraryModels(boolean isJarInferEnabled, boolean isJSpecifyJDKEnabled) {
String libraryModelLogName = "LM";
StubxCacheUtil cacheUtil = new StubxCacheUtil(libraryModelLogName);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// hardcoded loading of stubx files from android-jarinfer-models-sdkXX artifacts
try {
InputStream androidStubxIS =
Class.forName(ANDROID_MODEL_CLASS)
.getClassLoader()
.getResourceAsStream(ANDROID_ASTUBX_LOCATION);
if (androidStubxIS != null) {
cacheUtil.parseStubStream(androidStubxIS, "android.jar: " + ANDROID_ASTUBX_LOCATION);
astubxLoadLog("Loaded Android RT models.");
if (isJarInferEnabled) {
// hardcoded loading of stubx files from android-jarinfer-models-sdkXX artifacts
try (InputStream androidStubxIS =
castToNonNull(Class.forName(ANDROID_MODEL_CLASS).getClassLoader())
.getResourceAsStream(ANDROID_ASTUBX_LOCATION)) {
if (androidStubxIS != null) {
cacheUtil.parseStubStream(androidStubxIS, "android.jar: " + ANDROID_ASTUBX_LOCATION);
astubxLoadLog("Loaded Android RT models.");
}
} catch (ClassNotFoundException e) {
astubxLoadLog(
"Cannot find Android RT models locator class."
+ " This is expected if not in an Android project, or the Android SDK JarInfer models Jar has not been set up for this build.");

} catch (IOException e) {
astubxLoadLog("Loading Android RT models failed: " + e.getMessage());
}
} catch (ClassNotFoundException e) {
astubxLoadLog(
"Cannot find Android RT models locator class."
+ " This is expected if not in an Android project, or the Android SDK JarInfer models Jar has not been set up for this build.");
}

} catch (Exception e) {
astubxLoadLog("Cannot load Android RT models.");
if (isJSpecifyJDKEnabled) {
// hardcoded loading of JSpecify JDK astubx from jspecify-jdk.astubx
try (InputStream in =
castToNonNull(getClass().getClassLoader())
.getResourceAsStream(JSPECIFY_JDK_ASTUBX_FILENAME)) {
if (in == null) {
astubxLoadLog(
"JDK astubx model not found on classpath: %s"
.formatted(JSPECIFY_JDK_ASTUBX_FILENAME));
} else {
cacheUtil.parseStubStream(in, JSPECIFY_JDK_ASTUBX_FILENAME);
astubxLoadLog("Loaded JDK astubx model.");
}
} catch (IOException e) {
throw new UncheckedIOException(e);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Comment thread
msridhar marked this conversation as resolved.

argAnnotCache = cacheUtil.getArgAnnotCache();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ private void loadStubxFiles() {
for (String astubxPath : provider.pathsToStubxFiles()) {
Class<? extends JarInferStubxProvider> providerClass = provider.getClass();
InputStream stubxInputStream = providerClass.getResourceAsStream(astubxPath);
if (stubxInputStream == null) {
throw new RuntimeException("could not get input stream for " + astubxPath);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This change is due to the new JDK models (which model the return of getResourceAsStream as @Nullable)

String stubxLocation = providerClass + ":" + astubxPath;
try {
parseStubStream(stubxInputStream, stubxLocation);
Expand Down
Binary file added nullaway/src/main/resources/jspecify-jdk.astubx
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,15 @@ public void missingTypeAnnotationSymbolFlagForJSpecifyModeOnOlderJDK() {
assertTrue(
e.getMessage().contains("Running NullAway in JSpecify mode requires either JDK 22+"));
}

@Test
public void jspecifyJDKOutsideJSpecifyMode() {
CompilationTestHelper compilationTestHelper =
makeTestHelperWithArgs(
List.of(
"-XepOpt:NullAway:OnlyNullMarked", "-XepOpt:NullAway:JSpecifyJDKModels=true"))
.addSourceLines("Stub.java", "package com.uber; class Stub {}");
AssertionError e = assertThrows(AssertionError.class, () -> compilationTestHelper.doTest());
assertTrue(e.getMessage().contains("should only be set in JSpecify mode"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.uber.nullaway;

import com.google.errorprone.CompilationTestHelper;
import com.uber.nullaway.generics.JSpecifyJavacConfig;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class JSpecifyJDKModelsTest extends NullAwayTestsBase {

@Test
public void modelsDisabledDoesNotLoadAstubxModel() {
CompilationTestHelper compilationTestHelper =
makeTestHelperWithArgs(List.of("-XepOpt:NullAway:AnnotatedPackages=foo"))
.addSourceLines(
"Test.java",
"""
package foo;
import javax.naming.directory.Attributes;
import org.jspecify.annotations.NullMarked;
@NullMarked
class Test {
void use(Attributes attrs) {
// Attributes.get returns @Nullable in the models, but since we don't load
// models here, we get no warning
attrs.get("key").toString();
}
}
""");
compilationTestHelper.doTest();
Comment thread
msridhar marked this conversation as resolved.
}

@Test
public void listContainingNullsWithModel() {
makeTestHelperWithArgs(
JSpecifyJavacConfig.withJSpecifyModeArgs(
List.of(
"-XepOpt:NullAway:AnnotatedPackages=foo",
"-XepOpt:NullAway:JSpecifyJDKModels=true")))
.addSourceLines(
"Test.java",
"""
package foo;
import java.util.List;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
void testNullableContents(List<@Nullable String> list) {
list.add(null);
// BUG: Diagnostic contains: dereferenced expression 'list.get(0)' is @Nullable
list.get(0).toString();
}
void testNonNullContents(List<String> list) {
// BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required
list.add(null);
list.get(0).toString();
}
}
""")
.doTest();
}

@Test
public void listContainingNullsWithoutModel() {
makeTestHelperWithArgs(
JSpecifyJavacConfig.withJSpecifyModeArgs(
List.of("-XepOpt:NullAway:AnnotatedPackages=foo")))
.addSourceLines(
"Test.java",
"""
package foo;
import java.util.List;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
void use(List<@Nullable String> list) {
list.add(null);
// no warning, since List.get() is unmarked without the model
list.get(0).toString();
}
void testNonNullContents(List<String> list) {
// no warning, since List.add() is unmarked without the model
list.add(null);
list.get(0).toString();
}
}
""")
.doTest();
}
}
Loading