-
Notifications
You must be signed in to change notification settings - Fork 42
Introduce method contracts #757
Description
With projects like Spring, Micrometer and many others starting to leverage JSpecify combined with a nullness checker, there is a very popular ask (both from Spring commiters and from community members) that we need parameter contracts to be able to achieve 0 checker errors (green build) without too much annotations like @SupressWarnings("NullAway"). The scope of JSpecify 1.0.0 does not allow to reach that IMO reasonable goal, leading to the proliferation of multiple variants of @Contract (JetBrains one, Spring one) annotation + requiring special support in the various checkers. This is not ideal.
Method level @Contract leverages its own expression language, and my understanding is that a majority of members of the JSpecify working group (including me) want to avoid such expression language. I tend to think we could replace the method level @Contract annotation with an expression language by a parameter level annotation with enums. It would cover the majority of the use cases, even if not 100%.
I am wondering if it could make sense for JSpecify to introduce a new annotation like @ParameterContract (name to be refined, hopefully with a shorter one), with no retention at runtime, defined as follow:
@Documented
@Target(ElementType.PARAMETER)
public @interface ParameterContract {
Condition when();
Effect then();
}
public enum Condition {
NONE,
IS_TRUE,
IS_FALSE,
IS_NULL,
IS_NON_NULL
}
public enum Effect {
RETURN_TRUE,
RETURN_FALSE,
RETURN_NULL,
RETURN_NON_NULL,
RETURN_SAME_NULLNESS,
FAIL
}So
@Contract("null -> false")
public static boolean hasLength(@Nullable String str) {
return (str != null && !str.isEmpty());
}Would translate to:
public static boolean hasLength(@Nullable @ParameterContract(when = IS_NULL, then = RETURN_FALSE) String str) {
return (str != null && !str.isEmpty());
}I made the exercise with https://github.com/spring-projects/spring-framework to see how far we could go with @ParameterContract, and it seems to cover 97.5% of our use cases which is IMO pretty good. We could live with a couple of remaining @SupressWarnings("NullAway") for the few remaining unsupported use case.
In details:
- 32x
@Contract("null -> false")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_FALSE) - 13x
@Contract("null -> null")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_NULL) - 12x
@Contract("null, _ -> fail") -> @ParameterContract(when = Condition.IS_NULL, then = Effect.FAIL) - 8x
@Contract("null -> null; !null -> !null")->@ParameterContract(when = Condition.NONE, then = Effect.RETURN_SAME_NULLNESS) - 6x
@Contract("null, _ -> false; _, null -> false")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_FALSE) - 5x
@Contract("_, null -> fail")->@ParameterContract(when = Condition.IS_NULL, then = Effect.FAIL) - 5x
@Contract("null -> true")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_TRUE) - 5x
@Contract("null, _ -> false")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_FALSE) - 4x
@Contract("_, null -> false")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_FALSE) - 4x
@Contract("_, null, _ -> fail")->@ParameterContract(when = Condition.IS_NULL, then = Effect.FAIL) - 4x
@Contract("false, _ -> fail")->@ParameterContract(when = Condition.IS_FALSE, then = Effect.FAIL) - 3x
@Contract("_ -> fail")->@ParameterContract(when = Condition.NONE, then = Effect.FAIL) - 2x
@Contract("!null, _ -> fail")->@ParameterContract(when = Condition.IS_NON_NULL, then = Effect.FAIL) - 2x
@Contract("null -> fail")->@ParameterContract(when = Condition.IS_NULL, then = Effect.FAIL) - 2x
@Contract("null, _ -> param2; _, null -> param1") -> Not covered (no support for returning parameter values) - 1x
@Contract("_, !null -> fail")->@ParameterContract(when = Condition.IS_NON_NULL, then = Effect.FAIL) - 1x
@Contract("_, false -> fail")->@ParameterContract(when = Condition.IS_FALSE, then = Effect.FAIL) - 1x
@Contract("_, null -> true")->@ParameterContract(when = Condition.IS_NULL, then = Effect.TRUE) - 1x
@Contract("_, null -> true; null, _ -> false")->@ParameterContract(when = Condition.IS_NULL, then = Effect.FAIL) - 1x
@Contract("_, null, _ -> fail; _, _, null -> fail")->@ParameterContract(when = Condition.IS_NULL, then = Effect.FAIL) - 1x
@Contract("_, null, null -> fail") -> Not covered (no support for contract with conditions involving multiple parameters) - 1x
@Contract("_, true -> fail")->@ParameterContract(when = Value.TRUE, then = Effect.FAIL) - 1x
@Contract("null, _ -> false; _, null -> false;")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_FALSE) - 1x
@Contract("null, _ -> null")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_NULL) - 1x
@Contract("null, _ -> null; _, null -> null")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_NULL) - 1x
@Contract("null, _ -> true")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_TRUE) - 1x
@Contract("null, _, _ -> null")->@ParameterContract(when = Condition.IS_NULL, then = Effect.RETURN_NULL)
Any thoughts?