Skip to content

Comments

Option that loads nullness information of JDK package#1472

Open
haewiful wants to merge 12 commits intouber:masterfrom
haewiful:jdk-astubx
Open

Option that loads nullness information of JDK package#1472
haewiful wants to merge 12 commits intouber:masterfrom
haewiful:jdk-astubx

Conversation

@haewiful
Copy link
Contributor

@haewiful haewiful commented Feb 15, 2026

This pull request adds some nullness check support for modules in the JDK package.

  • Adds the astubx file that contains the nullness information for the modules in the JDK package.
  • Adds an option NullAway:JDKInferEnabled that lets the users decide whether to load the astubx file to NullAway.

Some related PRs are #1333.

The unit tests that are added to LibraryModelsHandlerTest.java file.

Summary by CodeRabbit

  • New Features

    • Added a configuration flag to enable optional JSpecify JDK models for improved null-safety checks on JDK APIs.
  • Behavior

    • Library model loading is now conditional—JDK models load only when the new flag is enabled; Android model loading remains tied to the existing flag.
  • Tests

    • Added tests verifying JSpecify JDK model loading and non-loading behavior.

@haewiful haewiful marked this pull request as ready for review February 15, 2026 04:39
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new Config API flag isJSpecifyJDKModels() and implements it in DummyOptionsConfig and ErrorProneCLIFlagsConfig (backed by a new Error Prone CLI flag). LibraryModelsHandler and its ExternalStubxLibraryModels constructor/signature are updated to accept and use both JarInfer and JSpecify-JDK flags, splitting loading paths so Android stubx models load only when JarInfer is enabled and JDK astubx models load only when the JSpecify-JDK flag is enabled. New tests exercise enabled/disabled behaviors.

Possibly related PRs

Suggested reviewers

  • yuxincs
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding an option to load JDK package nullness information, which is reflected in the new configuration flag and library models handler changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java`:
- Line 1564: The constructor ExternalStubxLibraryModels(boolean
isJarInferEnabled, boolean isJDKInferEnabled) uses two positional booleans that
are easy to swap; change the API to accept a descriptive config object or
factory so callers pass named fields. Create a small value class (e.g.,
LibraryInferOptions or InferConfig) with boolean fields jarInferEnabled and
jdkInferEnabled, update ExternalStubxLibraryModels to take that single config
parameter, and update the existing call site to construct and pass the config
(or alternatively add clearly named static factory methods on
ExternalStubxLibraryModels like createWith(jarEnabled, jdkEnabled)). Ensure the
constructor and any usages are updated consistently.
- Around line 1588-1600: Fix the typo in the comment inside LibraryModelsHandler
near the JDK astubx loading block: change "infered" to "inferred" in the comment
that references hardcoded loading of stubx files when isJDKInferEnabled (the
block using getClass().getClassLoader().getResourceAsStream("output.astubx") and
cacheUtil.parseStubStream). Do not change resource names or logic—only update
the comment wording.

private final Map<String, SetMultimap<Integer, NestedAnnotationInfo>> nestedAnnotationInfo;

ExternalStubxLibraryModels() {
ExternalStubxLibraryModels(boolean isJarInferEnabled, boolean isJDKInferEnabled) {
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

Constructor now accepts two separate booleans — consider clarity.

Two positional boolean parameters (isJarInferEnabled, isJDKInferEnabled) can be accidentally swapped at call sites without a compiler error. This is a minor readability concern for now since there's only one call site (Line 483), but worth noting.

🤖 Prompt for AI Agents
In `@nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java`
at line 1564, The constructor ExternalStubxLibraryModels(boolean
isJarInferEnabled, boolean isJDKInferEnabled) uses two positional booleans that
are easy to swap; change the API to accept a descriptive config object or
factory so callers pass named fields. Create a small value class (e.g.,
LibraryInferOptions or InferConfig) with boolean fields jarInferEnabled and
jdkInferEnabled, update ExternalStubxLibraryModels to take that single config
parameter, and update the existing call site to construct and pass the config
(or alternatively add clearly named static factory methods on
ExternalStubxLibraryModels like createWith(jarEnabled, jdkEnabled)). Ensure the
constructor and any usages are updated consistently.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java (2)

568-586: ⚠️ Potential issue | 🟡 Minor

Wrap the Android InputStream in try-with-resources to prevent resource leaks.

The InputStream obtained from the Android model class loader at line 1570 is never closed. Although parseStubStream does not close the stream internally, the JDK loading pattern at line 1590 correctly uses try-with-resources. Apply the same pattern to the Android code for consistency and to prevent the stream from leaking if parseStubStream throws an exception.

Proposed fix
       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.");
+          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) {

588-600: ⚠️ Potential issue | 🟠 Major

Generic resource name output.astubx risks classpath collisions.

The resource name "output.astubx" is not namespaced, so any other JAR on the classpath with the same resource name could shadow or be loaded instead of the intended JDK nullness data. Consider using a more specific name (e.g., "com/uber/nullaway/jdk-nullness.astubx") to avoid ambiguity.

The loading logic itself (try-with-resources, null check, exception handling) is well-structured.

#!/bin/bash
# Check how/where output.astubx is generated and placed
echo "=== Searching for output.astubx references ==="
rg -n "output\.astubx" --type-not class -C 2

echo ""
echo "=== Checking resource directories ==="
fd "output.astubx" --type f

echo ""
echo "=== Checking build configs for resource placement ==="
rg -n "output\.astubx" -g "*.gradle*" -g "*.xml" -C 3

@codecov
Copy link

codecov bot commented Feb 15, 2026

Codecov Report

❌ Patch coverage is 53.57143% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.42%. Comparing base (09b39c9) to head (146c824).

Files with missing lines Patch % Lines
...m/uber/nullaway/handlers/LibraryModelsHandler.java 48.00% 13 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1472      +/-   ##
============================================
- Coverage     88.46%   88.42%   -0.04%     
- Complexity     2757     2760       +3     
============================================
  Files            99       99              
  Lines          9196     9210      +14     
  Branches       1847     1850       +3     
============================================
+ Hits           8135     8144       +9     
- Misses          521      526       +5     
  Partials        540      540              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java`:
