Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
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 @@ -285,6 +285,13 @@ public interface Config {
*/
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 @@ -87,6 +87,8 @@ final class ErrorProneCLIFlagsConfig implements Config {
/** --- JarInfer configs --- */
static final String FL_JI_ENABLED = EP_FL_NAMESPACE + ":JarInferEnabled";

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

Comment on lines 87 to +91
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Nitpick: FL_JSPECIFY_JDK_ENABLED is grouped under the JarInfer section comment.

FL_JSPECIFY_JDK_ENABLED (and its companion field jspecifyJDKModelsEnabled at line 252) is placed under /** --- JarInfer configs --- */, but JSpecify JDK models are a conceptually distinct feature. Consider either adding a separate section comment or renaming the existing one.

♻️ Suggested section update
-  /** --- JarInfer configs --- */
+  /** --- JarInfer and JDK model configs --- */
   static final String FL_JI_ENABLED = EP_FL_NAMESPACE + ":JarInferEnabled";
   static final String FL_JSPECIFY_JDK_ENABLED = EP_FL_NAMESPACE + ":JSpecifyJDKModels";

And likewise for the field comment at line 249 and the constructor comment at line 338.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** --- JarInfer configs --- */
static final String FL_JI_ENABLED = EP_FL_NAMESPACE + ":JarInferEnabled";
static final String FL_JSPECIFY_JDK_ENABLED = EP_FL_NAMESPACE + ":JSpecifyJDKModels";
/** --- JarInfer and JDK model configs --- */
static final String FL_JI_ENABLED = EP_FL_NAMESPACE + ":JarInferEnabled";
static final String FL_JSPECIFY_JDK_ENABLED = EP_FL_NAMESPACE + ":JSpecifyJDKModels";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java`
around lines 87 - 91, The FL_JSPECIFY_JDK_ENABLED constant is incorrectly
grouped under the JarInfer section; update the file to separate JSpecify-related
flags by either adding a new section comment like "/** --- JSpecify configs ---
*/" above FL_JSPECIFY_JDK_ENABLED or renaming the existing JarInfer section to
reflect both features, and likewise move or add matching comments for the
jspecifyJDKModelsEnabled field and the constructor comment that references
JSpecify JDK models so all JSpecify symbols (FL_JSPECIFY_JDK_ENABLED,
jspecifyJDKModelsEnabled, and the constructor comment) are grouped under a
dedicated JSpecify section.

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

/** --- Serialization configs --- */
Expand Down Expand Up @@ -247,6 +249,8 @@ final class ErrorProneCLIFlagsConfig implements Config {
/** --- 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 @@ -333,6 +337,7 @@ final class ErrorProneCLIFlagsConfig implements Config {

/* --- JarInfer configs --- */
jarInferEnabled = flags.getBoolean(FL_JI_ENABLED).orElse(false);
jspecifyJDKModelsEnabled = flags.getBoolean(FL_JSPECIFY_JDK_ENABLED).orElse(false);
errorURL = flags.get(FL_ERROR_URL).orElse(DEFAULT_URL);
if (acknowledgeAndroidRecent && !isAcknowledgeRestrictive) {
throw new IllegalStateException(
Expand Down Expand Up @@ -589,6 +594,11 @@ public boolean isJarInferEnabled() {
return jarInferEnabled;
}

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

@Override
public String getErrorURL() {
return errorURL;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,8 +478,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 @@ -1560,26 +1561,42 @@ 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);
// 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) {
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.");
}
} 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.");
}
} 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.");
}
Comment on lines +1568 to +1586
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resource leak: androidStubxIS is never closed.

The Android InputStream obtained from getResourceAsStream on line 1570 is not closed in any code path. If cacheUtil.parseStubStream throws — caught by the outer catch (Exception e) — the stream is abandoned. Even on the happy path, no close() call exists.

This contrasts with the JDK loading block immediately below (lines 1590–1596) which correctly uses try-with-resources. Streams backed by JAR entries hold open JAR file connections and file descriptors at the JDK level; in a long-running Gradle daemon these accumulate.

🔒 Proposed fix — use nested try-with-resources to match the JDK path
       if (isJarInferEnabled) {
         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.");
+          ClassLoader androidLoader = Class.forName(ANDROID_MODEL_CLASS).getClassLoader();
+          try (InputStream androidStubxIS =
+              androidLoader.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 (Exception e) {
           astubxLoadLog("Cannot load Android RT models.");
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java`
around lines 1568 - 1586, The InputStream androidStubxIS opened via
Class.forName(...).getClassLoader().getResourceAsStream(ANDROID_ASTUBX_LOCATION)
must be closed; change the code to obtain the Class (using
Class.forName(ANDROID_MODEL_CLASS)) and then open the resource inside a
try-with-resources so the stream is always closed (e.g. Class.forName(...) then
try (InputStream androidStubxIS =
clazz.getClassLoader().getResourceAsStream(ANDROID_ASTUBX_LOCATION)) { if
(androidStubxIS != null) { cacheUtil.parseStubStream(androidStubxIS,
"android.jar: " + ANDROID_ASTUBX_LOCATION); astubxLoadLog("Loaded Android RT
models."); } } ), while preserving the existing catches for
ClassNotFoundException and Exception and using the same references to
ANDROID_MODEL_CLASS, ANDROID_ASTUBX_LOCATION, cacheUtil.parseStubStream, and
astubxLoadLog.


} catch (Exception e) {
astubxLoadLog("Cannot load Android RT models.");
// hardcoded loading of stubx files from jdk nullness inferred output.astubx
if (isJSpecifyJDKEnabled) {
try (InputStream in = getClass().getClassLoader().getResourceAsStream("output.astubx")) {
if (in == null) {
astubxLoadLog("JDK astubx model not found on classpath: output.astubx");
} else {
cacheUtil.parseStubStream(in, "output.astubx");
astubxLoadLog("Loaded JDK astubx model.");
}
} catch (Exception e) {
astubxLoadLog("Failed to load JDK astubx: " + e);
}
Comment on lines +1589 to +1599
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent failure when output.astubx is missing while JSpecifyJDKModels=true.

When the user explicitly opts in via NullAway:JSpecifyJDKModels=true but output.astubx is absent from the classpath, in is null (line 1591) and the only feedback is astubxLoadLog(...), which is gated on DEBUG_ASTUBX_LOADING = false (line 1770). The user silently gets no JDK nullness information without any indication of why.

Same concern applies to the catch (Exception e) path on line 1597 — a parse failure produces no visible warning.

Consider emitting an unconditional warning to System.err (or the compiler's Trees/Log) for the in == null case when the flag is explicitly enabled, matching how other missing-resource cases are handled in the ecosystem.

💡 Suggested minimal fix
       if (isJSpecifyJDKEnabled) {
         try (InputStream in = getClass().getClassLoader().getResourceAsStream("output.astubx")) {
           if (in == null) {
-            astubxLoadLog("JDK astubx model not found on classpath: output.astubx");
+            // Always warn: user explicitly enabled the flag but the resource is absent
+            System.err.println(
+                "[NullAway] WARNING: JSpecifyJDKModels is enabled but output.astubx was not found on the classpath.");
           } else {
             cacheUtil.parseStubStream(in, "output.astubx");
             astubxLoadLog("Loaded JDK astubx model.");
           }
         } catch (Exception e) {
-          astubxLoadLog("Failed to load JDK astubx: " + e);
+          System.err.println("[NullAway] WARNING: Failed to load JDK astubx model: " + e);
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java`
around lines 1589 - 1599, When isJSpecifyJDKEnabled is true and
getClass().getClassLoader().getResourceAsStream("output.astubx") returns null or
cacheUtil.parseStubStream(...) throws, do not rely only on astubxLoadLog (which
is gated by DEBUG_ASTUBX_LOADING); instead emit an unconditional user-visible
warning. Update the block around isJSpecifyJDKEnabled /
getResourceAsStream("output.astubx") / cacheUtil.parseStubStream so that in the
in == null branch and in the catch (Exception e) branch you print a clear
warning (e.g., via System.err.println or the compiler/annotation-processing log
API such as Messager/Tree/Log) indicating JSpecifyJDKModels was enabled but
output.astubx was not found or failed to parse and include the exception message
in the parse-failure case; keep the existing astubxLoadLog calls for debug
logging.

}

argAnnotCache = cacheUtil.getArgAnnotCache();
Expand Down
Binary file added nullaway/src/main/resources/output.astubx
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.uber.nullaway;

import com.google.errorprone.CompilationTestHelper;
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 modelsEnabledLoadsAstubxModel() {
CompilationTestHelper compilationTestHelper =
makeTestHelperWithArgs(
List.of(
"-XepOpt:NullAway:AnnotatedPackages=foo",
"-XepOpt:NullAway:JSpecifyJDKModels=true"))
.addSourceLines(
"Test.java",
"""
package foo;
import javax.naming.directory.Attributes;
import org.jspecify.annotations.NullMarked;
@NullMarked
class Test {
void use(Attributes attrs) {
// BUG: Diagnostic contains: @Nullable
attrs.get("key").toString();
}
}
""");
compilationTestHelper.doTest();
}

@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) {
attrs.get("key").toString();
}
}
""");
compilationTestHelper.doTest();
}
Comment on lines +36 to +54
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Negative test is implicitly fragile — explain the assumption.

modelsDisabledDoesNotLoadAstubxModel() passes today because no other NullAway model (built-in or JarInfer) annotates javax.naming.directory.Attributes.get() as @Nullable. If a future model is added for this API, the test will fail with an unexpected diagnostic and give no clear signal about why.

Consider adding a brief comment to document the assumption:

📝 Suggested comment to document assumption
  `@Test`
  public void modelsDisabledDoesNotLoadAstubxModel() {
+   // Without the JSpecifyJDKModels flag, no NullAway model annotates
+   // Attributes.get() as `@Nullable`, so the dereference is not flagged.
    CompilationTestHelper compilationTestHelper =
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nullaway/src/test/java/com/uber/nullaway/JSpecifyJDKModelsTest.java` around
lines 36 - 54, Add a short explanatory comment at the start of the test method
modelsDisabledDoesNotLoadAstubxModel stating the implicit assumption being
relied on: that no current NullAway model (built-in or JarInfer) marks
javax.naming.directory.Attributes.get() as `@Nullable`, and that if a future model
annotates that API the test will begin producing a diagnostic and should be
updated or adjusted; mention that the comment documents the fragility and what
to change if such a model is added.

}