diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/IfNullThrows.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/IfNullThrows.java new file mode 100644 index 000000000000..5e55baafb6b4 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/IfNullThrows.java @@ -0,0 +1,35 @@ +package org.checkerframework.checker.nullness.qual; + +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 annotation indicating that the method throws an exception if this parameter is null. + * + *
When the CFG is built, calls to methods with {@code @IfNullThrows} on a parameter are + * translated into an explicit branch: if the argument is null, the method throws; otherwise + * execution continues. This enables flow-sensitive refinement in type checkers (e.g., the Nullness + * Checker refines the argument to non-null on the continue path). + * + *
Semantic meaning: {@code @IfNullThrows} means: if this parameter is null, then the + * method throws. Equivalently: when the method returns normally, the parameter was non-null. + * + *
Example: + * + *
+ * public static <T> T requireNonNull(@IfNullThrows @Nullable T obj) {
+ * if (obj == null) throw new NullPointerException();
+ * return obj;
+ * }
+ *
+ *
+ * @checker_framework.manual #type-refinement Automatic type refinement (flow-sensitive type
+ * qualifier inference)
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface IfNullThrows {}
diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java
index f2ba3a3abb7a..59d706fcdab5 100644
--- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java
+++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java
@@ -6,6 +6,7 @@
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.AssertTree;
import com.sun.source.tree.BinaryTree;
+import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ClassTree;
@@ -35,9 +36,11 @@
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.source.util.TreePath;
+import com.sun.source.util.TreeScanner;
import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.checker.formatter.qual.FormatMethod;
+import org.checkerframework.checker.nullness.qual.IfNullThrows;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.basetype.BaseTypeChecker;
@@ -600,9 +603,51 @@ public void processMethodTree(String className, MethodTree tree) {
}
}
+ // @IfNullThrows well-formedness: method must throw (e.g. on null param)
+ checkIfNullThrowsEnforced(tree);
+
super.processMethodTree(className, tree);
}
+ /**
+ * Reports an error if the method has {@link IfNullThrows} on any parameter but its body does
+ * not contain a throw statement (so the contract cannot be satisfied).
+ */
+ private void checkIfNullThrowsEnforced(MethodTree tree) {
+ ExecutableElement methodElement = TreeUtils.elementFromDeclaration(tree);
+ if (methodElement.getModifiers().contains(javax.lang.model.element.Modifier.ABSTRACT)
+ || methodElement
+ .getModifiers()
+ .contains(javax.lang.model.element.Modifier.NATIVE)) {
+ return;
+ }
+ boolean hasIfNullThrows = false;
+ for (Element param : methodElement.getParameters()) {
+ if (atypeFactory.getDeclAnnotation(param, IfNullThrows.class) != null) {
+ hasIfNullThrows = true;
+ break;
+ }
+ }
+ if (!hasIfNullThrows) {
+ return;
+ }
+ BlockTree body = tree.getBody();
+ if (body == null) {
+ return;
+ }
+ boolean[] hasThrow = new boolean[] {false};
+ new TreeScannerAssigns a path to the artificial tree.
- *
- * @param tree the newly created Tree
- */
- @Override
- public void handleArtificialTree(Tree tree) {
- // Create a new child of the current path and assign to the artificial tree.
- // Although intuitively, using the sibling of the current path as the artificial tree
- // path makes more sense, it has the risk of improperly changing the defaulting scope
- // of the artificial tree.
- TreePath artificialPath = new TreePath(getCurrentPath(), tree);
- atypeFactory.setPathForArtificialTree(tree, artificialPath);
- }
-
- @Override
- protected VariableTree createEnhancedForLoopIteratorVariable(
- MethodInvocationTree iteratorCall, VariableElement variableElement) {
- Tree annotatedIteratorTypeTree =
- ((CFTreeBuilder) treeBuilder)
- .buildAnnotatedType(TreeUtils.typeOf(iteratorCall));
- handleArtificialTree(annotatedIteratorTypeTree);
-
- // Declare and initialize a new, unique iterator variable
- VariableTree iteratorVariable =
- treeBuilder.buildVariableDecl(
- annotatedIteratorTypeTree,
- uniqueName("iter"),
- variableElement.getEnclosingElement(),
- iteratorCall);
- return iteratorVariable;
- }
-
- @Override
- protected VariableTree createEnhancedForLoopArrayVariable(
- ExpressionTree expression, VariableElement variableElement) {
-
- TypeMirror type = null;
- if (TreeUtils.isLocalVariable(expression)) {
- // It is necessary to get the elt because just getting the type of expression
- // directly (via TreeUtils.typeOf) doesn't include annotations on the declarations
- // of local variables, for some reason.
- Element elt = TreeUtils.elementFromTree(expression);
- if (elt != null) {
- type = ElementUtils.getType(elt);
- }
- }
-
- // In all other cases, instead get the type of the expression. This case is
- // also triggered when the type from the element is not an array, which can occur
- // if the declaration of the local is a generic, such as in
- // framework/tests/all-systems/java8inference/Issue1775.java.
- // Getting the type from the expression itself guarantees the result will be an array.
- if (type == null || type.getKind() != TypeKind.ARRAY) {
- TypeMirror expressionType = TreeUtils.typeOf(expression);
- type = expressionType;
- }
-
- assert (type instanceof ArrayType) : "array types must be represented by ArrayType";
-
- Tree annotatedArrayTypeTree = ((CFTreeBuilder) treeBuilder).buildAnnotatedType(type);
- handleArtificialTree(annotatedArrayTypeTree);
-
- // Declare and initialize a temporary array variable
- VariableTree arrayVariable =
- treeBuilder.buildVariableDecl(
- annotatedArrayTypeTree,
- uniqueName("array"),
- variableElement.getEnclosingElement(),
- expression);
- return arrayVariable;
- }
- }
}
diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFCFGTranslationPhaseOne.java b/framework/src/main/java/org/checkerframework/framework/flow/CFCFGTranslationPhaseOne.java
new file mode 100644
index 000000000000..fdeabf043ce7
--- /dev/null
+++ b/framework/src/main/java/org/checkerframework/framework/flow/CFCFGTranslationPhaseOne.java
@@ -0,0 +1,141 @@
+package org.checkerframework.framework.flow;
+
+import com.sun.source.tree.AssertTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.TreePath;
+
+import org.checkerframework.common.basetype.BaseTypeChecker;
+import org.checkerframework.dataflow.cfg.builder.CFGTranslationPhaseOne;
+import org.checkerframework.dataflow.cfg.builder.ParameterConditionalThrowSpec;
+import org.checkerframework.framework.type.AnnotatedTypeFactory;
+import org.checkerframework.javacutil.ElementUtils;
+import org.checkerframework.javacutil.TreeUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Checker Framework phase-one CFG translation: uses the {@link AnnotatedTypeFactory} during
+ * translation (e.g. foreach, artificial trees, assertion-related CFG structure) and merges
+ * {@linkplain AnnotatedTypeFactory#getAdditionalParameterConditionalThrowSpecs(ExecutableElement)
+ * additional conditional-throw specs} from the type system.
+ */
+public class CFCFGTranslationPhaseOne extends CFGTranslationPhaseOne {
+
+ /** The associated checker. */
+ protected final BaseTypeChecker checker;
+
+ /** Type factory to provide types used during CFG building. */
+ protected final AnnotatedTypeFactory atypeFactory;
+
+ /**
+ * Creates a Checker Framework phase-one CFG translator.
+ *
+ * @param builder tree builder (typically a {@link CFTreeBuilder})
+ * @param checker the checker
+ * @param atypeFactory type factory (also the {@link
+ * org.checkerframework.javacutil.AnnotationProvider})
+ * @param assumeAssertionsEnabled whether assertions may be assumed enabled
+ * @param assumeAssertionsDisabled whether assertions may be assumed disabled
+ * @param env processing environment
+ */
+ public CFCFGTranslationPhaseOne(
+ CFTreeBuilder builder,
+ BaseTypeChecker checker,
+ AnnotatedTypeFactory atypeFactory,
+ boolean assumeAssertionsEnabled,
+ boolean assumeAssertionsDisabled,
+ ProcessingEnvironment env) {
+ super(builder, atypeFactory, assumeAssertionsEnabled, assumeAssertionsDisabled, env);
+ this.checker = checker;
+ this.atypeFactory = atypeFactory;
+ }
+
+ @Override
+ protected List Assigns a path to the artificial tree.
+ *
+ * @param tree the newly created Tree
+ */
+ @Override
+ public void handleArtificialTree(Tree tree) {
+ TreePath artificialPath = new TreePath(getCurrentPath(), tree);
+ atypeFactory.setPathForArtificialTree(tree, artificialPath);
+ }
+
+ @Override
+ protected VariableTree createEnhancedForLoopIteratorVariable(
+ MethodInvocationTree iteratorCall, VariableElement variableElement) {
+ Tree annotatedIteratorTypeTree =
+ ((CFTreeBuilder) treeBuilder).buildAnnotatedType(TreeUtils.typeOf(iteratorCall));
+ handleArtificialTree(annotatedIteratorTypeTree);
+
+ VariableTree iteratorVariable =
+ treeBuilder.buildVariableDecl(
+ annotatedIteratorTypeTree,
+ uniqueName("iter"),
+ variableElement.getEnclosingElement(),
+ iteratorCall);
+ return iteratorVariable;
+ }
+
+ @Override
+ protected VariableTree createEnhancedForLoopArrayVariable(
+ ExpressionTree expression, VariableElement variableElement) {
+
+ TypeMirror type = null;
+ if (TreeUtils.isLocalVariable(expression)) {
+ Element elt = TreeUtils.elementFromTree(expression);
+ if (elt != null) {
+ type = ElementUtils.getType(elt);
+ }
+ }
+
+ if (type == null || type.getKind() != TypeKind.ARRAY) {
+ TypeMirror expressionType = TreeUtils.typeOf(expression);
+ type = expressionType;
+ }
+
+ assert (type instanceof ArrayType) : "array types must be represented by ArrayType";
+
+ Tree annotatedArrayTypeTree = ((CFTreeBuilder) treeBuilder).buildAnnotatedType(type);
+ handleArtificialTree(annotatedArrayTypeTree);
+
+ VariableTree arrayVariable =
+ treeBuilder.buildVariableDecl(
+ annotatedArrayTypeTree,
+ uniqueName("array"),
+ variableElement.getEnclosingElement(),
+ expression);
+ return arrayVariable;
+ }
+}
diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java
index d066bdc37389..a552d00e65ef 100644
--- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java
+++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java
@@ -45,6 +45,7 @@
import org.checkerframework.common.reflection.MethodValChecker;
import org.checkerframework.common.reflection.ReflectionResolver;
import org.checkerframework.common.reflection.qual.MethodVal;
+import org.checkerframework.dataflow.cfg.builder.ParameterConditionalThrowSpec;
import org.checkerframework.dataflow.qual.SideEffectFree;
import org.checkerframework.framework.qual.AnnotatedFor;
import org.checkerframework.framework.qual.EnsuresQualifier;
@@ -901,6 +902,19 @@ public BaseTypeChecker getChecker() {
return checker;
}
+ /**
+ * Extra conditional-parameter throw specs merged during CFG construction (phase one). The
+ * default implementation returns none; {@link GenericAnnotatedTypeFactory} merges self-only
+ * contributions from composite checkers.
+ *
+ * @param method method or constructor being called
+ * @return additional {@link ParameterConditionalThrowSpec} entries
+ */
+ public List See {@code
- * org.checkerframework.framework.flow.CFCFGBuilder.CFCFGTranslationPhaseOne.handleArtificialTree(Tree)}.
+ * org.checkerframework.framework.flow.CFCFGTranslationPhaseOne#handleArtificialTree(Tree)}.
*
* @param tree the artificial {@link Tree} to set the path for
* @param path the {@link TreePath} for the artificial tree
diff --git a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java
index b4ba882f9aaf..6c1af7a24f27 100644
--- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java
+++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java
@@ -31,6 +31,8 @@
import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGLambda;
import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGMethod;
import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGStatement;
+import org.checkerframework.dataflow.cfg.builder.ConditionalThrowCompareValue;
+import org.checkerframework.dataflow.cfg.builder.ParameterConditionalThrowSpec;
import org.checkerframework.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.dataflow.cfg.node.Node;
import org.checkerframework.dataflow.cfg.node.ObjectCreationNode;
@@ -114,11 +116,13 @@
import java.util.Queue;
import java.util.Set;
import java.util.StringJoiner;
+import java.util.WeakHashMap;
import java.util.regex.Pattern;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
@@ -263,6 +267,30 @@ public abstract class GenericAnnotatedTypeFactory<
*/
private final Map Using the binary name avoids introducing a framework->checker module dependency.
+ */
+ private static final String IF_NULL_THROWS_ANNOTATION_NAME =
+ "org.checkerframework.checker.nullness.qual.IfNullThrows";
+
+ /**
+ * Cross-factory cache of parameter indexes annotated with framework-level null-throw hooks.
+ *
+ * Keys are weak to avoid retaining elements beyond javac rounds.
+ */
+ private static final Map