diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNullIfReturns.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNullIfReturns.java new file mode 100644 index 000000000000..cd7cc03d355d --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNullIfReturns.java @@ -0,0 +1,82 @@ +package org.checkerframework.checker.nullness.qual; + +import org.checkerframework.framework.qual.ParameterConditionalPostconditionAnnotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A parameter contract: expresses that the parameter is non-null when the method returns the + * given value. + * + *
Semantic meaning: {@code @NonNullIfReturns(v)} means: this parameter is non-null + * when the method returns v. That is: {@code method returns value() ⇒ parameter != null}. + * + *
This annotation only makes sense on parameters whose root type is {@code @Nullable} or + * parametric. The checker refines the parameter's type to {@code @NonNull} in code paths where the + * method's return value is known (e.g., inside {@code if (method(arg))} or {@code if + * (!method(arg))} blocks). + * + *
Example 1: hasLength — param is non-null when method returns true. + * + *
{@code
+ * public static boolean hasLength(@NonNullIfReturns(true) @Nullable String str) {
+ * return (str != null && !str.isEmpty());
+ * }
+ * }
+ *
+ * When {@code hasLength(s)} returns true, {@code s} was non-null.
+ *
+ * Example 2: equals — param is non-null when method returns true. + * + *
{@code
+ * @Override
+ * public boolean equals(@NonNullIfReturns(true) @Nullable Object other) {
+ * return this == other;
+ * }
+ * }
+ *
+ * Example 3: isNull — param is non-null when method returns false. + * + *
{@code
+ * public static boolean isNull(@NonNullIfReturns(false) @Nullable Object obj) {
+ * return obj == null;
+ * }
+ * }
+ *
+ * When {@code isNull(obj)} returns false, {@code obj} was non-null.
+ *
+ * For a fixed return value, use {@link NonNullIfReturnsTrue} or {@link NonNullIfReturnsFalse} + * instead. + * + * @see NonNullIfReturnsTrue + * @see NonNullIfReturnsFalse + * @see EnsuresNonNullIf + * @see NonNull + * @see Nullable + * @see org.checkerframework.checker.nullness.NullnessChecker + * @checker_framework.manual #nullness-checker Nullness Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +@ParameterConditionalPostconditionAnnotation(qualifier = NonNull.class) +public @interface NonNullIfReturns { + /** + * The return value under which the parameter is non-null. + * + * @return the return value under which the parameter has the qualifier (e.g., {@code true} for + * {@code hasLength}, {@code false} for {@code isNull}) + */ + boolean value(); +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNullIfReturnsFalse.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNullIfReturnsFalse.java new file mode 100644 index 000000000000..fe6d9420999e --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNullIfReturnsFalse.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.nullness.qual; + +import org.checkerframework.framework.qual.ParameterConditionalPostconditionAnnotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A parameter contract: the parameter is non-null when the method returns {@code false}. + * + *
Equivalent to {@code @NonNullIfReturns(false)} but with a fixed value for use when the return + * condition is always false (e.g. {@code isNull}). + * + * @see NonNullIfReturns + * @see NonNullIfReturnsTrue + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +@ParameterConditionalPostconditionAnnotation(qualifier = NonNull.class, result = false) +public @interface NonNullIfReturnsFalse {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNullIfReturnsTrue.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNullIfReturnsTrue.java new file mode 100644 index 000000000000..33e685eabb16 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNullIfReturnsTrue.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.nullness.qual; + +import org.checkerframework.framework.qual.ParameterConditionalPostconditionAnnotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A parameter contract: the parameter is non-null when the method returns {@code true}. + * + *
Equivalent to {@code @NonNullIfReturns(true)} but with a fixed value for use when the return + * condition is always true (e.g. {@code hasLength}, {@code equals}). + * + * @see NonNullIfReturns + * @see NonNullIfReturnsFalse + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +@ParameterConditionalPostconditionAnnotation(qualifier = NonNull.class, result = true) +public @interface NonNullIfReturnsTrue {} diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/ParameterConditionalPostconditionAnnotation.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/ParameterConditionalPostconditionAnnotation.java new file mode 100644 index 000000000000..170ab9eb6c08 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/ParameterConditionalPostconditionAnnotation.java @@ -0,0 +1,60 @@ +package org.checkerframework.framework.qual; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A meta-annotation that indicates that an annotation E is a parameter conditional + * postcondition annotation: E is written on a formal parameter and expresses that the annotated + * parameter has a given qualifier when the method returns a given boolean value. + * + *
E either has a single element {@code value()} of type {@code boolean} with the same meaning as + * {@link EnsuresQualifierIf#result()}, or E has no such element and the result is fixed by this + * meta-annotation's {@link #result()} (for concrete annotations like + * {@code @NonNullIfReturnsTrue}). The expression to which the qualifier applies is implicit: it is + * the annotated parameter (e.g. {@code "#1"} for the first parameter). + * + *
For example, the nullness checker defines {@link + * org.checkerframework.checker.nullness.qual.NonNullIfReturns}: + * + *
+ * {@literal @}ParameterConditionalPostconditionAnnotation(qualifier = NonNull.class)
+ * {@literal @}Target(ElementType.PARAMETER)
+ * public {@literal @}interface NonNullIfReturns {
+ * boolean value();
+ * }
+ *
+ *
+ * Usage: {@code @NonNullIfReturns(true)} or {@code @NonNullIfReturnsTrue} means the parameter is
+ * non-null when the method returns true; {@code @NonNullIfReturns(false)} or
+ * {@code @NonNullIfReturnsFalse} when it returns false.
+ *
+ * @see ConditionalPostconditionAnnotation
+ * @see EnsuresQualifierIf
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.ANNOTATION_TYPE})
+public @interface ParameterConditionalPostconditionAnnotation {
+ /**
+ * The qualifier that is established on the annotated parameter when the method returns the
+ * value specified by the parameter annotation's {@code value()} element, or by this {@link
+ * #result()} when the annotation has no {@code value()} (e.g. {@code @NonNullIfReturnsTrue}).
+ *
+ * @return the qualifier
+ */
+ Class extends Annotation> qualifier();
+
+ /**
+ * The return value under which the qualifier holds. Used when the concrete annotation has no
+ * {@code value()} element (e.g. {@code @NonNullIfReturnsTrue} /
+ * {@code @NonNullIfReturnsFalse}). Ignored when the concrete annotation has {@code value()}.
+ *
+ * @return true if the qualifier holds when the method returns true, false when it returns false
+ */
+ boolean result() default true;
+}
diff --git a/checker/tests/nullness/NonNullIfReturnsTest.java b/checker/tests/nullness/NonNullIfReturnsTest.java
new file mode 100644
index 000000000000..c9215c381f12
--- /dev/null
+++ b/checker/tests/nullness/NonNullIfReturnsTest.java
@@ -0,0 +1,162 @@
+// Tests @NonNullIfReturns, @NonNullIfReturnsTrue, and @NonNullIfReturnsFalse (parameter-level
+// conditional postconditions) for both correct refinement and expected errors.
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.NonNullIfReturns;
+import org.checkerframework.checker.nullness.qual.NonNullIfReturnsFalse;
+import org.checkerframework.checker.nullness.qual.NonNullIfReturnsTrue;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public class NonNullIfReturnsTest {
+
+ // -----------------------------------------------------------------------
+ // @NonNullIfReturns(true) / (false)
+ // -----------------------------------------------------------------------
+
+ public static boolean hasLength(@NonNullIfReturns(true) @Nullable String str) {
+ return (str != null && !str.isEmpty());
+ }
+
+ void useHasLength(@Nullable String s) {
+ if (hasLength(s)) {
+ @NonNull String n = s;
+ n.hashCode();
+ } else {
+ // :: error: (assignment.type.incompatible)
+ @NonNull String n = s;
+ }
+ }
+
+ @Override
+ public boolean equals(@NonNullIfReturns(true) @Nullable Object other) {
+ return this == other;
+ }
+
+ void useEquals(NonNullIfReturnsTest a, @Nullable Object b) {
+ if (a.equals(b)) {
+ @NonNull Object n = b;
+ n.hashCode();
+ }
+ }
+
+ public static boolean isNull(@NonNullIfReturns(false) @Nullable Object obj) {
+ return obj == null;
+ }
+
+ void useIsNull(@Nullable Object p) {
+ if (!isNull(p)) {
+ @NonNull Object n = p;
+ n.hashCode();
+ } else {
+ // :: error: (assignment.type.incompatible)
+ @NonNull Object n = p;
+ }
+ }
+
+ public static boolean bothNonNull(
+ @NonNullIfReturns(true) @Nullable Object a,
+ @NonNullIfReturns(true) @Nullable Object b) {
+ return a != null && b != null;
+ }
+
+ void useBothNonNull(@Nullable Object x, @Nullable Object y) {
+ if (bothNonNull(x, y)) {
+ @NonNull Object nx = x;
+ @NonNull Object ny = y;
+ nx.hashCode();
+ ny.hashCode();
+ } else {
+ // :: error: (assignment.type.incompatible)
+ @NonNull Object nx = x;
+ // :: error: (assignment.type.incompatible)
+ @NonNull Object ny = y;
+ }
+ }
+
+ // Checker trusts the annotation; does not verify method body.
+ public boolean alwaysTrue(@NonNullIfReturns(true) @Nullable Object obj) {
+ return true;
+ }
+
+ void useAlwaysTrue(@Nullable Object p) {
+ if (alwaysTrue(p)) {
+ @NonNull Object n = p;
+ n.hashCode();
+ } else {
+ // :: error: (assignment.type.incompatible)
+ @NonNull Object n = p;
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // @NonNullIfReturnsTrue
+ // -----------------------------------------------------------------------
+
+ public static boolean hasLengthTrue(@NonNullIfReturnsTrue @Nullable String str) {
+ return (str != null && !str.isEmpty());
+ }
+
+ void useHasLengthTrue(@Nullable String s) {
+ if (hasLengthTrue(s)) {
+ @NonNull String n = s;
+ n.hashCode();
+ } else {
+ // :: error: (assignment.type.incompatible)
+ @NonNull String n = s;
+ }
+ }
+
+ public static boolean bothNonNullTrue(
+ @NonNullIfReturnsTrue @Nullable Object a, @NonNullIfReturnsTrue @Nullable Object b) {
+ return a != null && b != null;
+ }
+
+ void useBothNonNullTrue(@Nullable Object x, @Nullable Object y) {
+ if (bothNonNullTrue(x, y)) {
+ @NonNull Object nx = x;
+ @NonNull Object ny = y;
+ nx.hashCode();
+ } else {
+ // :: error: (assignment.type.incompatible)
+ @NonNull Object nx = x;
+ // :: error: (assignment.type.incompatible)
+ @NonNull Object ny = y;
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // @NonNullIfReturnsFalse
+ // -----------------------------------------------------------------------
+
+ public static boolean isNullFalse(@NonNullIfReturnsFalse @Nullable Object obj) {
+ return obj == null;
+ }
+
+ void useIsNullFalse(@Nullable Object p) {
+ if (!isNullFalse(p)) {
+ @NonNull Object n = p;
+ n.hashCode();
+ } else {
+ // :: error: (assignment.type.incompatible)
+ @NonNull Object n = p;
+ }
+ }
+
+ public static boolean neitherNull(
+ @NonNullIfReturnsFalse @Nullable Object a, @NonNullIfReturnsFalse @Nullable Object b) {
+ return a == null || b == null;
+ }
+
+ void useNeitherNull(@Nullable Object x, @Nullable Object y) {
+ if (!neitherNull(x, y)) {
+ @NonNull Object nx = x;
+ @NonNull Object ny = y;
+ nx.hashCode();
+ } else {
+ // :: error: (assignment.type.incompatible)
+ @NonNull Object nx = x;
+ // :: error: (assignment.type.incompatible)
+ @NonNull Object ny = y;
+ }
+ }
+}
diff --git a/docs/manual/contributors.tex b/docs/manual/contributors.tex
index ed211a6a48ae..83ccd0c903c1 100644
--- a/docs/manual/contributors.tex
+++ b/docs/manual/contributors.tex
@@ -51,6 +51,7 @@
Hamed Taghani,
Hannes Greule,
Heath Borders,
+Henry Xi,
Ivory Wang,
Jakub Vr\'ana,
James Yoo,
diff --git a/framework/src/main/java/org/checkerframework/common/basetype/messages.properties b/framework/src/main/java/org/checkerframework/common/basetype/messages.properties
index b7fcaf3d5c8a..c78a741af6a3 100644
--- a/framework/src/main/java/org/checkerframework/common/basetype/messages.properties
+++ b/framework/src/main/java/org/checkerframework/common/basetype/messages.properties
@@ -95,6 +95,7 @@ contracts.precondition.not.satisfied=precondition of %s is not satisfied.%nfound
contracts.postcondition.not.satisfied=postcondition of %s is not satisfied.%nfound : %s%nrequired: %s
contracts.conditional.postcondition.not.satisfied=conditional postcondition is not satisfied when %s returns %s.%nfound : %s%nrequired: %s
contracts.conditional.postcondition.invalid.returntype=this annotation is only valid for methods with return type 'boolean'
+contracts.parameter.conditional.qualifier.not.supported=parameter conditional postcondition annotation %s uses a qualifier that is not supported by the current checker
# Same text for "override" and "methodref", but different key.
contracts.precondition.override.invalid=Subclass precondition is stronger for '%s' in %s.%n In superclass %s: %s%n In subclass %s: %s
contracts.postcondition.override.invalid=Subclass postcondition is weaker for '%s' in %s.%n In superclass %s: %s%n In subclass %s: %s
diff --git a/framework/src/main/java/org/checkerframework/framework/util/DefaultContractsFromMethod.java b/framework/src/main/java/org/checkerframework/framework/util/DefaultContractsFromMethod.java
index b911609f025b..ddf3d3ad5f3f 100644
--- a/framework/src/main/java/org/checkerframework/framework/util/DefaultContractsFromMethod.java
+++ b/framework/src/main/java/org/checkerframework/framework/util/DefaultContractsFromMethod.java
@@ -4,6 +4,7 @@
import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation;
import org.checkerframework.framework.qual.EnsuresQualifier;
import org.checkerframework.framework.qual.EnsuresQualifierIf;
+import org.checkerframework.framework.qual.ParameterConditionalPostconditionAnnotation;
import org.checkerframework.framework.qual.PostconditionAnnotation;
import org.checkerframework.framework.qual.PreconditionAnnotation;
import org.checkerframework.framework.qual.QualifierArgument;
@@ -12,6 +13,7 @@
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.TreeUtils;
+import org.plumelib.util.CollectionsPlume;
import org.plumelib.util.IPair;
import java.util.Collections;
@@ -25,6 +27,7 @@
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
+import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementFilter;
/**
@@ -36,17 +39,32 @@
* @see EnsuresQualifier
* @see ConditionalPostconditionAnnotation
* @see EnsuresQualifierIf
+ * @see ParameterConditionalPostconditionAnnotation
*/
// TODO: This class assumes that most annotations have a field named "expression". If not, issue a
// more helpful error message.
public class DefaultContractsFromMethod implements ContractsFromMethod {
+ /**
+ * 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;
+
/** The QualifierArgument.value field/element. */
protected final ExecutableElement qualifierArgumentValueElement;
/** The factory that this ContractsFromMethod is associated with. */
protected final GenericAnnotatedTypeFactory, ?, ?, ?> atypeFactory;
+ /**
+ * LRU cache for conditional postconditions. Methods like {@code equals} or {@code hasNext} may
+ * be invoked many times; caching avoids re-iterating parameters and re-scanning annotations on
+ * each invocation.
+ */
+ private final Map