Skip to content

Commit 89cf2e7

Browse files
committed
Add @IfNullThrows parameter annotation
1 parent 7b8915b commit 89cf2e7

3 files changed

Lines changed: 156 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.checkerframework.dataflow.qual;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
9+
/**
10+
* A parameter annotation indicating that the method throws an exception if this parameter is null.
11+
*
12+
* <p>When the CFG is built, calls to methods with {@code @IfNullThrows} on a parameter are
13+
* translated into an explicit branch: if the argument is null, the method throws; otherwise
14+
* execution continues. This enables flow-sensitive refinement in type checkers (e.g., the Nullness
15+
* Checker refines the argument to non-null on the continue path).
16+
*
17+
* <p><b>Semantic meaning:</b> {@code @IfNullThrows} means: <i>if this parameter is null, then the
18+
* method throws</i>. Equivalently: when the method returns normally, the parameter was non-null.
19+
*
20+
* <p><b>Example:</b>
21+
*
22+
* <pre><code>
23+
* public static &lt;T&gt; T requireNonNull(@IfNullThrows @Nullable T obj) {
24+
* if (obj == null) throw new NullPointerException();
25+
* return obj;
26+
* }
27+
* </code></pre>
28+
*
29+
* @checker_framework.manual #type-refinement Automatic type refinement (flow-sensitive type
30+
* qualifier inference)
31+
*/
32+
@Documented
33+
@Retention(RetentionPolicy.RUNTIME)
34+
@Target(ElementType.PARAMETER)
35+
public @interface IfNullThrows {}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Test that @IfNullThrows (parameter-level postcondition: if null then throw) is respected by the
2+
// Nullness Checker.
3+
4+
import org.checkerframework.checker.nullness.qual.Nullable;
5+
import org.checkerframework.dataflow.qual.IfNullThrows;
6+
7+
public class IfNullThrowsTest {
8+
9+
// requireNonNull-style: if param is null, throws; so when returns, param is non-null
10+
public static <T> T requireNonNull(@IfNullThrows @Nullable T obj) {
11+
if (obj == null) {
12+
throw new NullPointerException();
13+
}
14+
return obj;
15+
}
16+
17+
void useRequireNonNull(@Nullable String s) {
18+
requireNonNull(s);
19+
s.toString(); // OK: s refined to non-null after requireNonNull returns
20+
}
21+
22+
// With message parameter
23+
public static <T> T requireNonNull(@IfNullThrows @Nullable T obj, String msg) {
24+
if (obj == null) {
25+
throw new NullPointerException(msg);
26+
}
27+
return obj;
28+
}
29+
30+
void useRequireNonNullWithMsg(@Nullable String s) {
31+
requireNonNull(s, "s must not be null");
32+
s.toString(); // OK
33+
}
34+
35+
// Multiple parameters with @IfNullThrows - validates both must be non-null to return
36+
public static void requireBothNonNull(
37+
@IfNullThrows @Nullable Object a, @IfNullThrows @Nullable Object b) {
38+
if (a == null || b == null) {
39+
throw new NullPointerException();
40+
}
41+
}
42+
43+
void useRequireBothNonNull(@Nullable Object x, @Nullable Object y) {
44+
requireBothNonNull(x, y);
45+
x.toString(); // OK
46+
y.toString(); // OK
47+
}
48+
}

dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import com.sun.source.util.TreeScanner;
5959
import com.sun.source.util.Trees;
6060
import com.sun.tools.javac.code.Type;
61+
import com.sun.tools.javac.code.TypeTag;
6162