- Around line 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.

In `@nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java`:
- Around line 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.
- Around line 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.

---

Duplicate comments:
In `@nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java`:
- Line 1564: The constructor ExternalStubxLibraryModels(boolean
isJarInferEnabled, boolean isJSpecifyJDKEnabled) uses two same-typed booleans
that are easy to swap; make the API explicit by replacing the public constructor
with a private one and add a clearly named static factory or builder (e.g.,
ExternalStubxLibraryModels.createWithJarInferAndJSpecify(boolean
jarInferEnabled, boolean jSpecifyJDKEnabled) or a Builder class) and update all
call sites (calls to new ExternalStubxLibraryModels(...)) to use that
factory/builder so callers supply named intent and accidental argument
transposition is prevented; keep the class name ExternalStubxLibraryModels and
preserve the internal field names isJarInferEnabled/isJSpecifyJDKEnabled.

Comment on lines 87 to +91
/** --- JarInfer configs --- */
static final String FL_JI_ENABLED = EP_FL_NAMESPACE + ":JarInferEnabled";

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

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.

Comment on lines +1568 to +1586
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.");
}
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.

Comment on lines +1589 to +1599
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);
}
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nullaway/src/test/java/com/uber/nullaway/JSpecifyJDKModelsTest.java`:
- Around line 12-54: Add a third test method in JSpecifyJDKModelsTest (e.g.,
modelsEnabledWithJSpecifyModeLoadsAstubxModel) that calls makeTestHelperWithArgs
with both "-XepOpt:NullAway:JSpecifyMode=true" and
"-XepOpt:NullAway:JSpecifyJDKModels=true" (and
"-XepOpt:NullAway:AnnotatedPackages=foo"), add the same Test.java source as the
first test that expects a `@Nullable` diagnostic on attrs.get("key").toString(),
and call doTest(); this ensures the interaction between JSpecifyMode and
JSpecifyJDKModels is exercised.
- Around line 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.

Comment on lines +36 to +54
@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();
}
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nullaway/src/test/java/com/uber/nullaway/JSpecifyJDKModelsTest.java`:
- Around line 73-75: In JSpecifyJDKModelsTest, remove the unexplained dead-code
comment "// list.add(null);" (or replace it with a one-line explanation if it’s
intentionally omitted) and tighten the test assertion by replacing the empty "//
BUG: Diagnostic contains:" with an explicit expected fragment such as "// BUG:
Diagnostic contains: `@Nullable`" so the diagnostic on the following line
(list.get(0).toString();) is asserted precisely; update the comment near the
test method in JSpecifyJDKModelsTest to reflect this change.
- Around line 56-80: Add a companion test that mirrors
listContainingNullsWithModel() but omits the
"-XepOpt:NullAway:JSpecifyJDKModels=true" arg to verify the diagnostic depends
on the JSpecify JDK model flag; copy the same source snippet and create a new
test method (e.g., listContainingNullsWithoutModel) using
makeTestHelperWithArgs(JSpecifyJavacConfig.withJSpecifyModeArgs(List.of("-XepOpt:NullAway:AnnotatedPackages=foo")))
and assert the opposite outcome (adjust the BUG comment/expectation so this
variant does not report the diagnostic), keeping the test structure consistent
with modelsEnabledLoadsAstubxModel() / modelsDisabledDoesNotLoadAstubxModel().

