Skip to content

Add @NonNullIfReturn: parameter-level conditional postcondition for nullness#1530

Open
HenryXi1 wants to merge 7 commits intoeisop:masterfrom
HenryXi1:add-parameter-annotation-for-conditional-nullness
Open

Add @NonNullIfReturn: parameter-level conditional postcondition for nullness#1530
HenryXi1 wants to merge 7 commits intoeisop:masterfrom
HenryXi1:add-parameter-annotation-for-conditional-nullness

Conversation

@HenryXi1
Copy link
Copy Markdown

Goal

Enable expressing "parameter is non-null when the method returns true/false" by annotating the parameter directly (e.g., @NonNullIfReturn(true)) instead of the method-level @EnsuresNonNullIf(expression="#1", result=true).

Changes

checker-qual

  • New Meta-annotation: Added @ParameterConditionalPostconditionAnnotation to framework.qual.
    • Annotations using this must have a single value() of type boolean.
    • The expression is automatically mapped to the annotated parameter (#1, #2, etc.).
  • New Nullness Annotation: Added @NonNullIfReturn.
    • Target: PARAMETER
    • Value: boolean
    • Meta-annotation: @ParameterConditionalPostconditionAnnotation(qualifier = NonNull.class)

Framework

  • DefaultContractsFromMethod: Updated logic for CONDITIONALPOSTCONDITION.
    • Now iterates over method parameters to find annotations meta-annotated with ParameterConditionalPostconditionAnnotation.
    • Dynamically builds a Contract.ConditionalPostcondition using the parameter index for the expression and the annotation’s value() for the result.
    • Validates that the current type factory supports the qualifier before adding the contract.
    • Note: Verified that all temporary System.err.println debug logging has been removed.

Tests

  • NonNullIfReturnTest: Created a new test suite covering:
    • @NonNullIfReturn(true) and @NonNullIfReturn(false) behaviors.
    • Scenarios with single and multiple parameters.
    • Verification that the checker correctly refines the type in the then-branch and reports expected errors in the else-branch.

@wmdietl
Copy link
Copy Markdown
Member

wmdietl commented Feb 21, 2026

The misc failure is:

Checking the definition of qualifiers in Checker Framework against the JDK
File missing in JDK: checker/nullness/qual/NonNullIfReturn.java
File missing in JDK: framework/qual/ParameterConditionalPostconditionAnnotation.java
+ '[' false = true ']'
+ '[' true = true ']'
Differences found or files missing/removed. Exiting with failure.

Once we've settled on the annotation names in this PR, open a corresponding PR in https://github.com/eisop/jdk/ with the same branch name as what you're using in this PR. The build system should then pick up the corresponding changes and be happy again.

* each invocation.
*/
private final Map<ExecutableElement, Set<Contract.ConditionalPostcondition>>
conditionalPostconditionCache = new ConcurrentHashMap<>();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See how we do caching elsewhere:

We want a maximum size and LRU policy, to avoid using up too much memory.
Concurrency is not a concern, everything runs in one thread.
We can pick our own cache size, if we can't access the methods from the ATF.

Copy link
Copy Markdown
Member

@wmdietl wmdietl left a comment

Choose a reason for hiding this comment

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

Great job!

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@ParameterConditionalPostconditionAnnotation(qualifier = NonNull.class)
public @interface NotNullIfReturns {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For consistency with the qualifier, how about calling this NonNullIfReturns?

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface ParameterConditionalPostconditionAnnotation {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We might want NonNullIfReturnsTrue and NonNullIfReturnsFalse as concrete annotations.
How could we re-model this to support a fixed truth value?

// Example 2: equals — param is non-null when method returns true
@Override
public boolean equals(@NotNullIfReturns(true) @Nullable Object other) {
return this == other;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It should already come from the existing logic, but for completeness, add a test where the logic is incorrect - e.g. always return true, without checking the parameter.

}

// Example 3: isNull — param is non-null when method returns false
public static boolean isNull(@NotNullIfReturns(false) @Nullable Object obj) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should there be a well-formedness check that the parameter is @Nullable?
Should this be expressible in the Parameter conditional... Annotation?

* Maximum size for the conditional postcondition cache. Uses LRU eviction to avoid unbounded
* memory use. Concurrency is not a concern (single-threaded).
*/
private static final int CACHE_SIZE = 300;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of our own size, can you just use atypeFactory.getCacheSize()? That also allows adapting the size with a command line flag.

AnnotationMirror metaAnnotation = r.second;
AnnotationMirror enforcedQualifier =
getQualifierEnforcedByContractAnnotation(metaAnnotation, null, null);
if (enforcedQualifier == null) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Raise an error if the qualifier is used incorrectly.

@HenryXi1 HenryXi1 force-pushed the add-parameter-annotation-for-conditional-nullness branch from e1923a9 to 1eecfca Compare March 11, 2026 00:14
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