6263
import org.checkerframework.checker.interning.qual.FindDistinct;
6364
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -139,6 +140,7 @@
139140
import org.checkerframework.dataflow.cfg.node.VariableDeclarationNode;
140141
import org.checkerframework.dataflow.cfg.node.WideningConversionNode;
141142
import org.checkerframework.dataflow.qual.AssertMethod;
143+
import org.checkerframework.dataflow.qual.IfNullThrows;
142144
import org.checkerframework.dataflow.qual.TerminatesExecution;
143145
import org.checkerframework.javacutil.AnnotationProvider;
144146
import org.checkerframework.javacutil.AnnotationUtils;
@@ -1368,6 +1370,7 @@ protected List<Node> convertCallArguments(
13681370

13691371
ArrayList<Node> convertedNodes = new ArrayList<>(numFormals);
13701372
AssertMethodTuple assertMethodTuple = getAssertMethodTuple(executable);
1373+
Set<Integer> ifNullThrowsParams = getIfNullThrowsParameterIndices(executable);
13711374

13721375
int numActuals = actualExprs.size();
13731376
if (executable.isVarArgs()) {
@@ -1386,6 +1389,9 @@ protected List<Node> convertCallArguments(
13861389
treatMethodAsAssert(
13871390
(MethodInvocationTree) tree, assertMethodTuple, actualVal);
13881391
}
1392+
if (ifNullThrowsParams.contains(i)) {
1393+
treatMethodAsIfNullThrows((MethodInvocationTree) tree, actualVal);
1394+
}
13891395
if (actualVal == null) {
13901396
throw new BugInCF(
13911397
"CFGBuilder: scan returned null for %s [%s]",
@@ -1421,6 +1427,9 @@ protected List<Node> convertCallArguments(
14211427
treatMethodAsAssert(
14221428
(MethodInvocationTree) tree, assertMethodTuple, actualVal);
14231429
}
1430+
if (ifNullThrowsParams.contains(i)) {
1431+
treatMethodAsIfNullThrows((MethodInvocationTree) tree, actualVal);
1432+
}
14241433
convertedNodes.add(methodInvocationConvert(actualVal, formals.get(i)));
14251434
}
14261435

@@ -1453,6 +1462,9 @@ protected List<Node> convertCallArguments(
14531462
if (i == assertMethodTuple.booleanParam) {
14541463
treatMethodAsAssert((MethodInvocationTree) tree, assertMethodTuple, actualVal);
14551464
}
1465+
if (ifNullThrowsParams.contains(i)) {
1466+
treatMethodAsIfNullThrows((MethodInvocationTree) tree, actualVal);
1467+
}
14561468
convertedNodes.add(methodInvocationConvert(actualVal, formals.get(i)));
14571469
}
14581470
}
@@ -1494,6 +1506,67 @@ protected AssertMethodTuple getAssertMethodTuple(ExecutableElement method) {
14941506
return new AssertMethodTuple(booleanParam, exceptionType, isAssertFalse);
14951507
}
14961508

1509+
/**
1510+
* Returns the 0-based indices of parameters annotated with {@link IfNullThrows}. Such
1511+
* parameters cause the method to throw when null; the CFG is modified to add an explicit
1512+
* branch.
1513+
*
1514+
* @param method the method or constructor
1515+
* @return indices of parameters with {@code @IfNullThrows}
1516+
*/
1517+
protected Set<Integer> getIfNullThrowsParameterIndices(ExecutableElement method) {
1518+
Set<Integer> result = new HashSet<>();
1519+
List<? extends VariableElement> params = method.getParameters();
1520+
for (int i = 0; i < params.size(); i++) {
1521+
if (annotationProvider.getDeclAnnotation(params.get(i), IfNullThrows.class) != null) {
1522+
result.add(i);
1523+
}
1524+
}
1525+
return result;
1526+
}
1527+
1528+
/**
1529+
* Translates a method call with {@link IfNullThrows} on a parameter into CFG nodes: if the
1530+
* argument is null, the method throws; otherwise execution continues.
1531+
*
1532+
* @param tree the method invocation tree
1533+
* @param argNode the node for the argument (the parameter value)
1534+
*/
1535+
protected void treatMethodAsIfNullThrows(MethodInvocationTree tree, Node argNode) {
1536+
// Create (arg == null) condition
1537+
TypeMirror booleanType = types.getPrimitiveType(TypeKind.BOOLEAN);
1538+
LiteralTree nullTree = TreeUtils.createLiteral(TypeTag.BOT, null, types.getNullType(), env);
1539+
handleArtificialTree(nullTree);
1540+
Node nullNode = new NullLiteralNode(nullTree);
1541+
extendWithNode(nullNode);
1542+
1543+
ExpressionTree argTree = (ExpressionTree) argNode.getTree();
1544+
BinaryTree eqTree =
1545+
treeBuilder.buildBinary(booleanType, Tree.Kind.EQUAL_TO, argTree, nullTree);
1546+
handleArtificialTree(eqTree);
1547+
1548+
Node condition = new EqualToNode(eqTree, argNode, nullNode);
1549+
extendWithNode(condition);
1550+
1551+
// When (arg == null) is true, throw; else continue
1552+
Label throwLabel = new Label();
1553+
Label continueLabel = new Label();
1554+
ConditionalJump cjump = new ConditionalJump(throwLabel, continueLabel);
1555+
extendWithExtendedNode(cjump);
1556+
1557+
addLabelForNextNode(throwLabel);
1558+
AssertionErrorNode assertNode =
1559+
new AssertionErrorNode(tree, condition, null, nullPointerExceptionType);
1560+
extendWithNode(assertNode);
1561+
NodeWithExceptionsHolder exNode =
1562+
extendWithNodeWithException(
1563+
new ThrowNode(null, assertNode, env.getTypeUtils()),
1564+
nullPointerExceptionType);
1565+
exNode.setTerminatesExecution(true);
1566+
1567+
addLabelForNextNode(continueLabel);
1568+
}
1569+
14971570
/** Holds the elements of an {@link AssertMethod} annotation. */
14981571
protected static class AssertMethodTuple {
14991572

0 commit comments

Comments
 (0)