---

Duplicate comments:
In `@nullaway/src/test/java/com/uber/nullaway/JSpecifyJDKModelsTest.java`:
- Around line 36-54: The test modelsDisabledDoesNotLoadAstubxModel assumes no
other NullAway model (built-in or JarInfer) marks
javax.naming.directory.Attributes.get() as `@Nullable`, which would cause this
negative test to fail; add a concise comment above the test (near
modelsDisabledDoesNotLoadAstubxModel / the
makeTestHelperWithArgs/CompilationTestHelper setup) stating this hidden
assumption and that adding any model making Attributes.get() nullable will cause
the diagnostic and break the test so future contributors understand the
dependency.

Comment on lines +56 to +80
@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 use(List<@Nullable String> list) {
// list.add(null);
// BUG: Diagnostic contains:
list.get(0).toString();
}
}
""")
.doTest();
}
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

🧩 Analysis chain

🏁 Script executed:

# First, let's find the test file and examine its structure
find . -name "JSpecifyJDKModelsTest.java" -type f

Repository: uber/NullAway

Length of output: 126


🏁 Script executed:

# Look for tests with similar names or related to listContainingNulls
rg -n "listContainingNulls" --type java -A 5 -B 2

Repository: uber/NullAway

Length of output: 914


🏁 Script executed:

# Find JDK astubx files to see what's modeled
fd -e astubx -type f | head -20

Repository: uber/NullAway

Length of output: 227


🏁 Script executed:

# Look for List interface modeling in astubx files
fd -e astubx -type f -exec grep -l "List" {} \;

Repository: uber/NullAway

Length of output: 227


🏁 Script executed:

# Read the entire test file to see all tests and understand the structure
wc -l ./nullaway/src/test/java/com/uber/nullaway/JSpecifyJDKModelsTest.java

Repository: uber/NullAway

Length of output: 129


🏁 Script executed:

# Read the test file
cat -n ./nullaway/src/test/java/com/uber/nullaway/JSpecifyJDKModelsTest.java

Repository: uber/NullAway

Length of output: 3139


🏁 Script executed:

# Find astubx files using find command instead
find . -name "*.astubx" -type f

Repository: uber/NullAway

Length of output: 408


🏁 Script executed:

# Check the output.astubx file for List.get annotation
cat ./nullaway/src/main/resources/output.astubx | grep -A 10 "class List"

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

# Also check if there's a pattern in astubx for java.util.List
cat ./nullaway/src/main/resources/output.astubx | grep -A 5 -B 2 "List.*get"

Repository: uber/NullAway

Length of output: 100


🏁 Script executed:

# Check the file size and structure
head -50 ./nullaway/src/main/resources/output.astubx

Repository: uber/NullAway

Length of output: 50370


🏁 Script executed:

# Search for strings in the binary astubx file related to List
strings ./nullaway/src/main/resources/output.astubx | grep -i "java.util.list\|List.*get" | head -20

Repository: uber/NullAway

Length of output: 102


🏁 Script executed:

# Look for any other test methods related to JSpecifyJDKModels or similar patterns
rg "public void" nullaway/src/test/java/com/uber/nullaway/JSpecifyJDKModelsTest.java

Repository: uber/NullAway

Length of output: 206


🏁 Script executed:

# Check if there are any comments or documentation about the test
rg -B 5 "listContainingNullsWithModel" nullaway/src/test/java/com/uber/nullaway/JSpecifyJDKModelsTest.java

Repository: uber/NullAway

Length of output: 174


🏁 Script executed:

# Look for related tests that might be testing List behavior
rg -n "listContaining|List.*get" --type java -A 3 -B 1

Repository: uber/NullAway

Length of output: 49500


Add a test variant without JSpecifyJDKModels=true to isolate the flag's necessity.

The test pattern established by modelsEnabledLoadsAstubxModel() and modelsDisabledDoesNotLoadAstubxModel() shows the proper way to validate that a flag is causally required. listContainingNullsWithModel() should have a corresponding variant without the flag to confirm that the diagnostic actually depends on JDK model loading. In JSpecify mode, List<@Nullable String>.get(0) returning @Nullable String may result from generic type-argument substitution alone, making the "WithModel" suffix misleading if the flag doesn't affect the outcome.

🤖 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 56 - 80, Add a companion test that mirrors listContainingNullsWithModel()
but omits the "-XepOpt:NullAway:JSpecifyJDKModels=true" arg to verify the
diagnostic depends on the JSpecify JDK model flag; copy the same source snippet
and create a new test method (e.g., listContainingNullsWithoutModel) using
makeTestHelperWithArgs(JSpecifyJavacConfig.withJSpecifyModeArgs(List.of("-XepOpt:NullAway:AnnotatedPackages=foo")))
and assert the opposite outcome (adjust the BUG comment/expectation so this
variant does not report the diagnostic), keeping the test structure consistent
with modelsEnabledLoadsAstubxModel() / modelsDisabledDoesNotLoadAstubxModel().

Comment on lines +73 to +75
// list.add(null);
// BUG: Diagnostic contains:
list.get(0).toString();
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

Remove commented-out line; tighten the empty Diagnostic contains: message.

Two issues:

  1. Line 73// list.add(null); is unexplained dead code. Either remove it or add a comment explaining why it's intentionally excluded.
  2. Line 74// BUG: Diagnostic contains: has no expected text, so the assertion will pass for any diagnostic on the next line. The first test uses @Nullable as the expected fragment; applying the same precision here makes the assertion meaningful and catches regressions where the wrong diagnostic fires.
♻️ Proposed fix
  void use(List<@Nullable String> list) {
-   // list.add(null);
    // BUG: Diagnostic contains: `@Nullable`
    list.get(0).toString();
  }
📝 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
// list.add(null);
// BUG: Diagnostic contains:
list.get(0).toString();
void use(List<@Nullable String> list) {
// BUG: Diagnostic contains: `@Nullable`
list.get(0).toString();
}
🤖 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 73 - 75, In JSpecifyJDKModelsTest, remove the unexplained dead-code
comment "// list.add(null);" (or replace it with a one-line explanation if it’s
intentionally omitted) and tighten the test assertion by replacing the empty "//
BUG: Diagnostic contains:" with an explicit expected fragment such as "// BUG:
Diagnostic contains: `@Nullable`" so the diagnostic on the following line
(list.get(0).toString();) is asserted precisely; update the comment near the
test method in JSpecifyJDKModelsTest to reflect this change.

@msridhar
Copy link
Collaborator

@haewiful looking good overall. I made some cleanup changes and renamings. But I added another test that fails. The reason is that the parsing of the astubx file crashes in the middle. Right now we catch the exception at line 1598 of LibraryModelsHandler so we don't see the crash; probably we should get rid of that catch block, or only catch IOException there? In any case, here's the exception that gets caught there:

Caused by: java.lang.IllegalArgumentException: No enum constant com.uber.nullaway.librarymodel.NestedAnnotationInfo.TypePathEntry.Kind.java.util.logging.Handler
	at java.base/java.lang.Enum.valueOf(Enum.java:293)
	at com.uber.nullaway.librarymodel.NestedAnnotationInfo$TypePathEntry$Kind.valueOf(NestedAnnotationInfo.java:28)
	at com.uber.nullaway.handlers.StubxCacheUtil.parseStubStream(StubxCacheUtil.java:218)
	at com.uber.nullaway.handlers.LibraryModelsHandler$ExternalStubxLibraryModels.<init>(LibraryModelsHandler.java:1596)
	at com.uber.nullaway.handlers.LibraryModelsHandler.loadLibraryModels(LibraryModelsHandler.java:485)
	at com.uber.nullaway.handlers.LibraryModelsHandler.<init>(LibraryModelsHandler.java:99)
	at com.uber.nullaway.handlers.Handlers.buildDefault(Handlers.java:60)
	at com.uber.nullaway.NullAway.<init>(NullAway.java:308)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants